flydrive
Version:
File storage library with unified API to manage files across multiple cloud storage providers like S3, GCS, R2 and so on
295 lines (294 loc) • 11.9 kB
JavaScript
import { n as DriveFile, t as DriveDirectory } from "../../drive_directory-Av1MhRnI.js";
import string from "@poppinss/utils/string";
import { debuglog } from "node:util";
import mimeTypes from "mime-types";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { CopyObjectCommand, DeleteObjectCommand, DeleteObjectsCommand, GetObjectAclCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectAclCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
var debug_default = debuglog("flydrive:s3");
var S3Driver = class S3Driver {
#client;
#supportsACL = true;
publicGrantUri = "http://acs.amazonaws.com/groups/global/AllUsers";
constructor(options) {
this.options = options;
this.#client = "client" in options ? options.client : new S3Client(options);
if (options.supportsACL !== void 0) this.#supportsACL = options.supportsACL;
if (debug_default.enabled) debug_default("driver config %O", {
...options,
credentials: "REDACTED"
});
}
#createFileMetaData(apiFile) {
const metaData = {
contentType: apiFile.ContentType,
contentLength: apiFile.ContentLength,
etag: apiFile.ETag,
lastModified: new Date(apiFile.LastModified)
};
debug_default("file metadata %O", this.options.bucket, metaData);
return metaData;
}
#getSaveOptions(key, options) {
const { visibility, contentType, cacheControl, contentEncoding, contentLength, contentLanguage, contentDisposition, ...rest } = options || {};
const s3Options = {
Bucket: this.options.bucket,
ServerSideEncryption: this.options.encryption,
...rest
};
if (this.#supportsACL) s3Options.ACL = (visibility || this.options.visibility) === "public" ? "public-read" : "private";
if (contentType) s3Options.ContentType = contentType;
else {
const detectedContentType = mimeTypes.lookup(key);
if (detectedContentType) {
debug_default("setting \"%s\" file's content-type to \"%s\"", key, detectedContentType);
s3Options.ContentType = detectedContentType;
}
}
if (cacheControl) s3Options.CacheControl = cacheControl;
if (contentEncoding) s3Options.ContentEncoding = contentEncoding;
if (contentLength) s3Options.ContentLength = contentLength;
if (contentLanguage) s3Options.ContentLanguage = contentLanguage;
if (contentDisposition) s3Options.ContentDisposition = contentDisposition;
debug_default("s3 write options %O", s3Options);
return s3Options;
}
async #deleteFilesRecursively(prefix, paginationToken) {
const response = await this.#client.send(this.createListObjectsV2Command(this.#client, {
Bucket: this.options.bucket,
ContinuationToken: paginationToken,
...prefix !== "/" ? { Prefix: prefix } : {}
}));
if (!response.Contents || !response.Contents.length) return;
await this.#client.send(this.createDeleteObjectsCommand(this.#client, {
Bucket: this.options.bucket,
Delete: {
Objects: Array.from(response.Contents).map((file) => {
return { Key: file.Key };
}),
Quiet: true
}
}));
if (response.NextContinuationToken) {
debug_default("deleting next batch of files with token %s", response.NextContinuationToken);
await this.#deleteFilesRecursively(prefix, response.NextContinuationToken);
}
}
createPutObjectCommand(_, options) {
return new PutObjectCommand(options);
}
createGetObjectCommand(_, options) {
return new GetObjectCommand(options);
}
createHeadObjectCommand(_, options) {
return new HeadObjectCommand(options);
}
createGetObjectAclCommand(_, options) {
return new GetObjectAclCommand(options);
}
createPutObjectAclCommand(_, options) {
return new PutObjectAclCommand(options);
}
createDeleteObjectCommand(_, options) {
return new DeleteObjectCommand(options);
}
createCopyObjectCommand(_, options) {
return new CopyObjectCommand(options);
}
createListObjectsV2Command(_, options) {
return new ListObjectsV2Command(options);
}
createDeleteObjectsCommand(_, options) {
return new DeleteObjectsCommand(options);
}
async exists(key) {
debug_default("checking if file exists %s:%s", this.options.bucket, key);
try {
return (await this.#client.send(this.createHeadObjectCommand(this.#client, {
Key: key,
Bucket: this.options.bucket
}))).$metadata.httpStatusCode === 200;
} catch (error) {
if (error.$metadata?.httpStatusCode === 404) return false;
throw error;
}
}
async get(key) {
debug_default("reading file contents %s:%s", this.options.bucket, key);
return (await this.#client.send(this.createGetObjectCommand(this.#client, {
Key: key,
Bucket: this.options.bucket
}))).Body.transformToString();
}
async getStream(key) {
debug_default("reading file contents as a stream %s:%s", this.options.bucket, key);
return (await this.#client.send(this.createGetObjectCommand(this.#client, {
Key: key,
Bucket: this.options.bucket
}))).Body;
}
async getBytes(key) {
debug_default("reading file contents as array buffer %s:%s", this.options.bucket, key);
return (await this.#client.send(this.createGetObjectCommand(this.#client, {
Key: key,
Bucket: this.options.bucket
}))).Body.transformToByteArray();
}
async getMetaData(key) {
debug_default("fetching file metadata %s:%s", this.options.bucket, key);
const response = await this.#client.send(this.createHeadObjectCommand(this.#client, {
Key: key,
Bucket: this.options.bucket
}));
return this.#createFileMetaData(response);
}
async getVisibility(key) {
if (!this.#supportsACL) return this.options.visibility;
debug_default("fetching file visibility %s:%s", this.options.bucket, key);
return ((await this.#client.send(this.createGetObjectAclCommand(this.#client, {
Key: key,
Bucket: this.options.bucket
}))).Grants || []).find((grant) => {
return grant.Grantee?.URI === this.publicGrantUri && (grant.Permission === "READ" || grant.Permission === "FULL_CONTROL");
}) ? "public" : "private";
}
async getUrl(key) {
const generateURL = this.options.urlBuilder?.generateURL;
if (generateURL) {
debug_default("using custom implementation for generating public URL %s:%s", this.options.bucket, key);
return generateURL(key, this.options.bucket, this.#client);
}
debug_default("generating file URL %s:%s", this.options.bucket, key);
if (this.options.cdnUrl) return new URL(key, this.options.cdnUrl).toString();
if (this.#client.config.endpoint) {
const endpoint = await this.#client.config.endpoint();
let baseUrl = `${endpoint.protocol}//${endpoint.hostname}`;
if (endpoint.port) baseUrl += `:${endpoint.port}`;
return new URL(`/${this.options.bucket}/${key}`, baseUrl).toString();
}
return new URL(`/${key}`, `https://${this.options.bucket}.s3.amazonaws.com`).toString();
}
async getSignedUrl(key, options) {
const { contentDisposition, contentType, expiresIn, ...rest } = Object.assign({}, options);
const expires = string.seconds.parse(expiresIn || "30mins");
const signedURLOptions = {
Key: key,
Bucket: this.options.bucket,
ResponseContentType: contentType,
ResponseContentDisposition: contentDisposition,
...rest
};
const generateSignedURL = this.options.urlBuilder?.generateSignedURL;
if (generateSignedURL) {
debug_default("using custom implementation for generating signed URL %s:%s", this.options.bucket, key);
return generateSignedURL(key, signedURLOptions, this.#client, expiresIn);
}
debug_default("generating signed URL %s:%s", this.options.bucket, key);
return getSignedUrl(this.#client, this.createGetObjectCommand(this.#client, signedURLOptions), { expiresIn: expires });
}
async getSignedUploadUrl(key, options) {
const { contentType, expiresIn, ...rest } = Object.assign({}, options);
const expires = string.seconds.parse(expiresIn || "30mins");
const signedURLOptions = {
Key: key,
Bucket: this.options.bucket,
ContentType: contentType,
...rest
};
const generateSignedUploadURL = this.options.urlBuilder?.generateSignedUploadURL;
if (generateSignedUploadURL) {
debug_default("using custom implementation for generating signed upload URL %s:%s", this.options.bucket, key);
return generateSignedUploadURL(key, signedURLOptions, this.#client, expiresIn);
}
debug_default("generating signed upload URL %s:%s", this.options.bucket, key);
return getSignedUrl(this.#client, this.createPutObjectCommand(this.#client, signedURLOptions), { expiresIn: expires });
}
async setVisibility(key, visibility) {
if (!this.#supportsACL) return;
debug_default("updating file visibility %s:%s to %s", this.options.bucket, key, visibility);
await this.#client.send(this.createPutObjectAclCommand(this.#client, {
Key: key,
Bucket: this.options.bucket,
ACL: visibility === "public" ? "public-read" : "private"
}));
}
async put(key, contents, options) {
debug_default("creating/updating file %s:%s", this.options.bucket, key);
const command = this.createPutObjectCommand(this.#client, {
...this.#getSaveOptions(key, options),
Key: key,
Body: contents
});
await this.#client.send(command);
}
putStream(key, contents, options) {
debug_default("creating/updating file %s:%s", this.options.bucket, key);
return new Promise((resolve, reject) => {
contents.once("error", reject);
try {
const command = this.createPutObjectCommand(this.#client, {
...this.#getSaveOptions(key, options),
Key: key,
Body: contents
});
return this.#client.send(command).then(() => resolve()).catch(reject);
} catch (error) {
reject(error);
}
});
}
async copy(source, destination, options) {
debug_default("copying file from %s:%s to %s:%s", this.options.bucket, source, this.options.bucket, destination);
options = options || {};
if (!options.visibility && this.#supportsACL) options.visibility = await this.getVisibility(source);
await this.#client.send(this.createCopyObjectCommand(this.#client, {
...this.#getSaveOptions(destination, options),
Key: destination,
CopySource: `/${this.options.bucket}/${source}`,
Bucket: this.options.bucket
}));
}
async move(source, destination, options) {
debug_default("moving file from %s:%s to %s:%s", this.options.bucket, source, this.options.bucket, destination);
await this.copy(source, destination, options);
await this.delete(source);
}
async delete(key) {
debug_default("removing file %s:%s", this.options.bucket, key);
await this.#client.send(this.createDeleteObjectCommand(this.#client, {
Key: key,
Bucket: this.options.bucket
}));
}
async deleteAll(prefix) {
debug_default("removing all files matching prefix %s:%s", this.options.bucket, prefix);
await this.#deleteFilesRecursively(prefix);
}
async listAll(prefix, options) {
const self = this;
let { recursive, paginationToken, maxResults } = Object.assign({ recursive: false }, options);
if (prefix) prefix = !recursive ? `${prefix.replace(/\/$/, "")}/` : prefix;
debug_default("listing all files matching prefix %s:%s", this.options.bucket, prefix);
const response = await this.#client.send(this.createListObjectsV2Command(this.#client, {
Bucket: this.options.bucket,
Delimiter: !recursive ? "/" : "",
ContinuationToken: paginationToken,
...prefix !== "/" ? { Prefix: prefix } : {},
...maxResults !== void 0 ? { MaxKeys: maxResults } : {}
}));
function* filesGenerator() {
if (response.CommonPrefixes) for (const directory of response.CommonPrefixes) yield new DriveDirectory(directory.Prefix.replace(/\/$/, ""));
if (response.Contents) for (const file of response.Contents) yield new DriveFile(file.Key, self, self.#createFileMetaData(file));
}
return {
paginationToken: response.NextContinuationToken,
objects: { [Symbol.iterator]: filesGenerator }
};
}
bucket(bucket) {
return new S3Driver({
...this.options,
bucket
});
}
};
export { S3Driver };