@giancarl021/cli-core-vault-extension
Version:
Plain and secure storage extension for the @giancarl021/cli-core npm package
218 lines (215 loc) • 8.97 kB
JavaScript
import { existsSync } from 'node:fs';
import { readFile, unlink, rm, writeFile, rename } from 'node:fs/promises';
import { isAbsolute, resolve, dirname } from 'node:path';
import constants from '../util/constants.js';
import hash from '../util/hash.js';
import assertDir from '../util/assertDir.js';
/**
* Store data as JSON in a file on the filesystem.
* Also implements workspace functionality to isolate data for different contexts.
* Binary files can be stored using native APIs like `fs.promises.writeFile` and `fs.promises.readFile`
* in conjunction with the `getPath` method to get absolute paths without worrying about the root directory.
*
* @template Schema - The type of the data schema to be stored.
* @param rootPath The root directory where data files will be stored.
* @param lazyInitialization If `true`, the storage will only be initialized when first accessed.
* If `false`, it will be initialized immediately.
* Default is `true`.
* @param key The workspace key to isolate data. Default is `constants.workspace.defaultKey`.
* @returns An object implementing the StorageEngine interface for managing the data.
*/
function FileStorage(rootPath, lazyInitialization, key = constants.workspace.defaultKey) {
let initialized = false;
// Validate the provided path
if (!rootPath || !isAbsolute(rootPath))
throw new Error('Invalid file path');
const _rootPath = resolve(rootPath);
// Get the workspace path, which is a subdirectory of the root path
// every workspace gets its own directory hashed from the key,
// unless the key is `constants.workspace.defaultKey`, in which case it will not
// hash
const workspacePath = resolve(_rootPath, key === constants.workspace.defaultKey ? key : hash(key));
/**
* Initialize the storage by ensuring the root and workspace directories exist.
* This is called automatically on first access if `lazyInitialization` is enabled.
*/
function _init() {
if (initialized)
return;
assertDir(_rootPath);
assertDir(workspacePath);
initialized = true;
}
/**
* Atomically write data to a file by writing to a temporary file first,
* then renaming it to the target file. This prevents data corruption in case
* of a crash or interruption during the write process.
* @param path The absolute path to the file to write.
* @param data The data to write to the file.
*/
async function _atomicWrite(path, data) {
const tempPath = `${path}.tmp`;
await writeFile(tempPath, data);
await rename(tempPath, path);
}
// If not lazy, initialize immediately
if (!lazyInitialization) {
_init();
}
/**
* Writes the given value as JSON to the file. It must follow the Schema type.
* @param path The relative path within the workspace to write the object to.
* @param value The object to serialize and write.
* @returns A promise that resolves when the write is complete.
*/
async function writeObject(path, value) {
_init();
const _path = resolve(workspacePath, path);
const data = JSON.stringify(value, null, 2);
if (!existsSync(dirname(_path))) {
assertDir(dirname(_path));
}
await _atomicWrite(_path, data);
}
/**
* Reads and parses a JSON object from the file. If the file does not exist,
* it will be created with the provided default value.
* @param path The relative path within the workspace to read the object from.
* @param defaultValue The default value to write and return if the file does not exist.
* @returns A promise that resolves to the parsed object.
*/
async function readObject(path, defaultValue) {
_init();
const _path = resolve(workspacePath, path);
if (!existsSync(_path)) {
await writeObject(_path, defaultValue);
}
const stringData = await readFile(_path, 'utf-8');
const data = JSON.parse(stringData);
return data;
}
/**
* Removes the file at the specified relative path within the workspace.
* If the file does not exist, no action is taken.
* @param path The relative path within the workspace to remove the object from.
* @returns A promise that resolves when the file is removed.
*/
async function removeObject(path) {
_init();
const _path = resolve(workspacePath, path);
if (existsSync(_path))
await unlink(_path);
}
/**
* Destroys the entire storage by removing the root directory and all its contents.
* @returns A promise that resolves when the storage is destroyed.
*/
async function destroy() {
if (!existsSync(_rootPath))
return;
await rm(_rootPath, { recursive: true, force: true });
}
/**
* Get the root path of the storage.
* @returns The absolute path to the root directory.
*/
function getRootPath() {
_init();
return _rootPath;
}
/**
* Get the workspace path (root path + workspace).
* @returns The absolute path to the root directory for the current workspace.
*/
function getWorkspacePath() {
_init();
return workspacePath;
}
/**
* Get the absolute path for a given relative path within the storage root.
* @param relativePath The relative path to resolve.
* @returns The absolute path.
*/
function getPath(relativePath) {
_init();
return resolve(workspacePath, relativePath);
}
/**
* Create a new workspace with the given key.
* @param key The key to create a unique workspace directory.
* @returns A new FileStorage instance for the specified workspace.
*/
function createWorkspace(key) {
if (key === constants.workspace.defaultKey) {
throw new Error(`Workspace key cannot be the default key: ${constants.workspace.defaultKey}`);
}
_init();
return FileStorage(_rootPath, lazyInitialization, key);
}
/**
* Destroy the current workspace by removing its directory and all its contents.
* @returns A promise that resolves when the workspace is destroyed.
*/
async function destroyWorkspace() {
if (!existsSync(workspacePath))
return;
await rm(workspacePath, { recursive: true, force: true });
}
return {
/**
* Writes the given value as JSON to the file. It must follow the Schema type.
* @param path The relative path within the workspace to write the object to.
* @param value The object to serialize and write.
* @returns A promise that resolves when the write is complete.
*/
writeObject,
/**
* Reads and parses a JSON object from the file. If the file does not exist,
* it will be created with the provided default value.
* @param path The relative path within the workspace to read the object from.
* @param defaultValue The default value to write and return if the file does not exist.
* @returns A promise that resolves to the parsed object.
*/
readObject,
/**
* Removes the file at the specified relative path within the workspace. If the file does not exist,
* no action is taken.
* @param path The relative path within the workspace to remove the object from.
* @returns A promise that resolves when the file is removed.
*/
removeObject,
/**
* Get the root path of the storage.
* @returns The absolute path to the root directory.
*/
getRootPath,
/**
* Get the workspace path (root path + workspace).
* @returns The absolute path to the root directory for the current workspace.
*/
getWorkspacePath,
/**
* Get the absolute path for a given relative path within the storage root.
* @param relativePath The relative path to resolve.
* @returns The absolute path.
*/
getPath,
/**
* Destroys the entire storage by removing the root directory and all its contents.
* @returns A promise that resolves when the storage is destroyed.
*/
destroy,
/**
* Create a new workspace with the given key.
* @param key The key to create a unique workspace directory.
* @returns A new FileStorage instance for the specified workspace.
*/
createWorkspace,
/**
* Destroy the current workspace by removing its directory and all its contents.
* @returns A promise that resolves when the workspace is destroyed.
*/
destroyWorkspace
};
}
export { FileStorage as default };