diff --git a/core/download/orchestrator.py b/core/download/orchestrator.py index 231e5f2..d93ee39 100644 --- a/core/download/orchestrator.py +++ b/core/download/orchestrator.py @@ -239,6 +239,26 @@ def download_file( error_msg = f"SHA256 mismatch: expected {expected_sha256}, got {actual_sha256}" if bad_path: error_msg += f"; file kept at {bad_path}" + # Use the intended filename so removing .badsha also makes + # the already computed hash available to local model info. + # Never attach the rejected file's hash to an existing model. + sidecar_model_path = bad_path if os.path.exists(dest_path) else dest_path + try: + payload = facade.build_model_resolver_metadata( + sidecar_model_path, + {"sha256": actual_sha256, "size": downloaded}, + category, + ) + payload.update({ + "expected_sha256": expected_sha256, + "sha256_verified": False, + }) + metadata_path = facade.get_model_resolver_sidecar_path(sidecar_model_path) + facade.write_json_atomic(metadata_path, payload, indent=2) + result["metadata_path"] = metadata_path + log.info(f"Metadata saved for SHA256-mismatched download: {metadata_path}") + except Exception as exc: + log.warning(f"Could not save metadata for {bad_path}: {exc}") with download_lock: download_progress[download_id]["status"] = "error" download_progress[download_id]["error"] = error_msg diff --git a/tests/test_resolver_robustness.py b/tests/test_resolver_robustness.py index fb1c6f7..281fe0f 100644 --- a/tests/test_resolver_robustness.py +++ b/tests/test_resolver_robustness.py @@ -371,7 +371,6 @@ class ModelResolverRobustnessTests(unittest.TestCase): {}, ), ), - patch("core.download.api.context.write_model_resolver_metadata") as write_metadata, ): result = download_model( "https://example.com/mismatched.safetensors", @@ -391,7 +390,23 @@ class ModelResolverRobustnessTests(unittest.TestCase): self.assertTrue(os.path.isfile(bad_path)) with open(bad_path, "rb") as bad_handle: self.assertEqual(payload, bad_handle.read()) - write_metadata.assert_not_called() + with open(result["metadata_path"], encoding="utf-8") as handle: + saved = json.load(handle) + actual_sha256 = hashlib.sha256(payload).hexdigest() + self.assertEqual(actual_sha256, saved["sha256"]) + self.assertEqual(expected_sha256, saved["expected_sha256"]) + self.assertFalse(saved["sha256_verified"]) + self.assertEqual("completed", saved["hash_status"]) + self.assertEqual(len(payload), saved["size"]) + self.assertFalse(saved["from_civitai"]) + os.replace(bad_path, model_path) + with patch("core.path_utils.hashlib.sha256", side_effect=AssertionError("must reuse saved hash")): + self.assertEqual(actual_sha256, read_completed_metadata_sha256(model_path)) + from core.sources.civitai import get_model_info_for_file + + with patch("core.sources.civitai.calculate_file_sha256", side_effect=AssertionError("must reuse saved hash")): + info = get_model_info_for_file(model_path, local_only=True) + self.assertEqual(actual_sha256, info["sha256"]) response.close.assert_called_once_with() def test_base_model_mapping_fuzzy_resolution(self):