From 7a183464ae36c694445dcfe11a40c62127288ac2 Mon Sep 17 00:00:00 2001 From: hayden Date: Tue, 5 Nov 2024 16:42:36 +0800 Subject: [PATCH 1/9] fix: cross-platform paths --- __init__.py | 10 ++++----- py/download.py | 18 ++++++++-------- py/services.py | 12 +++++------ py/utils.py | 56 ++++++++++++++++++++++++++++---------------------- 4 files changed, 52 insertions(+), 44 deletions(-) diff --git a/__init__.py b/__init__.py index 87cbb38..6b1c0a6 100644 --- a/__init__.py +++ b/__init__.py @@ -5,7 +5,7 @@ from .py import utils # Init config settings -config.extension_uri = os.path.dirname(__file__) +config.extension_uri = utils.normalize_path(os.path.dirname(__file__)) utils.resolve_model_base_paths() version = utils.get_current_version() @@ -173,12 +173,12 @@ async def read_model_preview(request): try: folders = folder_paths.get_folder_paths(model_type) base_path = folders[index] - abs_path = os.path.join(base_path, filename) + abs_path = utils.join_path(base_path, filename) except: abs_path = extension_uri if not os.path.isfile(abs_path): - abs_path = os.path.join(extension_uri, "assets", "no-preview.png") + abs_path = utils.join_path(extension_uri, "assets", "no-preview.png") return web.FileResponse(abs_path) @@ -188,10 +188,10 @@ async def read_download_preview(request): extension_uri = config.extension_uri download_path = utils.get_download_path() - preview_path = os.path.join(download_path, filename) + preview_path = utils.join_path(download_path, filename) if not os.path.isfile(preview_path): - preview_path = os.path.join(extension_uri, "assets", "no-preview.png") + preview_path = utils.join_path(extension_uri, "assets", "no-preview.png") return web.FileResponse(preview_path) diff --git a/py/download.py b/py/download.py index ca8f42d..87e0e45 100644 --- a/py/download.py +++ b/py/download.py @@ -46,13 +46,13 @@ download_thread_pool = thread.DownloadThreadPool() def set_task_content(task_id: str, task_content: Union[TaskContent, dict]): download_path = utils.get_download_path() - task_file_path = os.path.join(download_path, f"{task_id}.task") + task_file_path = utils.join_path(download_path, f"{task_id}.task") utils.save_dict_pickle_file(task_file_path, utils.unpack_dataclass(task_content)) def get_task_content(task_id: str): download_path = utils.get_download_path() - task_file = os.path.join(download_path, f"{task_id}.task") + task_file = utils.join_path(download_path, f"{task_id}.task") if not os.path.isfile(task_file): raise RuntimeError(f"Task {task_id} not found") task_content = utils.load_dict_pickle_file(task_file) @@ -67,7 +67,7 @@ def get_task_status(task_id: str): if task_status is None: download_path = utils.get_download_path() task_content = get_task_content(task_id) - download_file = os.path.join(download_path, f"{task_id}.download") + download_file = utils.join_path(download_path, f"{task_id}.download") download_size = 0 if os.path.exists(download_file): download_size = os.path.getsize(download_file) @@ -103,7 +103,7 @@ async def scan_model_download_task_list(sid: str): task_files = folder_paths.filter_files_extensions(task_files, [".task"]) task_files = sorted( task_files, - key=lambda x: os.stat(os.path.join(download_dir, x)).st_ctime, + key=lambda x: os.stat(utils.join_path(download_dir, x)).st_ctime, reverse=True, ) task_list: list[dict] = [] @@ -135,7 +135,7 @@ async def create_model_download_task(post: dict): download_path = utils.get_download_path() task_id = uuid.uuid4().hex - task_path = os.path.join(download_path, f"{task_id}.task") + task_path = utils.join_path(download_path, f"{task_id}.task") if os.path.exists(task_path): raise RuntimeError(f"Task {task_id} already exists") @@ -183,7 +183,7 @@ async def delete_model_download_task(task_id: str): task_file_target = os.path.splitext(task_file)[0] if task_file_target == task_id: delete_task_status(task_id) - os.remove(os.path.join(download_dir, task_file)) + os.remove(utils.join_path(download_dir, task_file)) await socket.send_json("deleteDownloadTask", task_id) @@ -264,7 +264,7 @@ async def download_model_file( fullname = task_content.fullname # Write description file description = task_content.description - description_file = os.path.join(download_path, f"{task_id}.md") + description_file = utils.join_path(download_path, f"{task_id}.md") with open(description_file, "w") as f: f.write(description) @@ -273,7 +273,7 @@ async def download_model_file( utils.rename_model(download_tmp_file, model_path) time.sleep(1) - task_file = os.path.join(download_path, f"{task_id}.task") + task_file = utils.join_path(download_path, f"{task_id}.task") os.remove(task_file) await socket.send_json("completeDownloadTask", task_id) @@ -297,7 +297,7 @@ async def download_model_file( raise RuntimeError("No downloadUrl found") download_path = utils.get_download_path() - download_tmp_file = os.path.join(download_path, f"{task_id}.download") + download_tmp_file = utils.join_path(download_path, f"{task_id}.download") downloaded_size = 0 if os.path.isfile(download_tmp_file): diff --git a/py/services.py b/py/services.py index 0edd8ca..dde82ba 100644 --- a/py/services.py +++ b/py/services.py @@ -46,16 +46,16 @@ def scan_models(): image_dict = utils.file_list_to_name_dict(images) for fullname in models: - fullname = fullname.replace(os.path.sep, "/") + fullname = utils.normalize_path(fullname) basename = os.path.splitext(fullname)[0] extension = os.path.splitext(fullname)[1] - abs_path = os.path.join(base_path, fullname) + abs_path = utils.join_path(base_path, fullname) file_stats = os.stat(abs_path) # Resolve preview image_name = image_dict.get(basename, "no-preview.png") - abs_image_path = os.path.join(base_path, image_name) + abs_image_path = utils.join_path(base_path, image_name) if os.path.isfile(abs_image_path): image_state = os.stat(abs_image_path) image_timestamp = round(image_state.st_mtime_ns / 1000000) @@ -87,7 +87,7 @@ def get_model_info(model_path: str): metadata = utils.get_model_metadata(model_path) description_file = utils.get_model_description_name(model_path) - description_file = os.path.join(directory, description_file) + description_file = utils.join_path(directory, description_file) description = None if os.path.isfile(description_file): with open(description_file, "r", encoding="utf-8") as f: @@ -128,11 +128,11 @@ def remove_model(model_path: str): model_previews = utils.get_model_all_images(model_path) for preview in model_previews: - os.remove(os.path.join(model_dirname, preview)) + os.remove(utils.join_path(model_dirname, preview)) model_descriptions = utils.get_model_all_descriptions(model_path) for description in model_descriptions: - os.remove(os.path.join(model_dirname, description)) + os.remove(utils.join_path(model_dirname, description)) async def create_model_download_task(post): diff --git a/py/utils.py b/py/utils.py index ded4c53..18099c8 100644 --- a/py/utils.py +++ b/py/utils.py @@ -15,9 +15,18 @@ from typing import Any from . import config +def normalize_path(path: str): + normpath = os.path.normpath(path) + return normpath.replace(os.path.sep, "/") + + +def join_path(path: str, *paths: list[str]): + return normalize_path(os.path.join(path, *paths)) + + def get_current_version(): try: - pyproject_path = os.path.join(config.extension_uri, "pyproject.toml") + pyproject_path = join_path(config.extension_uri, "pyproject.toml") config_parser = configparser.ConfigParser() config_parser.read(pyproject_path) version = config_parser.get("project", "version") @@ -27,13 +36,13 @@ def get_current_version(): def download_web_distribution(version: str): - web_path = os.path.join(config.extension_uri, "web") - dev_web_file = os.path.join(web_path, "manager-dev.js") + web_path = join_path(config.extension_uri, "web") + dev_web_file = join_path(web_path, "manager-dev.js") if os.path.exists(dev_web_file): return web_version = "0.0.0" - version_file = os.path.join(web_path, "version.yaml") + version_file = join_path(web_path, "version.yaml") if os.path.exists(version_file): with open(version_file, "r") as f: version_content = yaml.safe_load(f) @@ -49,7 +58,7 @@ def download_web_distribution(version: str): response = requests.get(download_url, stream=True) response.raise_for_status() - temp_file = os.path.join(config.extension_uri, "temp.tar.gz") + temp_file = join_path(config.extension_uri, "temp.tar.gz") with open(temp_file, "wb") as f: for chunk in response.iter_content(chunk_size=8192): f.write(chunk) @@ -82,7 +91,8 @@ def resolve_model_base_paths(): continue if folder == "custom_nodes": continue - config.model_base_paths[folder] = folder_paths.get_folder_paths(folder) + folders = folder_paths.get_folder_paths(folder) + config.model_base_paths[folder] = [normalize_path(f) for f in folders] def get_full_path(model_type: str, path_index: int, filename: str): @@ -93,7 +103,8 @@ def get_full_path(model_type: str, path_index: int, filename: str): if not path_index < len(folders): raise RuntimeError(f"PathIndex {path_index} is not in {model_type}") base_path = folders[path_index] - return os.path.join(base_path, filename) + full_path = join_path(base_path, filename) + return full_path def get_valid_full_path(model_type: str, path_index: int, filename: str): @@ -104,7 +115,7 @@ def get_valid_full_path(model_type: str, path_index: int, filename: str): if not path_index < len(folders): raise RuntimeError(f"PathIndex {path_index} is not in {model_type}") base_path = folders[path_index] - full_path = os.path.join(base_path, filename) + full_path = join_path(base_path, filename) if os.path.isfile(full_path): return full_path elif os.path.islink(full_path): @@ -114,7 +125,7 @@ def get_valid_full_path(model_type: str, path_index: int, filename: str): def get_download_path(): - download_path = os.path.join(config.extension_uri, "downloads") + download_path = join_path(config.extension_uri, "downloads") if not os.path.exists(download_path): os.makedirs(download_path) return download_path @@ -124,12 +135,12 @@ def recursive_search_files(directory: str): files, folder_all = folder_paths.recursive_search( directory, excluded_dir_names=[".git"] ) - return files + return [normalize_path(f) for f in files] def search_files(directory: str): entries = os.listdir(directory) - files = [f for f in entries if os.path.isfile(os.path.join(directory, f))] + files = [f for f in entries if os.path.isfile(join_path(directory, f))] return files @@ -137,7 +148,6 @@ def file_list_to_name_dict(files: list[str]): file_dict: dict[str, str] = {} for file in files: filename = os.path.splitext(file)[0] - filename = filename.replace(os.path.sep, "/") file_dict[filename] = file return file_dict @@ -194,13 +204,13 @@ def save_model_preview_image(model_path: str, image_file: Any): for image in old_preview_images: if os.path.splitext(image)[1].endswith(".preview"): a1111_civitai_helper_image = True - image_path = os.path.join(base_dirname, image) + image_path = join_path(base_dirname, image) os.remove(image_path) # save new preview image basename = os.path.splitext(os.path.basename(model_path))[0] extension = f".{content_type.split('/')[1]}" - new_preview_path = os.path.join(base_dirname, f"{basename}{extension}") + new_preview_path = join_path(base_dirname, f"{basename}{extension}") with open(new_preview_path, "wb") as f: f.write(image_file.file.read()) @@ -210,7 +220,7 @@ def save_model_preview_image(model_path: str, image_file: Any): """ Keep preview image of a1111_civitai_helper """ - new_preview_path = os.path.join(base_dirname, f"{basename}.preview{extension}") + new_preview_path = join_path(base_dirname, f"{basename}.preview{extension}") with open(new_preview_path, "wb") as f: f.write(image_file.file.read()) @@ -244,13 +254,13 @@ def save_model_description(model_path: str, content: Any): # remove old descriptions old_descriptions = get_model_all_descriptions(model_path) for desc in old_descriptions: - description_path = os.path.join(base_dirname, desc) + description_path = join_path(base_dirname, desc) os.remove(description_path) # save new description basename = os.path.splitext(os.path.basename(model_path))[0] extension = ".md" - new_desc_path = os.path.join(base_dirname, f"{basename}{extension}") + new_desc_path = join_path(base_dirname, f"{basename}{extension}") with open(new_desc_path, "w", encoding="utf-8") as f: f.write(content) @@ -278,23 +288,21 @@ def rename_model(model_path: str, new_model_path: str): # move preview previews = get_model_all_images(model_path) for preview in previews: - preview_path = os.path.join(model_dirname, preview) + preview_path = join_path(model_dirname, preview) preview_name = os.path.splitext(preview)[0] preview_ext = os.path.splitext(preview)[1] new_preview_path = ( - os.path.join(new_model_dirname, new_model_name + preview_ext) + join_path(new_model_dirname, new_model_name + preview_ext) if preview_name == model_name - else os.path.join( - new_model_dirname, new_model_name + ".preview" + preview_ext - ) + else join_path(new_model_dirname, new_model_name + ".preview" + preview_ext) ) shutil.move(preview_path, new_preview_path) # move description description = get_model_description_name(model_path) - description_path = os.path.join(model_dirname, description) + description_path = join_path(model_dirname, description) if os.path.isfile(description_path): - new_description_path = os.path.join(new_model_dirname, f"{new_model_name}.md") + new_description_path = join_path(new_model_dirname, f"{new_model_name}.md") shutil.move(description_path, new_description_path) From 8bfe60158855bfa8d377ba5167011c602c4a67fd Mon Sep 17 00:00:00 2001 From: hayden Date: Tue, 5 Nov 2024 16:46:30 +0800 Subject: [PATCH 2/9] chore: optimize development address --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index c912982..4126f68 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -79,7 +79,7 @@ function dev(): Plugin { fs.mkdirSync(outDirPath) const port = server.config.server.port - const content = `import "http://127.0.0.1:${port}/src/main.ts";` + const content = `import "http://localhost:${port}/src/main.ts";` fs.writeFileSync(path.join(outDirPath, 'manager-dev.js'), content) }) }, From 0a8c53250666f7d54d4dd060698301c2f43bf462 Mon Sep 17 00:00:00 2001 From: hayden Date: Tue, 5 Nov 2024 17:02:10 +0800 Subject: [PATCH 3/9] feat: optimize model editing - close dialog after delete or rename - keep editing if model update fails - show more error message --- src/components/DialogModelDetail.vue | 2 +- src/hooks/model.ts | 24 ++++++++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/components/DialogModelDetail.vue b/src/components/DialogModelDetail.vue index ec0a047..6076c20 100644 --- a/src/components/DialogModelDetail.vue +++ b/src/components/DialogModelDetail.vue @@ -72,8 +72,8 @@ const handleCancel = () => { } const handleSave = async (data: BaseModel) => { - editable.value = false await update(props.model, data) + editable.value = false } const handleDelete = async () => { diff --git a/src/hooks/model.ts b/src/hooks/model.ts index 0e2d1dc..e5c8d40 100644 --- a/src/hooks/model.ts +++ b/src/hooks/model.ts @@ -7,7 +7,7 @@ import { cloneDeep } from 'lodash' import { app } from 'scripts/comfyAPI' import { bytesToSize, formatDate, previewUrlToFile } from 'utils/common' import { ModelGrid } from 'utils/legacy' -import { resolveModelTypeLoader } from 'utils/model' +import { genModelKey, resolveModelTypeLoader } from 'utils/model' import { computed, inject, @@ -20,7 +20,7 @@ import { } from 'vue' import { useI18n } from 'vue-i18n' -export const useModels = defineStore('models', () => { +export const useModels = defineStore('models', (store) => { const { data, refresh } = useRequest('/models', { defaultValue: [] }) const { toast, confirm } = useToast() const { t } = useI18n() @@ -28,6 +28,7 @@ export const useModels = defineStore('models', () => { const updateModel = async (model: BaseModel, data: BaseModel) => { const formData = new FormData() + let oldKey: string | null = null // Check current preview if (model.preview !== data.preview) { @@ -45,6 +46,7 @@ export const useModels = defineStore('models', () => { model.fullname !== data.fullname || model.pathIndex !== data.pathIndex ) { + oldKey = genModelKey(model) formData.append('type', data.type) formData.append('pathIndex', data.pathIndex.toString()) formData.append('fullname', data.fullname) @@ -59,19 +61,25 @@ export const useModels = defineStore('models', () => { method: 'PUT', body: formData, }) - .catch(() => { + .catch((err) => { + const error_message = err.message ?? err.error toast.add({ severity: 'error', summary: 'Error', - detail: 'Failed to update model', + detail: `Failed to update model: ${error_message}`, life: 15000, }) + throw new Error(error_message) }) .finally(() => { loading.hide() }) - await refresh() + if (oldKey) { + store.dialog.close({ key: oldKey }) + } + + refresh() } const deleteModel = async (model: BaseModel) => { @@ -90,6 +98,7 @@ export const useModels = defineStore('models', () => { severity: 'danger', }, accept: () => { + const dialogKey = genModelKey(model) loading.show() request(`/model/${model.type}/${model.pathIndex}/${model.fullname}`, { method: 'DELETE', @@ -101,6 +110,7 @@ export const useModels = defineStore('models', () => { detail: `${model.fullname} Deleted`, life: 2000, }) + store.dialog.close({ key: dialogKey }) return refresh() }) .then(() => { @@ -118,7 +128,9 @@ export const useModels = defineStore('models', () => { loading.hide() }) }, - reject: () => {}, + reject: () => { + resolve(void 0) + }, }) }) } From 288f026d476aee815ab92a9e9e097c74e449d49a Mon Sep 17 00:00:00 2001 From: hayden Date: Wed, 6 Nov 2024 13:51:38 +0800 Subject: [PATCH 4/9] feat: add display of directory information --- src/components/ModelBaseInfo.vue | 9 ++++++--- src/hooks/model.ts | 12 ++++++++++++ src/i18n.ts | 2 ++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/components/ModelBaseInfo.vue b/src/components/ModelBaseInfo.vue index e76096d..a1873d4 100644 --- a/src/components/ModelBaseInfo.vue +++ b/src/components/ModelBaseInfo.vue @@ -29,11 +29,13 @@ - + {{ $t(`info.${item.key}`) }} - {{ item.display }} + + {{ item.display }} + @@ -81,7 +83,8 @@ const pathOptions = computed(() => { const information = computed(() => { return Object.values(baseInfo.value).filter((row) => { if (editable.value) { - return row.key !== 'fullname' + const hiddenKeys = ['fullname', 'pathIndex'] + return !hiddenKeys.includes(row.key) } return true }) diff --git a/src/hooks/model.ts b/src/hooks/model.ts index e5c8d40..095b4a0 100644 --- a/src/hooks/model.ts +++ b/src/hooks/model.ts @@ -1,3 +1,4 @@ +import { useConfig } from 'hooks/config' import { useLoading } from 'hooks/loading' import { useMarkdown } from 'hooks/markdown' import { request, useRequest } from 'hooks/request' @@ -203,6 +204,8 @@ const baseInfoKey = Symbol('baseInfo') as InjectionKey< export const useModelBaseInfoEditor = (formInstance: ModelFormInstance) => { const { formData: model, modelData } = formInstance + const { modelFolders } = useConfig() + const type = computed({ get: () => { return model.value.type @@ -251,6 +254,15 @@ export const useModelBaseInfoEditor = (formInstance: ModelFormInstance) => { key: 'type', formatter: () => modelData.value.type, }, + { + key: 'pathIndex', + formatter: () => { + const modelType = modelData.value.type + const pathIndex = modelData.value.pathIndex + const folders = modelFolders.value[modelType] ?? [] + return `${folders[pathIndex]}` + }, + }, { key: 'fullname', formatter: (val) => val, diff --git a/src/i18n.ts b/src/i18n.ts index 39eb49b..3facd45 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -32,6 +32,7 @@ const messages = { }, info: { type: 'Model Type', + pathIndex: 'Directory', fullname: 'File Name', sizeBytes: 'File Size', createdAt: 'Created At', @@ -69,6 +70,7 @@ const messages = { }, info: { type: '类型', + pathIndex: '目录', fullname: '文件名', sizeBytes: '文件大小', createdAt: '创建时间', From 153dbc0788470cc58a1d0e10fe7d3a499fe535c7 Mon Sep 17 00:00:00 2001 From: hayden Date: Wed, 6 Nov 2024 15:39:06 +0800 Subject: [PATCH 5/9] fix: issue saving differences across platforms --- py/download.py | 2 +- py/services.py | 2 +- py/utils.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/py/download.py b/py/download.py index 87e0e45..31a2a35 100644 --- a/py/download.py +++ b/py/download.py @@ -265,7 +265,7 @@ async def download_model_file( # Write description file description = task_content.description description_file = utils.join_path(download_path, f"{task_id}.md") - with open(description_file, "w") as f: + with open(description_file, "w", encoding="utf-8", newline="") as f: f.write(description) model_path = utils.get_full_path(model_type, path_index, fullname) diff --git a/py/services.py b/py/services.py index dde82ba..9518a3f 100644 --- a/py/services.py +++ b/py/services.py @@ -90,7 +90,7 @@ def get_model_info(model_path: str): description_file = utils.join_path(directory, description_file) description = None if os.path.isfile(description_file): - with open(description_file, "r", encoding="utf-8") as f: + with open(description_file, "r", encoding="utf-8", newline="") as f: description = f.read() return { diff --git a/py/utils.py b/py/utils.py index 18099c8..1aa9dff 100644 --- a/py/utils.py +++ b/py/utils.py @@ -44,7 +44,7 @@ def download_web_distribution(version: str): web_version = "0.0.0" version_file = join_path(web_path, "version.yaml") if os.path.exists(version_file): - with open(version_file, "r") as f: + with open(version_file, "r", encoding="utf-8", newline="") as f: version_content = yaml.safe_load(f) web_version = version_content.get("version", web_version) @@ -262,7 +262,7 @@ def save_model_description(model_path: str, content: Any): extension = ".md" new_desc_path = join_path(base_dirname, f"{basename}{extension}") - with open(new_desc_path, "w", encoding="utf-8") as f: + with open(new_desc_path, "w", encoding="utf-8", newline="") as f: f.write(content) From b8cd3c28a5c49de440636c9ca8d4bc9c449a978a Mon Sep 17 00:00:00 2001 From: hayden Date: Wed, 6 Nov 2024 15:41:22 +0800 Subject: [PATCH 6/9] fix: bug in verification update description error --- src/components/DialogModelDetail.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DialogModelDetail.vue b/src/components/DialogModelDetail.vue index 6076c20..72ec588 100644 --- a/src/components/DialogModelDetail.vue +++ b/src/components/DialogModelDetail.vue @@ -72,7 +72,7 @@ const handleCancel = () => { } const handleSave = async (data: BaseModel) => { - await update(props.model, data) + await update(modelContent.value, data) editable.value = false } From cfd2bdea4a227788093fd6be4003c4674bd03999 Mon Sep 17 00:00:00 2001 From: hayden Date: Wed, 6 Nov 2024 15:55:49 +0800 Subject: [PATCH 7/9] fix(ResponseInput): unable input any text --- src/components/ResponseInput.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ResponseInput.vue b/src/components/ResponseInput.vue index 1083998..a353ec6 100644 --- a/src/components/ResponseInput.vue +++ b/src/components/ResponseInput.vue @@ -44,7 +44,7 @@ const [content, modifiers] = defineModel() const inputRef = ref() const innerValue = ref(content) -const trigger = computed(() => props.updateTrigger ?? 'input') +const trigger = computed(() => props.updateTrigger ?? 'change') const updateContent = () => { let value = innerValue.value From 652721ac9a6bd66e671acb720d98117f2f2dbfbf Mon Sep 17 00:00:00 2001 From: hayden Date: Wed, 6 Nov 2024 16:03:28 +0800 Subject: [PATCH 8/9] fix: hide action button until mouseover --- src/components/ModelCard.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ModelCard.vue b/src/components/ModelCard.vue index d8763c3..2d80484 100644 --- a/src/components/ModelCard.vue +++ b/src/components/ModelCard.vue @@ -34,7 +34,7 @@ -
+