feat: nested folder deletion logic reconstruction

This commit is contained in:
yanle
2024-01-21 16:49:44 +08:00
parent d6268b8411
commit 66900a6c3c
7 changed files with 186 additions and 80 deletions
+15 -1
View File
@@ -3,6 +3,7 @@ from aiohttp import web
import aiohttp
import requests
import folder_paths
import shutil
import os
import sys
import threading
@@ -303,4 +304,17 @@ async def scan_local_new_files(request):
if len(folder['list']) > 0:
folderList.append(folder)
return web.Response(text=json.dumps({'fileList': fileList, 'folderList': folderList}), content_type='application/json')
return web.Response(text=json.dumps({'fileList': fileList, 'folderList': folderList}), content_type='application/json')
@server.PromptServer.instance.routes.post("/workspace/delete_folder")
async def delete_folder(request):
data = await request.json()
folder_path = data['folder_path']
if os.path.exists(folder_path):
shutil.rmtree(folder_path)
return web.Response(text="Successfully deleted folder: {folder_path}")
else:
return web.Response(text="folder not found: {folder_path}", status=404)
+23 -2
View File
@@ -171,8 +171,29 @@ export async function getAllModelsList() {
},
});
const result = await response.json();
return result as {file_list: ModelsListRespItem[], populate_done: boolean};
return result as {
file_list: ModelsListRespItem[];
populate_done: boolean;
};
} catch (error) {
console.error("Error get all models list:", error);
}
}
}
export async function deleteLocalDiskFolder(folderPath: string) {
try {
const response = await fetch("/workspace/delete_folder", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
folder_path: folderPath,
}),
});
const result = await response.text();
return result;
} catch (error) {
console.error("Error move file:", error);
}
}
@@ -1,17 +1,4 @@
import {
Box,
Menu,
MenuList,
MenuItem,
Popover,
PopoverTrigger,
PopoverContent,
PopoverCloseButton,
PopoverBody,
Button,
PopoverArrow,
Text,
} from "@chakra-ui/react";
import { Box, Menu, MenuList, MenuItem } from "@chakra-ui/react";
import { MouseEvent, useContext, useRef, useState } from "react";
import { RecentFilesContext } from "../WorkspaceContext";
import { createFlow, foldersTable } from "../db-tables/WorkspaceDB";
@@ -23,9 +10,11 @@ import {
IconPlus,
IconTrash,
} from "@tabler/icons-react";
import { Folder } from "../types/dbTypes";
import { EFlowOperationType, Folder } from "../types/dbTypes";
import ImportFlowsFileInput from "./ImportFlowsFileInput";
import { defaultGraph } from "../defaultGraph";
import { useDialog } from "../components/AlertDialogProvider";
import { getFileCountInFolder } from "../db-tables/DiskFileUtils";
type Props = {
menuPosition: { x: number; y: number };
@@ -40,8 +29,9 @@ export default function FilesListFolderItemRightClickMenu({
onClose,
}: Props) {
const [isRenameOpen, setIsRenameOpen] = useState(false);
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
const { onRefreshFilesList } = useContext(RecentFilesContext);
const { showDialog } = useDialog();
const onClickNewFolder = async (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
await foldersTable?.create({
@@ -60,6 +50,43 @@ export default function FilesListFolderItemRightClickMenu({
};
const fileInputRef = useRef<HTMLInputElement>(null);
const openDeleteConfirm = async (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
const fileCount = await getFileCountInFolder(folder.id);
if (fileCount > 0) {
showDialog(
`Do you want to delete the ${fileCount} files inside the folder too?`,
[
{
label: "Keep and move to root directory",
colorScheme: "teal",
onClick: () => {
onDelete(fileCount, EFlowOperationType.MOVE_TO_ROOT_FOLDER);
},
},
{
label: "Delete all files",
colorScheme: "red",
onClick: () => {
onDelete(fileCount, EFlowOperationType.DELETE);
},
},
],
);
} else {
onDelete(fileCount);
}
};
const onDelete = async (
fileCount: number,
operationType?: EFlowOperationType,
) => {
await foldersTable?.deleteFolder(folder.id, fileCount, operationType);
onClose();
onRefreshFilesList && onRefreshFilesList();
};
return (
<>
<Box position="absolute" top={menuPosition.y} left={menuPosition.x}>
@@ -77,7 +104,7 @@ export default function FilesListFolderItemRightClickMenu({
</MenuItem>
<MenuItem
icon={<IconTrash size={19} />}
onClick={() => setIsDeleteOpen(true)}
onClick={openDeleteConfirm}
>
Delete
</MenuItem>
@@ -112,45 +139,6 @@ export default function FilesListFolderItemRightClickMenu({
onclose={() => setIsRenameOpen(false)}
/>
)}
{isDeleteOpen && (
<Popover
returnFocusOnClose={false}
isOpen={true}
onClose={() => setIsDeleteOpen(false)}
// placement="right"
closeOnBlur={false}
>
<PopoverTrigger>
<div></div>
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton />
<PopoverBody textAlign={"left"}>
<Text mb={2}>
Are you sure you want to delete this folder,
<b> {folder.name}</b>?
</Text>
<Text color={"GrayText"} mb={5}>
This will NOT delete any files in the folder. The files will be
moved to the root folder.
</Text>
<Button
colorScheme="red"
size={"sm"}
onClick={async () => {
setIsDeleteOpen(false);
onClose();
await foldersTable?.deleteFolder(folder.id);
onRefreshFilesList && onRefreshFilesList();
}}
>
Yes, delete
</Button>
</PopoverBody>
</PopoverContent>
</Popover>
)}
</>
);
}
+57 -1
View File
@@ -1,9 +1,10 @@
import { deleteFile, saveDB, updateFile } from "../Api";
import { deleteFile, updateFile } from "../Api";
import {
Workflow,
foldersTable,
updateFlow,
userSettingsTable,
listWorkflows,
} from "./WorkspaceDB";
import {
COMFYSPACE_TRACKING_FIELD_NAME,
@@ -71,3 +72,58 @@ export async function generateFilePathAbsolute(
}
return myWorkflowsDir + subPath;
}
export async function generateFolderPath(id: string): Promise<string | null> {
const folder = await foldersTable?.get(id);
let parentFolderID = folder?.parentFolderID;
let folderPath = folder?.name;
while (parentFolderID) {
const folder = await foldersTable?.get(parentFolderID);
if (folder == null) {
break;
}
folderPath = `${folder.name}/${folderPath}`;
parentFolderID = folder.parentFolderID ?? undefined;
}
let myWorkflowsDir = await userSettingsTable?.getSetting("myWorkflowsDir");
if (myWorkflowsDir == null) {
console.error("myWorkflowsDir is not set");
return null;
}
if (!myWorkflowsDir.endsWith("/")) {
myWorkflowsDir = myWorkflowsDir + "/";
}
return myWorkflowsDir + folderPath;
}
export async function getFileCountInFolder(folderId: string): Promise<number> {
const allFlows = await listWorkflows();
const allFolders = (await foldersTable?.listAll()) ?? [];
const nestedFolderIdStack = [folderId];
let count = 0;
while (nestedFolderIdStack.length > 0) {
const curFolderId = nestedFolderIdStack.shift();
if (curFolderId) {
for (const flow of allFlows) {
if (flow.parentFolderID === curFolderId) {
count++;
}
}
const curNestedFolderIds = allFolders
.filter((f) => f.parentFolderID === curFolderId)
.map((f) => f.id);
if (curNestedFolderIds.length) {
nestedFolderIdStack.push(...curNestedFolderIds);
}
}
}
return count;
}
+25 -13
View File
@@ -1,10 +1,11 @@
import { saveDB } from "../Api";
import { listWorkflows, updateFlow } from "./WorkspaceDB";
import { Folder } from "../types/dbTypes";
import { saveDB, deleteLocalDiskFolder } from "../Api";
import { deleteFlow, listWorkflows, updateFlow } 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";
export class FoldersTable extends TableBase<Folder> {
static readonly TABLE_NAME = "folders";
@@ -61,11 +62,16 @@ export class FoldersTable extends TableBase<Folder> {
validateOrSaveAllJsonFileMyWorkflows(true);
}
}
public async deleteFolder(id: string) {
public async deleteFolder(
id: string,
fileCount: number = 0,
flowOperationType: EFlowOperationType = EFlowOperationType.DELETE,
) {
const folderPath = await generateFolderPath(id);
/**
* When deleting a folder,
* breadth traverses all nested folders and deletes them from the DB one by one.
* And modify the related flow synchronously, setting parentFolderId to undefined
* 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.
*/
const allFlows = await listWorkflows();
const allFolders = await this.listAll();
@@ -77,12 +83,19 @@ export class FoldersTable extends TableBase<Folder> {
if (curFolderId) {
for (const flow of allFlows) {
if (flow.parentFolderID === curFolderId) {
await updateFlow(flow.id, { parentFolderID: undefined });
switch (flowOperationType) {
case EFlowOperationType.DELETE:
await deleteFlow(flow.id);
break;
case EFlowOperationType.MOVE_TO_ROOT_FOLDER:
await updateFlow(flow.id, { parentFolderID: undefined });
console.log("updateFlow");
break;
}
}
}
await indexdb.folders.delete(curFolderId);
const curNestedFolderIds = allFolders
.filter((f) => f.parentFolderID === curFolderId)
.map((f) => f.id);
@@ -92,11 +105,10 @@ export class FoldersTable extends TableBase<Folder> {
}
}
}
fileCount > 0 &&
(await saveDB("workflows", JSON.stringify(await listWorkflows())));
folderPath && (await deleteLocalDiskFolder(folderPath));
await saveDB("folders", JSON.stringify(await this.listAll()));
// TODO 在嵌套文件夹场景下,重建逻辑无法有效判断子文件夹是否为空,导致子文件夹为空时无法准确删除。
await validateOrSaveAllJsonFileMyWorkflows(true);
}
public async generateUniqueName(name?: string) {
+8 -7
View File
@@ -1,5 +1,5 @@
import { v4 as uuidv4 } from "uuid";
import { getDB, saveDB, updateFile } from "../Api";
import { getDB, saveDB } from "../Api";
import { generateUniqueName, sortFileItem, sortFlows } from "../utils";
import { ESortTypes, ImportWorkflow } from "../RecentFilesDrawer/types";
import { ChangelogsTable } from "./ChangelogsTable";
@@ -166,20 +166,21 @@ export async function updateFlow(id: string, input: Partial<Workflow>) {
// update memory
workspace[id] = newWorkflow;
//update indexdb
indexdb.workflows.update(id, newWorkflow);
await indexdb.workflows.update(id, newWorkflow);
//update legacy indexdb backup
updateWorkspaceIndexDB();
// update disk file db
saveDB("workflows", JSON.stringify(workspace));
await saveDB("workflows", JSON.stringify(workspace));
// save to my_workflows/
if (input.name != null || input.parentFolderID != null) {
input.name != null || input.parentFolderID != null;
if (input.name !== null || input.parentFolderID !== null) {
// renamed file or moved file folder
deleteJsonFileMyWorkflows(before);
saveJsonFileMyWorkflows(after);
await deleteJsonFileMyWorkflows(before);
await saveJsonFileMyWorkflows(after);
return;
}
if (input.json != null) {
saveJsonFileMyWorkflows(after);
await saveJsonFileMyWorkflows(after);
}
}
+14
View File
@@ -51,3 +51,17 @@ export type Media = {
localPath: string;
format: string;
};
/**
* When deleting a folder, how files in the folder and files in nested folders are handled
*/
export enum EFlowOperationType {
/**
* Move to root directory
*/
MOVE_TO_ROOT_FOLDER = "moveToRootFolder",
/**
* Delete
*/
DELETE = "delete",
}