From a9675a5d838cc70d7c6f5749bdc0b948711b7a98 Mon Sep 17 00:00:00 2001 From: Hayden <48267247+hayden-fr@users.noreply.github.com> Date: Mon, 11 Aug 2025 09:10:39 +0800 Subject: [PATCH] feat: Add model upload functionality (#194) --- __init__.py | 2 + py/upload.py | 79 +++++++++ src/App.vue | 21 +++ src/components/DialogUpload.vue | 274 ++++++++++++++++++++++++++++++++ src/i18n.ts | 4 + src/types/global.d.ts | 5 + 6 files changed, 385 insertions(+) create mode 100644 py/upload.py create mode 100644 src/components/DialogUpload.vue diff --git a/__init__.py b/__init__.py index d2cc278..8104049 100644 --- a/__init__.py +++ b/__init__.py @@ -41,12 +41,14 @@ utils.download_web_distribution(version) from .py import manager from .py import download from .py import information +from .py import upload routes = config.routes manager.ModelManager().add_routes(routes) download.ModelDownload().add_routes(routes) information.Information().add_routes(routes) +upload.ModelUploader().add_routes(routes) WEB_DIRECTORY = "web" diff --git a/py/upload.py b/py/upload.py new file mode 100644 index 0000000..9b86f8f --- /dev/null +++ b/py/upload.py @@ -0,0 +1,79 @@ +import os +import time + +import folder_paths + +from aiohttp import web + +from . import utils + + +class ModelUploader: + def add_routes(self, routes): + + @routes.get("/model-manager/supported-extensions") + async def fetch_model_exts(request): + """ + Get model exts + """ + try: + supported_extensions = list(folder_paths.supported_pt_extensions) + return web.json_response({"success": True, "data": supported_extensions}) + except Exception as e: + error_msg = f"Get model supported extension failed: {str(e)}" + utils.print_error(error_msg) + return web.json_response({"success": False, "error": error_msg}) + + @routes.post("/model-manager/upload") + async def upload_model(request): + """ + Upload model + """ + try: + reader = await request.multipart() + await self.upload_model(reader) + utils.print_info(f"Upload model success") + return web.json_response({"success": True, "data": None}) + except Exception as e: + error_msg = f"Upload model failed: {str(e)}" + utils.print_error(error_msg) + return web.json_response({"success": False, "error": error_msg}) + + async def upload_model(self, reader): + uploaded_size = 0 + last_update_time = time.time() + interval = 1.0 + + while True: + part = await reader.next() + if part is None: + break + + name = part.name + if name == "folder": + file_folder = await part.text() + + if name == "file": + filename = part.filename + filepath = f"{file_folder}/{filename}" + tmp_filepath = f"{file_folder}/{filename}.tmp" + + with open(tmp_filepath, "wb") as f: + while True: + chunk = await part.read_chunk() + if not chunk: + break + f.write(chunk) + uploaded_size += len(chunk) + + if time.time() - last_update_time >= interval: + update_upload_progress = { + "uploaded_size": uploaded_size, + } + await utils.send_json("update_upload_progress", update_upload_progress) + + update_upload_progress = { + "uploaded_size": uploaded_size, + } + await utils.send_json("update_upload_progress", update_upload_progress) + os.rename(tmp_filepath, filepath) diff --git a/src/App.vue b/src/App.vue index 45e7ae3..6904d01 100644 --- a/src/App.vue +++ b/src/App.vue @@ -10,6 +10,7 @@ import DialogDownload from 'components/DialogDownload.vue' import DialogExplorer from 'components/DialogExplorer.vue' import DialogManager from 'components/DialogManager.vue' import DialogScanning from 'components/DialogScanning.vue' +import DialogUpload from 'components/DialogUpload.vue' import GlobalDialogStack from 'components/GlobalDialogStack.vue' import GlobalLoading from 'components/GlobalLoading.vue' import GlobalToast from 'components/GlobalToast.vue' @@ -64,6 +65,21 @@ onMounted(() => { }) } + const openUploadDialog = () => { + dialog.open({ + key: 'model-manager-upload', + title: t('uploadModel'), + content: DialogUpload, + headerButtons: [ + { + key: 'refresh', + icon: 'pi pi-refresh', + command: refreshModelsAndConfig, + }, + ], + }) + } + const openManagerDialog = () => { const { cardWidth, gutter, aspect, flat } = config @@ -93,6 +109,11 @@ onMounted(() => { icon: 'pi pi-download', command: openDownloadDialog, }, + { + key: 'upload', + icon: 'pi pi-upload', + command: openUploadDialog, + }, ], minWidth: cardWidth * 2 + gutter + 42, minHeight: (cardWidth / aspect) * 0.5 + 162, diff --git a/src/components/DialogUpload.vue b/src/components/DialogUpload.vue new file mode 100644 index 0000000..34ac66c --- /dev/null +++ b/src/components/DialogUpload.vue @@ -0,0 +1,274 @@ + + + + + + + {{ $t('selectModelType') }} + {{ $t('selectSubdirectory') }} + {{ $t('chooseFile') }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ $t('selectedSpecialPath') }} + + + {{ selectedModelFolder }} + + + + + + + + + + + + + + + + + + + diff --git a/src/i18n.ts b/src/i18n.ts index 509fa4b..c786000 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -41,6 +41,8 @@ const messages = { scanMissInformation: 'Download missing information', scanFullInformation: 'Override full information', noModelsInCurrentPath: 'There are no models available in the current path', + uploadModel: 'Upload Model', + chooseFile: 'Choose File', sort: { name: 'Name', size: 'Largest', @@ -116,6 +118,8 @@ const messages = { scanMissInformation: '下载缺失信息', scanFullInformation: '覆盖所有信息', noModelsInCurrentPath: '当前路径中没有可用的模型', + uploadModel: '上传模型', + chooseFile: '选择文件', sort: { name: '名称', size: '最大', diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 7629249..5b1b166 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -12,6 +12,11 @@ declare namespace ComfyAPI { callback: (event: CustomEvent) => void, options?: AddEventListenerOptions, ) => void + removeEventListener: ( + type: string, + callback: (event: CustomEvent) => void, + options?: AddEventListenerOptions, + ) => void } const api: ComfyApi