storagekit

AWS S3 Driver

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

Config

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)
});

Upload

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.

Download

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 / exists / delete

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.

List

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 / move

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.

URLs

// 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.

Capabilities

signedUrls: true, multipartUpload: true, serverSideCopy: true, versioning: true, metadata: true, directories: false, bulkDelete: true.

Testing against LocalStack

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.