UNPKG

adonis-drive-r2

Version:
338 lines (337 loc) 12 kB
"use strict"; /* * adonis-drive-r2 * * (c) Ndianabasi Udonkang <ndianabasi@gotedo.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.R2Driver = void 0; const get_stream_1 = __importDefault(require("get-stream")); const lib_storage_1 = require("@aws-sdk/lib-storage"); const helpers_js_1 = require("@poppinss/utils/build/helpers.js"); const s3_request_presigner_1 = require("@aws-sdk/s3-request-presigner"); const standalone_1 = require("@adonisjs/core/build/standalone"); const client_s3_1 = require("@aws-sdk/client-s3"); /** * An implementation of the s3 driver for AdonisJS drive */ class R2Driver { constructor(config, logger) { this.config = config; this.logger = logger; /** * Name of the driver */ this.name = 'r2'; if (this.config.visibility === 'public' && !this.config.cdnUrl) { throw new Error("The bucket's public URL is required when the bucket is public"); } /** * Use the top level key and secret to define AWS credentials */ if (this.config.key && this.config.secret) { this.config.credentials = { accessKeyId: this.config.key, secretAccessKey: this.config.secret, }; } /** * Allow override of the Cloudflare R2 endpoint, especially for development * purposes with services like Localstack. */ this.config.endpoint = this.config.endpoint || `https://${this.config.accountId}.r2.cloudflarestorage.com`; /** * Allow override of the Cloudflare R2 region, especially for development * purposes with services like Localstack. */ this.config.region = this.config.region || 'auto'; this.adapter = new client_s3_1.S3Client(this.config); } /** * Transforms the write options to S3 properties */ transformWriteOptions(options) { const { visibility, contentType, contentDisposition, contentEncoding, contentLanguage, contentLength, cacheControl, ...adapterOptions } = Object.assign({ visibility: this.config.visibility }, options); if (contentLength) { adapterOptions['ContentLength'] = contentLength; } if (contentType) { adapterOptions['ContentType'] = contentType; } if (contentDisposition) { adapterOptions['ContentDisposition'] = contentDisposition; } if (contentEncoding) { adapterOptions['ContentEncoding'] = contentEncoding; } if (contentLanguage) { adapterOptions['ContentLanguage'] = contentLanguage; } if (cacheControl) { adapterOptions['CacheControl'] = cacheControl; } if (visibility === 'public') { adapterOptions.ACL = 'public-read'; } else if (visibility === 'private') { adapterOptions.ACL = 'private'; } this.logger.trace(adapterOptions, '@adonis-drive-r2 write options'); return adapterOptions; } /** * Transform content headers to S3 response content type */ transformContentHeaders(options) { const contentHeaders = {}; const { contentType, contentDisposition, contentEncoding, contentLanguage, cacheControl } = options || {}; if (contentType) { contentHeaders['ResponseContentType'] = contentType; } if (contentDisposition) { contentHeaders['ResponseContentDisposition'] = contentDisposition; } if (contentEncoding) { contentHeaders['ResponseContentEncoding'] = contentEncoding; } if (contentLanguage) { contentHeaders['ResponseContentLanguage'] = contentLanguage; } if (cacheControl) { contentHeaders['ResponseCacheControl'] = cacheControl; } this.logger.trace(contentHeaders, '@adonis-drive-r2 content headers'); return contentHeaders; } /** * Set a new bucket at runtime and return a new driver instance */ bucket(bucket) { return new R2Driver(Object.assign({}, this.config, { bucket }), this.logger); } /** * Set a new public url at runtime and return a new driver instance */ publicUrl(url) { return new R2Driver(Object.assign({}, this.config, { cdnUrl: url }), this.logger); } /** * Returns the file contents as a buffer. The buffer return * value allows you to self choose the encoding when * converting the buffer to a string. */ async get(location) { return get_stream_1.default.buffer(await this.getStream(location)); } /** * Returns the file contents as a stream */ async getStream(location) { try { const response = await this.adapter.send(new client_s3_1.GetObjectCommand({ Key: location, Bucket: this.config.bucket })); /** * The value as per the SDK can be a blob, NodeJS.ReadableStream or Readable stream. * However, at runtime it is always a readable stream. * * There is an open issue on the same https://github.com/aws/aws-sdk-js-v3/issues/3064 */ return response.Body; } catch (error) { throw standalone_1.CannotReadFileException.invoke(location, error); } } /** * A boolean to find if the location path exists or not */ async exists(location) { try { await this.adapter.send(new client_s3_1.HeadObjectCommand({ Key: location, Bucket: this.config.bucket, })); return true; } catch (error) { if (error.$metadata?.httpStatusCode === 404) { return false; } throw standalone_1.CannotGetMetaDataException.invoke(location, 'exists', error); } } /** * Get the visibility for the bucket. * For R2, visibility is set at the bucket level alone. */ async getVisibility() { return this.config.visibility; } /** * Returns the file stats */ async getStats(location) { try { const stats = await this.adapter.send(new client_s3_1.HeadObjectCommand({ Key: location, Bucket: this.config.bucket, })); return { modified: stats.LastModified, size: stats.ContentLength, isFile: true, etag: stats.ETag, }; } catch (error) { throw standalone_1.CannotGetMetaDataException.invoke(location, 'stats', error); } } /** * Returns the signed url for a given path */ async getSignedUrl(location, options) { try { return await (0, s3_request_presigner_1.getSignedUrl)(this.adapter, new client_s3_1.GetObjectCommand({ Key: location, Bucket: this.config.bucket, ...this.transformContentHeaders(options), }), { expiresIn: helpers_js_1.string.toMs(options?.expiresIn || '15min') / 1000, }); } catch (error) { throw standalone_1.CannotGetMetaDataException.invoke(location, 'signedUrl', error); } } /** * Returns URL to a given path */ async getUrl(location) { /** * Use the CDN URL if defined */ if (this.config.cdnUrl) { return Promise.resolve(`${this.config.cdnUrl}/${location}`); } return Promise.resolve(`https://${this.config.bucket}.${this.config.accountId}.r2.cloudflarestorage.com/${location}`); } /** * Write string|buffer contents to a destination. The missing * intermediate directories will be created (if required). */ async put(location, contents, options) { try { await this.adapter.send(new client_s3_1.PutObjectCommand({ Key: location, Body: contents, Bucket: this.config.bucket, ...this.transformWriteOptions(options), })); } catch (error) { throw standalone_1.CannotWriteFileException.invoke(location, error); } } /** * Write a stream to a destination. The missing intermediate * directories will be created (if required). */ async putStream(location, contents, options) { try { options = Object.assign({}, options); /** * Upload as multipart stream */ if (options.multipart) { const { tap, queueSize, partSize, leavePartsOnError, tags, ...others } = options; const upload = new lib_storage_1.Upload({ params: { Key: location, Body: contents, Bucket: this.config.bucket, ...this.transformWriteOptions(others), }, queueSize, partSize, leavePartsOnError, tags, client: this.adapter, }); if (typeof tap === 'function') { tap(upload); } await upload.done(); return; } await this.adapter.send(new client_s3_1.PutObjectCommand({ Key: location, Body: contents, Bucket: this.config.bucket, ...this.transformWriteOptions(options), })); } catch (error) { throw standalone_1.CannotWriteFileException.invoke(location, error); } } /** * Not supported */ async setVisibility() { this.logger.warn('adonis-drive-r2: This driver does not support `setVisibility`'); return Promise.resolve(); } /** * Remove a given location path */ async delete(location) { try { await this.adapter.send(new client_s3_1.DeleteObjectCommand({ Key: location, Bucket: this.config.bucket, })); } catch (error) { throw standalone_1.CannotDeleteFileException.invoke(location, error); } } /** * Copy a given location path from the source to the destination. * The missing intermediate directories will be created (if required) */ async copy(source, destination, options) { options = options || {}; try { await this.adapter.send(new client_s3_1.CopyObjectCommand({ Key: destination, CopySource: `/${this.config.bucket}/${source}`, Bucket: this.config.bucket, ...this.transformWriteOptions(options), })); } catch (error) { throw standalone_1.CannotCopyFileException.invoke(source, destination, error.original || error); } } /** * Move a given location path from the source to the destination. * The missing intermediate directories will be created (if required) */ async move(source, destination, options) { try { await this.copy(source, destination, options); await this.delete(source); } catch (error) { throw standalone_1.CannotMoveFileException.invoke(source, destination, error.original || error); } } } exports.R2Driver = R2Driver;