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] },
}))2. Create the upload link on the server
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
VersionIdonly hides it: the versions stay stored. To free the space, delete each version withDeleteObjectand itsVersionId; - 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
Connect applications to Storage
Set up the AWS CLI, JavaScript, Python and Django to use Zenifra Storage with an access key, multipart uploads and expiring signed links.
Storage limits and billing
Storage limits for buckets, total space, file size and traffic per key, how to ask for more, prices and how usage is measured.