UNPKG

@timothy-spaceman/multitrack-vcs

Version:

Version Control System for musicians

313 lines 11 kB
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