UNPKG

flydrive

Version:

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

306 lines (301 loc) 9.65 kB
import { DriveDirectory, DriveFile } from "./chunk-4VZHSF4K.js"; // drivers/fs/driver.ts import etag from "etag"; import mimeTypes from "mime-types"; import { slash } from "@poppinss/utils"; import * as fsp from "node:fs/promises"; import { fileURLToPath } from "node:url"; import { Retrier } from "@humanwhocodes/retry"; import { RuntimeException } from "@poppinss/utils"; import { dirname, join, relative } from "node:path"; import { existsSync, rmSync, createReadStream } from "node:fs"; // drivers/fs/debug.ts import { debuglog } from "node:util"; var debug_default = debuglog("flydrive:fs"); // drivers/fs/driver.ts var RETRY_ERROR_CODES = /* @__PURE__ */ new Set(["ENFILE", "EMFILE"]); var FSDriver = class { constructor(options) { this.options = options; this.#rootUrl = typeof options.location === "string" ? options.location : fileURLToPath(options.location); debug_default("driver config %O", options); } /** * The root directory for the driver */ #rootUrl; /** * Retrier is used to retry file system operations * when certain errors are raised. */ #retrier = new Retrier( (error) => error.code && RETRY_ERROR_CODES.has(error.code) ); /** * Reads the file for the provided path */ #read(key) { const location = join(this.#rootUrl, key); return this.#retrier.retry(() => fsp.readFile(location)); } /** * Reads dir and ignores non-existing errors */ async #readDir(location, recursive) { try { return await fsp.readdir(location, { recursive, withFileTypes: true }); } catch (error) { if (error.code !== "ENOENT") { throw error; } return []; } } /** * Generic implementation to write a file */ #write(key, contents, options) { const location = join(this.#rootUrl, key); return this.#retrier.retry(async () => { await fsp.mkdir(dirname(location), { recursive: true }); await fsp.writeFile(location, contents, options); }); } /** * Synchronously check if a file exists */ existsSync(key) { debug_default("checking if file exists %s:%s", this.#rootUrl, key); const location = join(this.#rootUrl, key); return existsSync(location); } /** * Returns a boolean indicating if the file exists or not. */ async exists(key) { debug_default("checking if file exists %s:%s", this.#rootUrl, key); const location = join(this.#rootUrl, key); try { const object = await fsp.stat(location); return object.isFile(); } catch (error) { if (error.code === "ENOENT") { return false; } throw error; } } /** * Returns the contents of the file as a UTF-8 string. An * exception is thrown when the file is missing. */ async get(key) { debug_default("reading file contents %s:%s", this.#rootUrl, key); return this.#read(key).then((value) => value.toString("utf-8")); } /** * Returns the contents of the file as a stream. An * exception is thrown when the file is missing. */ async getStream(key) { debug_default("reading file contents as a stream %s:%s", this.#rootUrl, key); const location = join(this.#rootUrl, key); return createReadStream(location); } /** * Returns the contents of the file as an Uint8Array. An * exception is thrown when the file is missing. */ async getBytes(key) { debug_default("reading file contents as array buffer %s:%s", this.#rootUrl, key); return this.#read(key).then((value) => new Uint8Array(value.buffer)); } /** * Returns the metadata of a file. */ async getMetaData(key) { debug_default("fetching file metadata %s:%s", this.#rootUrl, key); const location = join(this.#rootUrl, key); const stats = await fsp.stat(location); if (stats.isDirectory()) { throw new RuntimeException(`Cannot get metadata of a directory "${key}"`); } return { contentLength: stats.size, contentType: mimeTypes.lookup(key) || void 0, etag: etag(stats), lastModified: stats.mtime }; } /** * Returns the file visibility from the pre-defined config * value */ async getVisibility(_) { return this.options.visibility; } /** * Returns the public URL of the file. This method does not check * if the file exists or not. */ async getUrl(key) { const location = join(this.#rootUrl, key); const generateURL = this.options.urlBuilder?.generateURL; if (generateURL) { debug_default("generating public URL %s:%s", this.#rootUrl, key); return generateURL(key, location); } throw new RuntimeException('Cannot generate URL. The "fs" driver does not support it'); } /** * Returns the signed/temporary URL of the file. By default, the signed URLs * expire in 30mins, but a custom expiry can be defined using * "options.expiresIn" property. */ async getSignedUrl(key, options) { const location = join(this.#rootUrl, key); const normalizedOptions = Object.assign( { expiresIn: "30 mins" }, options ); const generateSignedURL = this.options.urlBuilder?.generateSignedURL; if (generateSignedURL) { debug_default("generating signed URL %s:%s", this.#rootUrl, key); return generateSignedURL(key, location, normalizedOptions); } throw new RuntimeException('Cannot generate signed URL. The "fs" driver does not support it'); } /** * Results in noop, since the local filesystem cannot have per * object visibility. */ async setVisibility(_, __) { } /** * Writes a file to the destination with the provided contents. * * - Missing directories will be created recursively. * - Existing file will be overwritten. */ put(key, contents, options) { debug_default("creating/updating file %s:%s", this.#rootUrl, key); return this.#write(key, contents, { signal: options?.signal }); } /** * Writes a file to the destination with the provided contents * as a readable stream. * * - Missing directories will be created recursively. * - Existing file will be overwritten. */ putStream(key, contents, options) { debug_default("creating/updating file using readable stream %s:%s", this.#rootUrl, key); return new Promise((resolve, reject) => { contents.once("error", (error) => reject(error)); return this.#write(key, contents, { signal: options?.signal }).then(resolve).catch(reject); }); } /** * Copies the source file to the destination. Both paths must * be within the root location. */ copy(source, destination) { debug_default("copying file from %s to %s", source, destination); const sourceLocation = join(this.#rootUrl, source); const destinationLocation = join(this.#rootUrl, destination); return this.#retrier.retry(async () => { await fsp.mkdir(dirname(destinationLocation), { recursive: true }); await fsp.copyFile(sourceLocation, destinationLocation); }); } /** * Moves the source file to the destination. Both paths must * be within the root location. */ move(source, destination) { debug_default("moving file from %s to %s", source, destination); const sourceLocation = join(this.#rootUrl, source); const destinationLocation = join(this.#rootUrl, destination); return this.#retrier.retry(async () => { await fsp.mkdir(dirname(destinationLocation), { recursive: true }); await fsp.copyFile(sourceLocation, destinationLocation); await fsp.unlink(sourceLocation); }); } /** * Deletes a file within the root location of the filesystem. * Attempting to delete a non-existing file will result in * a noop. */ delete(key) { debug_default("deleting file %s:%s", this.#rootUrl, key); const location = join(this.#rootUrl, key); return this.#retrier.retry(async () => { try { await fsp.unlink(location); } catch (error) { if (error.code !== "ENOENT") { throw error; } } }); } /** * Deletes the files and directories matching the provided * prefix. The method is same as running "rm -rf" unix * command */ deleteAll(prefix) { debug_default("deleting all files in folder %s:%s", this.#rootUrl, prefix); const location = join(this.#rootUrl, prefix); return this.#retrier.retry(async () => { return fsp.rm(location, { recursive: true, force: true }); }); } /** * Synchronously delete all files from the root location */ clearSync() { rmSync(this.#rootUrl, { recursive: true, force: true }); } /** * Returns a list of files. The pagination properties are ignored * by the fs driver, since it does not support pagination. */ async listAll(prefix, options) { const self = this; const location = join(this.#rootUrl, prefix); const { recursive } = Object.assign({ recursive: false }, options); debug_default("listing files from folder %s:%s %O", this.#rootUrl, prefix, options); const files = await this.#readDir(location, recursive); function* filesGenerator() { for (const file of files) { const relativeName = slash( relative(self.#rootUrl, join(file.parentPath || file.path, file.name)) ); if (file.isFile()) { yield new DriveFile(relativeName, self); } else if (!recursive) { yield new DriveDirectory(relativeName); } } } return { paginationToken: void 0, objects: { [Symbol.iterator]: filesGenerator } }; } }; export { FSDriver };