diff --git a/service/file_sync_service.py b/service/file_sync_service.py index 84ac2f8..724abd3 100644 --- a/service/file_sync_service.py +++ b/service/file_sync_service.py @@ -6,6 +6,7 @@ from aiohttp import web import json import uuid from pathlib import Path +from .twoway_sync_folder_service import * @server.PromptServer.instance.routes.post('/workspace/create_workflow_file') async def create_workflow_file(request): diff --git a/service/twoway_sync_folder_service.py b/service/twoway_sync_folder_service.py new file mode 100644 index 0000000..5a98649 --- /dev/null +++ b/service/twoway_sync_folder_service.py @@ -0,0 +1,66 @@ +import asyncio +import shutil +from aiohttp import web +import os +from pathlib import Path +import server + +@server.PromptServer.instance.routes.post('/workspace/folder/create') +async def create_folder(request): + reqJson = await request.json() + data = await asyncio.to_thread(create_folder_sync, reqJson) + return web.json_response(data, content_type='application/json') + +def create_folder_sync(reqJson): + folder_path = reqJson.get('path') + try: + Path(folder_path).mkdir(parents=True, exist_ok=True) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + +@server.PromptServer.instance.routes.post('/workspace/folder/delete') +async def delete_folder(request): + reqJson = await request.json() + data = await asyncio.to_thread(delete_folder_sync, reqJson) + return web.json_response(data, content_type='application/json') + +def delete_folder_sync(reqJson): + folder_path = reqJson.get('path') + try: + shutil.rmtree(folder_path) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + +@server.PromptServer.instance.routes.post('/workspace/folder/rename') +async def rename_folder(request): + reqJson = await request.json() + data = await asyncio.to_thread(rename_folder_sync, reqJson) + return web.json_response(data, content_type='application/json') + +def rename_folder_sync(reqJson): + current_path = reqJson.get('absPath') + new_name = reqJson.get('newName') + try: + new_path = Path(current_path).parent / new_name + Path(current_path).rename(new_path) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + +@server.PromptServer.instance.routes.post('/workspace/folder/move') +async def move_folder(request): + reqJson = await request.json() + data = await asyncio.to_thread(move_folder_sync, reqJson) + return web.json_response(data, content_type='application/json') + +def move_folder_sync(reqJson): + print("🍻Move folder:", reqJson) + current_path = reqJson.get('folder') + new_path = reqJson.get('newParentPath') + try: + shutil.move(current_path, new_path) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} diff --git a/ui/src/RecentFilesDrawer/FilesListFolderItem.tsx b/ui/src/RecentFilesDrawer/FilesListFolderItem.tsx index c1c683d..a7a1b7e 100644 --- a/ui/src/RecentFilesDrawer/FilesListFolderItem.tsx +++ b/ui/src/RecentFilesDrawer/FilesListFolderItem.tsx @@ -75,6 +75,10 @@ export default memo(function FilesListFolderItem({ folder }: Props) { setIsActive(false); }; useEffect(() => { + if (!isCollapsed) { + console.log("folder.unisCollapse", folder); + } + if (!!folder.isCollapse === isCollapsed) return; foldersTable?.update(folder.id, { isCollapse: isCollapsed, diff --git a/ui/src/apis/TwowaySyncApi.ts b/ui/src/apis/TwowaySyncApi.ts index 84ceb43..a5d17d6 100644 --- a/ui/src/apis/TwowaySyncApi.ts +++ b/ui/src/apis/TwowaySyncApi.ts @@ -1,8 +1,5 @@ import { updateFile } from "../Api"; -import { - COMFYSPACE_TRACKING_FIELD_NAME, - LEGACY_COMFYSPACE_TRACKING_FIELD_NAME, -} from "../const"; +import { COMFYSPACE_TRACKING_FIELD_NAME } from "../const"; import { userSettingsTable } from "../db-tables/WorkspaceDB"; import { indexdb } from "../db-tables/indexdb"; import { Workflow } from "../types/dbTypes"; @@ -181,11 +178,10 @@ export async function scanLocalFiles( "Content-Type": "application/json", }, body: JSON.stringify({ - path, + path: sanitizeAbsPath(path), }), }); const result = await response.json(); - console.log("scanLocalNewFiles", result); return result; } catch (error) { console.error("Error scan local new files:", error); diff --git a/ui/src/apis/TwowaySyncFolderApi.ts b/ui/src/apis/TwowaySyncFolderApi.ts new file mode 100644 index 0000000..ff85f7a --- /dev/null +++ b/ui/src/apis/TwowaySyncFolderApi.ts @@ -0,0 +1,110 @@ +import { userSettingsTable } from "../db-tables/WorkspaceDB"; +import { Folder } from "../types/dbTypes"; +import { sanitizeAbsPath, sanitizeRelPath } from "../utils/OsPathUtils"; + +export namespace TwowayFolderSyncAPI { + export async function genFolderRelPath({ + parentFolderID, + name, + }: { + parentFolderID: string | null; + name: string; + }): Promise { + return sanitizeRelPath(`${parentFolderID ?? ""}/${name}`); + } + export async function genFolderAbsPath({ + parentFolderID, + name, + }: Folder): Promise { + const myWorkflowsDir = + await userSettingsTable?.getSetting("myWorkflowsDir"); + const absPath = sanitizeAbsPath( + `${myWorkflowsDir}/${parentFolderID ?? ""}/${name}`, + ); + return absPath; + } + export async function genAbsPathByRelPath(relPath: string): Promise { + const myWorkflowsDir = + await userSettingsTable?.getSetting("myWorkflowsDir"); + const absPath = sanitizeAbsPath(`${myWorkflowsDir}/${relPath}`); + return absPath; + } + export async function createFolder(folder: Folder) { + console.log("🥳savecreateFolder", folder); + const abs = await genFolderAbsPath(folder); + await fetch("/workspace/folder/create", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + path: abs, + }), + }); + } + export async function moveFolder( + folderToBeMoved: string, + newParentPath: string, + ) { + console.log("🥳moveFolder", folderToBeMoved, newParentPath); + const absPath = await genAbsPathByRelPath(folderToBeMoved); + const absNewParentPath = await genAbsPathByRelPath(newParentPath); + const resp = await fetch("/workspace/folder/move", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + folder: absPath, + newParentPath: absNewParentPath, + }), + }); + const result = await resp.json(); + if (result.error) { + alert("Failed to move folder: " + result.error); + } + return; + } + export async function deleteFolder(folder: Folder) { + const absPath = await genFolderAbsPath(folder); + try { + const response = await fetch("/workspace/folder/delete", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + path: absPath, + }), + }); + const result = await response.text(); + return result; + } catch (error) { + console.error("Error deleting file:", error); + } + } + export async function renameFolder( + folderRelPath: string, + newName: string, + ): Promise { + const oldAbsPath = await genAbsPathByRelPath(folderRelPath); + try { + const response = await fetch("/workspace/folder/rename", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + absPath: oldAbsPath, + newName: newName, + }), + }); + const result = await response.json(); + if (result.error) { + alert("Failed to rename folder: " + result.error); + } + } catch (error) { + console.error("Error renaming file:", error); + } + } +} diff --git a/ui/src/db-tables/DiskFileUtils.ts b/ui/src/db-tables/DiskFileUtils.ts index 022daa9..17983e6 100644 --- a/ui/src/db-tables/DiskFileUtils.ts +++ b/ui/src/db-tables/DiskFileUtils.ts @@ -33,6 +33,7 @@ export async function deleteJsonFileMyWorkflows(workflow: Workflow) { return; } const file_path = await generateFilePath(workflow); + console.log("deleteJsonFileMyWorkflows", file_path); file_path != null && deleteFile(file_path); } diff --git a/ui/src/db-tables/FoldersTable.ts b/ui/src/db-tables/FoldersTable.ts index c631070..b8f8c4a 100644 --- a/ui/src/db-tables/FoldersTable.ts +++ b/ui/src/db-tables/FoldersTable.ts @@ -1,11 +1,12 @@ import { deleteLocalDiskFolder } from "../Api"; -import { workflowsTable } from "./WorkspaceDB"; +import { userSettingsTable, workflowsTable } from "./WorkspaceDB"; import { EFlowOperationType, Folder } from "../types/dbTypes"; import { validateOrSaveAllJsonFileMyWorkflows } from "../utils"; import { v4 as uuidv4 } from "uuid"; import { TableBase } from "./TableBase"; import { indexdb } from "./indexdb"; import { generateFolderPath } from "./DiskFileUtils"; +import { TwowayFolderSyncAPI } from "../apis/TwowaySyncFolderApi"; export class FoldersTable extends TableBase { static readonly TABLE_NAME = "folders"; @@ -37,10 +38,23 @@ export class FoldersTable extends TableBase { }; await indexdb.folders.add(folder); this.saveDiskDB(); + if (await userSettingsTable?.getSetting("twoWaySync")) { + await TwowayFolderSyncAPI.createFolder(folder); + } return folder; } public async update(id: string, input: Partial) { + const twoWaySyncEnabled = await userSettingsTable?.getSetting("twoWaySync"); + if (twoWaySyncEnabled) { + if ("parentFolderID" in input) { + await TwowayFolderSyncAPI.moveFolder(id, input["parentFolderID"] ?? ""); + } + if ("name" in input) { + await TwowayFolderSyncAPI.renameFolder(id, input["name"] ?? ""); + } + return null; + } const folder = await this.get(id); if (folder == null) { return null; @@ -75,8 +89,7 @@ export class FoldersTable extends TableBase { id: string, flowOperationType: EFlowOperationType = EFlowOperationType.DELETE, ) { - const folderPath = await generateFolderPath(id); - + const folder = await indexdb.folders.get(id); /** * When deleting a folder, if there are files in the folder * Breadth traverse all nested folders, find all files, move to root directory or delete as needed. @@ -115,7 +128,7 @@ export class FoldersTable extends TableBase { } } - folderPath && (await deleteLocalDiskFolder(folderPath)); + folder && (await TwowayFolderSyncAPI.deleteFolder(folder)); this.saveDiskDB(); } @@ -138,65 +151,4 @@ export class FoldersTable extends TableBase { } return newFlowName; } - public async genRelativePath(id: string): Promise { - let currentFolderId: string | null | undefined = id; - const pathSegments: string[] = []; - - while (currentFolderId != null) { - const folder = await this.get(currentFolderId); - if (!folder) { - console.error(`Folder with ID <${currentFolderId}> not found`); - return null; - } - // Prepend the current folder name to the path segments - pathSegments.unshift(folder.name); - - // Move up to the parent folder for the next iteration - currentFolderId = folder.parentFolderID; - } - - return pathSegments.join("/"); - } - - // given a relative path, create the folder structure if not exist, and return the folder - public async putWithRelativePath( - relativePath: string, - ): Promise { - console.log("putWithRelativePath", relativePath); - if (relativePath === "") { - // root folder - return null; - } - const pathSegments = relativePath.split("/"); - let parentFolderID: string | null = null; - let folder: Folder | null = null; - for (const name of pathSegments) { - if (name === "") { - console.warn('Invalid folder name "" in path', relativePath); - continue; - } - folder = - (await indexdb.folders - .where({ name }) - .filter((f) => { - return f.parentFolderID == parentFolderID; - }) - .first()) ?? null; - if (folder == null) { - const newFolder = await this.add({ - name, - parentFolderID, - updateTime: Date.now(), - type: "folder", - }); - folder = (await this.get(newFolder.id)) ?? null; - } - if (folder == null) { - console.error("Failed to create folder", name); - throw new Error("Failed to create folder"); - } - parentFolderID = folder.id; - } - return folder; - } } diff --git a/ui/src/db-tables/WorkflowsTable.ts b/ui/src/db-tables/WorkflowsTable.ts index 3212af6..9f30d5c 100644 --- a/ui/src/db-tables/WorkflowsTable.ts +++ b/ui/src/db-tables/WorkflowsTable.ts @@ -184,11 +184,12 @@ export class WorkflowsTable extends TableBase { const twoWaySyncEnabled = await userSettingsTable?.getSetting("twoWaySync"); // renamed file or moved file folder - await deleteJsonFileMyWorkflows(before); if (twoWaySyncEnabled) { await TwowaySyncAPI.saveWorkflow(after); + await TwowaySyncAPI.deleteWorkflow(before); } else { await saveJsonFileMyWorkflows(after); + await deleteJsonFileMyWorkflows(before); } return; } @@ -237,8 +238,15 @@ export class WorkflowsTable extends TableBase { public async deleteFlow(id: string) { const workflow = await this.get(id); + if (workflow) { - deleteJsonFileMyWorkflows({ ...workflow }); + const twoWaySyncEnabled = + await userSettingsTable?.getSetting("twoWaySync"); + if (twoWaySyncEnabled) { + await TwowaySyncAPI.deleteWorkflow(workflow); + } else { + deleteJsonFileMyWorkflows({ ...workflow }); + } } //add to IndexDB await indexdb.workflows.delete(id); @@ -248,7 +256,14 @@ export class WorkflowsTable extends TableBase { public async batchDeleteFlow(ids: string[]) { const workflowList = (await indexdb.workflows.bulkGet(ids)) as Workflow[]; for (const flow of workflowList) { - flow && (await deleteJsonFileMyWorkflows(flow)); + if (!flow) return; + const twoWaySyncEnabled = + await userSettingsTable?.getSetting("twoWaySync"); + if (twoWaySyncEnabled) { + await TwowaySyncAPI.deleteWorkflow(flow); + } else { + deleteJsonFileMyWorkflows({ ...flow }); + } } await indexdb.workflows.bulkDelete(ids); this.saveDiskDB(); diff --git a/ui/src/utils/twowaySyncUtils.ts b/ui/src/utils/twowaySyncUtils.ts index b1572f3..9c0e484 100644 --- a/ui/src/utils/twowaySyncUtils.ts +++ b/ui/src/utils/twowaySyncUtils.ts @@ -1,3 +1,4 @@ +import { ESortTypes } from "../RecentFilesDrawer/types"; import { ScanLocalFile, ScanLocalFolder, @@ -6,10 +7,12 @@ import { import { isFolder, userSettingsTable } from "../db-tables/WorkspaceDB"; import { indexdb } from "../db-tables/indexdb"; import { Folder, Workflow } from "../types/dbTypes"; +import { sortFileItem } from "../utils"; import { sanitizeAbsPath } from "./OsPathUtils"; export async function scanMyWorkflowsDir( parentFolderID: string | null, + sortBy?: ESortTypes, ): Promise<(Workflow | Folder)[]> { const myWorkflowsDir = await userSettingsTable?.getSetting("myWorkflowsDir"); if (myWorkflowsDir == null || myWorkflowsDir === "") { @@ -30,9 +33,9 @@ export async function scanMyWorkflowsDir( if (scanFileIsFolder(file)) { // is folder const folder: Folder = { - id: file.name, + id: relPath, name: file.name, - parentFolderID: relPath, + parentFolderID: parentRelPath, type: "folder", createTime: Date.now(), updateTime: Date.now(), @@ -49,7 +52,8 @@ export async function scanMyWorkflowsDir( ...existingWorkflow, id: file.id, filePath: absPath, - json: "{}", //we don't need to load json data for file list display + lastSavedJson: file.json ?? "{}", + json: file.json ?? "{}", name: fileName.replace(".json", ""), parentFolderID: parentRelPath, }; @@ -61,7 +65,8 @@ export async function scanMyWorkflowsDir( indexdb.workflows.bulkPut( result.filter((item) => !isFolder(item)) as Workflow[], ); - return result.filter((item) => item != null) as (Workflow | Folder)[]; + const all = result.filter((item) => item != null) as (Workflow | Folder)[]; + return sortFileItem(all, sortBy ?? ESortTypes.RECENTLY_MODIFIED); } function scanFileIsFolder( scanFile: ScanLocalFile | ScanLocalFolder,