Skip to content
Files SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

files-sdk@3.1.0

Minor Changes

  • d02e8ca: Add an Effect v4 bridge (files-sdk/effect). Files is a Context.Service provided by Files.layer(), from a Files instance or the options to build one. Its methods mirror Files and return Effects, with listAll, search, and download bodies as Streams. Every operation fails with a FilesError whose reason is a tagged error for the SDK’s code (NotFound, Unauthorized, Conflict, ReadOnly, Invalid, Unsupported, Provider), so Effect.catchReason("FilesError", "NotFound", …) recovers from one code, and each reason keeps the SDK error’s flags and the original error as its cause. Each call receives its fiber’s AbortSignal, so interrupting the fiber (a timeout, a lost race, a closed scope) aborts the provider request. With the events() plugin, files.events.on() runs an Effect handler for each storage event and keeps delivery at-least-once, and files.events.stream() serves the events as a Stream. tryPromise runs plugin methods with the same error mapping and cancellation, and make() provides the service under your own key for multiple buckets or typed plugin methods. effect is an optional peer dependency.

Patch Changes

  • 203ecfe: files-sdk/openai: a needsApproval override in createAgentsFileTools({ overrides }) no longer breaks the tool. It used to replace the Agents SDK’s approval function with a boolean, so the runner failed with needsApproval is not a function on the first call. Overrides are now applied when each tool is built, an override takes precedence over requireApproval (and can gate a read tool), and every single-tool factory accepts { description, needsApproval }.
  • 16c1079: Fix files mcp failing to start from the published package. The bundled MCP server read its version from a fixed ../../package.json that only resolved from source, so in an installed package it failed at startup with a misleading “the mcp subcommand requires @modelcontextprotocol/sdk and zod” error even when both were installed. The CLI now finds its own package.json from wherever the build places each module.
  • 203ecfe: The files upload CLI’s --metadata flag now takes one key=value pair per flag; repeat the flag for more (--metadata team=finance --metadata quarter=q1). It used to be variadic and swallowed the positional key that followed it, so files upload --metadata a=1 report.pdf --file r.pdf failed. Several pairs after a single --metadata are now refused as extra arguments.
  • 8529ee6: Bulk upload([...]) and download([...]) with stopOnError in files-sdk/client (and the useFiles bindings) now behave like the SDK: items run one at a time in input order, and the call resolves at the first failure with { results, errors: [thatFailure] }. They used to reject while sibling items kept running, so writes could land after the call had failed and isUploading could read false while uploads were still in flight. Items that never started are still reported as "aborted" in the upload state.
  • 8529ee6: download() in files-sdk/client now reports the right size and lastModified more often. On a full proxied download it takes size from the gateway’s metadata header instead of Content-Length, which compression middleware can drop (giving 0) or rewrite. A download the gateway redirected to storage now falls back to the storage host’s Last-Modified header for lastModified.
  • 8529ee6: files-sdk/client and the useFiles bindings (files-sdk/react, files-sdk/vue, files-sdk/svelte) now reject with a FilesError when a request never gets an answer. A network failure used to surface as the runtime’s bare TypeError and a cancelled call as a raw AbortError, and the hooks recorded a hook-level abort() in error as a Provider failure with aborted: false. A network failure is now a Provider FilesError with the original error as its cause, and a cancelled call (per-call signal, hook-level signal, or abort()) is flagged aborted: true, matching the XHR upload path. The hooks rethrow the same error they record in error.
  • 8529ee6: files-sdk/client now classifies error responses that don’t come from the gateway by HTTP status, and says why. A body such as Supabase’s { "error": "InvalidJWT", "message": "jwt expired" } used to be read as the gateway’s envelope and became a Provider error with an empty message, and a 401 from auth middleware in front of the gateway became a generic Provider error. Only a body whose error.code is a string is now treated as the envelope. Anything else maps 401/403 to Unauthorized, 404 to NotFound and 409/412 to Conflict (else Provider), and the message keeps the old gateway responded 401 or upload failed (403) prefix with the body’s reason appended when it has one.
  • 8529ee6: A keyless upload() from files-sdk/client (and the useFiles bindings) of a React Native file reference without a size now works. The client used to declare such a file’s size as 0, and since the gateway holds an upload to its declared size, the upload was refused. The client now leaves size out of the presign request when it doesn’t know it, so the gateway applies only its maxUploadSize.
  • 8529ee6: The reactive useFile, useList and useSearch in files-sdk/react and files-sdk/vue no longer show the previous input’s data. Switching to a new key, prefix, pattern, or endpoint used to keep the old result visible while loading (with isLoading false) and next to the new input’s error, and a disabled query (useFile(undefined)) kept it too. A new input now starts with no data and isLoading: true, and a disabled query has no data or error. A refetch() of the same input still keeps its data while it reloads, and when the reload fails.
  • 8529ee6: The reactive queries in files-sdk/react, files-sdk/vue and files-sdk/svelte (useFile, useList, useSearch) now honor the signal option. It was documented as merged into every call but was ignored. Aborting it cancels the query’s request and settles the query with an aborted FilesError, and the query removes its listener once its request settles or is replaced.
  • 8529ee6: A string body uploaded with files-sdk/client (or the useFiles bindings) and no contentType is now stored as text/plain; charset=utf-8, as the server SDK stores it. It used to be sent untyped and stored as application/octet-stream.
  • 8529ee6: A global Content-Type in the headers option of files-sdk/client (and the useFiles bindings) no longer overrides the type of keyed uploads. It used to replace every upload(key, body) request’s type, even an explicit contentType, so files were stored as, say, application/json. The body’s own type or contentType now always wins, and the JSON verbs always send application/json.
  • b557950: files-sdk/cloudinary now invalidates the CDN copy when copy() or a resumable (control) upload overwrites an existing asset, as a plain upload() already did. Without it the CDN kept serving the replaced content until its cache expired.
  • b557950: files-sdk/netlify-blobs now rejects keys that @netlify/blobs can’t address, with an Invalid error before any request. The SDK puts keys into request URLs unencoded, so upload("Invoice #42.pdf") stored Invoice , a ? cut the key short in the same way, a \ became /, tabs, newlines and trailing spaces were dropped, and .. segments could even reach another store. Keys containing #, ?, %, \, tabs or newlines, ending in a space or control character, or with . / .. segments now throw instead of silently naming a different blob.
  • b557950: files-sdk/pocketbase now prefers adminEmail / adminPassword passed as options over a POCKETBASE_AUTH_TOKEN environment variable. The env token was checked first, so a stale token left in the environment overrode valid credentials passed in code. The order is now the authToken option, admin credentials from options, POCKETBASE_AUTH_TOKEN, then POCKETBASE_ADMIN_EMAIL / POCKETBASE_ADMIN_PASSWORD.
  • b557950: files-sdk/supabase copy() (and so move()) now replaces an existing destination. Supabase only overwrites on copy when asked to, so copying or moving onto a key that already existed failed with a Conflict error, which also broke re-deleting a key with softDelete() and restoring over an existing key. Copies are now sent with x-upsert: true.
  • b557950: files-sdk/supabase bulk deletes of more than 1000 keys now succeed. All keys went out in one request, which Supabase Storage refuses past 1000 objects, so every key was reported as failed. Keys are now deleted in batches of 1000, and a refused batch fails only its own keys.
  • b557950: files-sdk/supabase now percent-encodes keys in every request URL. Keys went into the path unencoded, so a # or ? cut the key short: upload("uploads/Invoice #42.pdf") stored uploads/Invoice , a later #43 upload silently overwrote it, and head, download and delete disagreed about which object the key named. Uploads, downloads, head, exists, url() (signed and public) and signedUploadUrl() now address exactly the key given, including keys with ?, %, spaces or +. Keys that Supabase Storage itself refuses (such as ones containing # or non-ASCII characters) now fail with Supabase’s Invalid key error instead of being truncated.
  • b557950: files-sdk/uploadthing list() now returns only files that finished uploading. It also returned files UploadThing reported as still uploading, failed, or pending deletion, which can’t be downloaded. Pagination is unchanged: the cursor still advances past every row UploadThing returned.
  • b557950: files-sdk/vercel-blob copy() now keeps the source’s content type and cache max-age. blob.copy doesn’t carry them over, so every copy, move and versioning snapshot got a content type guessed from the destination’s extension and the default one-month cache. The adapter now reads both from the source and passes them to the copy.
  • b557950: files-sdk/vercel-blob downloads no longer return stale bytes right after an overwrite. The body came through the CDN cache while the size and ETag came from a fresh head(), so for up to a minute the old content was returned labelled with the new metadata. Private downloads now read from origin (useCache: false), and public downloads add a v=<etag> query parameter to the blob URL so each new version misses the cache once.
  • b557950: files-sdk/azure no longer lets environment credentials for one storage account authenticate an adapter configured for another. Previously, azure({ container, accountName: "a" }) picked up an AZURE_STORAGE_CONNECTION_STRING for account b, so data calls went to b and SAS URLs were signed with a’s name and b’s key (a 403). Now an env connection string whose AccountName differs from an explicit accountName is ignored, and so are AZURE_STORAGE_ACCOUNT_KEY and AZURE_STORAGE_SAS_TOKEN when AZURE_STORAGE_ACCOUNT_NAME (or AZURE_STORAGE_ACCOUNT) names a different account. Env credentials for the same account, or that name no account, still apply.
  • b557950: files-sdk/azure copy() now copies blobs larger than 256 MiB. It used Copy Blob From URL only, which refuses bigger sources with a 409 that surfaced as a Conflict error. Now, when that cap is the cause, it falls back to the asynchronous Copy Blob and resolves once the copy finishes. A failed or aborted copy rejects with a Provider error carrying Azure’s reason.
  • b557950: files-sdk/convex upload() no longer fails after the file is stored when reading its metadata back fails. Previously that error escaped unmapped, and the caller never learned the new storage id, which orphaned the file. Now the upload resolves with the new id and the uploaded content type and size.
  • b557950: files-sdk/firebase-storage now prefers a passed app’s own storageBucket over FIREBASE_STORAGE_BUCKET. Previously the env var won, so in a process serving several Firebase projects, an adapter built from one project’s app used the env var’s bucket with that app’s credentials. The order is now bucket, then the app’s storageBucket, then FIREBASE_STORAGE_BUCKET.
  • b557950: files-sdk/gcs and files-sdk/firebase-storage upload() now take the result’s etag, lastModified, and size from the upload response instead of reading the object back afterwards. A service account that can create objects but not read them (Storage Object Creator) no longer gets an Unauthorized error for an upload that landed, and concurrent writers to the same key no longer risk reporting each other’s etag. If the response lacks an etag, the follow-up read is best-effort and can’t fail the upload.
  • b557950: files-sdk/gcs, files-sdk/firebase-storage, and files-sdk/bunny-storage now honor signal (and timeout) on uploads. Previously an aborted or timed-out upload kept running in the background and could land later, over a newer write. Now aborting cancels the in-flight request: GCS and Firebase destroy the upload stream, and Bunny Storage, whose SDK takes no signal, errors the request body instead. Bunny Storage’s download() and copy() honor the signal the same way.
  • db60e17: A conditional upload() in files-sdk whose write committed but whose adapter returned a malformed or weak ETag now rejects with applied: true. Before, the post-commit ETag check failed the call as a plain provider error, hiding that the object had already changed.
  • db60e17: url() and signedUploadUrl() in files-sdk now reject an expiresIn that isn’t a positive whole number of seconds with an Invalid error before anything is signed. Before, values like -60, 0, NaN, or 0.5 were passed to the signer and came back as URLs that never worked.
  • db60e17: list(), listAll(), and search() in files-sdk now reject a limit that isn’t a positive integer, and search() a negative, fractional, or NaN maxResults, with an Invalid error before any provider call. Before, limit: -1 made the fs, memory, FTP, and SFTP adapters silently drop the last key of each page, 0 or NaN walked nothing, and a bad maxResults returned nothing or everything. maxResults: 0 still yields no matches.
  • db60e17: upload() in files-sdk now rejects a multipart.partSize or multipart.concurrency that isn’t a positive integer with an Invalid error before any request, on every upload path (single, bulk, and resumable). Before, a fractional or non-positive value could spin a resumable upload in an endless loop that starved the event loop, silently drop the last byte on Azure, or commit an empty object. The resumable orchestrator also refuses to finalize a part list that doesn’t add up to the whole body, and fails a chunk the provider acknowledges without advancing instead of re-sending it forever.
  • db60e17: The files-sdk/ftp, files-sdk/sftp, and files-sdk/webdav adapters now reject keys that contain a backslash or start with a Windows drive letter (C:) with an Invalid error. Before, a key like ..\..\secret passed the .. traversal guard as a single segment, and C:/Windows/win.ini resolved to an absolute path, so on servers running on Windows hosts either could reach files outside the configured root.
  • db60e17: Resumable uploads in files-sdk now open, probe, and finalize their provider session under the same per-attempt timeout, caller signal, control.abort(), and retries as each part. Before, a hung session call ignored the timeout and abort, and a transient provider error there failed the upload despite retries. A session that opens after its attempt timed out or was aborted is discarded as soon as it lands, and a finalize retry that finds the session already gone now reports that the object may have been committed instead of a misleading “not found”. Custom resumable drivers receive an optional signal in begin(), probe(), and complete().
  • 21c88cc: files-sdk/box OAuth auth now survives Box’s single-use refresh tokens. Before, the rotated refresh token lived only in the instance’s memory, so after a restart or in a second instance the configured token was already spent and every call failed with Unauthorized. A new oauth.tokenStorage option persists the rotated tokens (the configured refreshToken becomes the first-run seed), and concurrent refreshes now share one exchange instead of spending the same token several times.
  • 21c88cc: files-sdk/dropbox now strips rootFolderPath from listed keys regardless of case. Dropbox paths are case-insensitive, so with rootFolderPath: "uploads" and a real folder named /Uploads, list() used to return keys like Uploads/a.txt that then addressed /uploads/Uploads/a.txt. Listed keys are now relative to the root (a.txt), as they are when the casing matches.
  • 21c88cc: files-sdk/google-drive no longer trusts a cached file id after another process or the Drive UI deleted, trashed, or replaced the file. Before, head reported NotFound, exists returned false, and delete resolved while the live file stayed. Now, when Drive answers that a cached id is gone, the adapter drops it and looks the key up again once. A trashed file reads as missing, and delete, copy, and url check that a cached id isn’t trashed before acting on it.
  • 21c88cc: files-sdk/onedrive and files-sdk/sharepoint now create a missing destination folder before copy(), so copy() and move() into a new folder succeed. Before, Graph’s copy failed because its destination folder must already exist, even though upload() to the same key creates intermediate folders, as copy does on Box, Dropbox, and WebDAV.
  • 21c88cc: files-sdk/onedrive and files-sdk/sharepoint no longer reject valid list() cursors from page 2 onward when the drive path is percent-encoded differently in Graph’s @odata.nextLink. This affected a siteId (its commas) and folder names containing characters such as @ , ; = + $ &. Cursors are now compared on their decoded paths, and a cursor for another folder is still refused.
  • 21c88cc: files-sdk/dropbox and files-sdk/onedrive (and files-sdk/sharepoint) refresh-token auth now shares one in-flight token exchange. Before, a cold burst of calls sent one token request per call, which risked rate limiting and, with rotating refresh tokens, rejected redemptions. A failed exchange is retried by the next call instead of being replayed.
  • 203ecfe: files-sdk/events: a Google OIDC token whose signature segment isn’t base64 is now refused with a 401 (“malformed OIDC token”), before any key fetch. It used to escape as an unexpected error, answered 500 (so Pub/Sub redelivered it) and reported to onError.
  • 203ecfe: files-sdk/events: SNS signature verification now accepts a SigningCertURL only at https://sns.<region>.amazonaws.com/SimpleNotificationService-<id>.pem (or .amazonaws.com.cn). The previous host check also matched S3 bucket endpoints such as sns.s3-accelerate.amazonaws.com, so a bucket named sns could have served a certificate.
  • 203ecfe: files-sdk/events: a webhook with verify: { sns } now parses only the fields of an SNS message that its signature covers. Before, the raw body was parsed after verification, so unsigned EventSource/Sns fields added beside a genuine signed message could feed forged S3 events (a delete, say) to your handlers. The s3 format also refuses a body that is both an SNS notification and a Lambda SNS record.
  • 203ecfe: files-sdk/events no longer reports a deleted event when only one version of an object was removed, since the key may still have a live version. That covers S3 ObjectRemoved:Delete and LifecycleExpiration:Delete carrying a version id (including NoncurrentVersionExpiration cleanups) and EventBridge Permanently Deleted events with a version-id, Backblaze B2 b2:ObjectDeleted:*, and GCS OBJECT_DELETE / Eventarc deleted events whose payload shows the generation was already noncurrent (timeDeleted more than a minute before the event). Delete markers, B2 hide markers, GCS archives and unversioned deletes are still reported, so cleanup handlers no longer delete live data when a lifecycle rule prunes old versions.
  • 51204c2: The files-sdk/api gateway’s url operation, and files-sdk/signed-url-policy, now accept a requested responseContentDisposition as an attachment only when its type is exactly attachment. Values such as attachment, inline, attachment inline, or attachment/x passed the old check but render inline in Chromium; they are now replaced with a plain attachment.
  • 51204c2: The files-sdk/api gateway no longer lets a comma-separated content type such as image/png, text/html skip the download sandbox. A browser renders such a value as its last entry, so a proxied download opened inline could run stored HTML as your app; the sandbox now applies to any stored type that isn’t exactly one well-formed media type. The gateway also refuses such a content type from a client with a 422 (reason: "type") on the keyed upload PUT, presign, and signed-upload-url, while an absent content type is still accepted.
  • 51204c2: Proxied downloads from the files-sdk/api gateway now name the file in their default Content-Disposition, as attachment; filename="report.pdf" (with an RFC 6266 filename* for a non-ASCII name), taken from the key’s last segment. Before, the bare attachment made a browser save a download opened directly from its URL as files. A disposition returned by authorize is still sent exactly as given.
  • 51204c2: The files-sdk/api gateway now answers HEAD on the download route instead of refusing it with 422. A proxied download returns the same headers a GET would (Content-Length, ETag, Last-Modified, the disposition) with no body, without reading the object, and a redirected download returns the same redirect.
  • 51204c2: A full (200) proxied download from the files-sdk/api gateway now carries the object’s size in its X-Files-Meta header, so a client can read the size even when compression middleware drops Content-Length. A 206 range response leaves it out.
  • 51204c2: A zero-byte upload through the files-sdk/api gateway no longer fails with 422 missing request body on Bun, Deno, or Cloudflare Workers. Those runtimes give a PUT with Content-Length: 0 a null body, which the keyed upload and the proxy upload now treat as an empty body.
  • 51204c2: The Node gateway bindings (files-sdk/express, files-sdk/fastify, files-sdk/koa, files-sdk/nitro, and files-sdk/nestjs) now work over HTTP/2. Under Fastify’s http2: true or node:http2’s compatibility API every request failed with a 500, because the HTTP/2 pseudo-headers (:method, :path, :authority, :scheme) were copied into the Web Request; they are now skipped, and the host is taken from :authority when there is no Host header.
  • 51204c2: The files-sdk/api gateway no longer forwards a storage provider’s error message to the client. Messages from an adapter’s NotFound, Unauthorized, Conflict, and Provider errors could carry absolute filesystem paths, internal hostnames, or bucket names, so those now reach the client as a fixed message per code (not found, storage provider error, …) with the same code and status, and Provider and Unauthorized errors are passed to onError so the detail still reaches your logs. The SDK’s own Invalid, Unsupported, and ReadOnly messages, and any FilesError thrown by your authorize, files factory, or onUploadComplete, are still sent as written.
  • 51204c2: The files-sdk/api gateway now refuses keys inside a plugin’s reserved storage however they’re spelled. On a case-insensitive store or a Windows filesystem, .TRASH/notes.txt or .trash\notes.txt reached the softDelete() trash (and .VERSIONS/… the version store), so a client allowed to delete but not purge could hard-delete a trashed file; those now get 403 like .trash/notes.txt. A restoreVersion versionId containing a backslash, .., or a NUL byte is now refused with 422, like one containing a slash.
  • 51204c2: A search through the files-sdk/api gateway now always walks storage in pages of maxListLimit keys. The client’s limit was used as the page size while maxSearchScan counts keys, so limit: 1 turned one request into up to 10,000 provider list() calls; limit is now ignored by the gateway, and maxResults still caps the matches.
  • 51204c2: A keyless upload through the files-sdk/api gateway is now held to the size the client declared at presign, capped by maxUploadSize. Before, authorize saw the declared size but the upload token allowed anything up to maxUploadSize, so a client could understate the size to pass a per-user quota. The proxy PUT and complete now refuse a larger body, a storage-signed target binds the declared size where the adapter can, and a client that doesn’t know a file’s size may omit size (that upload is held to maxUploadSize alone). A declared size that isn’t a non-negative whole number is a 422.
  • 51204c2: Upload tokens from the files-sdk/api gateway are now bound to the origin (scheme and host) of the request that minted them, as well as its path and query. With a files factory that picks the instance from the host, a token minted on one tenant’s host could previously be redeemed by the proxy upload PUT on another’s; that request, and complete on the wrong host, now get Unauthorized. The docs now also warn that a factory must select the instance from the URL, not from a cookie or header, because the proxy PUT runs no authorize.
  • 9fdc79f: files-sdk/cache no longer caches a stale read that overlapped a write. A slow download(), head(), or url() miss that was in flight while an upload() (or delete, copy, move) of the same key completed used to store what it had fetched after the write invalidated the key, serving the old value for the full ttl; such a read now returns its result without caching it. Writes also drop the key before they run and after any failure, not only after a success or a Conflict, since a timed-out write may still have landed.
  • 9fdc79f: files-sdk/content-type now classifies a document behind an <?xml prolog by its root element, so an XHTML or SVG document can no longer pass as an arbitrary +xml type. Previously any declared +xml type agreed with an XML sniff, so an XHTML page uploaded as image/x+xml passed onMismatch: "reject" (and an image/* allowlist after it) and ran its scripts when served. An html or svg root (or one in the XHTML or SVG namespace) now needs exactly application/xhtml+xml or image/svg+xml, an XML document never agrees with an image/, audio/, video/, or font/ type other than image/svg+xml, and detectContentType() reports application/xhtml+xml for an XHTML root.
  • 9fdc79f: files-sdk/dedup now reserves its blob store (.dedup by default) for the instance, the way versioning() and softDelete() reserve theirs. Previously a files-sdk/api gateway client could list .dedup/, download another user’s content by its hash, or delete or move a blob and break every pointer to it; those requests now get a 403. A move out of the store through the instance is also refused (it would remove the blob), and a backslash spelling such as .dedup\x is recognized as a store key.
  • 9fdc79f: files-sdk/failover no longer fails over an upload that carries a resumable control. An UploadControl drives exactly one upload, so replaying it on a secondary after the primary failed threw This UploadControl has already driven an upload and hid the primary’s real error. Such uploads now go to the primary only, like stream uploads, and surface its error.
  • 9fdc79f: files-sdk/validation and files-sdk/content-type no longer accept a declared content type that is really a list, such as image/png;a=b, text/html. Both plugins used to check only the text before the first ; and then store the caller’s value verbatim, which a browser renders as its last entry (text/html), so a PNG/HTML polyglot could pass allowedTypes: ["image/*"] or contentType({ onMismatch: "reject" }). A declared type that isn’t exactly one well-formed media type is now refused with an Invalid error (by validation() whenever allowedTypes is set, and by contentType() in every mode), and the type either plugin forwards is stored with its type and subtype normalized.
  • 9fdc79f: files-sdk/soft-delete no longer hard-deletes a live file through a trash key that resolves out of the trash. A delete(".trash/../notes.txt"), which a filesystem resolves to notes.txt, used to be treated as a delete inside the trash and destroyed the live file; it’s now trashed like any live key. purge() and restoreTrashed() refuse a key that resolves out of the trash, such as ../notes.txt, with an Invalid error.
  • 9fdc79f: files-sdk/soft-delete’s purge() is now idempotent on adapters whose delete throws NotFound for a missing key, such as GCS and Firebase. purge(key) with nothing trashed used to reject with NotFound there, and a whole-trash purge() failed when an object was purged concurrently; both now resolve.
  • 9fdc79f: files-sdk/soft-delete now explains a trash collision on hierarchical stores such as files-sdk/fs and SFTP. With a already in the trash, deleting a/b needs a .trash/a/ folder where the trashed file sits, and used to fail with a bare EEXIST … mkdir .trash/a error; it now throws a Conflict that names the trashed entry in the way and says to purge() (or restoreTrashed()) it first. The live object is untouched either way.
  • 9fdc79f: files-sdk/tiering with fallback: true now works over tiers whose delete throws NotFound for a missing key, as GCS and Firebase do. The plugin’s cleanup deletes assumed a missing key was a no-op, so every first upload rejected with NotFound after it had landed, a copy or move rejected after landing, and a delete of a key held by the other tier threw before reaching it, leaving the object in place. Those cleanup deletes now ignore NotFound; a delete removes the key from whichever tier holds it, and throws NotFound only when both tiers report the key missing.
  • 9fdc79f: files-sdk/versioning’s restoreVersion() now refuses a versionId that is empty or ., or that contains a backslash, .., or a NUL byte, as it already refused one containing / (the files-sdk/api gateway refuses the same ids). On a Windows filesystem a versionId such as ..\..\users\2\secret\<id> restored another key’s version (possibly another tenant’s) into the caller’s key. A write to a key such as .versions/../notes.txt, which a filesystem resolves to notes.txt, is also no longer mistaken for a write inside the version store, so it’s snapshotted like the live key it is.
  • 22f8f5d: files-sdk/s3, files-sdk/s3-fetch, and every S3-compatible adapter now refuse user metadata values that aren’t printable ASCII, including Latin-1 text such as café, with an Invalid error before any request. Such a value used to go out and fail as SignatureDoesNotMatch, reported as Unauthorized, because the HTTP client sends a Latin-1 character as one byte while SigV4 signs it as two. Encode the value first, for example with encodeURIComponent. The R2 binding stores metadata without HTTP headers, so it still accepts any text.
  • 22f8f5d: files-sdk/bun-s3 now reports bad credentials on list(), upload(), delete(), and copy() as Unauthorized. Bun’s S3 errors carry no HTTP status, and SignatureDoesNotMatch, InvalidAccessKeyId, ExpiredToken, and InvalidToken weren’t recognized, so a wrong key or secret was a Provider error that retries reissued.
  • 22f8f5d: files-sdk/s3, files-sdk/s3-fetch, every S3-compatible adapter, and files-sdk/bun-s3 now refuse a contentType or cacheControl containing control characters (such as CR/LF) or non-ASCII text with an Invalid error before any request, on uploads, resumable uploads, and signedUploadUrl(). Such a value used to fail as a retried Provider error, or as Unauthorized on the aws-sdk client while the fetch client stored it mangled.
  • 22f8f5d: A download() whose range starts past the end of the object (416 InvalidRange) is no longer retried on files-sdk/s3, files-sdk/s3-fetch, the S3-compatible adapters, files-sdk/bun-s3, or the files-sdk/r2 binding (error code 10039). It stays a Provider error, since it’s the provider’s answer, but is now marked permanent, so retries doesn’t reissue a request that can only fail the same way.
  • 22f8f5d: copy() and move() on files-sdk/s3, files-sdk/s3-fetch, and the S3-compatible adapters now handle sources over 5 GiB. S3’s single CopyObject request refuses them, which used to surface as a retried Provider error; the adapter now confirms the size with a HEAD and copies the object server-side with a multipart copy (UploadPartCopy), carrying its content type, cache and disposition headers, and user metadata. On AWS every part is pinned to the source’s ETag, so a source overwritten mid-copy fails with Conflict. A conditional copy of such a source throws Unsupported, since it can’t stay one atomic request.
  • 22f8f5d: multipart.partSize on files-sdk/s3 and the S3-compatible adapters’ aws-sdk client is now rounded into what S3 accepts. A size under 5 MiB is raised to S3’s minimum instead of failing every attempt with EntityTooSmall, a size over 5 GiB is lowered to the maximum, and when the body’s length is known the size grows until the body fits in S3’s 10,000 parts, instead of failing at part 10,001 after about 48.8 GiB had uploaded.
  • 22f8f5d: url() and signedUploadUrl() on files-sdk/s3, files-sdk/s3-fetch, the S3-compatible adapters, files-sdk/bun-s3, and the files-sdk/r2 binding’s hybrid signer now throw Invalid for an expiresIn or defaultUrlExpiresIn that isn’t a whole number of seconds of at least 1. A zero, negative, fractional, or NaN lifetime used to mint a URL that was dead on arrival; only the seven-day ceiling was checked.
  • 22f8f5d: A resumable (control) upload on files-sdk/s3 and the S3-compatible adapters no longer fails after the object has committed when the follow-up HeadObject is denied or fails (for example, a principal allowed PutObject but not GetObject). The upload now resolves with the summed part sizes and the upload’s content type, where it used to reject with Unauthorized and leave a resume that could only hit NoSuchUpload.
  • 22f8f5d: An expired or malformed session token (ExpiredToken, InvalidToken, which S3 returns as HTTP 400) is now Unauthorized on files-sdk/s3, files-sdk/s3-fetch, and the S3-compatible adapters, instead of a Provider error that retries reissued.
  • 21c88cc: files-sdk/fs resumable uploads no longer follow a symlink planted at the <key>.fls-part staging path. Starting an upload used to open that path and truncate whatever the link pointed at, inside the root or not, and completing it then renamed the link into place. Now the upload replaces anything at the staging path and creates the file exclusively, and a chunk or completion that finds a symlink there fails with Conflict instead of writing or reading through it.
  • 21c88cc: files-sdk/fs now rejects keys that end in /, or in a . or .. segment, with Invalid. Path resolution used to drop the trailing part, so upload("dir/") wrote a file named dir and delete("a.txt/") deleted a.txt. A delete("dir/") that used to be a silent no-op now throws Invalid too.
  • 21c88cc: files-sdk/ftp now runs calls on an injected client one at a time. An FTP connection can only run one command at a time, and basic-ftp closes the whole connection when a second command starts, so concurrent calls on a shared client (Promise.all, or the bulk forms of upload, download, and head) used to fail and leave the client closed for good. Calls now queue on the client, a failed call no longer blocks the ones behind it, and a streamed download holds the client until the stream is read to the end or cancelled.
  • 21c88cc: files-sdk/ftp and files-sdk/sftp move() now overwrite an existing destination like every other adapter. A plain rename failed whenever the destination existed: OpenSSH and most SFTP servers answered with a generic failure that was retried and then thrown as Provider, and FTP servers that refuse to rename over a file, such as IIS, reported a misleading NotFound. When the server refuses, the destination is now deleted and the rename retried (so the key is briefly absent), except for a case-only rename, which could otherwise delete the source on a case-insensitive server.
  • 21c88cc: files-sdk/webdav streamed downloads (as: "stream") no longer report size: 0 when the server sends a chunked response without a Content-Length header. The adapter now takes the size from a PROPFIND, narrowed to the requested slice for a ranged read.
  • 21c88cc: files-sdk/zip unzip() now decodes entry names the way the archive declares them. Names were always read as UTF-8 with invalid bytes replaced, so archives from Windows Explorer and other tools that write code page 437 produced garbled keys, and two such entries could collapse into one name and fail as duplicates. Names flagged UTF-8, or carried in an Info-ZIP Unicode Path field, are now read as strict UTF-8 and fail closed on invalid bytes. Unflagged names are read as UTF-8 when they are valid UTF-8 and as code page 437 otherwise.

Last updated on

Was this page helpful?