fix: improve example image upload reliability and error handling, #804

- Sequential per-file upload to avoid client_max_size limits
- Add backend exception handler with proper 500 responses
- Increase standalone server upload limit to 256MB
- Add partial success localization support
This commit is contained in:
Will Miao
2026-02-08 09:16:48 +08:00
parent 199c9f742c
commit b4ad03c9bf
13 changed files with 63 additions and 27 deletions

View File

@@ -1466,6 +1466,7 @@
"folderTreeFailed": "Fehler beim Laden des Ordnerbaums", "folderTreeFailed": "Fehler beim Laden des Ordnerbaums",
"folderTreeError": "Fehler beim Laden des Ordnerbaums", "folderTreeError": "Fehler beim Laden des Ordnerbaums",
"imagesImported": "Beispielbilder erfolgreich importiert", "imagesImported": "Beispielbilder erfolgreich importiert",
"imagesPartial": "{success} Bild(er) importiert, {failed} fehlgeschlagen",
"importFailed": "Fehler beim Importieren der Beispielbilder: {message}" "importFailed": "Fehler beim Importieren der Beispielbilder: {message}"
}, },
"triggerWords": { "triggerWords": {

View File

@@ -1466,6 +1466,7 @@
"folderTreeFailed": "Failed to load folder tree", "folderTreeFailed": "Failed to load folder tree",
"folderTreeError": "Error loading folder tree", "folderTreeError": "Error loading folder tree",
"imagesImported": "Example images imported successfully", "imagesImported": "Example images imported successfully",
"imagesPartial": "{success} image(s) imported, {failed} failed",
"importFailed": "Failed to import example images: {message}" "importFailed": "Failed to import example images: {message}"
}, },
"triggerWords": { "triggerWords": {

View File

@@ -1466,6 +1466,7 @@
"folderTreeFailed": "Error al cargar árbol de carpetas", "folderTreeFailed": "Error al cargar árbol de carpetas",
"folderTreeError": "Error al cargar árbol de carpetas", "folderTreeError": "Error al cargar árbol de carpetas",
"imagesImported": "Imágenes de ejemplo importadas exitosamente", "imagesImported": "Imágenes de ejemplo importadas exitosamente",
"imagesPartial": "{success} imagen(es) importada(s), {failed} fallida(s)",
"importFailed": "Error al importar imágenes de ejemplo: {message}" "importFailed": "Error al importar imágenes de ejemplo: {message}"
}, },
"triggerWords": { "triggerWords": {

View File

@@ -1466,6 +1466,7 @@
"folderTreeFailed": "Échec du chargement de l'arborescence des dossiers", "folderTreeFailed": "Échec du chargement de l'arborescence des dossiers",
"folderTreeError": "Erreur lors du chargement de l'arborescence des dossiers", "folderTreeError": "Erreur lors du chargement de l'arborescence des dossiers",
"imagesImported": "Images d'exemple importées avec succès", "imagesImported": "Images d'exemple importées avec succès",
"imagesPartial": "{success} image(s) importée(s), {failed} échouée(s)",
"importFailed": "Échec de l'importation des images d'exemple : {message}" "importFailed": "Échec de l'importation des images d'exemple : {message}"
}, },
"triggerWords": { "triggerWords": {

View File

@@ -1466,6 +1466,7 @@
"folderTreeFailed": "טעינת עץ התיקיות נכשלה", "folderTreeFailed": "טעינת עץ התיקיות נכשלה",
"folderTreeError": "שגיאה בטעינת עץ התיקיות", "folderTreeError": "שגיאה בטעינת עץ התיקיות",
"imagesImported": "תמונות הדוגמה יובאו בהצלחה", "imagesImported": "תמונות הדוגמה יובאו בהצלחה",
"imagesPartial": "{success} תמונה/ות יובאו, {failed} נכשלו",
"importFailed": "ייבוא תמונות הדוגמה נכשל: {message}" "importFailed": "ייבוא תמונות הדוגמה נכשל: {message}"
}, },
"triggerWords": { "triggerWords": {

View File

@@ -1466,6 +1466,7 @@
"folderTreeFailed": "フォルダツリーの読み込みに失敗しました", "folderTreeFailed": "フォルダツリーの読み込みに失敗しました",
"folderTreeError": "フォルダツリー読み込みエラー", "folderTreeError": "フォルダツリー読み込みエラー",
"imagesImported": "例画像が正常にインポートされました", "imagesImported": "例画像が正常にインポートされました",
"imagesPartial": "{success} 件の画像をインポート、{failed} 件失敗",
"importFailed": "例画像のインポートに失敗しました:{message}" "importFailed": "例画像のインポートに失敗しました:{message}"
}, },
"triggerWords": { "triggerWords": {

View File

@@ -1466,6 +1466,7 @@
"folderTreeFailed": "폴더 트리 로딩 실패", "folderTreeFailed": "폴더 트리 로딩 실패",
"folderTreeError": "폴더 트리 로딩 오류", "folderTreeError": "폴더 트리 로딩 오류",
"imagesImported": "예시 이미지가 성공적으로 가져와졌습니다", "imagesImported": "예시 이미지가 성공적으로 가져와졌습니다",
"imagesPartial": "{success}개 이미지 가져오기 성공, {failed}개 실패",
"importFailed": "예시 이미지 가져오기 실패: {message}" "importFailed": "예시 이미지 가져오기 실패: {message}"
}, },
"triggerWords": { "triggerWords": {

View File

@@ -1466,6 +1466,7 @@
"folderTreeFailed": "Не удалось загрузить дерево папок", "folderTreeFailed": "Не удалось загрузить дерево папок",
"folderTreeError": "Ошибка загрузки дерева папок", "folderTreeError": "Ошибка загрузки дерева папок",
"imagesImported": "Примеры изображений успешно импортированы", "imagesImported": "Примеры изображений успешно импортированы",
"imagesPartial": "{success} изображ. импортировано, {failed} не удалось",
"importFailed": "Не удалось импортировать примеры изображений: {message}" "importFailed": "Не удалось импортировать примеры изображений: {message}"
}, },
"triggerWords": { "triggerWords": {

View File

@@ -1466,6 +1466,7 @@
"folderTreeFailed": "加载文件夹树失败", "folderTreeFailed": "加载文件夹树失败",
"folderTreeError": "加载文件夹树出错", "folderTreeError": "加载文件夹树出错",
"imagesImported": "示例图片导入成功", "imagesImported": "示例图片导入成功",
"imagesPartial": "成功导入 {success} 张图片,{failed} 张失败",
"importFailed": "导入示例图片失败:{message}" "importFailed": "导入示例图片失败:{message}"
}, },
"triggerWords": { "triggerWords": {

View File

@@ -1466,6 +1466,7 @@
"folderTreeFailed": "載入資料夾樹狀結構失敗", "folderTreeFailed": "載入資料夾樹狀結構失敗",
"folderTreeError": "載入資料夾樹狀結構錯誤", "folderTreeError": "載入資料夾樹狀結構錯誤",
"imagesImported": "範例圖片匯入成功", "imagesImported": "範例圖片匯入成功",
"imagesPartial": "成功匯入 {success} 張圖片,{failed} 張失敗",
"importFailed": "匯入範例圖片失敗:{message}" "importFailed": "匯入範例圖片失敗:{message}"
}, },
"triggerWords": { "triggerWords": {

View File

@@ -1,11 +1,14 @@
"""Handler set for example image routes.""" """Handler set for example image routes."""
from __future__ import annotations from __future__ import annotations
import logging
from dataclasses import dataclass from dataclasses import dataclass
from typing import Callable, Mapping from typing import Callable, Mapping
from aiohttp import web from aiohttp import web
logger = logging.getLogger(__name__)
from ...services.use_cases.example_images import ( from ...services.use_cases.example_images import (
DownloadExampleImagesConfigurationError, DownloadExampleImagesConfigurationError,
DownloadExampleImagesInProgressError, DownloadExampleImagesInProgressError,
@@ -122,6 +125,9 @@ class ExampleImagesManagementHandler:
return web.json_response({'success': False, 'error': str(exc)}, status=400) return web.json_response({'success': False, 'error': str(exc)}, status=400)
except ExampleImagesImportError as exc: except ExampleImagesImportError as exc:
return web.json_response({'success': False, 'error': str(exc)}, status=500) return web.json_response({'success': False, 'error': str(exc)}, status=500)
except Exception as exc:
logger.exception("Unexpected error importing example images")
return web.json_response({'success': False, 'error': str(exc)}, status=500)
async def delete_example_image(self, request: web.Request) -> web.StreamResponse: async def delete_example_image(self, request: web.Request) -> web.StreamResponse:
return await self._processor.delete_custom_image(request) return await self._processor.delete_custom_image(request)

View File

@@ -154,6 +154,7 @@ class StandaloneServer:
self.app = web.Application( self.app = web.Application(
logger=logger, logger=logger,
middlewares=[cache_control], middlewares=[cache_control],
client_max_size=256 * 1024 * 1024,
handler_args={ handler_args={
"max_field_size": HEADER_SIZE_LIMIT, "max_field_size": HEADER_SIZE_LIMIT,
"max_line_size": HEADER_SIZE_LIMIT, "max_line_size": HEADER_SIZE_LIMIT,

View File

@@ -455,26 +455,41 @@ async function handleImportFiles(files, modelHash, importContainer) {
} }
try { try {
// Use FormData to upload files // Upload files one at a time to avoid exceeding server size limits
const formData = new FormData(); let lastSuccessResult = null;
formData.append('model_hash', modelHash); let successCount = 0;
const errors = [];
validFiles.forEach(file => { for (const file of validFiles) {
formData.append('files', file); try {
}); const formData = new FormData();
formData.append('model_hash', modelHash);
formData.append('files', file);
// Call API to import files const response = await fetch('/api/lm/import-example-images', {
const response = await fetch('/api/lm/import-example-images', { method: 'POST',
method: 'POST', body: formData
body: formData });
});
const result = await response.json(); const result = await response.json();
if (!result.success) { if (!result.success) {
throw new Error(result.error || 'Failed to import example files'); errors.push(`${file.name}: ${result.error || 'Unknown error'}`);
} else {
lastSuccessResult = result;
successCount++;
}
} catch (err) {
errors.push(`${file.name}: ${err.message}`);
}
} }
if (successCount === 0) {
throw new Error(errors.join('; '));
}
const result = lastSuccessResult;
// Get updated local files // Get updated local files
const updatedFilesResponse = await fetch(`/api/lm/example-image-files?model_hash=${modelHash}`); const updatedFilesResponse = await fetch(`/api/lm/example-image-files?model_hash=${modelHash}`);
const updatedFilesResult = await updatedFilesResponse.json(); const updatedFilesResult = await updatedFilesResponse.json();
@@ -502,7 +517,11 @@ async function handleImportFiles(files, modelHash, importContainer) {
// Initialize the import UI for the new content // Initialize the import UI for the new content
initExampleImport(modelHash, showcaseTab); initExampleImport(modelHash, showcaseTab);
showToast('toast.import.imagesImported', {}, 'success'); if (errors.length > 0) {
showToast('toast.import.imagesPartial', { success: successCount, failed: errors.length }, 'warning');
} else {
showToast('toast.import.imagesImported', {}, 'success');
}
// Update VirtualScroller if available // Update VirtualScroller if available
if (state.virtualScroller && result.model_file_path) { if (state.virtualScroller && result.model_file_path) {