UNPKG

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
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 };