UNPKG

scriptable-testlab

Version:

A lightweight, efficient tool designed to manage and update scripts for Scriptable.

1,023 lines (889 loc) 29.6 kB
import * as fs from 'fs'; import * as path from 'path'; import {AbsFileManager} from 'scriptable-abstract'; import {FILE_MANAGER_ERROR_CODES, FILE_MANAGER_ERROR_MESSAGES, FileManagerError} from '../../types/errors'; import { BookmarkSource, DirectoryNode, FileManagerInstance, FileManagerOptions, FileManagerResetOptions, FileManagerState, FileNode, FileSystemEvent, FileSystemNode, isDirectoryNode, isFileNode, } from '../../types/file'; import {DEFAULT_BASE_DIRECTORIES, DEFAULT_ROOT_PATH, FileManagerType, FileUtils} from '../../utils/paths'; import {MockData} from '../data'; import {MockImage} from '../media'; /** * Mock implementation of Scriptable's FileManager. * Provides a virtual file system implementation for testing. * @implements FileManager */ export class MockFileManager extends AbsFileManager<FileManagerState> implements FileManagerInstance { private bookmarks: Map<string, {path: string; source: BookmarkSource}> = new Map(); private static localInstance: MockFileManager | null = null; private static iCloudInstance: MockFileManager | null = null; private eventListeners: ((event: FileSystemEvent) => void)[] = []; /** * @inheritdoc * Gets or creates a local file manager instance */ static local(): FileManager { if (!this.localInstance) { this.localInstance = MockFileManager.create(FileManagerType.LOCAL, { rootPath: path.join(DEFAULT_ROOT_PATH, 'local'), }); } return this.localInstance; } /** * @inheritdoc * Gets or creates an iCloud file manager instance */ static iCloud(): FileManager { if (!this.iCloudInstance) { this.iCloudInstance = MockFileManager.create(FileManagerType.ICLOUD, { rootPath: path.join(DEFAULT_ROOT_PATH, 'icloud'), }); } return this.iCloudInstance; } /** * @additional * Creates a new file manager instance */ static create(type: FileManagerType, options: FileManagerOptions = {}): MockFileManager { const instance = new MockFileManager(type, options); instance.initializeBaseDirectories(); return instance; } /** * @additional * Resets both local and iCloud instances */ static reset(options: FileManagerResetOptions = {}): void { if (this.localInstance) { this.localInstance.resetInstance(options); } if (this.iCloudInstance) { this.iCloudInstance.resetInstance(options); } if (!options.preserveInstances) { this.localInstance = null; this.iCloudInstance = null; } } /** * @additional * Reset this instance */ resetInstance(options: FileManagerResetOptions = {}): void { if (!options.preserveBookmarks) { this.bookmarks.clear(); } const baseDirectories = options.preserveBaseDirectories ? this.state.baseDirectories : {...DEFAULT_BASE_DIRECTORIES}; this.setState({ store: {}, rootPath: this.state.rootPath, baseDirectories, type: this.state.type, fileSystem: { type: 'directory', children: new Map(), metadata: { creationDate: new Date(), modificationDate: new Date(), size: 0, }, }, }); if (!options.preserveBaseDirectories) { this.initializeBaseDirectories(); } } constructor(type: FileManagerType, options: FileManagerOptions = {}) { super(); const rootPath = options.rootPath ?? DEFAULT_ROOT_PATH; const normalizedRootPath = FileUtils.normalizePath(rootPath); const baseDirectories = {...DEFAULT_BASE_DIRECTORIES}; Object.entries(options.baseDirectories ?? {}).forEach(([key, value]) => { if (typeof value === 'string' && key in DEFAULT_BASE_DIRECTORIES) { (baseDirectories as Record<string, string>)[key] = FileUtils.joinPaths(normalizedRootPath, value); } }); this.setState({ store: {}, rootPath: normalizedRootPath, baseDirectories, type: type === FileManagerType.LOCAL ? 'local' : 'icloud', fileSystem: { type: 'directory', children: new Map(), metadata: { creationDate: new Date(), modificationDate: new Date(), size: 0, }, }, }); } // Core file operations /** * @inheritdoc * Read data from a file * @throws {FileManagerError} If file does not exist or is a directory */ read(filePath: string): Data { try { const content = this.readString(filePath); return MockData.fromString(content); } catch (error) { throw this.wrapError(error, FILE_MANAGER_ERROR_CODES.IO_ERROR, filePath); } } /** * @inheritdoc * Write data to a file * @throws {FileManagerError} If parent directory does not exist */ write(filePath: string, content: Data): void { try { this.writeString(filePath, content.toRawString()); } catch (error) { throw this.wrapError(error, FILE_MANAGER_ERROR_CODES.IO_ERROR, filePath); } } /** * @inheritdoc * Read string content from a file * @throws {FileManagerError} If file does not exist or is a directory */ readString(filePath: string): string { const normalizedPath = this.resolvePath(filePath); const node = this.getNode(normalizedPath); if (!node) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.NOT_FOUND], FILE_MANAGER_ERROR_CODES.NOT_FOUND, filePath, ); } if (node.type !== 'file') { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.NOT_A_FILE], FILE_MANAGER_ERROR_CODES.NOT_A_FILE, filePath, ); } return node.content; } /** * @inheritdoc * Write string content to a file * @throws {FileManagerError} If parent directory does not exist */ writeString(filePath: string, content: string): void { const normalizedPath = this.resolvePath(filePath); const parentDir = this.ensureParentDirectory(normalizedPath); const fileName = path.basename(normalizedPath); const now = new Date(); const fileNode: FileNode = { type: 'file', content, metadata: { creationDate: now, modificationDate: now, size: content.length, isInCloud: this.state.type === 'icloud', isDownloaded: this.state.type === 'local', }, }; const existingNode = parentDir.children.get(fileName); if (existingNode?.type === 'file') { parentDir.metadata.size -= existingNode.metadata.size; } parentDir.children.set(fileName, fileNode); parentDir.metadata.modificationDate = now; parentDir.metadata.size += content.length; this.emitEvent({ type: existingNode ? 'modify' : 'create', path: normalizedPath, timestamp: now, metadata: fileNode.metadata, }); } // Image operations /** * @inheritdoc * Read image from a file * @throws {FileManagerError} If file does not exist or is a directory */ readImage(filePath: string): Image { const node = this.getNode(this.resolvePath(filePath)); if (!node) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.NOT_FOUND], FILE_MANAGER_ERROR_CODES.NOT_FOUND, filePath, ); } if (!isFileNode(node)) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.NOT_A_FILE], FILE_MANAGER_ERROR_CODES.NOT_A_FILE, filePath, ); } return new MockImage(); } /** * @inheritdoc * Write image to a file * @throws {FileManagerError} If parent directory does not exist */ writeImage(filePath: string, _image: Image): void { this.write(filePath, new MockData()); } // File system operations /** * @inheritdoc * Remove a file or directory * @throws {FileManagerError} If path does not exist */ remove(filePath: string): void { const normalizedPath = this.resolvePath(filePath); const parentPath = path.dirname(normalizedPath); const fileName = path.basename(normalizedPath); const parentNode = this.getNode(parentPath); if (!parentNode || !isDirectoryNode(parentNode)) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.NOT_FOUND], FILE_MANAGER_ERROR_CODES.NOT_FOUND, parentPath, ); } const node = parentNode.children.get(fileName); if (!node) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.NOT_FOUND], FILE_MANAGER_ERROR_CODES.NOT_FOUND, filePath, ); } // 递归删除目录内容 if (isDirectoryNode(node)) { for (const childName of node.children.keys()) { this.remove(path.join(normalizedPath, childName)); } } // 删除节点 parentNode.children.delete(fileName); // 更新父目录元数据 parentNode.metadata.modificationDate = new Date(); if (isFileNode(node)) { parentNode.metadata.size -= node.metadata.size; } // 发送删除事件 this.emitEvent({ type: 'delete', path: normalizedPath, timestamp: new Date(), metadata: node.metadata, }); } fileExists(filePath: string): boolean { return this.getNode(this.resolvePath(filePath)) !== undefined; } isDirectory(filePath: string): boolean { const node = this.getNode(this.resolvePath(filePath)); return node?.type === 'directory'; } /** * @inheritdoc * Create a directory * @throws {FileManagerError} If parent directory does not exist and createParents is false */ createDirectory(dirPath: string, createParents: boolean = false): void { // Normalize the path first const normalizedPath = this.resolvePath(dirPath); const rootPath = FileUtils.normalizePath(this.state.rootPath); // Early return for root path if (normalizedPath === rootPath) { return; } // Get parent path and check if it exists const parentPath = path.dirname(normalizedPath); const parentExists = this.fileExists(parentPath); // If parent doesn't exist and createParents is false, throw error if (!parentExists && !createParents) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.PARENT_DIRECTORY_NOT_FOUND], FILE_MANAGER_ERROR_CODES.PARENT_DIRECTORY_NOT_FOUND, parentPath, ); } // Create parent directories if needed if (!parentExists && createParents) { this.createDirectory(parentPath, true); } // Get parent node const parentNode = this.getNode(parentPath); if (!parentNode || !isDirectoryNode(parentNode)) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.NOT_A_DIRECTORY], FILE_MANAGER_ERROR_CODES.NOT_A_DIRECTORY, parentPath, ); } // Get directory name and check if it already exists const dirName = path.basename(normalizedPath); const existingNode = parentNode.children.get(dirName); // If exists but is not a directory, throw error if (existingNode && !isDirectoryNode(existingNode)) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.NOT_A_DIRECTORY], FILE_MANAGER_ERROR_CODES.NOT_A_DIRECTORY, normalizedPath, ); } // If directory already exists, return if (existingNode) { return; } // Create new directory node const now = new Date(); const newNode: DirectoryNode = { type: 'directory', children: new Map(), metadata: { creationDate: now, modificationDate: now, size: 0, isInCloud: this.state.type === 'icloud', isDownloaded: this.state.type === 'local', }, }; // Add to parent parentNode.children.set(dirName, newNode); parentNode.metadata.modificationDate = now; // Emit create event this.emitEvent({ type: 'create', path: normalizedPath, timestamp: now, metadata: newNode.metadata, }); } /** * @inheritdoc * List contents of a directory * @throws {FileManagerError} If directory does not exist */ listContents(directoryPath: string): string[] { const node = this.getNode(this.resolvePath(directoryPath)); if (!node || !isDirectoryNode(node)) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.NOT_A_DIRECTORY], FILE_MANAGER_ERROR_CODES.NOT_A_DIRECTORY, directoryPath, ); } return Array.from(node.children.keys()); } // Directory paths documentsDirectory(): string { return this.state.baseDirectories.documents; } libraryDirectory(): string { return this.state.baseDirectories.library; } cacheDirectory(): string { return this.state.baseDirectories.cache; } temporaryDirectory(): string { return this.state.baseDirectories.temporary; } // Path operations joinPath(lhs: string, rhs: string): string { return FileUtils.joinPaths(lhs, rhs); } // File operations /** * @inheritdoc * Move a file or directory * @throws {FileManagerError} If source or destination parent directory does not exist */ move(sourceFile: string, destinationFile: string): void { const normalizedSourcePath = this.resolvePath(sourceFile); const normalizedDestPath = this.resolvePath(destinationFile); const sourceParentPath = path.dirname(normalizedSourcePath); const sourceFileName = path.basename(normalizedSourcePath); const destParentPath = path.dirname(normalizedDestPath); const destFileName = path.basename(normalizedDestPath); const sourceParentNode = this.getNode(sourceParentPath); if (!sourceParentNode || !isDirectoryNode(sourceParentNode)) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.NOT_FOUND], FILE_MANAGER_ERROR_CODES.NOT_FOUND, sourceParentPath, ); } const sourceNode = sourceParentNode.children.get(sourceFileName); if (!sourceNode) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.NOT_FOUND], FILE_MANAGER_ERROR_CODES.NOT_FOUND, sourceFile, ); } const destParentNode = this.getNode(destParentPath); if (!destParentNode || !isDirectoryNode(destParentNode)) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.NOT_FOUND], FILE_MANAGER_ERROR_CODES.NOT_FOUND, destParentPath, ); } sourceParentNode.children.delete(sourceFileName); destParentNode.children.set(destFileName, sourceNode); const now = new Date(); sourceParentNode.metadata.modificationDate = now; destParentNode.metadata.modificationDate = now; if (isFileNode(sourceNode)) { sourceParentNode.metadata.size -= sourceNode.metadata.size; destParentNode.metadata.size += sourceNode.metadata.size; } } /** * @inheritdoc * Copy a file or directory * @throws {FileManagerError} If source file does not exist or destination parent directory does not exist */ copy(sourceFile: string, destinationFile: string): void { const normalizedSourcePath = this.resolvePath(sourceFile); const normalizedDestPath = this.resolvePath(destinationFile); const sourceNode = this.getNode(normalizedSourcePath); if (!sourceNode) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.NOT_FOUND], FILE_MANAGER_ERROR_CODES.NOT_FOUND, sourceFile, ); } const clonedNode: FileSystemNode = isDirectoryNode(sourceNode) ? { type: 'directory', children: new Map(sourceNode.children), metadata: { ...sourceNode.metadata, creationDate: new Date(), modificationDate: new Date(), }, } : { type: 'file', content: sourceNode.content, metadata: { ...sourceNode.metadata, creationDate: new Date(), modificationDate: new Date(), }, }; this.createNode(normalizedDestPath, clonedNode); } // Bookmark operations bookmarkedPath(name: string): string { const bookmark = this.bookmarks.get(name); if (!bookmark) { throw new Error('Bookmark not found'); } return bookmark.path; } bookmarkExists(name: string): boolean { return this.bookmarks.has(name); } createBookmark(name: string, path: string): void { this.bookmarks.set(name, { path, source: this.state.type === 'local' ? 'local' : 'icloud', }); } removeBookmark(name: string): void { this.bookmarks.delete(name); } allFileBookmarks(): Array<{name: string; path: string; source: BookmarkSource}> { return Array.from(this.bookmarks.entries()).map(([name, data]) => ({ name, path: data.path, source: data.source, })); } // Cloud operations downloadFileFromiCloud(filePath: string): Promise<void> { const node = this.getNode(this.resolvePath(filePath)); if (!node) { throw new Error('File not found'); } node.metadata.isDownloaded = true; return Promise.resolve(); } isFileDownloaded(filePath: string): boolean { const node = this.getNode(filePath); return node?.metadata.isDownloaded ?? false; } isFileStoredIniCloud(filePath: string): boolean { const node = this.getNode(this.resolvePath(filePath)); return node?.metadata.isInCloud ?? false; } // Metadata operations modificationDate(filePath: string): Date { const node = this.getNode(filePath); if (!node) { throw new Error('File not found'); } return node.metadata.modificationDate; } creationDate(filePath: string): Date { const node = this.getNode(filePath); if (!node) { throw new Error('File not found'); } return node.metadata.creationDate; } fileSize(filePath: string): number { const node = this.getNode(filePath); if (!node) { throw new Error('File not found'); } return node.metadata.size; } // File information fileName(filePath: string, includeFileExtension: boolean = true): string { const name = FileUtils.getFileName(filePath); if (!includeFileExtension) { const extIndex = name.lastIndexOf('.'); return extIndex > 0 ? name.substring(0, extIndex) : name; } return name; } fileExtension(filePath: string): string { return FileUtils.getFileExtension(filePath).slice(1); } getUTI(filePath: string): string { const extension = this.fileExtension(filePath); return FileUtils.getUTI(extension); } // Extended Attributes operations writeExtendedAttribute(filePath: string, attributeName: string, value: string): void { const node = this.getNode(this.resolvePath(filePath)); if (!node) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.FILE_NOT_FOUND], FILE_MANAGER_ERROR_CODES.FILE_NOT_FOUND, filePath, ); } if (!node.metadata.extendedAttributes) { node.metadata.extendedAttributes = new Map(); } node.metadata.extendedAttributes.set(attributeName, value); } readExtendedAttribute(filePath: string, attributeName: string): string { const node = this.getNode(this.resolvePath(filePath)); if (!node) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.FILE_NOT_FOUND], FILE_MANAGER_ERROR_CODES.FILE_NOT_FOUND, filePath, ); } const value = node.metadata.extendedAttributes?.get(attributeName); if (value === undefined) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.EXTENDED_ATTRIBUTE_NOT_FOUND], FILE_MANAGER_ERROR_CODES.EXTENDED_ATTRIBUTE_NOT_FOUND, ); } return value; } allExtendedAttributes(filePath: string): string[] { const node = this.getNode(this.resolvePath(filePath)); if (!node) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.FILE_NOT_FOUND], FILE_MANAGER_ERROR_CODES.FILE_NOT_FOUND, filePath, ); } return Array.from(node.metadata.extendedAttributes?.keys() ?? []); } removeExtendedAttribute(filePath: string, attributeName: string): void { const node = this.getNode(this.resolvePath(filePath)); if (!node) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.FILE_NOT_FOUND], FILE_MANAGER_ERROR_CODES.FILE_NOT_FOUND, filePath, ); } if (!node.metadata.extendedAttributes?.delete(attributeName)) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.EXTENDED_ATTRIBUTE_NOT_FOUND], FILE_MANAGER_ERROR_CODES.EXTENDED_ATTRIBUTE_NOT_FOUND, ); } } // Tags operations addTag(filePath: string, tag: string): void { const node = this.getNode(this.resolvePath(filePath)); if (!node) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.FILE_NOT_FOUND], FILE_MANAGER_ERROR_CODES.FILE_NOT_FOUND, filePath, ); } if (!node.metadata.tags) { node.metadata.tags = new Set(); } node.metadata.tags.add(tag); } removeTag(filePath: string, tag: string): void { const node = this.getNode(this.resolvePath(filePath)); if (!node) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.FILE_NOT_FOUND], FILE_MANAGER_ERROR_CODES.FILE_NOT_FOUND, filePath, ); } if (!node.metadata.tags?.delete(tag)) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.TAG_NOT_FOUND], FILE_MANAGER_ERROR_CODES.TAG_NOT_FOUND, ); } } allTags(filePath: string): string[] { const node = this.getNode(this.resolvePath(filePath)); if (!node) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.FILE_NOT_FOUND], FILE_MANAGER_ERROR_CODES.FILE_NOT_FOUND, filePath, ); } return Array.from(node.metadata.tags ?? []); } // Event handling /** * @additional * Add a file system event listener */ addEventListener(listener: (event: FileSystemEvent) => void): void { this.eventListeners.push(listener); } /** * @additional * Remove a file system event listener */ removeEventListener(listener: (event: FileSystemEvent) => void): void { const index = this.eventListeners.indexOf(listener); if (index !== -1) { this.eventListeners.splice(index, 1); } } /** * @additional * Emit a file system event */ private emitEvent(event: FileSystemEvent): void { this.eventListeners.forEach(listener => listener(event)); } // Error handling /** * @additional * Wrap an error in a FileManagerError */ private wrapError(error: unknown, code: keyof typeof FILE_MANAGER_ERROR_CODES, path?: string): FileManagerError { if (error instanceof FileManagerError) { return error; } const message = FILE_MANAGER_ERROR_MESSAGES[code]; return new FileManagerError(message, code, path, error instanceof Error ? error : undefined); } // Private helper methods /** * @additional * Initialize base directories */ private initializeBaseDirectories(): void { if (!fs.existsSync(this.state.rootPath)) { fs.mkdirSync(this.state.rootPath, {recursive: true}); } Object.values(this.state.baseDirectories).forEach(dirPath => { const normalizedPath = FileUtils.normalizePath(dirPath); if (!fs.existsSync(normalizedPath)) { fs.mkdirSync(normalizedPath, {recursive: true}); } const parts = normalizedPath.split('/').filter(Boolean); let current: DirectoryNode = this.state.fileSystem; for (const part of parts) { if (!current.children.has(part)) { const newNode: DirectoryNode = { type: 'directory', children: new Map(), metadata: { creationDate: new Date(), modificationDate: new Date(), size: 0, }, }; current.children.set(part, newNode); } const nextNode = current.children.get(part); if (!nextNode || !isDirectoryNode(nextNode)) { throw new Error(`Path component ${part} exists but is not a directory`); } current = nextNode; } }); } /** * @additional * Resolve a path to its normalized form */ private resolvePath(filePath: string): string { // Normalize both paths using platform-specific handling const normalizedPath = FileUtils.normalizePath(filePath); const rootPath = FileUtils.normalizePath(this.state.rootPath); // If the path is already the root path, return it directly if (normalizedPath === rootPath) { return rootPath; } // Handle absolute paths if (path.isAbsolute(normalizedPath)) { // Check if it's a base directory const isBaseDir = Object.values(this.state.baseDirectories).some( dir => FileUtils.normalizePath(dir) === normalizedPath, ); // If it's a base directory, return as is if (isBaseDir) { return normalizedPath; } // Check if path is outside root const relativePath = path.relative(rootPath, normalizedPath); if (relativePath.startsWith('..')) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.OUTSIDE_ROOT], FILE_MANAGER_ERROR_CODES.OUTSIDE_ROOT, ); } // Prevent path duplication by getting relative path if it's under root if (normalizedPath.startsWith(rootPath)) { const relPath = path.relative(rootPath, normalizedPath); return path.join(rootPath, relPath).replace(/\\/g, '/'); } return normalizedPath; } // Handle relative paths const resolvedPath = path.join(rootPath, normalizedPath); return FileUtils.normalizePath(resolvedPath); } /** * @additional * Get a node from the file system */ private getNode(filePath: string): FileSystemNode | undefined { const normalizedPath = FileUtils.normalizePath(filePath); const rootPath = FileUtils.normalizePath(this.state.rootPath); if (normalizedPath === rootPath) { return this.state.fileSystem; } // Get the path relative to root if it's a subdirectory of root let relativePath: string; if (normalizedPath.startsWith(rootPath)) { relativePath = path.relative(rootPath, normalizedPath); } else { relativePath = path.relative(rootPath, path.resolve(rootPath, normalizedPath)); } const parts = relativePath.split(/[/\\]/).filter(Boolean); let current: DirectoryNode = this.state.fileSystem; for (const part of parts) { const next = current.children.get(part); if (!next) return undefined; // If we're not at the last part, the node must be a directory if (part !== parts[parts.length - 1] && !isDirectoryNode(next)) { return undefined; } if (isDirectoryNode(next)) { current = next; } else { return next; } } return current; } /** * @additional * Ensure a parent directory exists */ private ensureParentDirectory(filePath: string): DirectoryNode { const normalizedPath = this.resolvePath(filePath); const parentPath = path.dirname(normalizedPath); if (!this.fileExists(parentPath)) { this.createDirectory(parentPath, true); } const node = this.getNode(parentPath); if (!node || !isDirectoryNode(node)) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.NOT_A_DIRECTORY], FILE_MANAGER_ERROR_CODES.NOT_A_DIRECTORY, parentPath, ); } return node; } private createNode(filePath: string, node: FileSystemNode, createParents: boolean = true): void { const normalizedPath = this.resolvePath(filePath); const parentPath = path.dirname(normalizedPath); const fileName = path.basename(normalizedPath); if (parentPath !== this.state.rootPath && !this.fileExists(parentPath)) { if (createParents) { this.createDirectory(parentPath, true); } else { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.NOT_FOUND], FILE_MANAGER_ERROR_CODES.NOT_FOUND, parentPath, ); } } const parentNode = this.getNode(parentPath); if (!parentNode || !isDirectoryNode(parentNode)) { throw new FileManagerError( FILE_MANAGER_ERROR_MESSAGES[FILE_MANAGER_ERROR_CODES.NOT_A_DIRECTORY], FILE_MANAGER_ERROR_CODES.NOT_A_DIRECTORY, parentPath, ); } if (this.state.type === 'icloud') { node.metadata = { ...node.metadata, isInCloud: true, isDownloaded: false, }; } parentNode.children.set(fileName, node); parentNode.metadata.modificationDate = new Date(); if (isFileNode(node)) { parentNode.metadata.size += node.metadata.size; } } }