UNPKG

@timothy-spaceman/multitrack-vcs

Version:

Version Control System for musicians

385 lines (321 loc) 13.6 kB
import {IFile, IStorageProvider} from "../types.js" import {createHash} from "crypto"; import micromatch from 'micromatch'; import path from "node:path"; export class VirtualFile implements IFile { path: string name: string extension: string = "" fullPath: string // path + name + extension data: Buffer constructor(fullPath: string, data: Buffer) { 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(): Promise<Buffer> { try { return this.data } catch (err: any) { throw new Error(`Failed to read file content: ${err.message}`) } } async writeData(data: Buffer): Promise<void> { try { this.data = data } catch (err: any) { throw new Error(`Failed to write file content: ${err.message}`) } } async getDataHash(algo: string = "sha256"): Promise<string> { const hash = createHash(algo) hash.update(this.data) return hash.digest("hex") } } export type VirtualDirectoryMockCallbackArgs = ["has", boolean, string] | ["get", VirtualMockedEntry | undefined, string] | ["set", VirtualMockedEntry, string, VirtualMockedEntry] | ["delete", boolean, string] | ["clear", void] | ["keys", IterableIterator<string>] | ["values", IterableIterator<VirtualMockedEntry>] | ["entries", IterableIterator<[string, VirtualMockedEntry]>] | ["forEach", void, (value: VirtualMockedEntry, path: string, map: VirtualMockedDirectory) => void, any] export class VirtualDirectoryMock { content: VirtualDirectory = new Map<string, VirtualMockedEntry>() constructor( public callback: (...args: VirtualDirectoryMockCallbackArgs) => any ) { this.initializeProxies() } initializeProxies() { const methodsToMock = ["has", "get", "set", "delete", "clear", "keys", "values", "entries", "forEach"] as const const chainableMethods = ["set"] for (const methodName of methodsToMock) { const originalMethod = this.content[methodName as keyof VirtualDirectory] as any; (this as any)[methodName] = (...args: any) => { const result = originalMethod.apply(this.content, args); this.callback(...[methodName, result, ...args] as VirtualDirectoryMockCallbackArgs); if (chainableMethods.includes(methodName)) { return this; } return result; }; } } get size() { return this.content.size } has!: (path: string) => boolean; get!: (path: string) => VirtualMockedEntry | undefined; set!: (path: string, value: VirtualMockedEntry) => this; delete!: (path: string) => boolean; clear!: () => void; keys!: () => IterableIterator<string>; values!: () => IterableIterator<VirtualMockedEntry>; entries!: () => IterableIterator<[string, VirtualMockedEntry]>; forEach!: (callback: (value: VirtualMockedEntry, path: string, map: VirtualDirectoryMock) => void, thisArg?: any) => void; } export class VirtualDirectoryOverrideMock extends VirtualDirectoryMock { initializeProxies() { const methodsToMock = ["has", "get", "set", "delete", "clear", "keys", "values", "entries", "forEach"] as const const chainableMethods = ["set"] for (const methodName of methodsToMock) { const originalMethod = this.content[methodName as keyof VirtualDirectory] as any; (this as any)[methodName] = (...args: any) => { const originalResult = originalMethod.apply(this.content, args); const result = this.callback(...[methodName, originalResult, ...args] as VirtualDirectoryMockCallbackArgs); if (chainableMethods.includes(methodName)) { return this; } return result; }; } } } export type VirtualEntry = VirtualFile | VirtualDirectory export type VirtualMockedEntry = VirtualEntry | VirtualDirectoryMock export type VirtualMockedDirectory = VirtualDirectory | VirtualDirectoryMock export type VirtualDirectory = Map<string, VirtualMockedEntry> export class VirtualStorageProvider implements IStorageProvider { static separator: string = "/" files: VirtualMockedDirectory = new Map<string, VirtualMockedEntry>() constructor() { } normalizePath(targetPath: string): string { const resolvedPath = path.resolve(targetPath) return resolvedPath.replaceAll(/\\/g, VirtualStorageProvider.separator) } relativePath(targetPath: string, basePath = ".") { return path.relative(basePath, targetPath) } splitPath(path: string): [string, string] { 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: string): VirtualMockedDirectory { 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<string, VirtualMockedEntry>(); 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<string, VirtualMockedEntry>(); currentDir.set(part, nextDir); } currentDir = nextDir; } return currentDir; } rm(path: string) { 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: string): VirtualMockedEntry | undefined { 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: string) { try { return this.getEntry(path) !== undefined; } catch { return false; } } async isFile(path: string) { try { return this.getEntry(path) instanceof VirtualFile; } catch { return false; } } async readFile(filePath: string): Promise<VirtualFile> { 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: string, content: Buffer): Promise<VirtualFile> { 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: string, targetPath: string): Promise<VirtualFile> { const targetFile = await this.copyFile(sourcePath, targetPath); await this.deleteFileOrDir(sourcePath); return targetFile; } async copyFile(sourcePath: string, targetPath: string): Promise<VirtualFile> { 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: string) { try { const entry = this.getEntry(path); return !!entry && !(entry instanceof VirtualFile); } catch { return false; } } async readDir(dirPath: string, ignore: string[] = []): Promise<string[]> { 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: string, ignore: string[] = []): Promise<string[]> { 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: string): Promise<string> { const normalizedPath = this.normalizePath(dirPath); this.mkDir(normalizedPath); return this.relativePath(normalizedPath); } async mockDir(dirPath: string, mock: VirtualMockedDirectory): Promise<string> { const normalizedPath = this.normalizePath(dirPath); const [parentDirPath, dirName] = this.splitPath(normalizedPath); this.mkDir(parentDirPath).set(dirName, mock); return this.relativePath(normalizedPath); } async deleteFileOrDir(path: string): Promise<void> { this.rm(path); } }