Skip to content

files-sdk@2.3.0

Latest

Choose a tag to compare

@github-actions github-actions released this 27 Aug 06:25
Immutable release. Only release title and notes can be modified.
7efe9aa

Minor Changes

  • 4563817: Add provider-native conditional create, replace, exact-read, delete, and copy operations to the existing plugin, hook, retry, prefix, read-only, and receipt pipeline. AWS S3 implements the initial atomic primitives; unsupported adapters, filesystem, custom S3 endpoints, R2, bulk, and multipart paths fail closed.

    The S3 adapter exposes the primitives only for canonical AWS — no endpoint and no AWS_ENDPOINT_URL_S3 / AWS_ENDPOINT_URL redirect — with a new conditional option to override that in either direction, and verifies per request that the resolved hostname is AWS (so a shared-config endpoint_url fails closed too) and that the installed @aws-sdk/client-s3 serialized every predicate header (conditional copy needs 3.919.0+; the @aws-sdk/client-s3 peer range moves from ^3.700.0 to ^3.1079.0). The CLI gains --if-match / --if-none-match / --dest-if-match on upload, download, delete, and copy, and the MCP tools accept a matching condition input. cache() now invalidates after a failed write too; softDelete() forwards a conditional delete of an already-trashed key; dedup() rejects every conditional mode.

    A conditional mutation that commits before an awaited plugin rejects the call now surfaces as FilesError.applied === true (with appliedEtag for uploads) — on the rejected error, in onError / onAction, and in the audit() record — so callers can reconcile instead of retrying a predicate that can only conflict. A plugin that re-invokes next() after the native call failed gets that first failure as cause. rejectConditional(op, plugin, reason) is exported as the one veto shape for plugins with out-of-band side effects. Ordinary operations type mode as undefined (there never was an "overwrite" value to branch on).

Patch Changes

  • 5bc70b1: Fix azure() buffered download() opening a GET whose body was never read or destroyed before issuing the real downloadToBuffer request, which held the first response's socket open until garbage collection. The adapter now fetches the metadata with a lightweight getProperties() call instead; the returned file is unchanged.
  • 5bc70b1: Fix azure() list() never returning blob metadata on its items. Azure only includes metadata in listing responses when explicitly asked, so the adapter now passes includeMetadata: true to both flat and hierarchical listings.
  • 5bc70b1: Fix azure() resumable uploads throwing NotFound when resuming a session that was paused or persisted before its first block landed. Azure answers GetBlockList with a 404 when the blob has no committed or uncommitted blocks yet; the adapter now treats that as an empty session and uploads from the start.
  • 5bc70b1: Fix azure() copy() and move() failing with CannotVerifyCopySource in SAS-token mode. The service client is built with the SAS in its URL and the SDK carries that query through to every blob URL, so the adapter was appending the token a second time (?sv=…?sv=…). The copy source now reuses the SAS already present on the blob URL.
  • 5bc70b1: Re-uploading a key larger than 50 MB to Box now creates a new version of the existing file instead of failing with Conflict (item_name_in_use). The Box SDK's uploadBigFile helper always opens a new-file upload session, so the adapter now opens an existing-file session for the resolved file ID and drives the same part-upload and commit loop itself.
  • 5bc70b1: Fixed the Bunny Storage adapter returning a directory listing instead of the file's contents when reading the body of an item returned by list() (or head()). The lazy body now downloads by the entry's full key rather than relying on the Bunny SDK's entry.data(), which fetches the entry's containing directory for listing results.
  • 5bc70b1: Fixed the CLI's --application-key-id and --application-key flags being ignored for --provider backblaze-b2, which made the documented invocation fail with "missing credentials". The B2 provider now also lists --region as required (the adapter has no environment fallback for it), and the provider catalog's backblaze-b2 entry lists region in its required config.
  • 5bc70b1: Fixed the CLI's exists command with multiple keys exiting with code 1 ("missing") when a hard error such as an authentication failure occurred alongside a missing key. The hard error's mapped exit code now wins.
  • 5bc70b1: Fixed the CLI silently dropping the --endpoint flag for --provider r2, so jurisdiction-specific buckets and S3-compatible stand-ins can now be targeted without an account id.
  • 5bc70b1: The CLI's search --match <mode> flag now validates its value against glob, regex, substring, and exact, so a typo is reported as an invalid choice instead of falling through to a confusing regex error.
  • 5bc70b1: The browser client's XHR transport now rejects immediately when handed an already-aborted signal. It previously called xhr.abort() before send(), which per the XMLHttpRequest spec fires no abort event, so the upload promise never settled in real browsers and callers awaiting an upload with a pre-aborted signal hung forever. The rejection is the same abort error an in-flight abort produces, and no request is opened.
  • 5bc70b1: Bulk upload([...]) no longer silently retries buffered bodies using the client's retries setting. It now matches the documented behavior of every other bulk verb: each item is attempted once and a failure lands in errors. Single-key upload() keeps its retry budget, and onRetry still fires only for single-operation calls.
  • 5bc70b1: A failed ReadableStream upload with onProgress no longer leaves the caller's stream locked. The progress-counting wrapper now acquires its reader lazily on first pull, and on failure the SDK cancels the wrapper so the source stream's cancel runs and body.locked is false afterward. Previously the reader was grabbed eagerly, so an adapter that failed before reading a byte left the caller unable to cancel or reuse their stream.
  • 5bc70b1: A plugin wrap that throws a plain Error now surfaces to the caller as a FilesError regardless of whether onAction or onError hooks are installed. Previously the hook-free fast path let the raw error escape unwrapped, so error instanceof FilesError checks behaved differently depending on the client's hook configuration.
  • 5bc70b1: UploadControl.abort() is now honored when it lands while a resumable upload is finalizing. Previously an abort that arrived after the last chunk but during the provider's complete() call was overwritten: the control flipped from "aborted" to "completed" and upload() resolved. The orchestrator now checks for an abort before finalizing and again after, so upload() rejects with the aborted FilesError and the control stays aborted.
  • 5bc70b1: files.search() now finds keys when the glob contains a backslash escape such as a\*b/x. The literal prefix pushed down to the provider's list call used to keep the escape verbatim (a\*b/x), so nothing was listed even though the matcher correctly accepted the key a*b/x. Escapes are now unwrapped to the literal characters they stand for before the prefix is applied.
  • 5bc70b1: A caller-supplied signal now keeps reaching a lazily-streamed body after the operation call has resolved. Previously, when a timeout was configured or a per-call signal was combined with the client's constructor signal, the SDK minted a merged signal and detached it from its sources as soon as the adapter call settled, so aborting the caller's signal no longer interrupted a download() body still being read. Caller signals are now folded with AbortSignal.any (with a manual fallback on older runtimes) and stay wired for the life of the operation, while only the per-attempt timeout timer is disarmed once the call resolves, so a timeout still never cuts off a body that is streaming after the call succeeded.
  • 5bc70b1: A timeout of Infinity (or any value past the 32-bit setTimeout limit) no longer aborts every operation after about a millisecond with "Operation timed out after Infinityms". Non-finite timeouts now mean "no timeout", and finite values beyond the limit are clamped to it, consistently across single operations, bulk calls, and resumable-upload chunks.
  • 5bc70b1: Dropbox url() on a public shared link now rewrites dl=0 to dl=1 correctly on current /scl/fi/...?rlkey=...&dl=0 links. The old rewrite matched the literal ?dl=0 prefix, which no longer comes first on these links, and appended a second parameter, yielding ...&dl=0&dl=1 and serving the preview page instead of the raw bytes. The parameter is now set through the URL parser regardless of its position.
  • 5bc70b1: The Dropbox adapter now caps multipart.partSize at the 150 MB per-request limit of Dropbox upload sessions (rounded down to the required 4 MiB multiple, so 148 MiB). Previously a larger partSize was rounded but never capped, producing session appends the API rejects.
  • 5bc70b1: dropbox({ publicByDefault: true }) now reuses an existing shared link instead of failing with Conflict. The Dropbox SDK stores the whole parsed error body on the thrown error, so the shared_link_already_exists metadata sits one envelope deeper than the adapter was reading; url() on a key whose public link had already been created therefore never found the existing URL and fell through to the generic conflict error. The adapter now reads the enveloped shape (and still tolerates the bare one).
  • 5bc70b1: Fixed failover() secondaries and the tiering() cold tier ignoring the instance's constructor-level timeout, retries, and signal. The internal Files each plugin builds around its extra adapter was created without those defaults, so after the primary timed out and the chain failed over, a hung secondary (or a hung cold backend) stalled the operation forever instead of timing out. Those internal instances now inherit the outer instance's defaults, and Files exposes them through a new read-only defaults getter for plugins that need to do the same.
  • 5bc70b1: The fs adapter no longer corrupts or fails concurrent uploads to the same key. Two uploads that landed in the same millisecond shared one staging file, so one of them failed with a spurious NotFound from its rename and the survivor's stored etag came from the other call's bytes. Each upload now stages under a per-call unique name, stages its sidecar the same way, and commits both under a per-key lock, so every concurrent upload resolves and the final body and its etag always belong to the same call.
  • 5bc70b1: The fs adapter's in-flight upload staging files no longer surface as objects. A crash between writing the staging file and renaming it into place, or a list() racing an upload, used to expose a <key>.<pid>.<ms>.tmp entry whose download served half-written bytes. Staging files now use the reserved .fls-tmp suffix, which list() skips and which keys can no longer target, alongside the existing .meta.json and .fls-part reservations.
  • 5bc70b1: The fs adapter's write paths now enforce the same symlink boundary as its read paths. upload, delete, move, copy (destination), and resumable uploads previously followed a symlinked directory inside the root to wherever it pointed, so a link to a directory outside the root let a key write or unlink files there even though download/head/exists rejected the same key. Those operations now resolve the nearest existing ancestor through symlinks before touching anything, and reject keys that resolve outside the adapter root with the existing "resolves outside adapter root" Provider error.
  • 5bc70b1: The FTP adapter now restores the working directory even when creating a nested directory fails partway through. basic-ftp's ensureDir changes into the tree one segment at a time, and the adapter only restored the original directory after a successful walk, so a refused mkdir left a reused connection parked inside the tree and later relative paths resolved against the wrong directory.
  • 5bc70b1: FTP resumable uploads (multipart: true or an UploadControl) now create the destination's parent directory before the first chunk, matching plain upload() and move(). Previously upload("videos/clip.mp4", body, { multipart: true }) with no videos/ directory failed on the first APPE with a 550 that surfaced as NotFound.
  • 5bc70b1: The gateway's presign upload tokens are now bound to the endpoint query they were minted under. With a per-request files factory that picks the instance from the query string (the ?bucket= pattern), a token minted under one bucket could previously be replayed against the proxy upload endpoint with the bucket switched, and the bytes landed in an instance where authorize would have refused the upload; the PUT ?op=proxy path never re-ran authorize and the token carried no memory of the query. The token now records the request's non-routing query (everything except op, key, and token, in a canonical sorted form), and both the proxy upload and complete refuse a token presented under a different query with a 401 Unauthorized error. Single-bucket gateways with a bare endpoint are unaffected: an empty bound query matches an empty query.
  • 5bc70b1: The gateway's search op now matches against the caller-facing key when authorize returns a keyPrefix scope, the same way list already returns unscoped keys. Previously the pattern was tested against the full storage key with the scope prefix still attached, so a client scoped to users/1/ searching *.png, a.png with match: "exact", or ^a as a regex got no matches for users/1/a.png. The handler also validates match against glob, regex, substring, and exact and answers a 422 for anything else instead of silently treating the value as a regex.
  • 5bc70b1: Overwriting a key on Google Drive now replaces its metadata instead of merging it with the previous version's. Drive merges appProperties on update and only removes a key when it is sent as null, so re-uploading with metadata: { b: "2" } after { a: "1" } read back as { a: "1", b: "2" } (and a dropped cacheControl lingered) while every other adapter yields { b: "2" }. Both buffered and resumable overwrites now clear the stale keys explicitly.
  • 5bc70b1: The memory adapter no longer hands out its own stored bytes on read. download(), head(), and ranged downloads passed the store's Uint8Array (or a view over it) straight into the returned file, and stream() enqueues that exact array, so a reader mutating a chunk silently corrupted the stored object without changing its etag. Read paths now copy the bytes out, matching the value semantics uploads already had.
  • 5bc70b1: Fix onedrive() and sharepoint() copy() surfacing every failure as a generic Provider error (copy returned 404 without monitor URL). The Graph client returns raw responses without checking their status, so the adapter now classifies non-2xx copy responses itself: a 404 becomes NotFound, 409/412 become Conflict, and 401/403 become Unauthorized, with the Graph error message preserved, so retry and failover treat them correctly.
  • 5bc70b1: Fix onedrive() and sharepoint() rejecting every chunk when multipart.partSize exceeded Graph's 60 MiB upload-session fragment limit (for example partSize: 100 MiB). The requested part size is now clamped to the largest 320 KiB multiple at or below 60 MiB.
  • 5bc70b1: Fix onedrive() (and therefore sharepoint()) streaming downloads throwing ERR_INVALID_ARG_TYPE in every real runtime. The Graph client resolves ResponseType.STREAM with the fetch Response body, which is already a web ReadableStream, and the adapter was unconditionally passing it through Readable.toWeb(). It now only converts when the client actually hands back a Node Readable.
  • 5bc70b1: Fixed createResponsesFileTools emitting a schema that OpenAI rejects with a 400 when a tool override sets strict: true. Strict tools now list every property in required, mark optional fields nullable, set additionalProperties: false on every object, and drop free-form maps (so uploadFile has no metadata field under strict mode); execute treats null arguments for optional fields as absent.
  • 5bc70b1: Fixed the PocketBase adapter's url() percent-encoding the slashes of nested keys (docs/a.txt became docs%2Fa.txt) when publicBaseUrl is configured. Keys are now encoded per path segment, matching every other adapter.
  • 5bc70b1: The R2 HTTP adapter now forwards per-operation options (signal, and therefore timeout) for copy, delete, exists, and head. The lazy-loaded proxy over the inner S3 adapter dropped the third argument for those four verbs, so an abort signal never reached the underlying request even though download, list, and upload on the same adapter honored it.
  • 5bc70b1: useList, useFile, and useSearch from files-sdk/react now refetch when endpoint changes. The hooks rebuilt their client on a new endpoint but only re-ran the query when the call options changed, so switching from /api/files to /api/files?bucket=images kept showing the previous endpoint's data until a manual refetch(). The client is now part of the query's dependencies and is rebound only by endpoint; fetchImpl is read live on each request like headers, so passing it inline does not refetch on every render.
  • 5bc70b1: S3 multipart, progress-reporting, and unsized-stream uploads now honor a signal that was already aborted by the time the upload started. The lib-storage path attached only an "abort" listener, which never fires for a signal that flipped during the body normalization and lazy import that run first, so the upload proceeded and the object landed after the caller had been told it was aborted. The upload now aborts immediately and rejects with the usual aborted error.
  • 5bc70b1: The SFTP adapter now uploads into dot-prefixed directories such as .well-known/acme-challenge/token correctly. ssh2-sftp-client treats any relative path starting with . as if it began with ./ and strips two characters, so with the default root the adapter created ell-known/acme-challenge and the following write failed with a bogus NotFound. Relative parent directories are now passed with an explicit ./ anchor; absolute and already-anchored paths are unchanged.
  • 5bc70b1: Fix sharepoint() not supporting resumable uploads even though the underlying OneDrive adapter does: upload(key, body, { control }) threw an unsupported-operation error and files.capabilities.multipart reported false. The wrapper now forwards resumableUpload to the inner Graph upload-session driver, created lazily after site and drive resolution like every other verb.
  • 5bc70b1: Fixed softDelete()'s whole-trash purge() silently swallowing per-key delete failures. It emptied the trash through the bulk delete, which collects errors instead of throwing, and discarded the result, so purge() resolved while trashed() still listed the key. It now removes everything it can and then throws a FilesError naming how many objects failed, with the first failure as its cause, matching how purge(key) already surfaced errors.
  • 5bc70b1: Fixed the Supabase adapter's list() reporting Supabase's system metadata block (eTag, size, mimetype, cacheControl, contentLength, lastModified) as user metadata on every item, which head() and download() never reported. List items now only surface user metadata when the listing response carries it under user_metadata.
  • 5bc70b1: Fixed the UploadThing adapter resolving the lazy bodies of list() items with an error page as file contents when the file had been deleted or the request was rejected. Those reads now check the response status like download() and head() do, throwing NotFound on 404 and Provider otherwise.
  • 5bc70b1: Fixed the Vercel Blob adapter resolving the lazy bodies of head() and list() results with the CDN's error page as file contents when the blob had been deleted or the request was rejected. Those reads now check the response status like download() does, throwing NotFound on 404 and Provider otherwise.
  • 5bc70b1: Fixed versioning() destroying the version being restored when a limit is set. restore() snapshots the current bytes first, and that snapshot could push the oldest kept version past the limit; the plugin pruned it before copying it back, so the restore failed with NotFound and the version was gone for good (with limit: 1 no restore ever worked). Copies and moves now enforce the limit only after the operation has landed, so restoring the oldest kept version succeeds and the pre-restore bytes take the freed slot.
  • 5bc70b1: Fix webdav() streaming downloads failing with stream.getReader is not a function under Node. The webdav package routes requests through node-fetch there, whose Response body is a Node Readable rather than a web stream; the adapter now normalizes it to a web ReadableStream so download(key, { as: "stream" }) works in Node as well as Bun and the browser.
  • 5bc70b1: The zip() plugin now requests each entry's body as a stream when building an archive, so large files are no longer fully buffered in memory before being written. Archive contents are unchanged.