UNPKG

@rytass/storages-adapter-r2

Version:

Cloudflare R2 storage adapter

186 lines (183 loc) 6.24 kB
import { Storage, StorageError, ErrorCode } from '@rytass/storages'; import { S3Client, GetObjectCommand, CopyObjectCommand, DeleteObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3'; import { Upload } from '@aws-sdk/lib-storage'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { PassThrough } from 'stream'; import { v4 } from 'uuid'; class StorageR2Service extends Storage { bucket; client; parseSignedURL; constructor(options){ super(options); this.bucket = options.bucket; if (options.customDomain) { const re = new RegExp(`^https://${options.bucket}.${options.account}.r2.cloudflarestorage.com`); this.parseSignedURL = (url)=>{ return url.replace(re, options.customDomain); }; } this.client = new S3Client({ endpoint: `https://${options.account}.r2.cloudflarestorage.com`, credentials: { accessKeyId: options.accessKey, secretAccessKey: options.secretKey }, region: 'auto', forcePathStyle: true }); } async url(key, options) { const command = new GetObjectCommand({ Bucket: this.bucket, Key: key }); const signedURL = await getSignedUrl(this.client, command, { ...options?.expires ? { expiresIn: options.expires } : {} }); if (this.parseSignedURL) { return this.parseSignedURL(signedURL); } return signedURL; } async read(key, options) { try { const command = new GetObjectCommand({ Bucket: this.bucket, Key: key }); const response = await this.client.send(command); if (!response.Body) { throw new StorageError(ErrorCode.READ_FILE_ERROR, 'Empty response body'); } // AWS SDK v3 returns a ReadableStream (web) or Readable (node) const bodyStream = response.Body; if (options?.format === 'buffer') { const chunks = []; for await (const chunk of bodyStream){ chunks.push(Buffer.from(chunk)); } return Buffer.concat(chunks); } return bodyStream; } catch (ex) { if (ex && typeof ex === 'object' && 'name' in ex && ex.name === 'NoSuchKey') { throw new StorageError(ErrorCode.READ_FILE_ERROR, 'File not found'); } throw ex; } } async writeStreamFile(stream, options) { const givenFilename = options?.filename; if (givenFilename) { const upload = new Upload({ client: this.client, params: { Bucket: this.bucket, Key: givenFilename, Body: stream, ...options?.contentType ? { ContentType: options?.contentType } : {} } }); await upload.done(); return { key: givenFilename }; } const tempFilename = v4(); const uploadStream = new PassThrough(); const getFilenamePromise = this.getStreamFilename(stream); const upload = new Upload({ client: this.client, params: { Bucket: this.bucket, Key: tempFilename, Body: uploadStream, ...options?.contentType ? { ContentType: options?.contentType } : {} } }); stream.pipe(uploadStream); const [[filename, mime]] = await Promise.all([ getFilenamePromise, upload.done() ]); const copyCommand = new CopyObjectCommand({ Bucket: this.bucket, CopySource: `/${this.bucket}/${tempFilename}`, Key: filename, ...mime ? { ContentType: mime } : {}, ...options?.contentType ? { ContentType: options?.contentType } : {} }); await this.client.send(copyCommand); const deleteCommand = new DeleteObjectCommand({ Bucket: this.bucket, Key: tempFilename }); await this.client.send(deleteCommand); return { key: filename }; } async writeBufferFile(buffer, options) { const fileInfo = options?.filename || await this.getBufferFilename(buffer); const filename = Array.isArray(fileInfo) ? fileInfo[0] : fileInfo; const upload = new Upload({ client: this.client, params: { Key: filename, Bucket: this.bucket, Body: buffer, ...Array.isArray(fileInfo) && fileInfo[1] ? { ContentType: fileInfo[1] } : {}, ...options?.contentType ? { ContentType: options?.contentType } : {} } }); await upload.done(); return { key: filename }; } write(file, options) { if (file instanceof Buffer) { return this.writeBufferFile(file, options); } return this.writeStreamFile(file, options); } batchWrite(files) { return Promise.all(files.map((file)=>this.write(file))); } async remove(key) { const command = new DeleteObjectCommand({ Bucket: this.bucket, Key: key }); await this.client.send(command); } async isExists(key) { try { const command = new HeadObjectCommand({ Bucket: this.bucket, Key: key }); await this.client.send(command); return true; } catch (ex) { if (ex && typeof ex === 'object' && 'name' in ex && ex.name === 'NotFound') return false; throw ex; } } } export { StorageR2Service };