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