@timothy-spaceman/multitrack-vcs
Version:
Version Control System for musicians
313 lines • 11 kB
JavaScript
import { createHash } from "crypto";
import micromatch from 'micromatch';
import path from "node:path";
export class VirtualFile {
path;
name;
extension = "";
fullPath; // path + name + extension
data;
constructor(fullPath, data) {
this.fullPath = fullPath;
const parts = fullPath.split(VirtualStorageProvider.separator);
const fileName = parts.pop();
if (!fileName) {
throw new Error("Unable to parse file name");
}
this.name = fileName;
if (fileName.split(".").length > 1) {
this.name = fileName.split(".").slice(0, -1).join(".");
this.extension = fileName.split(".").slice(-1).join("");
}
this.path = parts.join(VirtualStorageProvider.separator);
this.data = data;
}
async readData() {
try {
return this.data;
}
catch (err) {
throw new Error(`Failed to read file content: ${err.message}`);
}
}
async writeData(data) {
try {
this.data = data;
}
catch (err) {
throw new Error(`Failed to write file content: ${err.message}`);
}
}
async getDataHash(algo = "sha256") {
const hash = createHash(algo);
hash.update(this.data);
return hash.digest("hex");
}
}
export class VirtualDirectoryMock {
callback;
content = new Map();
constructor(callback) {
this.callback = callback;
this.initializeProxies();
}
initializeProxies() {
const methodsToMock = ["has", "get", "set", "delete", "clear", "keys", "values", "entries", "forEach"];
const chainableMethods = ["set"];
for (const methodName of methodsToMock) {
const originalMethod = this.content[methodName];
this[methodName] = (...args) => {
const result = originalMethod.apply(this.content, args);
this.callback(...[methodName, result, ...args]);
if (chainableMethods.includes(methodName)) {
return this;
}
return result;
};
}
}
get size() {
return this.content.size;
}
has;
get;
set;
delete;
clear;
keys;
values;
entries;
forEach;
}
export class VirtualDirectoryOverrideMock extends VirtualDirectoryMock {
initializeProxies() {
const methodsToMock = ["has", "get", "set", "delete", "clear", "keys", "values", "entries", "forEach"];
const chainableMethods = ["set"];
for (const methodName of methodsToMock) {
const originalMethod = this.content[methodName];
this[methodName] = (...args) => {
const originalResult = originalMethod.apply(this.content, args);
const result = this.callback(...[methodName, originalResult, ...args]);
if (chainableMethods.includes(methodName)) {
return this;
}
return result;
};
}
}
}
export class VirtualStorageProvider {
static separator = "/";
files = new Map();
constructor() {
}
normalizePath(targetPath) {
const resolvedPath = path.resolve(targetPath);
return resolvedPath.replaceAll(/\\/g, VirtualStorageProvider.separator);
}
relativePath(targetPath, basePath = ".") {
return path.relative(basePath, targetPath);
}
splitPath(path) {
const normalizedPath = this.normalizePath(path);
const lastSeparatorIndex = normalizedPath.lastIndexOf(VirtualStorageProvider.separator);
if (lastSeparatorIndex === -1) {
return ["", normalizedPath];
}
const dirPath = normalizedPath.substring(0, lastSeparatorIndex);
const entryName = normalizedPath.substring(lastSeparatorIndex + 1);
return [dirPath, entryName];
}
mkDir(path) {
const normalizedPath = this.normalizePath(path);
if (normalizedPath === "") {
return this.files;
}
const parts = normalizedPath.split(VirtualStorageProvider.separator).filter(p => p !== "");
let currentDir = this.files;
if (parts.length > 0 && /^[A-Za-z]:$/.test(parts[0])) {
const diskRoot = parts[0];
let diskDir = currentDir.get(diskRoot);
if (!diskDir) {
diskDir = new Map();
currentDir.set(diskRoot, diskDir);
}
else if (diskDir instanceof VirtualFile) {
throw new Error(`File ${diskRoot} already exists`);
}
currentDir = diskDir;
parts.shift();
}
for (const part of parts) {
let nextDir = currentDir.get(part);
if (nextDir instanceof VirtualFile) {
throw new Error(`File ${part} already exists`);
}
if (!nextDir) {
nextDir = new Map();
currentDir.set(part, nextDir);
}
currentDir = nextDir;
}
return currentDir;
}
rm(path) {
const normalizedPath = this.normalizePath(path);
const [parentPath, entryName] = this.splitPath(normalizedPath);
if (!entryName) {
throw new Error("Invalid path");
}
const parentDir = parentPath === "" ? this.files : this.getEntry(parentPath);
if (!parentDir) {
throw new Error(`Directory ${parentPath} does not exist`);
}
if (parentDir instanceof VirtualFile) {
throw new Error(`'${parentPath}' is a file but treated as a directory`);
}
parentDir.delete(entryName);
}
getEntry(path) {
const normalizedPath = this.normalizePath(path);
if (normalizedPath === "") {
return this.files;
}
const parts = normalizedPath.split(VirtualStorageProvider.separator).filter(p => p !== "");
let currentDir = this.files;
const entry = parts.pop();
for (const part of parts) {
const currentEntry = currentDir.get(part);
if (!currentEntry) {
throw new Error(`${normalizedPath} does not exist`);
}
if (currentEntry instanceof VirtualFile) {
throw new Error(`'${part}' in ${normalizedPath} is a file but treated as a directory`);
}
currentDir = currentEntry;
}
return currentDir.get(entry);
}
async exists(path) {
try {
return this.getEntry(path) !== undefined;
}
catch {
return false;
}
}
async isFile(path) {
try {
return this.getEntry(path) instanceof VirtualFile;
}
catch {
return false;
}
}
async readFile(filePath) {
const entry = this.getEntry(filePath);
if (entry instanceof VirtualFile) {
return entry;
}
const message = entry ? "is not a file" : "does not exist";
throw new Error(`${filePath} ${message}`);
}
async createFile(filePath, content) {
const normalizedPath = this.normalizePath(filePath);
const [dirPath, fileName] = this.splitPath(normalizedPath);
if (!fileName) {
throw new Error("Invalid path");
}
const dir = this.mkDir(dirPath);
const file = new VirtualFile(normalizedPath, content);
dir.set(fileName, file);
return file;
}
async moveFile(sourcePath, targetPath) {
const targetFile = await this.copyFile(sourcePath, targetPath);
await this.deleteFileOrDir(sourcePath);
return targetFile;
}
async copyFile(sourcePath, targetPath) {
const sourceFile = this.getEntry(sourcePath);
if (!sourceFile) {
throw new Error(`${sourcePath} does not exist`);
}
if (!(sourceFile instanceof VirtualFile)) {
throw new Error(`${sourcePath} is not a file`);
}
return await this.createFile(targetPath, sourceFile.data);
}
async isDir(path) {
try {
const entry = this.getEntry(path);
return !!entry && !(entry instanceof VirtualFile);
}
catch {
return false;
}
}
async readDir(dirPath, ignore = []) {
const normalizedPath = this.normalizePath(dirPath);
const entry = this.getEntry(normalizedPath);
if (!entry) {
throw new Error(`'${dirPath}' does not exist`);
}
if (entry instanceof VirtualFile) {
throw new Error(`'${dirPath}' is a file but treated as a directory`);
}
const separator = normalizedPath ? VirtualStorageProvider.separator : "";
const prefix = normalizedPath + separator;
let paths = Array.from(entry.keys()).map(p => this.relativePath(`${prefix}${p}`));
if (ignore.length > 0) {
paths = micromatch.not(paths, ignore, {
dot: true,
basename: false,
cwd: "",
windows: VirtualStorageProvider.separator === "/"
});
}
return paths;
}
async readDirDeep(dirPath, ignore = []) {
const normalizedPath = this.normalizePath(dirPath);
const entry = this.getEntry(normalizedPath);
if (!entry) {
throw new Error(`'${dirPath}' does not exist`);
}
if (entry instanceof VirtualFile) {
throw new Error(`'${dirPath}' is a file but treated as a directory`);
}
const separator = normalizedPath ? VirtualStorageProvider.separator : "";
const prefix = normalizedPath + separator;
let paths = Array.from(entry.keys()).map(p => this.relativePath(`${prefix}${p}`));
if (ignore.length > 0) {
paths = micromatch.not(paths, ignore, {
dot: true,
basename: false,
cwd: "",
windows: VirtualStorageProvider.separator === "/"
});
}
const result = [...paths];
for (const path of paths) {
if (await this.isDir(path)) {
result.push(...await this.readDirDeep(path, ignore));
}
}
return result;
}
async createDir(dirPath) {
const normalizedPath = this.normalizePath(dirPath);
this.mkDir(normalizedPath);
return this.relativePath(normalizedPath);
}
async mockDir(dirPath, mock) {
const normalizedPath = this.normalizePath(dirPath);
const [parentDirPath, dirName] = this.splitPath(normalizedPath);
this.mkDir(parentDirPath).set(dirName, mock);
return this.relativePath(normalizedPath);
}
async deleteFileOrDir(path) {
this.rm(path);
}
}
//# sourceMappingURL=virtual-storage-provider.js.map