UNPKG

flydrive

Version:

File storage library with unified API to manage files across multiple cloud storage providers like S3, GCS, R2 and so on

221 lines (220 loc) 9.37 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 { Storage } from "@google-cloud/storage"; var debug_default = debuglog("flydrive:gcs"); var GCSDriver = class GCSDriver { #storage; #usingUniformAcl = true; constructor(options) { this.options = options; this.#storage = "storage" in options ? options.storage : new Storage(options); if (options.usingUniformAcl !== void 0) this.#usingUniformAcl = options.usingUniformAcl; if (debug_default.enabled) debug_default("driver config %O", { ...options, credentials: "REDACTED" }); } #getSaveOptions(options) { const { visibility, contentType, cacheControl, contentEncoding, contentLength, contentLanguage, contentDisposition, ...rest } = options || {}; const gcsOptions = { resumable: false, ...rest }; gcsOptions.metadata = Object.assign(gcsOptions.metadata || {}, { contentType, cacheControl, contentEncoding }); if (this.#usingUniformAcl === false) { gcsOptions.public = (visibility || this.options.visibility) === "public"; gcsOptions.private = !gcsOptions.public; gcsOptions.predefinedAcl = gcsOptions.public ? "publicRead" : "private"; } debug_default("gcs write options %O", gcsOptions); return gcsOptions; } #createFileMetaData(apiFile) { const metaData = { contentType: apiFile.contentType, contentLength: Number(apiFile.size), etag: apiFile.etag, lastModified: new Date(apiFile.updated) }; debug_default("file metadata %O", this.options.bucket, metaData); return metaData; } #getGCSObjects(options) { const bucket = this.#storage.bucket(this.options.bucket); debug_default("fetching files list %O", options); return new Promise((resolve, reject) => { bucket.request({ uri: "/o", qs: options }, (error, response) => { if (error) { debug_default("list files API error %O", error); reject(error); } else { debug_default("list files API response %O", response); resolve({ files: response.items || [], paginationToken: response.nextPageToken, prefixes: response.prefixes || [] }); } }); }); } async exists(key) { debug_default("checking if file exists %s:%s", this.options.bucket, key); return (await this.#storage.bucket(this.options.bucket).file(key).exists())[0]; } async get(key) { debug_default("reading file contents %s:%s", this.options.bucket, key); return (await this.#storage.bucket(this.options.bucket).file(key).download())[0].toString("utf-8"); } async getStream(key) { debug_default("reading file contents as a stream %s:%s", this.options.bucket, key); return this.#storage.bucket(this.options.bucket).file(key).createReadStream(); } async getBytes(key) { debug_default("reading file contents as array buffer %s:%s", this.options.bucket, key); const response = await this.#storage.bucket(this.options.bucket).file(key).download(); return new Uint8Array(response[0]); } async getMetaData(key) { debug_default("fetching file metadata %s:%s", this.options.bucket, key); const response = await this.#storage.bucket(this.options.bucket).file(key).getMetadata(); return this.#createFileMetaData(response[0]); } async getVisibility(key) { debug_default("fetching file visibility %s:%s", this.options.bucket, key); const [isFilePublic] = await this.#storage.bucket(this.options.bucket).file(key).isPublic(); return isFilePublic ? "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.#storage); } debug_default("generating public URL %s:%s", this.options.bucket, key); return this.#storage.bucket(this.options.bucket).file(key).publicUrl(); } async getSignedUrl(key, options) { const { contentDisposition, contentType, expiresIn, ...rest } = Object.assign({}, options); const expires = /* @__PURE__ */ new Date(); expires.setSeconds((/* @__PURE__ */ new Date()).getSeconds() + string.seconds.parse(expiresIn || "30mins")); const signedURLOptions = { action: "read", expires, responseType: contentType, responseDisposition: 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, this.options.bucket, signedURLOptions, this.#storage); } debug_default("generating signed URL %s:%s", this.options.bucket, key); return (await this.#storage.bucket(this.options.bucket).file(key).getSignedUrl(signedURLOptions))[0]; } async getSignedUploadUrl(key, options) { const { expiresIn, ...rest } = Object.assign({}, options); const expires = /* @__PURE__ */ new Date(); expires.setSeconds((/* @__PURE__ */ new Date()).getSeconds() + string.seconds.parse(expiresIn || "30mins")); const signedURLOptions = { action: "write", expires, ...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, this.options.bucket, signedURLOptions, this.#storage); } debug_default("generating signed URL %s:%s", this.options.bucket, key); return (await this.#storage.bucket(this.options.bucket).file(key).getSignedUrl(signedURLOptions))[0]; } async setVisibility(key, visibility) { debug_default("updating file visibility %s:%s to %s", this.options.bucket, key, visibility); const file = this.#storage.bucket(this.options.bucket).file(key); if (visibility === "private") await file.makePrivate(); else await file.makePublic(); } async put(key, contents, options) { debug_default("creating/updating file %s:%s", this.options.bucket, key); await this.#storage.bucket(this.options.bucket).file(key).save(typeof contents === "string" ? Buffer.from(contents) : Buffer.from(contents), this.#getSaveOptions(options)); } putStream(key, contents, options) { debug_default("creating/updating file using readable stream %s:%s", this.options.bucket, key); const bucket = this.#storage.bucket(this.options.bucket); return new Promise((resolve, reject) => { const writeable = bucket.file(key).createWriteStream(this.#getSaveOptions(options)); writeable.once("error", reject); contents.once("error", reject); contents.pipe(writeable).on("finish", resolve).on("error", reject); }); } async copy(source, destination, options) { debug_default("copying file from %s:%s to %s:%s", this.options.bucket, source, this.options.bucket, destination); const bucket = this.#storage.bucket(this.options.bucket); options = options || {}; if (!options.visibility && !this.#usingUniformAcl) { const [isFilePublic] = await bucket.file(source).isPublic(); options.visibility = isFilePublic ? "public" : "private"; } await bucket.file(source).copy(destination, this.#getSaveOptions(options)); } async move(source, destination, options) { debug_default("moving file from %s:%s to %s:%s", this.options.bucket, source, this.options.bucket, destination); const bucket = this.#storage.bucket(this.options.bucket); options = options || {}; if (!options.visibility && !this.#usingUniformAcl) { const [isFilePublic] = await bucket.file(source).isPublic(); options.visibility = isFilePublic ? "public" : "private"; } await bucket.file(source).move(destination, this.#getSaveOptions(options)); } async delete(key) { debug_default("removing file %s:%s", this.options.bucket, key); await this.#storage.bucket(this.options.bucket).file(key).delete({ ignoreNotFound: true }); } async deleteAll(prefix) { const bucket = this.#storage.bucket(this.options.bucket); debug_default("removing all files matching prefix %s:%s", this.options.bucket, prefix); await bucket.deleteFiles({ prefix: `${prefix.replace(/\/$/, "")}/` }); } 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.#getGCSObjects({ autoPaginate: false, delimiter: !recursive ? "/" : "", includeTrailingDelimiter: !recursive, includeFoldersAsPrefixes: !recursive, pageToken: paginationToken, ...prefix !== "/" ? { prefix } : {}, ...maxResults !== void 0 ? { maxResults } : {} }); function* filesGenerator() { for (const directory of response.prefixes) yield new DriveDirectory(directory.replace(/\/$/, "")); for (const file of response.files) yield new DriveFile(file.name, self, self.#createFileMetaData(file)); } return { paginationToken: response.paginationToken, objects: { [Symbol.iterator]: filesGenerator } }; } bucket(bucket) { return new GCSDriver({ ...this.options, bucket }); } }; export { GCSDriver };