feat: show live SHA-256 verification progress and surface mismatches in UI for aria2 downloads
This commit is contained in:
@@ -230,6 +230,12 @@ context._run_huggingface_xet_transfer = _bind(
|
||||
)
|
||||
|
||||
context.download_file_with_aria2 = _bind(aria2_backend.download_file_with_aria2)
|
||||
context.verify_download_sha256_with_progress = _bind(
|
||||
aria2_backend.verify_download_sha256_with_progress
|
||||
)
|
||||
context._verify_download_sha256_with_progress = (
|
||||
context.verify_download_sha256_with_progress
|
||||
)
|
||||
context.start_background_sha256_verification = _bind(
|
||||
aria2_backend.start_background_sha256_verification
|
||||
)
|
||||
|
||||
+242
-30
@@ -858,6 +858,7 @@ def download_file_with_aria2(
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
category: str = "",
|
||||
*,
|
||||
expected_sha256: Optional[str] = None,
|
||||
dependencies: Any = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Download a file with an aria2c JSON-RPC process."""
|
||||
@@ -872,7 +873,9 @@ def download_file_with_aria2(
|
||||
}
|
||||
start_time = facade.time.time()
|
||||
filename = facade.get_filename_from_path(dest_path)
|
||||
expected_sha256 = facade._extract_expected_sha256(metadata)
|
||||
expected_sha256 = (
|
||||
expected_sha256 or facade._extract_expected_sha256(metadata)
|
||||
)
|
||||
max_url_recovery = getattr(
|
||||
facade,
|
||||
"MAX_ARIA2_URL_RECOVERY_ATTEMPTS",
|
||||
@@ -1144,6 +1147,126 @@ def download_file_with_aria2(
|
||||
if facade.os.path.exists(completed_path)
|
||||
else downloaded
|
||||
)
|
||||
clean_expected = str(expected_sha256 or "").lower().strip()
|
||||
|
||||
if clean_expected and facade.os.path.isfile(completed_path):
|
||||
with facade.download_lock:
|
||||
if download_id in facade.download_progress:
|
||||
facade.download_progress[download_id].update(
|
||||
{
|
||||
"status": "verifying",
|
||||
"progress": 0,
|
||||
"downloaded": 0,
|
||||
"total_size": size,
|
||||
"speed": 0,
|
||||
"path": completed_path,
|
||||
"directory": facade.os.path.dirname(completed_path),
|
||||
"expected_sha256": clean_expected,
|
||||
}
|
||||
)
|
||||
log.info(
|
||||
f"aria2 transfer complete for {filename}; verifying SHA256 integrity..."
|
||||
)
|
||||
|
||||
verify_fn = getattr(
|
||||
facade,
|
||||
"verify_download_sha256_with_progress",
|
||||
None,
|
||||
) or verify_download_sha256_with_progress
|
||||
|
||||
is_match, actual_sha256, is_cancelled = verify_fn(
|
||||
completed_path,
|
||||
clean_expected,
|
||||
download_id=download_id,
|
||||
size=size,
|
||||
dependencies=facade,
|
||||
)
|
||||
|
||||
if is_cancelled or download_id in facade.cancelled_downloads:
|
||||
facade._delete_partial_download_files(completed_path)
|
||||
facade.cancelled_downloads.discard(download_id)
|
||||
with facade.download_lock:
|
||||
if download_id in facade.download_progress:
|
||||
facade.download_progress[download_id]["status"] = "cancelled"
|
||||
facade.download_progress[download_id]["speed"] = 0
|
||||
result["error"] = "Download cancelled"
|
||||
return result
|
||||
|
||||
if not is_match:
|
||||
log.error(
|
||||
f"✗ SHA256 mismatch detected for {filename}! "
|
||||
f"Expected {clean_expected}, got {actual_sha256}"
|
||||
)
|
||||
bad_path = f"{completed_path}.badsha"
|
||||
try:
|
||||
facade.os.replace(completed_path, bad_path)
|
||||
except OSError as exc:
|
||||
log.warning(
|
||||
f"Could not preserve SHA256-mismatched download at {bad_path}: {exc}"
|
||||
)
|
||||
bad_path = ""
|
||||
|
||||
error_msg = f"SHA256 mismatch: expected {clean_expected}, got {actual_sha256}"
|
||||
if bad_path:
|
||||
error_msg += f"; file kept at {bad_path}"
|
||||
sidecar_model_path = (
|
||||
bad_path
|
||||
if facade.os.path.exists(completed_path)
|
||||
else completed_path
|
||||
)
|
||||
try:
|
||||
payload = facade.build_model_resolver_metadata(
|
||||
sidecar_model_path,
|
||||
{"sha256": actual_sha256, "size": size},
|
||||
category,
|
||||
)
|
||||
payload.update(
|
||||
{
|
||||
"expected_sha256": clean_expected,
|
||||
"sha256_verified": False,
|
||||
}
|
||||
)
|
||||
sidecar_meta_path = (
|
||||
facade.get_model_resolver_sidecar_path(
|
||||
sidecar_model_path
|
||||
)
|
||||
)
|
||||
facade.write_json_atomic(
|
||||
sidecar_meta_path, payload, indent=2
|
||||
)
|
||||
except Exception as exc:
|
||||
log.warning(
|
||||
f"Could not save metadata for {bad_path}: {exc}"
|
||||
)
|
||||
|
||||
with facade.download_lock:
|
||||
if download_id in facade.download_progress:
|
||||
facade.download_progress[download_id].update(
|
||||
{
|
||||
"status": "error",
|
||||
"error": error_msg,
|
||||
"speed": 0,
|
||||
"sha256": actual_sha256,
|
||||
"actual_sha256": actual_sha256,
|
||||
"expected_sha256": clean_expected,
|
||||
"sha256_verified": False,
|
||||
"path": bad_path or completed_path,
|
||||
}
|
||||
)
|
||||
|
||||
result.update(
|
||||
{
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"path": bad_path or completed_path,
|
||||
"size": size,
|
||||
"sha256": actual_sha256,
|
||||
"expected_sha256": clean_expected,
|
||||
"sha256_verified": False,
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
metadata_path = facade.write_model_resolver_metadata(
|
||||
completed_path,
|
||||
metadata or {},
|
||||
@@ -1151,6 +1274,13 @@ def download_file_with_aria2(
|
||||
url,
|
||||
create_preview=True,
|
||||
)
|
||||
if metadata_path and facade.os.path.isfile(metadata_path) and clean_expected:
|
||||
meta = facade.read_json_safe(metadata_path, {})
|
||||
if isinstance(meta, dict):
|
||||
meta["sha256_verified"] = True
|
||||
meta["sha256"] = clean_expected
|
||||
facade.write_json_atomic(metadata_path, meta, indent=2)
|
||||
|
||||
with facade.download_lock:
|
||||
facade.download_progress[download_id].update(
|
||||
{
|
||||
@@ -1161,6 +1291,9 @@ def download_file_with_aria2(
|
||||
"speed": 0,
|
||||
"path": completed_path,
|
||||
"directory": facade.os.path.dirname(completed_path),
|
||||
"sha256_verified": True if clean_expected else None,
|
||||
"sha256": clean_expected or None,
|
||||
"expected_sha256": clean_expected or None,
|
||||
}
|
||||
)
|
||||
if metadata_path:
|
||||
@@ -1173,6 +1306,8 @@ def download_file_with_aria2(
|
||||
"path": completed_path,
|
||||
"size": size,
|
||||
"metadata_path": metadata_path,
|
||||
"sha256_verified": True if clean_expected else None,
|
||||
"sha256": clean_expected or None,
|
||||
}
|
||||
)
|
||||
guard_fn = getattr(facade, "get_connectivity_guard", None)
|
||||
@@ -1187,15 +1322,6 @@ def download_file_with_aria2(
|
||||
)
|
||||
facade.invalidate_model_caches()
|
||||
|
||||
if expected_sha256:
|
||||
start_background_sha256_verification(
|
||||
completed_path,
|
||||
expected_sha256,
|
||||
download_id=download_id,
|
||||
metadata_path=metadata_path,
|
||||
dependencies=facade,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
if state == "error":
|
||||
@@ -1315,6 +1441,86 @@ def download_file_with_aria2(
|
||||
facade._schedule_aria2_idle_stop()
|
||||
|
||||
|
||||
def verify_download_sha256_with_progress(
|
||||
completed_path: str,
|
||||
expected_sha256: str,
|
||||
*,
|
||||
download_id: str = "",
|
||||
size: int = 0,
|
||||
dependencies: Any = None,
|
||||
) -> tuple[bool, str, bool]:
|
||||
"""Verify downloaded model SHA256 while streaming progress to download_progress.
|
||||
|
||||
Returns (is_match, actual_sha256, is_cancelled).
|
||||
"""
|
||||
facade = require_download_dependencies(dependencies, "aria2 backend")
|
||||
clean_expected = str(expected_sha256 or "").lower().strip()
|
||||
if not clean_expected or not facade.os.path.isfile(completed_path):
|
||||
return True, "", False
|
||||
|
||||
filename = facade.get_filename_from_path(completed_path)
|
||||
file_size = (
|
||||
size
|
||||
or (
|
||||
facade.os.path.getsize(completed_path)
|
||||
if facade.os.path.isfile(completed_path)
|
||||
else 0
|
||||
)
|
||||
)
|
||||
hasher = facade.hashlib.sha256()
|
||||
|
||||
start_t = facade.time.time()
|
||||
last_ui_update = start_t
|
||||
last_cli_log = start_t
|
||||
verified_bytes = 0
|
||||
last_verified_bytes = 0
|
||||
|
||||
with open(completed_path, "rb") as file_handle:
|
||||
while True:
|
||||
if download_id and download_id in facade.cancelled_downloads:
|
||||
return False, "", True
|
||||
|
||||
chunk = file_handle.read(4 * 1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
hasher.update(chunk)
|
||||
verified_bytes += len(chunk)
|
||||
|
||||
now = facade.time.time()
|
||||
if now - last_ui_update >= 0.25:
|
||||
time_delta = now - last_ui_update
|
||||
bytes_delta = verified_bytes - last_verified_bytes
|
||||
verify_speed = int(bytes_delta / time_delta) if time_delta > 0 else 0
|
||||
last_ui_update = now
|
||||
last_verified_bytes = verified_bytes
|
||||
pct = int((verified_bytes / file_size) * 100) if file_size > 0 else 0
|
||||
|
||||
if download_id:
|
||||
with facade.download_lock:
|
||||
if download_id in facade.download_progress:
|
||||
facade.download_progress[download_id].update(
|
||||
{
|
||||
"status": "verifying",
|
||||
"progress": pct,
|
||||
"downloaded": verified_bytes,
|
||||
"total_size": file_size,
|
||||
"speed": verify_speed,
|
||||
}
|
||||
)
|
||||
|
||||
if now - last_cli_log >= facade.CLI_LOG_INTERVAL:
|
||||
last_cli_log = now
|
||||
pct = int((verified_bytes / file_size) * 100) if file_size > 0 else 0
|
||||
log.info(
|
||||
f"Verifying SHA256 for {filename}: "
|
||||
f"{facade.format_bytes(verified_bytes)} / {facade.format_bytes(file_size)} "
|
||||
f"({pct}%)"
|
||||
)
|
||||
|
||||
actual_sha256 = hasher.hexdigest().lower()
|
||||
return (actual_sha256 == clean_expected), actual_sha256, False
|
||||
|
||||
|
||||
def start_background_sha256_verification(
|
||||
completed_path: str,
|
||||
expected_sha256: str,
|
||||
@@ -1333,26 +1539,29 @@ def start_background_sha256_verification(
|
||||
filename = facade.get_filename_from_path(completed_path)
|
||||
log.debug(f"Starting background SHA256 verification for {filename}...")
|
||||
try:
|
||||
hasher = facade.hashlib.sha256()
|
||||
with open(completed_path, "rb") as file_handle:
|
||||
while True:
|
||||
chunk = file_handle.read(4 * 1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
hasher.update(chunk)
|
||||
actual_sha256 = hasher.hexdigest().lower()
|
||||
is_match, actual_sha256, is_cancelled = verify_download_sha256_with_progress(
|
||||
completed_path,
|
||||
expected_sha256,
|
||||
download_id=download_id,
|
||||
dependencies=facade,
|
||||
)
|
||||
if is_cancelled:
|
||||
return
|
||||
|
||||
if actual_sha256 == clean_expected:
|
||||
target_meta_path = (
|
||||
metadata_path
|
||||
or facade.get_model_resolver_sidecar_path(completed_path)
|
||||
)
|
||||
if is_match:
|
||||
log.info(f"✓ Background SHA256 verified for: {filename}")
|
||||
if download_id:
|
||||
with facade.download_lock:
|
||||
if download_id in facade.download_progress:
|
||||
facade.download_progress[download_id]["status"] = "completed"
|
||||
facade.download_progress[download_id]["progress"] = 100
|
||||
facade.download_progress[download_id]["speed"] = 0
|
||||
facade.download_progress[download_id]["sha256_verified"] = True
|
||||
facade.download_progress[download_id]["sha256"] = actual_sha256
|
||||
target_meta_path = (
|
||||
metadata_path
|
||||
or facade.get_model_resolver_sidecar_path(completed_path)
|
||||
)
|
||||
if target_meta_path and facade.os.path.isfile(target_meta_path):
|
||||
meta = facade.read_json_safe(target_meta_path, {})
|
||||
if isinstance(meta, dict):
|
||||
@@ -1364,18 +1573,21 @@ def start_background_sha256_verification(
|
||||
f"✗ SHA256 mismatch detected for {filename}! "
|
||||
f"Expected {clean_expected}, got {actual_sha256}"
|
||||
)
|
||||
bad_path = f"{completed_path}.badsha"
|
||||
try:
|
||||
facade.os.replace(completed_path, bad_path)
|
||||
except OSError:
|
||||
bad_path = ""
|
||||
error_msg = f"SHA256 mismatch: expected {clean_expected}, got {actual_sha256}"
|
||||
if bad_path:
|
||||
error_msg += f"; file kept at {bad_path}"
|
||||
if download_id:
|
||||
with facade.download_lock:
|
||||
if download_id in facade.download_progress:
|
||||
facade.download_progress[download_id]["status"] = "error"
|
||||
facade.download_progress[download_id]["sha256_verified"] = False
|
||||
facade.download_progress[download_id]["actual_sha256"] = actual_sha256
|
||||
facade.download_progress[download_id]["error"] = (
|
||||
f"SHA256 mismatch: expected {clean_expected}, got {actual_sha256}"
|
||||
)
|
||||
target_meta_path = (
|
||||
metadata_path
|
||||
or facade.get_model_resolver_sidecar_path(completed_path)
|
||||
)
|
||||
facade.download_progress[download_id]["error"] = error_msg
|
||||
if target_meta_path and facade.os.path.isfile(target_meta_path):
|
||||
meta = facade.read_json_safe(target_meta_path, {})
|
||||
if isinstance(meta, dict):
|
||||
|
||||
@@ -605,6 +605,139 @@ class DownloaderBoundaryTests(unittest.TestCase):
|
||||
self.assertEqual(result["error"], "Download cancelled")
|
||||
self.assertEqual(mock_rpc.call_count, 0)
|
||||
|
||||
def test_aria2_download_sha256_verification_success(self):
|
||||
import hashlib
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
dest_path = os.path.join(temp_dir, "model_verified.safetensors")
|
||||
content = b"valid model file content for aria2 verification"
|
||||
with open(dest_path, "wb") as f:
|
||||
f.write(content)
|
||||
|
||||
expected_sha = hashlib.sha256(content).hexdigest()
|
||||
download_id = "test-aria2-verify-success"
|
||||
|
||||
mock_resolve = MagicMock(return_value=("https://example.com/model.safetensors", {}))
|
||||
mock_rpc = MagicMock(return_value="gid-verify-1")
|
||||
mock_status = MagicMock(return_value={
|
||||
"status": "complete",
|
||||
"completedLength": str(len(content)),
|
||||
"totalLength": str(len(content)),
|
||||
})
|
||||
|
||||
with patch.object(downloader, "_ensure_aria2_daemon"), \
|
||||
patch.object(downloader, "_resolve_download_url_for_aria2", mock_resolve), \
|
||||
patch.object(downloader, "_aria2_rpc", mock_rpc), \
|
||||
patch.object(downloader, "_aria2_tell_status", mock_status), \
|
||||
patch.object(downloader, "write_model_resolver_metadata", return_value=""), \
|
||||
patch.object(downloader, "_schedule_aria2_idle_stop"), \
|
||||
patch.object(downloader, "invalidate_model_caches"):
|
||||
|
||||
result = download_file_with_aria2(
|
||||
"https://example.com/model.safetensors",
|
||||
dest_path,
|
||||
download_id,
|
||||
expected_sha256=expected_sha,
|
||||
dependencies=downloader,
|
||||
)
|
||||
|
||||
self.assertTrue(result["success"])
|
||||
self.assertEqual(dest_path, result["path"])
|
||||
self.assertTrue(os.path.isfile(dest_path))
|
||||
with downloader.download_lock:
|
||||
progress = downloader.download_progress.get(download_id, {})
|
||||
self.assertEqual("completed", progress.get("status"))
|
||||
self.assertTrue(progress.get("sha256_verified"))
|
||||
self.assertEqual(expected_sha, progress.get("sha256"))
|
||||
|
||||
def test_aria2_download_sha256_verification_mismatch(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
dest_path = os.path.join(temp_dir, "model_corrupt.safetensors")
|
||||
content = b"corrupted model file content"
|
||||
with open(dest_path, "wb") as f:
|
||||
f.write(content)
|
||||
|
||||
expected_sha = "f" * 64
|
||||
download_id = "test-aria2-verify-mismatch"
|
||||
|
||||
mock_resolve = MagicMock(return_value=("https://example.com/model.safetensors", {}))
|
||||
mock_rpc = MagicMock(return_value="gid-verify-2")
|
||||
mock_status = MagicMock(return_value={
|
||||
"status": "complete",
|
||||
"completedLength": str(len(content)),
|
||||
"totalLength": str(len(content)),
|
||||
})
|
||||
|
||||
with patch.object(downloader, "_ensure_aria2_daemon"), \
|
||||
patch.object(downloader, "_resolve_download_url_for_aria2", mock_resolve), \
|
||||
patch.object(downloader, "_aria2_rpc", mock_rpc), \
|
||||
patch.object(downloader, "_aria2_tell_status", mock_status), \
|
||||
patch.object(downloader, "write_model_resolver_metadata", return_value=""), \
|
||||
patch.object(downloader, "_schedule_aria2_idle_stop"), \
|
||||
patch.object(downloader, "invalidate_model_caches"):
|
||||
|
||||
result = download_file_with_aria2(
|
||||
"https://example.com/model.safetensors",
|
||||
dest_path,
|
||||
download_id,
|
||||
expected_sha256=expected_sha,
|
||||
dependencies=downloader,
|
||||
)
|
||||
|
||||
self.assertFalse(result["success"])
|
||||
self.assertIn("sha256 mismatch", result["error"].lower())
|
||||
self.assertFalse(os.path.exists(dest_path))
|
||||
bad_path = f"{dest_path}.badsha"
|
||||
self.assertTrue(os.path.isfile(bad_path))
|
||||
|
||||
with downloader.download_lock:
|
||||
progress = downloader.download_progress.get(download_id, {})
|
||||
self.assertEqual("error", progress.get("status"))
|
||||
self.assertFalse(progress.get("sha256_verified"))
|
||||
self.assertIn("mismatch", progress.get("error", "").lower())
|
||||
|
||||
def test_aria2_download_sha256_verification_cancelled(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
dest_path = os.path.join(temp_dir, "model_cancelled.safetensors")
|
||||
content = b"content being verified"
|
||||
with open(dest_path, "wb") as f:
|
||||
f.write(content)
|
||||
|
||||
download_id = "test-aria2-verify-cancel"
|
||||
mock_resolve = MagicMock(return_value=("https://example.com/model.safetensors", {}))
|
||||
mock_rpc = MagicMock(return_value="gid-verify-3")
|
||||
mock_status = MagicMock(return_value={
|
||||
"status": "complete",
|
||||
"completedLength": str(len(content)),
|
||||
"totalLength": str(len(content)),
|
||||
})
|
||||
|
||||
def mock_verify(*args, **kwargs):
|
||||
return False, "", True
|
||||
|
||||
with patch.object(downloader, "_ensure_aria2_daemon"), \
|
||||
patch.object(downloader, "_resolve_download_url_for_aria2", mock_resolve), \
|
||||
patch.object(downloader, "_aria2_rpc", mock_rpc), \
|
||||
patch.object(downloader, "_aria2_tell_status", mock_status), \
|
||||
patch.object(downloader, "verify_download_sha256_with_progress", mock_verify), \
|
||||
patch.object(downloader, "write_model_resolver_metadata", return_value=""), \
|
||||
patch.object(downloader, "_schedule_aria2_idle_stop"), \
|
||||
patch.object(downloader, "invalidate_model_caches"):
|
||||
|
||||
result = download_file_with_aria2(
|
||||
"https://example.com/model.safetensors",
|
||||
dest_path,
|
||||
download_id,
|
||||
expected_sha256="expected_sha",
|
||||
dependencies=downloader,
|
||||
)
|
||||
|
||||
self.assertFalse(result["success"])
|
||||
self.assertEqual("Download cancelled", result["error"])
|
||||
self.assertFalse(os.path.exists(dest_path))
|
||||
with downloader.download_lock:
|
||||
progress = downloader.download_progress.get(download_id, {})
|
||||
self.assertEqual("cancelled", progress.get("status"))
|
||||
|
||||
def test_parse_retry_after(self):
|
||||
self.assertEqual(parse_retry_after("120"), 120)
|
||||
self.assertEqual(parse_retry_after("0"), 1)
|
||||
|
||||
@@ -784,6 +784,17 @@
|
||||
box-shadow: 0 0 10px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.mr-download-queue-status.mr-download-queue-status-verifying {
|
||||
color: var(--mr-color-code-blue, #38bdf8);
|
||||
background: rgba(56, 189, 248, 0.16);
|
||||
border-color: rgba(56, 189, 248, 0.34);
|
||||
}
|
||||
|
||||
.mr-download-queue-progress .mr-progress-fill.mr-progress-fill-verifying {
|
||||
background: var(--mr-color-code-blue, #38bdf8);
|
||||
box-shadow: 0 0 10px rgba(56, 189, 248, 0.3);
|
||||
}
|
||||
|
||||
.mr-download-queue-stat.mr-download-queue-stat-error {
|
||||
max-width: 220px;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -4,13 +4,14 @@ export const downloadProgressMethods = {
|
||||
*/
|
||||
getDownloadProgressPollDelay(progress = {}, info = {}) {
|
||||
if (progress.status === 'paused') return 1500;
|
||||
if (progress.status === 'verifying') return 300;
|
||||
const backend = String(
|
||||
progress.download_backend || info.downloadBackend || info.download_backend || ''
|
||||
).toLowerCase();
|
||||
return backend === 'huggingface_xet' ? 200 : 1000;
|
||||
},
|
||||
|
||||
getDownloadProgressStatusLabel(status = '', percent = 0, isFinalizing = false) {
|
||||
getDownloadProgressStatusLabel(status = '', percent = 0, isFinalizing = false, errorMessage = '') {
|
||||
if (status === 'downloading') {
|
||||
if (isFinalizing) return 'Finalizing';
|
||||
const percentLabel = this.formatDownloadPercent?.(percent) ?? String(Math.round(percent));
|
||||
@@ -18,7 +19,13 @@ export const downloadProgressMethods = {
|
||||
}
|
||||
if (status === 'starting') return 'Starting';
|
||||
if (status === 'paused') return 'Paused';
|
||||
if (status === 'error' || status === 'refresh_error') return 'Interrupted';
|
||||
if (status === 'verifying') return 'Verifying SHA-256';
|
||||
if (status === 'error' || status === 'refresh_error') {
|
||||
if (String(errorMessage || '').toLowerCase().includes('sha256')) {
|
||||
return 'SHA-256 mismatch';
|
||||
}
|
||||
return 'Interrupted';
|
||||
}
|
||||
if (status === 'completed_checking') return 'Checking';
|
||||
return String(status || '').replace(/_/g, ' ') || 'Download';
|
||||
},
|
||||
@@ -28,6 +35,7 @@ export const downloadProgressMethods = {
|
||||
return status === 'starting'
|
||||
|| status === 'downloading'
|
||||
|| status === 'paused'
|
||||
|| status === 'verifying'
|
||||
|| (isActive && !terminalStatuses.has(status));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1231,13 +1231,28 @@ export const queueMethods = {
|
||||
const backend = String(progress.download_backend || info.downloadBackend || '').toLowerCase();
|
||||
const isAria2 = backend === 'aria2';
|
||||
const isPaused = status === 'paused';
|
||||
const isVerifying = status === 'verifying';
|
||||
const isError = status === 'error' || status === 'refresh_error';
|
||||
const errorMessage = progress.error || info.statusSnapshot?.message || '';
|
||||
const isShaMismatch = isError && String(errorMessage).toLowerCase().includes('sha256');
|
||||
const statusLabel = status === 'downloading'
|
||||
? (presentation.isFinalizing ? 'Finalizing' : '')
|
||||
: (status === 'starting' ? 'Starting' : (isPaused ? 'Paused' : (isError ? 'Interrupted' : status.replace(/_/g, ' '))));
|
||||
const sizeTextBase = presentation.isFinalizing && logicalTotal
|
||||
? `Finalizing file: ${logicalDownloaded} / ${logicalTotal}`
|
||||
: (presentation.totalSize ? `${downloaded} / ${total}` : downloaded);
|
||||
: (isVerifying
|
||||
? 'Verifying SHA-256'
|
||||
: (status === 'starting'
|
||||
? 'Starting'
|
||||
: (isPaused
|
||||
? 'Paused'
|
||||
: (isShaMismatch
|
||||
? 'SHA-256 mismatch'
|
||||
: (isError
|
||||
? 'Interrupted'
|
||||
: status.replace(/_/g, ' '))))));
|
||||
const sizeTextBase = isVerifying
|
||||
? `Verifying integrity: ${downloaded} / ${total}`
|
||||
: (presentation.isFinalizing && logicalTotal
|
||||
? `Finalizing file: ${logicalDownloaded} / ${logicalTotal}`
|
||||
: (presentation.totalSize ? `${downloaded} / ${total}` : downloaded));
|
||||
const sizeText = presentation.totalSize ? `${sizeTextBase} (${percentLabel}%)` : sizeTextBase;
|
||||
const speed = Number(progress.speed) || 0;
|
||||
const speedText = speed > 0 ? `${this.formatBytes(speed)}/s` : '';
|
||||
@@ -1268,9 +1283,11 @@ export const queueMethods = {
|
||||
html += `<div class="mr-download-queue-heading">`;
|
||||
html += `<span class="mr-download-queue-filename" data-tooltip="${this.escapeHtml(filename)}">${this.escapeHtml(filename)}</span>`;
|
||||
if (statusLabel) {
|
||||
const errorMessage = progress.error || info.statusSnapshot?.message || '';
|
||||
const errorTooltip = isError && errorMessage ? ` data-tooltip="${this.escapeHtml(errorMessage)}"` : '';
|
||||
html += `<span class="mr-download-queue-status${isError ? ' mr-download-queue-status-error' : ''}"${errorTooltip}>${this.escapeHtml(statusLabel)}</span>`;
|
||||
const statusClass = isError
|
||||
? ' mr-download-queue-status-error'
|
||||
: (isVerifying ? ' mr-download-queue-status-verifying' : '');
|
||||
html += `<span class="mr-download-queue-status${statusClass}"${errorTooltip}>${this.escapeHtml(statusLabel)}</span>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
html += `<div class="mr-download-queue-actions" role="group" aria-label="Download actions">`;
|
||||
@@ -1281,7 +1298,7 @@ export const queueMethods = {
|
||||
isPaused ? 'mr-download-queue-resume' : 'mr-download-queue-pause',
|
||||
isPaused ? 'Resume download' : 'Pause download',
|
||||
isPaused ? 'play' : 'pause',
|
||||
!isAria2
|
||||
!isAria2 || isVerifying
|
||||
);
|
||||
}
|
||||
html += actionButton('mr-download-queue-cancel', 'Cancel download', 'x');
|
||||
@@ -1293,12 +1310,14 @@ export const queueMethods = {
|
||||
html += `</div>`;
|
||||
html += `</div>`;
|
||||
html += `<div class="mr-download-queue-progress">`;
|
||||
html += `<div class="mr-progress-bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${percent}" aria-label="Download progress"><div class="mr-progress-fill${isError ? ' mr-progress-fill-error' : ''}" style="width: ${percent}%;"></div></div>`;
|
||||
html += `<div class="mr-progress-bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${percent}" aria-label="Download progress"><div class="mr-progress-fill${isError ? ' mr-progress-fill-error' : (isVerifying ? ' mr-progress-fill-verifying' : '')}" style="width: ${percent}%;"></div></div>`;
|
||||
html += `<div class="mr-download-queue-stats">`;
|
||||
html += `<span class="mr-download-queue-size">${this.escapeHtml(sizeText)}</span>`;
|
||||
html += `<span class="mr-download-queue-stat-group">`;
|
||||
if (speedText && !isError) {
|
||||
html += `<span class="mr-download-queue-stat">${renderIcon('download', 'mr-download-queue-stat-icon')}<span>${this.escapeHtml(speedText)}</span></span>`;
|
||||
const speedIcon = isVerifying ? 'hash' : 'download';
|
||||
const speedTooltip = isVerifying ? 'Hashing speed' : 'Download speed';
|
||||
html += `<span class="mr-download-queue-stat" data-tooltip="${speedTooltip}">${renderIcon(speedIcon, 'mr-download-queue-stat-icon')}<span>${this.escapeHtml(speedText)}</span></span>`;
|
||||
}
|
||||
if (etaText && !isError) {
|
||||
html += `<span class="mr-download-queue-stat"><span>${this.escapeHtml(etaText)}</span></span>`;
|
||||
|
||||
@@ -1291,9 +1291,11 @@ export const resolveDownloadMethods = {
|
||||
: '';
|
||||
const label = displayProgress.isFinalizing
|
||||
? 'Finalizing download...'
|
||||
: (status === 'paused'
|
||||
? `Paused ${percentLabel}`.trim()
|
||||
: (percentLabel ? `Downloading ${percentLabel}` : 'Starting download...'));
|
||||
: (status === 'verifying'
|
||||
? (percentLabel ? `Verifying SHA ${percentLabel}` : 'Verifying SHA-256...')
|
||||
: (status === 'paused'
|
||||
? `Paused ${percentLabel}`.trim()
|
||||
: (percentLabel ? `Downloading ${percentLabel}` : 'Starting download...')));
|
||||
if (downloadBtn.classList.contains('search-download-btn')) {
|
||||
this.ensureSearchDownloadButtonIcon(downloadBtn);
|
||||
downloadBtn.setAttribute('data-tooltip', label);
|
||||
@@ -1301,9 +1303,11 @@ export const resolveDownloadMethods = {
|
||||
} else {
|
||||
downloadBtn.textContent = displayProgress.isFinalizing
|
||||
? 'Finalizing...'
|
||||
: (status === 'paused'
|
||||
? 'Paused'
|
||||
: (Number.isFinite(percent) && percent > 0 ? `${Math.round(percent)}%` : 'Starting...'));
|
||||
: (status === 'verifying'
|
||||
? (Number.isFinite(percent) && percent > 0 ? `SHA ${Math.round(percent)}%` : 'Verifying...')
|
||||
: (status === 'paused'
|
||||
? 'Paused'
|
||||
: (Number.isFinite(percent) && percent > 0 ? `${Math.round(percent)}%` : 'Starting...')));
|
||||
}
|
||||
} else if (status === 'cancelling') {
|
||||
downloadBtn.disabled = true;
|
||||
@@ -2210,7 +2214,7 @@ export const resolveDownloadMethods = {
|
||||
const { missing, category } = info;
|
||||
const downloadFolderContext = this.getDownloadFolderContext(progress, info);
|
||||
|
||||
if (progress.status === 'downloading' || progress.status === 'starting' || progress.status === 'paused') {
|
||||
if (progress.status === 'downloading' || progress.status === 'starting' || progress.status === 'paused' || progress.status === 'verifying') {
|
||||
this.renderDownloadSnapshot(downloadId, snapshot, { progressDiv, downloadBtn });
|
||||
if (typeof this.requestQueuePanelUpdate === 'function') {
|
||||
this.requestQueuePanelUpdate();
|
||||
@@ -2282,6 +2286,7 @@ export const resolveDownloadMethods = {
|
||||
this.updateDownloadAllButtonState();
|
||||
this.updateQueuePanel?.();
|
||||
this.refreshLocalMatchesUiForMissing?.(missing);
|
||||
this.showNotification(progress.error || 'Download failed', 'error');
|
||||
|
||||
} else if (progress.status === 'cancelled') {
|
||||
this.finalizeCancelledDownloadFrontend?.(downloadId, info, progress);
|
||||
|
||||
Reference in New Issue
Block a user