Verify SHA256 for existing downloads and surface already_exists in UI

core/downloader.py: When a file already exists at dest_path, extract the expected
SHA256 from metadata, prefers a trusted LoRA Manager sidecar hash, falls back to
calculating the file hash, and returns already_exists=true plus sha256_source
when the hashes match. If the hashes differ, the download is skipped with a
clear warning instead of silently re-downloading.

web/resolver/actions/resolve_download_methods.js: Thread already_exists through
resolveDownloadMethods so notifications, progress messages, and snapshot payloads
reflect 'Already downloaded' when the backend determined the model was a SHA256
match.
This commit is contained in:
Dariusz L
2026-06-19 04:27:53 +02:00
parent 936f1bed1a
commit 66bbaedf66
2 changed files with 192 additions and 14 deletions
+165
View File
@@ -6,6 +6,7 @@ Handles downloading models from various sources with progress tracking.
import os
import json
import hashlib
import threading
import time
import requests
@@ -304,6 +305,78 @@ def _find_metadata_file_info(
return {}
def _normalize_sha256(value: Any) -> str:
text = str(value or "").strip().lower()
for prefix in ("sha256:", "sha256="):
if text.startswith(prefix):
text = text[len(prefix):].strip()
break
if len(text) == 64 and all(char in "0123456789abcdef" for char in text):
return text
return ""
def _extract_expected_sha256(metadata: Optional[Dict[str, Any]]) -> str:
source = metadata if isinstance(metadata, dict) else {}
details = _as_dict(source.get("civitai_details") or source.get("details"))
selected_version = _as_dict(
source.get("selected_version") or details.get("selected_version")
)
path_metadata = _as_dict(source.get("path_metadata"))
filename = _first_present(
source.get("filename"),
path_metadata.get("filename"),
)
file_info = _find_metadata_file_info(source, selected_version, str(filename))
hashes = _as_dict(_first_present(file_info.get("hashes"), source.get("hashes")))
return _normalize_sha256(
_first_present(
source.get("sha256"),
source.get("hash"),
file_info.get("sha256"),
file_info.get("hash"),
hashes.get("SHA256"),
hashes.get("sha256"),
)
)
def read_completed_metadata_sha256(file_path: str) -> str:
"""Read a trusted SHA256 from a LoRA Manager-compatible sidecar if available."""
metadata_path = get_metadata_sidecar_path(file_path)
if not os.path.exists(metadata_path):
return ""
try:
with open(metadata_path, "r", encoding="utf-8") as handle:
payload = json.load(handle)
except Exception as e:
log_warn(f"Could not read metadata sidecar for SHA256 check: {metadata_path} ({e})")
return ""
if not isinstance(payload, dict):
return ""
hash_status = str(payload.get("hash_status") or "completed").strip().lower()
if hash_status != "completed":
log_debug(
f"Skipping metadata SHA256 for {metadata_path}: hash_status={hash_status}"
)
return ""
return _normalize_sha256(payload.get("sha256") or payload.get("hash"))
def calculate_file_sha256(file_path: str) -> str:
"""Calculate SHA256 for an existing local file."""
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as handle:
for chunk in iter(lambda: handle.read(CHUNK_SIZE), b""):
if chunk:
sha256_hash.update(chunk)
return sha256_hash.hexdigest()
def build_lora_manager_metadata(
dest_path: str,
metadata: Optional[Dict[str, Any]] = None,
@@ -1004,6 +1077,98 @@ def download_model(
# Check if file already exists
if os.path.exists(dest_path):
expected_sha256 = _extract_expected_sha256(metadata)
if expected_sha256:
metadata_sha256 = read_completed_metadata_sha256(dest_path)
sha256_source = "metadata"
existing_sha256 = metadata_sha256
if metadata_sha256:
if metadata_sha256 == expected_sha256:
log_info(f"File exists, metadata SHA256 matches: {dest_path}")
else:
log_info(
"File exists, metadata SHA256 differs from source; "
f"verifying file content: {dest_path}"
)
existing_sha256 = ""
try:
if not existing_sha256:
sha256_source = "file"
log_info(f"File exists, verifying SHA256: {dest_path}")
existing_sha256 = calculate_file_sha256(dest_path)
except Exception as e:
error_msg = (
f"File already exists and its SHA256 could not be verified: {dest_path}"
)
log_warn(f"{error_msg} ({e})")
return {
"success": False,
"download_id": download_id,
"error": error_msg,
"path": dest_path,
}
if existing_sha256 == expected_sha256:
message = "This model is already downloaded and matches the source hash."
metadata_path = get_metadata_sidecar_path(dest_path)
if not os.path.exists(metadata_path):
metadata_path = write_lora_manager_metadata(
dest_path,
metadata or {},
category,
url,
) or ""
size = os.path.getsize(dest_path)
with download_lock:
if download_id in download_progress:
download_progress[download_id].update(
{
"status": "completed",
"progress": 100,
"total_size": size,
"downloaded": size,
"speed": 0,
"path": dest_path,
"directory": os.path.dirname(dest_path),
"error": None,
"already_exists": True,
"message": message,
"sha256": existing_sha256,
"expected_sha256": expected_sha256,
"sha256_source": sha256_source,
}
)
if metadata_path:
download_progress[download_id]["metadata_path"] = metadata_path
log_info(f"{message} Path: {dest_path}")
return {
"success": True,
"download_id": download_id,
"path": dest_path,
"size": size,
"already_exists": True,
"message": message,
"metadata_path": metadata_path,
"sha256_source": sha256_source,
}
error_msg = (
"File already exists, but its SHA256 does not match the selected "
f"source: {dest_path}"
)
log_warn(
f"{error_msg} (existing={existing_sha256}, expected={expected_sha256})"
)
return {
"success": False,
"download_id": download_id,
"error": error_msg,
"path": dest_path,
"existing_sha256": existing_sha256,
"expected_sha256": expected_sha256,
}
return {
"success": False,
"download_id": download_id,
@@ -970,7 +970,7 @@ export const resolveDownloadMethods = {
if (noneEl) noneEl.textContent = `${stats.none} no match`;
},
async refreshAfterDownload(missing, downloadedFilename, { progressDiv = null, downloadBtn = null, category = '', downloadPath = '', downloadDirectory = '' } = {}) {
async refreshAfterDownload(missing, downloadedFilename, { progressDiv = null, downloadBtn = null, category = '', downloadPath = '', downloadDirectory = '', alreadyExists = false } = {}) {
try {
const matches = await this.refreshLocalMatchesForDownloadedMissing(missing, downloadedFilename, {
progressDiv,
@@ -985,8 +985,8 @@ export const resolveDownloadMethods = {
|| (downloadedLower && matchFilename.toLowerCase() === downloadedLower);
});
const message = perfectMatch?.model
? 'Download complete. Exact local match is ready.'
: 'Download complete. Local matches updated.';
? (alreadyExists ? 'This model is already downloaded. Exact local match is ready.' : 'Download complete. Exact local match is ready.')
: (alreadyExists ? 'This model is already downloaded. Local matches updated.' : 'Download complete. Local matches updated.');
const snapshot = this.rememberDownloadSnapshotForMissing(missing, {
downloadId: null,
category,
@@ -997,7 +997,8 @@ export const resolveDownloadMethods = {
status: 'completed',
filename: downloadedFilename,
path: downloadPath,
directory: downloadDirectory
directory: downloadDirectory,
already_exists: alreadyExists
},
status: 'completed',
message,
@@ -1026,7 +1027,9 @@ export const resolveDownloadMethods = {
directory: downloadDirectory
},
status: 'refresh_error',
message: `Downloaded, but local re-check failed: ${error.message}`,
message: alreadyExists
? `Already downloaded, but local re-check failed: ${error.message}`
: `Downloaded, but local re-check failed: ${error.message}`,
type: 'warning',
isActive: false
});
@@ -1039,12 +1042,16 @@ export const resolveDownloadMethods = {
downloadBtn.disabled = false;
downloadBtn.textContent = 'Link manually';
}
this.showNotification('Downloaded, but local re-check failed: ' + error.message, 'warning', {
contextMenuModel: this.getDownloadFolderContext(
{ filename: downloadedFilename, path: downloadPath, directory: downloadDirectory },
{ category, downloadPath, downloadDirectory, filename: downloadedFilename }
)
});
this.showNotification(
`${alreadyExists ? 'Already downloaded' : 'Downloaded'}, but local re-check failed: ${error.message}`,
'warning',
{
contextMenuModel: this.getDownloadFolderContext(
{ filename: downloadedFilename, path: downloadPath, directory: downloadDirectory },
{ category, downloadPath, downloadDirectory, filename: downloadedFilename }
)
}
);
}
},
@@ -1228,9 +1235,12 @@ export const resolveDownloadMethods = {
setTimeout(() => this.pollDownloadProgress(downloadId), 1000);
} else if (progress.status === 'completed') {
const alreadyExists = Boolean(progress.already_exists);
const completedSnapshot = this.rememberDownloadUiState(downloadId, info, progress, {
status: 'completed_checking',
message: 'Download complete. Checking local matches...',
message: alreadyExists
? (progress.message || 'This model is already downloaded. Checking local matches...')
: 'Download complete. Checking local matches...',
type: 'success',
isActive: false
});
@@ -1238,7 +1248,9 @@ export const resolveDownloadMethods = {
delete this.activeDownloads[downloadId];
this.updateDownloadAllButtonState();
this.updateQueuePanel?.();
this.showNotification(`Downloaded: ${progress.filename}`, 'success', {
this.showNotification(alreadyExists
? `Already downloaded: ${progress.filename}`
: `Downloaded: ${progress.filename}`, 'success', {
contextMenuModel: downloadFolderContext
});
@@ -1251,7 +1263,8 @@ export const resolveDownloadMethods = {
downloadBtn: currentElements.downloadBtn,
category,
downloadPath: progress.path || info.downloadPath || '',
downloadDirectory: progress.directory || info.downloadDirectory || ''
downloadDirectory: progress.directory || info.downloadDirectory || '',
alreadyExists
});
}, 500);