Storage interface, types, and helper utilities for portable file storage across R2, S3, and local filesystem.
Works in any JavaScript runtime (Cloudflare Workers, Node.js, Deno, Bun, browsers).
npm install @arraypress/storageThis package defines the Storage interface that all adapter packages implement. You typically don't use this package directly -- instead, install one of the adapters:
| Adapter | Backend |
|---|---|
@arraypress/storage-r2 |
Cloudflare R2 native bindings |
@arraypress/storage-s3 |
AWS S3, R2 via S3 API, MinIO |
@arraypress/storage-local |
Local filesystem (Node.js) |
Every adapter returns an object implementing this interface:
interface Storage {
upload(options: UploadOptions): Promise<UploadResult>;
download(key: string): Promise<DownloadResult | null>;
delete(key: string): Promise<void>;
exists(key: string): Promise<boolean>;
list(options?: ListOptions): Promise<ListResult>;
getSignedDownloadUrl(options: SignedUrlOptions): Promise<SignedUrl>;
getSignedUploadUrl(options: SignedUploadUrlOptions): Promise<SignedUrl>;
getPublicUrl(key: string): string;
createMultipartUpload(key: string, options?: MultipartUploadOptions): Promise<MultipartUpload>;
resumeMultipartUpload(key: string, uploadId: string): MultipartUpload;
}The package also exports helper utilities for common storage operations:
import { contentHash, contentAddressedKey, safeDisposition, StorageError } from '@arraypress/storage';
// SHA-256 content hashing for deduplication
const hash = await contentHash(fileBuffer);
// => 'a1b2c3d4e5f6...'
// Generate content-addressed storage keys
const key = contentAddressedKey(hash, 'photo.jpg', 'media/');
// => 'media/a1b2c3d4e5f6.jpg'
// Determine safe Content-Disposition for downloads
safeDisposition('image/jpeg'); // => 'inline'
safeDisposition('image/svg+xml'); // => 'attachment' (XSS risk)
safeDisposition('application/pdf'); // => 'inline'
safeDisposition('text/html'); // => 'attachment'
// Typed storage errors
throw new StorageError('File not found', 'NOT_FOUND');Combine contentHash and contentAddressedKey to skip duplicate uploads:
import { contentHash, contentAddressedKey } from '@arraypress/storage';
async function uploadDeduped(storage, file, originalName) {
const hash = await contentHash(file);
const key = contentAddressedKey(hash, originalName, 'uploads/');
// Skip upload if identical file already exists
if (await storage.exists(key)) {
return { key, skipped: true };
}
const result = await storage.upload({
key,
body: file,
contentType: 'application/octet-stream',
});
return { key: result.key, skipped: false };
}Storage-- Main storage interface implemented by all adaptersUploadOptions-- Options forupload():key,body,contentType,metadata?UploadResult-- Result fromupload():key,size,etag?DownloadResult-- Result fromdownload():body,contentType,size,etag?ListOptions-- Options forlist():prefix?,limit?,cursor?ListResult-- Result fromlist():objects,truncated,cursor?ListObject-- Individual object in list:key,size,lastModified?,etag?SignedUrlOptions-- Options for signed URLs:key,expiresIn?SignedUploadUrlOptions-- ExtendsSignedUrlOptionswithcontentTypeSignedUrl-- Signed URL result:url,expiresAtMultipartUploadOptions-- Options for multipart:contentType?,metadata?MultipartUpload-- Multipart upload handle:uploadId,key,uploadPart(),complete(),abort()MultipartPart-- Completed part:partNumber,etagStorageErrorCode--'NOT_FOUND' | 'NOT_SUPPORTED' | 'PERMISSION_DENIED' | 'ALREADY_EXISTS' | 'UNKNOWN'
StorageError-- Error class with acodeproperty (StorageErrorCode)
contentHash(body)-- Compute SHA-256 hash of a body. Returns lowercase hex string. Uses Web Crypto API.contentAddressedKey(hash, originalName, prefix?)-- Generate a storage key from a hash and original filename, preserving the file extension.safeDisposition(mimeType)-- Returns'inline'for safe types (images, audio, video, PDF) or'attachment'for everything else (SVG, HTML, etc.).
MIT