adonis-drive-r2
Version:
R2 driver for AdonisJS drive
338 lines (337 loc) • 12 kB
JavaScript
"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;