Storage

Code examples

The examples use the client set up in Connect applications. They work as they do on S3: if you already have S3 code, just change the endpoint and the region.

Metadata

Metadata are name and value pairs stored with the file, such as who uploaded it or its original name. Use values without accents.

import { HeadObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'

await client.send(new PutObjectCommand({
  Bucket: 'contracts',
  Key: '2026/contract.pdf',
  Body: file,
  ContentType: 'application/pdf',
  Metadata: { 'uploaded-by': 'maria', 'original-name': 'contract-2026.pdf' },
}))

const { Metadata, ContentType } = await client.send(new HeadObjectCommand({ Bucket: 'contracts', Key: '2026/contract.pdf' }))
client.put_object(Bucket='contracts', Key='2026/contract.pdf', Body=data, ContentType='application/pdf', Metadata={'uploaded-by': 'maria'})
metadata = client.head_object(Bucket='contracts', Key='2026/contract.pdf')['Metadata']

List files

Each response returns up to 1,000 files. Use the SDK paginator to go through all of them:

import { paginateListObjectsV2 } from '@aws-sdk/client-s3'

for await (const page of paginateListObjectsV2({ client }, { Bucket: 'contracts', Prefix: '2026/' })) {
  for (const file of page.Contents ?? []) console.log(file.Key, file.Size)
}
for page in client.get_paginator('list_objects_v2').paginate(Bucket='contracts', Prefix='2026/'):
    for file in page.get('Contents', []):
        print(file['Key'], file['Size'])

Delete files

Deleting requires a key with Full access. DeleteObjects deletes up to 1,000 files per request.

import { DeleteObjectCommand, DeleteObjectsCommand } from '@aws-sdk/client-s3'

await client.send(new DeleteObjectCommand({ Bucket: 'contracts', Key: '2026/draft.pdf' }))
await client.send(new DeleteObjectsCommand({
  Bucket: 'contracts',
  Delete: { Objects: [{ Key: '2026/a.pdf' }, { Key: '2026/b.pdf' }] },
}))
client.delete_object(Bucket='contracts', Key='2026/draft.pdf')
client.delete_objects(Bucket='contracts', Delete={'Objects': [{'Key': '2026/a.pdf'}, {'Key': '2026/b.pdf'}]})

Handle errors

SDKs return the Storage error code, the same as S3. The code table is in Connect applications.

import { GetObjectCommand, NoSuchKey, S3ServiceException } from '@aws-sdk/client-s3'

try {
  await client.send(new GetObjectCommand({ Bucket: 'contracts', Key: '2026/missing.pdf' }))
} catch (error) {
  if (error instanceof NoSuchKey) {
    // the file does not exist
  } else if (error instanceof S3ServiceException) {
    console.error(error.name, error.$metadata.httpStatusCode)
  } else {
    throw error
  }
}
from botocore.exceptions import ClientError

try:
    client.get_object(Bucket='contracts', Key='2026/missing.pdf')
except client.exceptions.NoSuchKey:
    pass  # the file does not exist
except ClientError as error:
    print(error.response['Error']['Code'])

Upload straight from the browser

The user's browser sends the file straight to Storage, with an upload link your application creates. The file does not go through your server, the key never reaches the browser and the link only allows uploading that file, for a short time.

1. Allow your site's domain on the bucket

Once per bucket, with a Full key, add a CORS rule for your site's domain. The bucket already has a rule used by the console: read the current rules and add yours, instead of replacing them.

import { GetBucketCorsCommand, PutBucketCorsCommand } from '@aws-sdk/client-s3'

const current = await client.send(new GetBucketCorsCommand({ Bucket: 'photos' }))
  .then((response) => response.CORSRules ?? [])
  .catch((error) => { if (error.name === 'NoSuchCORSConfiguration') return []; throw error })

const mySite = {
  ID: 'my-site',
  AllowedOrigins: ['https://www.mysite.com'],
  AllowedMethods: ['PUT', 'GET'],
  AllowedHeaders: ['*'],
  ExposeHeaders: ['ETag'],
  MaxAgeSeconds: 3600,
}

await client.send(new PutBucketCorsCommand({
  Bucket: 'photos',
  CORSConfiguration: { CORSRules: [...current.filter((rule) => rule.ID !== mySite.ID), mySite] },
}))

The route checks who is uploading, picks the file name and returns the link. The link is valid for the given file type; the browser must send the same Content-Type.

In JavaScript, create the client that signs links with requestChecksumCalculation: 'WHEN_REQUIRED'. Without this option, recent SDK versions put in the link a checksum computed over an empty body, which does not match the file uploaded later.

import { PutObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'

const url = await getSignedUrl(client, new PutObjectCommand({
  Bucket: 'photos',
  Key: `users/${userId}/${crypto.randomUUID()}.png`,
  ContentType: 'image/png',
}), { expiresIn: 900 })
url = client.generate_presigned_url(
    'put_object',
    Params={'Bucket': 'photos', 'Key': f'users/{user_id}/{uuid.uuid4()}.png', 'ContentType': 'image/png'},
    ExpiresIn=900,
)

3. Upload from the browser

const { url } = await fetch('/api/upload-links', { method: 'POST' }).then((response) => response.json())

const response = await fetch(url, { method: 'PUT', headers: { 'Content-Type': file.type }, body: file })
if (!response.ok) throw new Error(`Upload refused: ${response.status}`)

To check size and type on the server side, inspect the file with HeadObject after the upload and before using it.

Large files: multipart upload from the browser

For large files, the browser splits the file into parts and sends each one with its own link. The application opens the upload, creates one link per part and, at the end, joins the parts. Each part has at least 5 MB (the last one can be smaller), and an upload has up to 10,000 parts; the limit is 20 GB per file. If the connection drops, only the interrupted part needs to be sent again.

On the server, open the upload and create the part links:

import { CompleteMultipartUploadCommand, CreateMultipartUploadCommand, S3Client, UploadPartCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'

const signer = new S3Client({
  endpoint: 'https://armazem.zenifra.com',
  region: 'br-sp1',
  credentials: { accessKeyId: process.env.ACCESS_KEY_ID!, secretAccessKey: process.env.SECRET_ACCESS_KEY! },
  requestChecksumCalculation: 'WHEN_REQUIRED',
})

const { UploadId } = await signer.send(new CreateMultipartUploadCommand({ Bucket: 'videos', Key: key, ContentType: 'video/mp4' }))
const links = await Promise.all(Array.from({ length: partCount }, (_, i) =>
  getSignedUrl(signer, new UploadPartCommand({ Bucket: 'videos', Key: key, UploadId, PartNumber: i + 1 }), { expiresIn: 3600 })))

In the browser, upload the parts and keep each part's ETag. The bucket's CORS rule must expose the ETag (ExposeHeaders: ['ETag'], as in step 1):

const PART_SIZE = 50 * 1024 * 1024
const parts = await Promise.all(links.map(async (link, i) => {
  const response = await fetch(link, { method: 'PUT', body: file.slice(i * PART_SIZE, (i + 1) * PART_SIZE) })
  if (!response.ok) throw new Error(`Part ${i + 1} refused: ${response.status}`)
  return { PartNumber: i + 1, ETag: response.headers.get('ETag') }
}))

For many parts, limit how many are sent at once (three or four are usually enough).

Back on the server, join the parts:

await signer.send(new CompleteMultipartUploadCommand({ Bucket: 'videos', Key: key, UploadId, MultipartUpload: { Parts: parts } }))

Uploaded parts stay stored until the upload is completed or aborted. To resume an upload, ListParts shows the parts that already arrived; to give up, call AbortMultipartUpload, which deletes the parts. ListMultipartUploads lists the bucket's open uploads.

File versions

There are two ways to keep a file's history.

One file per version

Save each version under its own name, for example documents/notice/2026-11-03T10-00-00.pdf, and list the prefix to see the history. It works with Read and upload keys and needs no bucket setting.

Bucket versioning

With versioning on, each upload with the same name keeps the previous version, and each version gets a VersionId. Turning it on and off requires a Full key.

import { GetObjectCommand, ListObjectVersionsCommand, PutBucketVersioningCommand } from '@aws-sdk/client-s3'

await client.send(new PutBucketVersioningCommand({ Bucket: 'documents', VersioningConfiguration: { Status: 'Enabled' } }))

const { Versions } = await client.send(new ListObjectVersionsCommand({ Bucket: 'documents', Prefix: 'notice.pdf' }))
const previous = Versions.find((version) => !version.IsLatest)
const file = await client.send(new GetObjectCommand({ Bucket: 'documents', Key: 'notice.pdf', VersionId: previous.VersionId }))

Before turning it on, keep in mind that:

  • old versions count toward billed storage until they are deleted;
  • deleting a file without a VersionId only hides it: the versions stay stored. To free the space, delete each version with DeleteObject and its VersionId;
  • the console shows only the latest version of each file;
  • once on, versioning cannot go back to off, only to suspended (Status: 'Suspended'): new uploads stop creating versions, and the stored ones remain.

Next steps

Last updated on

On this page