feat: download - download dwnload limieten in settings

This commit is contained in:
kodi
2026-03-14 13:38:44 +01:00
parent ea337338e3
commit 8ea2bd1498
12 changed files with 228 additions and 26 deletions
+9
View File
@@ -103,12 +103,21 @@ class FileInfoResponse(BaseModel):
height: int | None = None
class ZipDownloadLimitsResponse(BaseModel):
max_items: int
max_total_input_bytes: int
max_individual_file_bytes: int
scan_timeout_seconds: float
symlink_policy: str
class SettingsResponse(BaseModel):
show_thumbnails: bool
preferred_startup_path_left: str | None = None
preferred_startup_path_right: str | None = None
selected_theme: str
selected_color_mode: str
zip_download_limits: ZipDownloadLimitsResponse
class SettingsUpdateRequest(BaseModel):
@@ -1,9 +1,10 @@
from __future__ import annotations
from backend.app.api.errors import AppError
from backend.app.api.schemas import SettingsResponse, SettingsUpdateRequest
from backend.app.api.schemas import SettingsResponse, SettingsUpdateRequest, ZipDownloadLimitsResponse
from backend.app.db.settings_repository import SettingsRepository
from backend.app.security.path_guard import PathGuard
from backend.app.services.file_ops_service import ZIP_DOWNLOAD_PREFLIGHT_LIMITS
VALID_THEMES = {
@@ -38,6 +39,13 @@ class SettingsService:
preferred_startup_path_right=preferred_right,
selected_theme=selected_theme,
selected_color_mode=selected_color_mode,
zip_download_limits=ZipDownloadLimitsResponse(
max_items=ZIP_DOWNLOAD_PREFLIGHT_LIMITS.max_items,
max_total_input_bytes=ZIP_DOWNLOAD_PREFLIGHT_LIMITS.max_total_input_bytes,
max_individual_file_bytes=ZIP_DOWNLOAD_PREFLIGHT_LIMITS.max_individual_file_bytes,
scan_timeout_seconds=ZIP_DOWNLOAD_PREFLIGHT_LIMITS.scan_timeout_seconds,
symlink_policy="not_allowed",
),
)
def update_settings(self, request: SettingsUpdateRequest) -> SettingsResponse:
Binary file not shown.
@@ -49,6 +49,16 @@ class SettingsApiGoldenTest(unittest.TestCase):
return asyncio.run(_run())
@staticmethod
def _default_zip_download_limits() -> dict:
return {
"max_items": 1000,
"max_total_input_bytes": 2147483648,
"max_individual_file_bytes": 524288000,
"scan_timeout_seconds": 10.0,
"symlink_policy": "not_allowed",
}
def test_settings_default_response(self) -> None:
response = self._request("GET", "/api/settings")
@@ -61,6 +71,7 @@ class SettingsApiGoldenTest(unittest.TestCase):
"preferred_startup_path_right": None,
"selected_theme": "default",
"selected_color_mode": "dark",
"zip_download_limits": self._default_zip_download_limits(),
},
)
@@ -79,6 +90,7 @@ class SettingsApiGoldenTest(unittest.TestCase):
"preferred_startup_path_right": None,
"selected_theme": "default",
"selected_color_mode": "dark",
"zip_download_limits": self._default_zip_download_limits(),
},
)
@@ -102,6 +114,7 @@ class SettingsApiGoldenTest(unittest.TestCase):
"preferred_startup_path_right": "storage1/docs",
"selected_theme": "default",
"selected_color_mode": "dark",
"zip_download_limits": self._default_zip_download_limits(),
},
)
self.assertEqual(
@@ -112,6 +125,7 @@ class SettingsApiGoldenTest(unittest.TestCase):
"preferred_startup_path_right": "storage1/docs",
"selected_theme": "default",
"selected_color_mode": "dark",
"zip_download_limits": self._default_zip_download_limits(),
},
)
@@ -123,6 +137,7 @@ class SettingsApiGoldenTest(unittest.TestCase):
self.assertEqual(response.json()["preferred_startup_path_right"], None)
self.assertEqual(response.json()["selected_theme"], "default")
self.assertEqual(response.json()["selected_color_mode"], "dark")
self.assertEqual(response.json()["zip_download_limits"], self._default_zip_download_limits())
def test_settings_preferred_startup_path_right_persistence(self) -> None:
response = self._request("POST", "/api/settings", {"preferred_startup_path_right": "storage1/docs"})
@@ -132,6 +147,7 @@ class SettingsApiGoldenTest(unittest.TestCase):
self.assertEqual(response.json()["preferred_startup_path_right"], "storage1/docs")
self.assertEqual(response.json()["selected_theme"], "default")
self.assertEqual(response.json()["selected_color_mode"], "dark")
self.assertEqual(response.json()["zip_download_limits"], self._default_zip_download_limits())
def test_settings_preferred_startup_path_empty_string_resets_only_left_to_null(self) -> None:
self._request(
@@ -149,6 +165,7 @@ class SettingsApiGoldenTest(unittest.TestCase):
self.assertEqual(response.json()["preferred_startup_path_right"], "storage1/docs")
self.assertEqual(response.json()["selected_theme"], "default")
self.assertEqual(response.json()["selected_color_mode"], "dark")
self.assertEqual(response.json()["zip_download_limits"], self._default_zip_download_limits())
def test_settings_selected_theme_persistence(self) -> None:
response = self._request("POST", "/api/settings", {"selected_theme": "midnight"})
@@ -156,6 +173,7 @@ class SettingsApiGoldenTest(unittest.TestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["selected_theme"], "midnight")
self.assertEqual(response.json()["selected_color_mode"], "dark")
self.assertEqual(response.json()["zip_download_limits"], self._default_zip_download_limits())
def test_settings_selected_theme_accepts_new_built_in_family(self) -> None:
response = self._request("POST", "/api/settings", {"selected_theme": "commander-electric"})
@@ -163,6 +181,7 @@ class SettingsApiGoldenTest(unittest.TestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["selected_theme"], "commander-electric")
self.assertEqual(response.json()["selected_color_mode"], "dark")
self.assertEqual(response.json()["zip_download_limits"], self._default_zip_download_limits())
def test_settings_selected_color_mode_persistence(self) -> None:
response = self._request("POST", "/api/settings", {"selected_color_mode": "light"})
@@ -170,6 +189,13 @@ class SettingsApiGoldenTest(unittest.TestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["selected_theme"], "default")
self.assertEqual(response.json()["selected_color_mode"], "light")
self.assertEqual(response.json()["zip_download_limits"], self._default_zip_download_limits())
def test_settings_includes_read_only_zip_download_limits(self) -> None:
response = self._request("GET", "/api/settings")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["zip_download_limits"], self._default_zip_download_limits())
def test_settings_rejects_invalid_selected_theme(self) -> None:
response = self._request("POST", "/api/settings", {"selected_theme": "unknown"})
@@ -120,6 +120,7 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn('id="rename-apply-btn"', body)
self.assertIn('id="settings-general-tab"', body)
self.assertIn('id="settings-interface-tab"', body)
self.assertIn('id="settings-downloads-tab"', body)
self.assertIn('id="settings-logs-tab"', body)
self.assertIn('id="settings-show-thumbnails"', body)
self.assertIn("Show thumbnails", body)
@@ -141,6 +142,13 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn("Preferred startup path (right)", body)
self.assertIn('id="settings-general-save-btn"', body)
self.assertIn('id="settings-interface-save-btn"', body)
self.assertIn('id="settings-downloads-panel"', body)
self.assertIn('id="settings-download-max-items"', body)
self.assertIn('id="settings-download-max-total-size"', body)
self.assertIn('id="settings-download-max-file-size"', body)
self.assertIn('id="settings-download-scan-timeout"', body)
self.assertIn('id="settings-download-symlink-policy"', body)
self.assertIn("ZIP download limits are shown for reference and cannot be changed here.", body)
self.assertIn('id="settings-logs-list"', body)
self.assertIn('id="viewer-content"', body)
self.assertIn('id="editor-modal"', body)
@@ -224,6 +232,7 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn('function markZipDownloadReady(fileName)', app_js)
self.assertIn('function markZipDownloadFailed(err)', app_js)
self.assertIn('function closeDownloadModal()', app_js)
self.assertIn('function zipDownloadRequestKey(paths)', app_js)
self.assertIn('function contextMenuElements()', app_js)
self.assertIn('function openContextMenu(pane, entry, event)', app_js)
self.assertIn('function closeContextMenu()', app_js)
@@ -231,13 +240,16 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn('async function downloadFileRequest(paths)', app_js)
self.assertIn('const zipDownload = isZipDownloadSelection(selectedItems);', app_js)
self.assertIn('openZipDownloadModal(selectedItems);', app_js)
self.assertIn('statusText: "preparing"', app_js)
self.assertIn('statusText: "packaging items"', app_js)
self.assertIn('statusText: "ready"', app_js)
self.assertIn('statusText: `failed: ${err.message}`', app_js)
self.assertIn('countText: "Step 1/3"', app_js)
self.assertIn('countText: "Step 2/3"', app_js)
self.assertIn('countText: "Step 3/3"', app_js)
self.assertIn('targetText: "Preparing download..."', app_js)
self.assertIn('statusText: "Preparing download..."', app_js)
self.assertIn('countText: "Preparing zip download"', app_js)
self.assertIn('countText: "Zip preflight and packaging"', app_js)
self.assertIn('statusText: "Download started"', app_js)
self.assertIn('countText: "Browser download started"', app_js)
self.assertIn('countText: "Zip download failed"', app_js)
self.assertIn('statusText: err.message || "Download failed"', app_js)
self.assertIn('downloadProgressState.requestKey === requestKey', app_js)
self.assertIn('setStatus("Preparing download...");', app_js)
self.assertIn('function applyContextMenuSelection()', app_js)
self.assertIn('function startContextMenuOpen()', app_js)
self.assertIn('function startContextMenuEdit()', app_js)
@@ -304,6 +316,12 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn('settings.interfaceSaveButton.onclick = handleInterfaceSave;', app_js)
self.assertIn('preferredStartupPathLeft', app_js)
self.assertIn('preferredStartupPathRight', app_js)
self.assertIn('zipDownloadLimits', app_js)
self.assertIn('zip_download_limits', app_js)
self.assertIn('function renderDownloadSettings()', app_js)
self.assertIn('function formatBinarySize(bytes)', app_js)
self.assertIn('function formatSeconds(seconds)', app_js)
self.assertIn('function formatSymlinkPolicy(policy)', app_js)
self.assertIn('selected_theme', app_js)
self.assertIn('selected_color_mode', app_js)
self.assertNotIn("localStorage", app_js)
@@ -314,6 +332,7 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn('paneState("right").currentPath = settingsState.preferredStartupPathRight || "/Volumes";', app_js)
self.assertIn('applyTheme(settingsState.selectedTheme, settingsState.selectedColorMode);', app_js)
self.assertIn('settings.interfaceTab.onclick = () => setSettingsTab("interface");', app_js)
self.assertIn('settings.downloadsTab.onclick = () => setSettingsTab("downloads");', app_js)
self.assertIn('"/api/settings"', app_js)
self.assertIn('function uploadElements()', app_js)
self.assertIn('function openUploadPicker()', app_js)