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