Documentation menu

 

Upload Files to DigitalOcean Spaces from the Browser

DigitalOcean Spaces is S3-compatible object storage. It implements a subset of the S3 API that includes presigned URLs and multipart uploads, which covers every operation upup uses. The datacenter region (nyc3, sfo3, ams3, fra1, sgp1 and the others) is part of the endpoint hostname, and the endpoint decides which datacenter receives a request.

This page covers server mode, where @useupup/server holds the credentials. See Client Mode vs Server Mode if you would rather sign URLs yourself and have the browser upload directly.

The config

Only the storage block differs from the AWS S3 example. Keep uploadTokenSecret and the rest of the handler as they are there.

ts
const storage = {
    type: 'digitalocean',
    bucket: process.env.SPACES_BUCKET!, // the Space name
    region: process.env.SPACES_REGION!, // e.g. 'nyc3'
    endpoint: `https://${process.env.SPACES_REGION}.digitaloceanspaces.com`,
    accessKeyId: process.env.SPACES_KEY,
    secretAccessKey: process.env.SPACES_SECRET,
}

bucket is the Space name. DigitalOcean's UI says "Space" or "bucket", the S3 API says "bucket", and they are the same thing. The endpoint host is the region, not the Space: https://nyc3.digitaloceanspaces.com, never https://my-space.nyc3.digitaloceanspaces.com. upup turns on path-style addressing whenever endpoint is set, so the Space name is added to the path for you; putting it in the hostname as well produces requests aimed at a Space inside a Space.

Console setup

  1. Create the Space. DigitalOcean control panel → Spaces Object Storage → Create Bucket. Pick the datacenter region, which cannot be changed later, and Standard Storage: Cold Storage buckets do not support CORS, so a browser cannot upload to them. File listing is private by default; leave it that way unless you want the contents publicly enumerable.
  2. Create an access key. Spaces Object Storage → Access Keys → Create Access Key. Choose Limited access and give the key Read/Write/Delete on this one bucket, which covers the object and multipart operations upup performs. The secret key is shown once.
  3. Add a CORS configuration from the bucket's Settings tab, as described below. The browser uploads straight to Spaces by default in both client and server mode.

A DigitalOcean API token is not a Spaces key

Spaces access keys and DigitalOcean personal access tokens are different credentials issued from different pages. The API token that works with doctl and the DigitalOcean API will not authenticate an S3 request. If uploads come back as 403 with credentials you are sure are correct, check that they came from the Access Keys tab under Spaces Object Storage.

CORS

In the bucket's Settings tab, under CORS Configurations, click Add. Enter your app's full origin (for example https://app.example.com; one wildcard is allowed in the hostname), allow PUT (plus GET and HEAD if the browser reads files back), and add Content-Type under Allowed Headers, because upup sends it with every upload. That is enough for single-request uploads.

The control panel has no field for exposed headers. Multipart uploads need the ETag header exposed, so if you enable them, upload the rule as XML instead; see Multipart uploads fail at the last step.

CORS applies in both client and server mode: by default the browser uploads straight to Spaces either way, since server mode hands it a presigned URL instead of proxying the bytes. The exceptions are cloud-drive transfers (server to Spaces) and a deployment using onPresignResponse to rewrite the URL to a same-origin proxy. See Credentials And CORS.

Large files and multipart uploads

A single PUT to Spaces can be up to 5 GB. Past that, or when you want uploads to survive a dropped connection, use multipart: Spaces accepts parts of at least 5 MiB (except the last) and up to 5 GB, up to 10,000 parts, and objects up to 5 TB.

In upup, multipart is part of server mode. Turn it on in the uploader:

tsx
<UpupUploader
    mode="server"
    serverUrl="/api/upup"
    provider="digitalocean"
    resumable={{ protocol: 'multipart' }}
/>

Files of 5 MiB or more (thresholdBytes) are split into parts of at least 5 MiB (chunkSizeBytes), and the part size grows for very large files so that no upload needs more than 10,000 parts. The /multipart/* routes of @useupup/server start, sign and complete the upload, each part goes to Spaces through its own presigned URL, and a failed part is retried without restarting the file. See Resumable uploads.

Interrupted uploads keep their parts so a later attempt can continue (cross-reload resume is on by default). Spaces deletes incomplete multipart uploads older than 30 days on its own, and a bucket lifecycle rule can remove them sooner.

Serving files back through the CDN

Each Space can enable a CDN endpoint on a hostname of the form <space>.<region>.cdn.digitaloceanspaces.com. Use it for reads, and keep uploading to the origin endpoint in the config above: requests with presigned URLs are never cached by the CDN, and DigitalOcean caps PUT and multipart requests sent to the CDN with presigned URLs at 8100 KiB.

The CDN serves public files from its cache. A private file needs a presigned URL, which bypasses the cache and works on the CDN hostname only when it was generated in virtual-hosted style, so for private files the origin URL that upup returns is the simpler choice.

Troubleshooting

CORS preflight fails

The browser console shows the PUT blocked by CORS policy. Check, in order:

  • The Space has a CORS rule, and it is a Standard Storage bucket. Cold Storage buckets do not support CORS.
  • The origin matches exactly: scheme, host and port.
  • PUT is an allowed method, and Content-Type is an allowed header, along with any other header your endpoint returns in uploadHeaders.
  • The browser is not reusing a cached preflight. DigitalOcean recommends an Access Control Max Age of 5 seconds while you test.
  • If you read files through the CDN, purge its cache after changing CORS. Responses cached before the change carry no CORS headers.

Multipart uploads fail at the last step

Every part uploads, then completion fails. upup reads each part's ETag response header in the browser and sends the list to your server, which completes the upload with it. If the CORS rule does not expose ETag, the browser cannot read the header and Spaces rejects the part list.

The control panel cannot set exposed headers, so upload the rule as XML with s3cmd, configured with the Space's region endpoint:

xml
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <CORSRule>
    <AllowedOrigin>https://app.example.com</AllowedOrigin>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>HEAD</AllowedMethod>
    <AllowedHeader>*</AllowedHeader>
    <ExposeHeader>ETag</ExposeHeader>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
  </CORSRule>
</CORSConfiguration>
sh
s3cmd setcors cors.xml s3://my-space

Changing a bucket's CORS configuration through the API needs a Full access key. The Limited access key upup uses cannot do it, so run this with a separate key.

403 SignatureDoesNotMatch

Spaces computed a different signature from the one in the URL. The usual causes:

  • The credentials are not a Spaces access key. A DigitalOcean API token, or a mistyped secret, produces a signature Spaces cannot match.
  • The browser sent a different Content-Type from the one that was signed. @useupup/server signs the content type and length and returns the Content-Type header in uploadHeaders, which upup sends unchanged. A client-mode endpoint that signs a content type must return it the same way; see S3 Presign Responses.
  • The URL changed after signing. A presigned URL is bound to its host and path, so moving a path-style URL onto the CDN hostname, or rewriting it into virtual-hosted style, breaks the signature.

Other 403 errors on the upload

  • The key lacks write access. A Limited access key with only Read permission on the bucket cannot upload; it needs Read/Write/Delete.
  • The presigned URL expired before the upload started. Keep expiresIn longer than the time between signing and uploading, and keep the server's clock synced: presigned URLs are time-scoped.

Uploaded files return 403 when opened

Files in a Space are private by default, and @useupup/server uploads without an ACL, so a plain https://my-space.nyc3.digitaloceanspaces.com/<key> link returns 403. Either:

  • Share them through presigned download URLs. @useupup/server returns a signed downloadUrl with each upload, valid for downloadUrlExpiresIn seconds (three days by default).
  • Make them public: in the control panel, open the Files tab, then the file's … menu → Manage Permissions → Public, or run s3cmd setacl s3://my-space/<key> --acl-public.

The bucket's File Listing setting is separate. It controls whether anyone can list the Space's keys, not whether a file can be read.

The CDN serves an old file

The CDN keeps each file for the edge cache TTL, one hour by default. @useupup/server's default key layout puts a random UUID in every key, so a new upload never replaces a cached file. Stale content comes from objects you overwrite at the same key, or from responses cached before a CORS change. Purge them in the control panel (the bucket's Settings tab → CDN → Purge CDN Cache, or a file's … menu → Purge from CDN cache) or with doctl:

sh
doctl compute cdn flush <cdn-id> --files /path/to/file.jpg

doctl authenticates with a DigitalOcean API token, not a Spaces key.

FAQ

Can I upload files to DigitalOcean Spaces directly from the browser?

Yes. Your server signs a short-lived presigned URL with a Spaces access key, and the browser sends the file straight to that URL. The key never reaches the browser. The Space needs a CORS rule that allows your origin, the PUT method and the Content-Type header.

Does DigitalOcean Spaces support multipart uploads?

Yes. Spaces implements the S3 multipart API: parts of at least 5 MiB (except the last) and up to 5 GB, up to 10,000 parts, and up to 5 TB per object. A single PUT is limited to 5 GB. In upup, multipart runs in server mode with resumable: { protocol: 'multipart' }, and the CORS rule must expose the ETag header.

How large a file can I upload in the Spaces control panel?

The control panel uploads files smaller than 2 GB, in batches of fewer than 100 files. For larger files or batches, use an S3-compatible tool such as s3cmd or the AWS CLI, or the Spaces API with multipart uploads.

Are files uploaded to a Space public?

No. Files in a Space are private by default, and @useupup/server uploads them without an ACL, so they stay private. Share them through presigned download URLs, or make individual files public in the control panel or with s3cmd setacl.

Can I upload through the Spaces CDN endpoint?

Upload to the origin endpoint, such as nyc3.digitaloceanspaces.com, and use the CDN hostname for reads. Requests with presigned URLs are never cached by the CDN. DigitalOcean also caps PUT and multipart requests sent to the CDN with presigned URLs at 8100 KiB.

Next steps