flydrive
Version:
File storage library with unified API to manage files across multiple cloud storage providers like S3, GCS, R2 and so on
362 lines (356 loc) • 10.2 kB
JavaScript
import {
FSDriver
} from "./chunk-KYJJJH3D.js";
import {
DriveDirectory,
DriveFile,
E_CANNOT_COPY_FILE,
E_CANNOT_DELETE_DIRECTORY,
E_CANNOT_DELETE_FILE,
E_CANNOT_MOVE_FILE,
E_CANNOT_SET_VISIBILITY,
E_CANNOT_WRITE_FILE,
KeyNormalizer,
errors_exports
} from "./chunk-TZSKXVQT.js";
// src/disk.ts
import { unlink } from "fs/promises";
import { createReadStream } from "fs";
var Disk = class {
constructor(driver) {
this.driver = driver;
}
/**
* The normalizer is used to normalize and validate keys
*/
#normalizer = new KeyNormalizer();
/**
* Creates a new instance of the DriveFile. It can be used
* to lazily fetch file contents or convert it into a
* snapshot for persistence
*/
file(key) {
return new DriveFile(key, this.driver);
}
/**
* Creates a new instance of the DriveFile from the snapshot.
*/
fromSnapshot(snapshot) {
return new DriveFile(snapshot.key, this.driver, {
contentLength: snapshot.contentLength,
etag: snapshot.etag,
lastModified: new Date(snapshot.lastModified),
contentType: snapshot.contentType
});
}
/**
* Check if the file exists. This method cannot check existence
* of directories.
*/
exists(key) {
return this.file(key).exists();
}
/**
* Returns file contents as a UTF-8 string. Use "getArrayBuffer" method
* if you need more control over the file contents decoding.
*/
get(key) {
return this.file(key).get();
}
/**
* Returns file contents as a Readable stream.
*/
getStream(key) {
return this.file(key).getStream();
}
/**
* Returns file contents as a Uint8Array.
*/
getBytes(key) {
return this.file(key).getBytes();
}
/**
* @deprecated
* @see {@link Disk.getBytes}
*/
getArrayBuffer(key) {
return this.file(key).getArrayBuffer();
}
/**
* Returns metadata of the given file.
*/
getMetaData(key) {
return this.file(key).getMetaData();
}
/**
* Returns the visibility of the file
*/
getVisibility(key) {
return this.file(key).getVisibility();
}
/**
* Returns the public URL of the file
*/
getUrl(key) {
return this.file(key).getUrl();
}
/**
* Returns a signed/temporary URL of the file
*/
getSignedUrl(key, options) {
return this.file(key).getSignedUrl(options);
}
/**
* Returns a signed/temporary URL that can be used to directly upload
* the file contents to the storage.
*/
getSignedUploadUrl(key, options) {
return this.file(key).getSignedUploadUrl(options);
}
/**
* Update the visibility of the file
*/
async setVisibility(key, visibility) {
key = this.#normalizer.normalize(key);
try {
return await this.driver.setVisibility(key, visibility);
} catch (error) {
throw new E_CANNOT_SET_VISIBILITY([key], { cause: error });
}
}
/**
* Create new file or update an existing file. In case of an error,
* the "E_CANNOT_WRITE_FILE" exception is thrown
*/
async put(key, contents, options) {
key = this.#normalizer.normalize(key);
try {
return await this.driver.put(key, contents, options);
} catch (error) {
throw new E_CANNOT_WRITE_FILE([key], { cause: error });
}
}
/**
* Create new file or update an existing file using a Readable Stream
* In case of an error, the "E_CANNOT_WRITE_FILE" exception is thrown
*/
async putStream(key, contents, options) {
key = this.#normalizer.normalize(key);
try {
return await this.driver.putStream(key, contents, options);
} catch (error) {
throw new E_CANNOT_WRITE_FILE([key], { cause: error });
}
}
/**
* Copies file from the "source" to the "destination" within the
* same bucket or the root location of local filesystem.
*
* Use "copyFromFs" method to copy files from local filesystem to
* a cloud provider
*/
async copy(source, destination, options) {
source = this.#normalizer.normalize(source);
destination = this.#normalizer.normalize(destination);
try {
return await this.driver.copy(source, destination, options);
} catch (error) {
throw new E_CANNOT_COPY_FILE([source, destination], { cause: error });
}
}
/**
* Copies file from the local filesystem to the cloud provider.
*/
copyFromFs(source, destination, options) {
return this.putStream(destination, createReadStream(source), options);
}
/**
* Moves file from the "source" to the "destination" within the
* same bucket or the root location of local filesystem.
*
* Use "moveFromFs" method to move files from local filesystem to
* a cloud provider
*/
async move(source, destination, options) {
source = this.#normalizer.normalize(source);
destination = this.#normalizer.normalize(destination);
try {
return await this.driver.move(source, destination, options);
} catch (error) {
throw new E_CANNOT_MOVE_FILE([source, destination], { cause: error });
}
}
/**
* Moves file from the local filesystem to the cloud provider.
*/
async moveFromFs(source, destination, options) {
await this.putStream(destination, createReadStream(source), options);
await unlink(source);
}
/**
* Deletes a file for the given key. Use "deleteAll" method to delete
* files for a matching folder prefix.
*/
async delete(key) {
key = this.#normalizer.normalize(key);
try {
return await this.driver.delete(key);
} catch (error) {
throw new E_CANNOT_DELETE_FILE([key], { cause: error });
}
}
/**
* Delete all files matching the given prefix. In case of "fs" driver,
* the mentioned folder will be deleted.
*/
async deleteAll(prefix) {
prefix = prefix && prefix !== "/" ? this.#normalizer.normalize(prefix) : "/";
try {
return await this.driver.deleteAll(prefix);
} catch (error) {
throw new E_CANNOT_DELETE_DIRECTORY([prefix], { cause: error });
}
}
/**
* Returns a list of objects which includes and files and directories.
* In case of "recursive" listing, no directories are returned.
*/
listAll(prefix, options) {
prefix = prefix && prefix !== "/" ? this.#normalizer.normalize(prefix) : "/";
return this.driver.listAll(prefix, options);
}
};
// src/drive_manager.ts
import { RuntimeException } from "@poppinss/utils";
// src/debug.ts
import { debuglog } from "util";
var debug_default = debuglog("flydrive:core");
// src/fake_disk.ts
import { join } from "path";
import { AssertionError } from "assert";
var FakeDisk = class extends Disk {
constructor(disk, fakesConfig) {
super(
new FSDriver({
location: typeof fakesConfig.location === "string" ? join(fakesConfig.location, disk) : new URL(disk, fakesConfig.location),
visibility: "public",
urlBuilder: fakesConfig.urlBuilder
})
);
this.disk = disk;
}
/**
* Assert the expected file(s) exists. Otherwise an assertion
* error is thrown
*/
assertExists(paths) {
const pathsToVerify = Array.isArray(paths) ? paths : [paths];
for (let filePath of pathsToVerify) {
if (!this.driver.existsSync(filePath)) {
throw new AssertionError({
message: `Expected "${filePath}" to exist, but file not found.`
});
}
}
}
/**
* Assert the expected file(s) to not exist. Otherwise an assertion
* error is thrown
*/
assertMissing(paths) {
const pathsToVerify = Array.isArray(paths) ? paths : [paths];
for (let filePath of pathsToVerify) {
if (this.driver.existsSync(filePath)) {
throw new AssertionError({
message: `Expected "${filePath}" to be missing, but file exists`
});
}
}
}
/**
* Clear storage
*/
clear() {
this.driver.clearSync();
}
};
// src/drive_manager.ts
var DriveManager = class {
/**
* Registered config
*/
#config;
/**
* A collection of cached service. We re-use disk instances for a
* service, since there isn't any need to reconstruct them
* everytime.
*/
#cachedServices = /* @__PURE__ */ new Map();
/**
* A collection of fakes created for the services.
*/
#fakes = /* @__PURE__ */ new Map();
constructor(config) {
this.#config = config;
debug_default("driver manager config %O", config);
}
/**
* Returns an instance of a Disk for the given service. By default
* use the "default" service from config
*/
use(service) {
const serviceToUse = service || this.#config.default;
const fake = this.#fakes.get(serviceToUse);
if (fake) {
debug_default("returning fake for service %s", serviceToUse);
return fake;
}
const cachedDisk = this.#cachedServices.get(serviceToUse);
if (cachedDisk) {
debug_default("use cached disk instance for service %s", serviceToUse);
return cachedDisk;
}
const disk = new Disk(this.#config.services[serviceToUse]());
debug_default("creating disk instance for service %s", serviceToUse);
this.#cachedServices.set(serviceToUse, disk);
return disk;
}
/**
* Deploy fake for a given service. The "use" method for the same service
* will now return an instance of the "FakeDisk" class and not the
* real implementation.
*/
fake(service) {
const serviceToUse = service || this.#config.default;
if (!this.#config.fakes) {
throw new RuntimeException(
'Cannot use "drive.fake". Make sure to define fakes configuration when creating DriveManager instance'
);
}
this.restore(serviceToUse);
debug_default("creating fake for service %s", serviceToUse);
const fake = new FakeDisk(serviceToUse, this.#config.fakes);
this.#fakes.set(serviceToUse, fake);
return fake;
}
/**
* Restore fake for a given service
*/
restore(service) {
const serviceToUse = service || this.#config.default;
const fake = this.#fakes.get(serviceToUse);
if (fake) {
debug_default("restoring fake for service %s", serviceToUse);
fake.clear();
this.#fakes.delete(serviceToUse);
}
}
};
export {
Disk,
DriveDirectory,
DriveFile,
DriveManager,
KeyNormalizer,
errors_exports as errors
};