213 lines
8.2 KiB
JavaScript
213 lines
8.2 KiB
JavaScript
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("/"));
|
|
}
|
|
}
|