import { createS3Storage } from '@mohamedhabibwork/storagekit/s3'
Backed by the official AWS SDK v3: @aws-sdk/client-s3,
@aws-sdk/lib-storage (multipart), @aws-sdk/s3-request-presigner.
The SDKs are optional peer dependencies and load lazily.
npm install @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner
| Option | Type | Description |
|---|---|---|
bucket |
string |
Required. Bucket name |
region |
string |
AWS region (falls back to the SDK’s region chain) |
endpoint |
string |
Custom S3-compatible endpoint (LocalStack, MinIO, …) |
credentials |
{ accessKeyId, secretAccessKey, sessionToken? } or provider |
Explicit credentials; omit to use the AWS default chain (env, shared config, IAM roles) |
forcePathStyle |
boolean |
Path-style URLs — required for most S3-compatible servers |
prefix |
string |
Virtual prefix for every key (e.g. 'production/') |
publicUrlBase |
string |
CDN base URL used by getUrl() |
client |
S3Client |
Inject an existing client (DI/testing) |
clientOptions |
Partial<S3ClientConfig> |
Forwarded to new S3Client(...) — retries (maxAttempts), timeouts, custom agents |
const storage = await createStorage({
type: 's3',
bucket: 'uploads',
region: 'eu-central-1',
clientOptions: { maxAttempts: 5 }, // native SDK retry policy (§ retries)
});
Uses Upload from @aws-sdk/lib-storage for every body: streams are
chunked into parts (no full-file buffering), buffers/strings upload in a
single part.
await storage.upload('videos/movie.mp4', stream, {
contentType: 'video/mp4',
multipart: { partSize: 10 * 1024 * 1024, concurrency: 4 }, // partSize / queueSize
native: {
StorageClass: 'INTELLIGENT_TIERING',
ServerSideEncryption: 'aws:kms',
SSEKMSKeyId: 'arn:aws:kms:…',
ACL: 'private',
Tagging: 'team=media',
},
});
native accepts any PutObjectCommandInput field the package does not map
(StorageClass, ServerSideEncryption, SSEKMSKeyId, ACL, Tagging,
ChecksumAlgorithm, IfNoneMatch, …) and is merged last.
GetObjectCommand → the response Body becomes a Node Readable.
const dl = await storage.download('docs/report.pdf', {
versionId: 'v123', // versioned buckets
range: { offset: 1024, length: 2048 }, // → Range header
native: { ResponseCacheControl: 'no-cache' }, // any GetObjectCommandInput field
});
stat uses HeadObjectCommand, exists a HEAD with 404 → false.
delete sends DeleteObjectCommand (idempotent) and accepts versionId.
deleteMany batches up to 1000 keys per DeleteObjectsCommand call and
reports per-key failures from the response Errors.
ListObjectsV2Command — one level by default (Delimiter: '/',
CommonPrefixes → directories with trailing slashes), flat when
recursive: true. The native NextContinuationToken is the package cursor.
await storage.list({ prefix: 'users/100/', limit: 100, native: { FetchOwner: true } });
copy uses CopyObject (server-side, same bucket or same account).
Passing any of contentType/metadata/cacheControl/contentDisposition/
contentEncoding sets MetadataDirective: 'REPLACE'. native accepts
CopySourceIfMatch, CopySourceIfNoneMatch, RequestPayer, etc.
move = copy + delete. Cross-account copies need the source object to
be readable — use nativeRequest() with a full CopyObject if you need a
CopySource in another bucket.
// priority: publicUrlBase > endpoint (path-style) > virtual-hosted AWS URL
await storage.getUrl('images/logo.png');
// https://uploads.s3.eu-central-1.amazonaws.com/images/logo.png
await storage.getSignedUrl('private.pdf', { expiresIn: 3600, action: 'read',
native: { ResponseContentDisposition: 'attachment; filename="invoice.pdf"' } });
await storage.getSignedUrl('upload.bin', { action: 'write', expiresIn: 900 });
await storage.getSignedUrl('stale.bin', { action: 'delete', expiresIn: 300 });
Signed URLs sign GetObjectCommand/PutObjectCommand/DeleteObjectCommand;
any command-input field can ride along in native (e.g. VersionId,
ResponseContentType). expiresIn: 1–604800 seconds.
signedUrls: true, multipartUpload: true, serverSideCopy: true,
versioning: true, metadata: true, directories: false,
bulkDelete: true.
docker run -p 4566:4566 localstack/localstack -e SERVICES=s3
aws --endpoint-url=http://localhost:4566 s3api create-bucket --bucket test-bucket
S3_TEST_BUCKET=test-bucket S3_TEST_ENDPOINT=http://localhost:4566 \
S3_TEST_REGION=us-east-1 AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test \
npx vitest run tests/integrations/s3.test.ts
The contract suite (tests/integrations/s3.test.ts) runs the full shared
driver contract against any S3-compatible endpoint — it also passes against
MinIO.