import { PathUtils } from "./PathUtils.js"; export class FileSaveService { constructor() { this.fallbackHandles = new Map(); } async saveFile(projectStore, path, content) { const normalizedPath = PathUtils.normalizeRelative(path); const resolvedPath = projectStore.resolveFilePath(normalizedPath) || normalizedPath; if (projectStore.rootHandle) { await this.#writeWithRootHandle(projectStore.rootHandle, resolvedPath, content); return { mode: "inplace", path: resolvedPath }; } const knownHandle = projectStore.getFileHandle(resolvedPath) || this.fallbackHandles.get(resolvedPath); if (knownHandle && typeof knownHandle.createWritable === "function") { await this.#writeWithFileHandle(knownHandle, content); return { mode: "inplace", path: resolvedPath }; } if (typeof window.showSaveFilePicker === "function") { const pickerOptions = { suggestedName: PathUtils.basename(resolvedPath), id: this.#buildProjectSaveId(projectStore) }; const startInHandle = projectStore.rootHandle || knownHandle || this.fallbackHandles.get(resolvedPath); if (startInHandle) pickerOptions.startIn = startInHandle; const handle = await window.showSaveFilePicker(pickerOptions); await this.#writeWithFileHandle(handle, content); this.fallbackHandles.set(resolvedPath, handle); return { mode: "save_as", path: resolvedPath }; } this.#downloadFile(resolvedPath, content); return { mode: "download", path: resolvedPath }; } async saveExistingFile(projectStore, path, content) { const normalizedPath = PathUtils.normalizeRelative(path); const resolvedPath = projectStore.resolveFilePath(normalizedPath) || normalizedPath; if (projectStore.rootHandle) { await this.#writeWithRootHandle(projectStore.rootHandle, resolvedPath, content); return { mode: "inplace", path: resolvedPath }; } const knownHandle = projectStore.getFileHandle(resolvedPath) || this.fallbackHandles.get(resolvedPath); if (knownHandle && typeof knownHandle.createWritable === "function") { await this.#writeWithFileHandle(knownHandle, content); return { mode: "inplace", path: resolvedPath }; } throw new Error("Нет доступа к существующему файлу для записи без выбора новой директории."); } async saveNewFileWithPicker(projectStore, suggestedPath, content) { if (typeof window.showSaveFilePicker !== "function") { throw new Error("showSaveFilePicker is not supported"); } const normalizedPath = PathUtils.normalizeRelative(suggestedPath); const pickerOptions = { suggestedName: PathUtils.basename(normalizedPath), id: this.#buildProjectSaveId(projectStore) }; if (projectStore.rootHandle) pickerOptions.startIn = projectStore.rootHandle; const handle = await window.showSaveFilePicker(pickerOptions); await this.#writeWithFileHandle(handle, content); const resolvedPath = await this.#resolveRelativePath(projectStore.rootHandle, handle); if (!resolvedPath) { throw new Error("Выберите путь внутри директории проекта."); } this.fallbackHandles.set(resolvedPath, handle); return { mode: "save_as", path: resolvedPath }; } async deleteFile(projectStore, path) { const normalizedPath = PathUtils.normalizeRelative(path); const resolvedPath = projectStore.resolveFilePath(normalizedPath) || normalizedPath; if (!projectStore.rootHandle) return false; const parts = resolvedPath.split("/"); const fileName = parts.pop(); let dir = projectStore.rootHandle; for (const part of parts) { dir = await dir.getDirectoryHandle(part); } await dir.removeEntry(fileName); return true; } async createDirectory(projectStore, path) { const normalizedPath = PathUtils.normalizeRelative(path); if (!projectStore.rootHandle) throw new Error("Нет доступа к директории проекта."); const parts = normalizedPath.split("/"); let dir = projectStore.rootHandle; for (const part of parts) { dir = await dir.getDirectoryHandle(part, { create: true }); } return true; } async deleteDirectory(projectStore, path) { const normalizedPath = PathUtils.normalizeRelative(path); if (!projectStore.rootHandle) throw new Error("Нет доступа к директории проекта."); const parts = normalizedPath.split("/"); const dirName = parts.pop(); let parent = projectStore.rootHandle; for (const part of parts) { parent = await parent.getDirectoryHandle(part); } await parent.removeEntry(dirName, { recursive: true }); return true; } async renameDirectory(projectStore, oldPath, newPath) { const normalizedOld = PathUtils.normalizeRelative(oldPath); const normalizedNew = PathUtils.normalizeRelative(newPath); if (!projectStore.rootHandle) throw new Error("Нет доступа к директории проекта."); if (normalizedOld === normalizedNew) return true; if (normalizedNew.startsWith(`${normalizedOld}/`)) { throw new Error("Нельзя переместить папку внутрь самой себя."); } const sourceDir = await this.#getDirectoryHandleByPath(projectStore.rootHandle, normalizedOld, false); const newParts = normalizedNew.split("/"); const newName = newParts.pop(); const newParentPath = newParts.join("/"); const targetParent = await this.#getDirectoryHandleByPath(projectStore.rootHandle, newParentPath, true); const targetDir = await targetParent.getDirectoryHandle(newName, { create: true }); await this.#copyDirectoryRecursive(sourceDir, targetDir); await this.deleteDirectory(projectStore, normalizedOld); return true; } async #writeWithRootHandle(rootHandle, path, content) { const parts = path.split("/"); const fileName = parts.pop(); let dir = rootHandle; for (const part of parts) { dir = await dir.getDirectoryHandle(part, { create: true }); } const handle = await dir.getFileHandle(fileName, { create: true }); await this.#writeWithFileHandle(handle, content); } async #getDirectoryHandleByPath(rootHandle, path, createMissing) { if (!path) return rootHandle; let dir = rootHandle; const parts = path.split("/").filter(Boolean); for (const part of parts) { dir = await dir.getDirectoryHandle(part, { create: createMissing }); } return dir; } async #copyDirectoryRecursive(sourceDir, targetDir) { for await (const [entryName, entryHandle] of sourceDir.entries()) { if (entryHandle.kind === "directory") { const childTarget = await targetDir.getDirectoryHandle(entryName, { create: true }); await this.#copyDirectoryRecursive(entryHandle, childTarget); continue; } const sourceFile = await entryHandle.getFile(); const targetFile = await targetDir.getFileHandle(entryName, { create: true }); const writable = await targetFile.createWritable(); await writable.write(sourceFile); await writable.close(); } } async #writeWithFileHandle(handle, content) { const writable = await handle.createWritable(); await writable.write(content); await writable.close(); } #downloadFile(path, content) { const blob = new Blob([content], { type: "text/plain;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = PathUtils.basename(path); a.style.display = "none"; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } #buildProjectSaveId(projectStore) { const projectName = projectStore?.rootNode?.name || "project"; const normalized = projectName.toLowerCase().replaceAll(/[^a-z0-9_-]/g, "-").replaceAll(/-+/g, "-"); return `save-${normalized || "project"}`; } async #resolveRelativePath(rootHandle, fileHandle) { if (!rootHandle || typeof rootHandle.resolve !== "function") return fileHandle?.name || ""; const parts = await rootHandle.resolve(fileHandle); if (!Array.isArray(parts) || !parts.length) return ""; return PathUtils.normalizeRelative(parts.join("/")); } }