@occupop/lib-s3-storage
Version: 
Tiny S3 storage helper (AWS & LocalStack) with injectable bucket, signed URLs and public URLs.
1 lines • 9.96 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/s3-storage.ts"],"sourcesContent":["export * from './s3-storage'\n","import {GetObjectCommand, PutObjectCommand, PutObjectCommandInput, S3Client, S3ClientConfig,} from '@aws-sdk/client-s3'\nimport {getSignedUrl as presign} from '@aws-sdk/s3-request-presigner'\nimport crc32 from 'crc-32'\n\nexport type PublicUrlMode = 'raw' | 'signed'\n\nexport type S3StorageConfig = {\n    region: string\n    credentials: { accessKeyId: string; secretAccessKey: string }\n    endpoint?: string\n    accelerate?: boolean\n    defaultBucket?: string\n    defaultExpiresInSeconds?: number\n    publicUrlMode?: PublicUrlMode\n    publicAcl?: boolean\n    cacheControl?: string\n    httpConnectionTimeoutMs?: number\n    forcePathStyle?: boolean\n}\n\nexport type SignedUrlResult = {\n    url: string\n    bucket: string\n    key: string\n    expiresIn: number\n}\n\nexport type PutObjectResult = {\n    url: string\n    bucket: string\n    key: string\n}\n\nexport type PublicUrlInput = {\n    path: string\n    bucket?: string\n    mode?: PublicUrlMode\n    expiresInSeconds?: number\n}\n\nexport type SignedPutInput = {\n    path: string\n    bucket?: string\n    contentType?: string\n    fileName?: string\n    expiresInSeconds?: number\n    checksumCRC32Base64?: string\n}\n\nexport type PutObjectInput = {\n    path: string\n    bucket?: string\n    contentType: string\n    fileName?: string\n    bodyBase64: string\n    cacheControl?: string\n}\n\nexport type S3Storage = {\n    getSignedPutUrl: (input: SignedPutInput) => Promise<SignedUrlResult>\n    putObject: (input: PutObjectInput) => Promise<PutObjectResult>\n    getPublicUrl: (input: PublicUrlInput) => Promise<string>\n    getSignedGetUrl: (input: { path: string; bucket?: string; expiresInSeconds?: number }) => Promise<SignedUrlResult>\n}\n\nexport const createS3Storage = (cfg: S3StorageConfig): S3Storage => {\n    const isLocal = !!cfg.endpoint && /localstack/i.test(cfg.endpoint)\n    const accelerate = !isLocal && !!cfg.accelerate\n    const region = cfg.region\n    const defaultBucket = cfg.defaultBucket ?? ''\n    const defaultExpires = cfg.defaultExpiresInSeconds ?? 180\n    const publicMode = cfg.publicUrlMode ?? 'raw'\n    const publicAcl = !!cfg.publicAcl\n    const cacheControl = cfg.cacheControl ?? 'private, max-age=0, no-cache'\n\n    const client = new S3Client({\n        region: cfg.region,\n        credentials: cfg.credentials,\n        ...(cfg.endpoint && {endpoint: cfg.endpoint}),\n        ...(typeof cfg.forcePathStyle !== 'undefined' && {forcePathStyle: cfg.forcePathStyle}),\n        ...(cfg.accelerate && {useAccelerateEndpoint: true}),\n    } as any)\n\n    const pickBucket = (override?: string): string => {\n        const b = (override ?? defaultBucket).trim()\n        if (!b) throw new Error('Bucket is required')\n        return b\n    }\n\n    const normalizeKey = (bucket: string, raw: string): string =>\n        raw.replace(/^\\/+/, '').replace(/\\/{2,}/g, '/').replace(`${bucket}/`, '')\n\n    const encodeKeyPath = (k: string): string =>\n        k.split('/').map(encodeURIComponent).join('/')\n\n    const buildRawUrl = (bucket: string, key: string): string => {\n        const encoded = encodeKeyPath(key)\n        if (isLocal && cfg.endpoint) {\n            return `${cfg.endpoint.replace(/\\/$/, '')}/${bucket}/${encoded}`\n        }\n        if (accelerate) {\n            return `https://${bucket}.s3-accelerate.amazonaws.com/${encoded}`\n        }\n        return `https://${bucket}.s3.${region}.amazonaws.com/${encoded}`\n    }\n\n    const crc32Base64 = (buf: Buffer): string => {\n        const signed = crc32.buf(buf) >>> 0\n        const b = Buffer.alloc(4)\n        b.writeUInt32BE(signed, 0)\n        return b.toString('base64')\n    }\n\n    const contentDisposition = (fileName?: string): string | undefined =>\n        fileName ? `inline; filename=\"${encodeURIComponent(fileName)}\"` : undefined\n\n    const getSignedPutUrl = async (input: SignedPutInput): Promise<SignedUrlResult> => {\n        const bucket = pickBucket(input.bucket)\n        const key = normalizeKey(bucket, input.path)\n        const expiresIn = input.expiresInSeconds ?? defaultExpires\n\n        const cmd = new PutObjectCommand({\n            Bucket: bucket,\n            Key: key,\n            ContentType: input.contentType,\n            // ContentDisposition: contentDisposition(input.fileName),\n            ...(input.checksumCRC32Base64 ? {ChecksumCRC32: input.checksumCRC32Base64} : {}),\n        })\n\n        const url = await presign(client, cmd, {expiresIn})\n        return {url, bucket, key, expiresIn}\n    }\n\n    const putObject = async (input: PutObjectInput): Promise<PutObjectResult> => {\n        const bucket = pickBucket(input.bucket)\n        const key = normalizeKey(bucket, input.path)\n        const body = Buffer.from(input.bodyBase64, 'base64')\n        const checksum = crc32Base64(body)\n\n        const commandInput: PutObjectCommandInput = {\n            Bucket: bucket,\n            Key: key,\n            Body: body,\n            ContentType: input.contentType,\n            // ContentDisposition: contentDisposition(input.fileName),\n            CacheControl: input.cacheControl ?? cacheControl,\n            ChecksumCRC32: checksum,\n            ...(publicAcl ? {ACL: 'public-read'} : {}),\n        }\n\n        const command = new PutObjectCommand(commandInput) as any\n\n        await client.send(command)\n\n        const url = publicMode === 'signed'\n            ? (await getSignedGetUrl({path: key, bucket})).url\n            : buildRawUrl(bucket, key)\n\n        return {url, bucket, key}\n    }\n\n    const getSignedGetUrl = async ({\n                                       path,\n                                       bucket,\n                                       expiresInSeconds,\n                                   }: {\n        path: string\n        bucket?: string\n        expiresInSeconds?: number\n    }): Promise<SignedUrlResult> => {\n        const b = pickBucket(bucket)\n        const key = normalizeKey(b, path)\n        const expiresIn = expiresInSeconds ?? defaultExpires\n        const cmd = new GetObjectCommand({Bucket: b, Key: key})\n        const url = await presign(client, cmd, {expiresIn})\n        return {url, bucket: b, key, expiresIn}\n    }\n\n    const getPublicUrl = async (input: PublicUrlInput): Promise<string> => {\n        const bucket = pickBucket(input.bucket)\n        const key = normalizeKey(bucket, input.path)\n        const mode = input.mode ?? publicMode\n        if (mode === 'signed') {\n            return (await getSignedGetUrl({path: key, bucket, expiresInSeconds: input.expiresInSeconds ?? 300})).url\n        }\n        return buildRawUrl(bucket, key)\n    }\n\n    return {\n        getSignedPutUrl,\n        putObject,\n        getPublicUrl,\n        getSignedGetUrl,\n    }\n}\n"],"mappings":"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","names":["crc32","presign"]}