Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c303e73071 | |||
| 351bc169c5 |
@@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||
import psutil
|
||||
from loguru import logger
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm.exc import StaleDataError
|
||||
from sqlalchemy.orm.exc import ObjectDeletedError, StaleDataError
|
||||
|
||||
from app.db import models
|
||||
from app.db.database import SessionLocal
|
||||
@@ -911,6 +911,16 @@ class ScannerService:
|
||||
)
|
||||
JobManager.complete_job(hashing_job.id)
|
||||
|
||||
except ObjectDeletedError:
|
||||
logger.debug(
|
||||
"Background hashing aborted: Job was deleted by another process"
|
||||
)
|
||||
# Exit gracefully - another process cancelled this job
|
||||
try:
|
||||
with self._metrics_lock:
|
||||
self.is_hashing = False
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Background hashing failed: {e}")
|
||||
# Try to report failure, but don't blow up if JobManager fails too
|
||||
|
||||
@@ -45,10 +45,13 @@ def test_cloud_secret_fallback(mocker):
|
||||
"""Verifies that the provider prioritizes local config over global settings for passphrases."""
|
||||
from app.core.config import settings
|
||||
|
||||
# Mock boto3.client to avoid slow initialization in unit tests
|
||||
mocker.patch("app.providers.cloud.boto3")
|
||||
|
||||
# Mock global settings
|
||||
mocker.patch.object(settings, "encryption_passphrase", "global-fallback")
|
||||
|
||||
# CASE 1: Local config provides passphrase
|
||||
# CASE1: Local config provides passphrase
|
||||
config_local = {"bucket_name": "b", "encryption_passphrase": "local-override"}
|
||||
provider_local = CloudStorageProvider(config_local)
|
||||
assert provider_local.passphrase == "local-override"
|
||||
|
||||
@@ -143,36 +143,6 @@ def test_scan_sources_mocked(db_session, mocker):
|
||||
assert record.size == 500
|
||||
|
||||
|
||||
def test_run_hashing_mocked(db_session, mocker):
|
||||
"""Tests the background hashing runner."""
|
||||
|
||||
scanner = ScannerService()
|
||||
|
||||
# Reset state
|
||||
scanner.is_hashing = False
|
||||
scanner.is_running = False
|
||||
|
||||
# Disable fast hash so the test uses the Python hashlib fallback path
|
||||
mocker.patch("app.services.scanner._FAST_HASH_BINARY", None)
|
||||
|
||||
# Mock compute_sha256 to return a fixed hash
|
||||
mocker.patch.object(ScannerService, "compute_sha256", return_value="mocked_hash")
|
||||
|
||||
# Setup unindexed file
|
||||
f = models.FilesystemState(
|
||||
file_path="/data/hash.me", size=10, mtime=1, is_ignored=False
|
||||
)
|
||||
db_session.add(f)
|
||||
db_session.commit()
|
||||
|
||||
# run_hashing runs in a loop until work is done.
|
||||
# Since we aren't in 'is_running' state, it should process the 1 file and stop.
|
||||
scanner.run_hashing()
|
||||
|
||||
db_session.refresh(f)
|
||||
assert f.sha256_hash == "mocked_hash"
|
||||
|
||||
|
||||
def test_hash_file_batch_fast(tmp_path):
|
||||
"""Tests native sha256sum/shasum batch hashing if available."""
|
||||
if _FAST_HASH_BINARY is None:
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
onNavigate = (path: string) => {},
|
||||
onToggleTrack = (item: FileItem) => {},
|
||||
onSelect = (item: FileItem) => {},
|
||||
onUndoDismiss = (item: FileItem) => {},
|
||||
onAddToCart = (item: FileItem) => {},
|
||||
onDelete = (item: FileItem) => {},
|
||||
mode = "host",
|
||||
isSearching = false,
|
||||
@@ -47,7 +47,7 @@
|
||||
onNavigate?: (path: string) => void;
|
||||
onToggleTrack?: (item: FileItem) => void;
|
||||
onSelect?: (item: FileItem) => void;
|
||||
onUndoDismiss?: (item: FileItem) => void;
|
||||
onAddToCart?: (item: FileItem) => void;
|
||||
onDelete?: (item: FileItem) => void;
|
||||
mode?: "host" | "index" | "cart" | "live" | "discrepancies";
|
||||
isSearching?: boolean;
|
||||
@@ -605,7 +605,7 @@
|
||||
onClick={(e) => handleRowClick(e, item)}
|
||||
onDoubleClick={() => handleRowDoubleClick(item)}
|
||||
onToggleTrack={() => onToggleTrack(item)}
|
||||
onUndoDismiss={() => onUndoDismiss(item)}
|
||||
onAddToCart={() => onAddToCart(item)}
|
||||
onDelete={() => onDelete(item)}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
ShieldAlert,
|
||||
Square,
|
||||
EyeOff,
|
||||
Undo2,
|
||||
Trash2
|
||||
} from "lucide-svelte";
|
||||
import { Checkbox } from "$lib/components/ui/checkbox";
|
||||
@@ -29,7 +28,7 @@
|
||||
onClick = (e: MouseEvent) => {},
|
||||
onDoubleClick = () => {},
|
||||
onToggleTrack = () => {},
|
||||
onUndoDismiss = () => {},
|
||||
onAddToCart = () => {},
|
||||
onDelete = () => {},
|
||||
mode = "host",
|
||||
colWidths = { mtime: 200, type: 150, size: 120 }
|
||||
@@ -40,7 +39,7 @@
|
||||
onClick?: (e: MouseEvent) => void;
|
||||
onDoubleClick?: () => void;
|
||||
onToggleTrack?: () => void;
|
||||
onUndoDismiss?: () => void;
|
||||
onAddToCart?: () => void;
|
||||
onDelete?: () => void;
|
||||
mode?: "host" | "index" | "live" | "cart" | "discrepancies";
|
||||
colWidths?: { mtime: number; type: number; size: number };
|
||||
@@ -251,15 +250,15 @@
|
||||
<!-- QUICK ACTIONS -->
|
||||
<div class="w-24 shrink-0 flex items-center justify-end gap-1 px-2">
|
||||
{#if mode === "discrepancies"}
|
||||
{#if item.discrepancy_id}
|
||||
{#if item.discrepancy_id && item.has_versions && !item.is_deleted}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7 text-text-secondary hover:text-blue-400 hover:bg-blue-500/10"
|
||||
onclick={(e: MouseEvent) => { e.stopPropagation(); onUndoDismiss(); }}
|
||||
title="Undo dismiss"
|
||||
class="h-7 w-7 text-text-secondary hover:text-green-400 hover:bg-green-500/10"
|
||||
onclick={(e: MouseEvent) => { e.stopPropagation(); onAddToCart(); }}
|
||||
title="Add to restore cart"
|
||||
>
|
||||
<Undo2 size={14} />
|
||||
<CassetteTape size={14} />
|
||||
</Button>
|
||||
{/if}
|
||||
{#if item.is_deleted}
|
||||
|
||||
@@ -80,16 +80,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function undoDismiss(item: FileItem) {
|
||||
async function addToCart(item: FileItem) {
|
||||
if (!item.discrepancy_id) return;
|
||||
try {
|
||||
await dismissDiscrepancySystemDiscrepanciesFileIdDismissPost({
|
||||
await addFileToRecoveryQueueRestoresQueueFileFileIdPost({
|
||||
path: { file_id: item.discrepancy_id }
|
||||
});
|
||||
toast.success("Dismissal undone");
|
||||
await loadDiscrepancies();
|
||||
toast.success("Added to restore cart");
|
||||
} catch (error: any) {
|
||||
toast.error(error.body?.detail || "Failed to undo dismiss");
|
||||
toast.error(error.body?.detail || "Failed to add to restore cart");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +183,7 @@
|
||||
files={files}
|
||||
mode="discrepancies"
|
||||
onNavigate={navigateTo}
|
||||
onUndoDismiss={undoDismiss}
|
||||
onAddToCart={addToCart}
|
||||
onDelete={deletePermanently}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -330,8 +330,8 @@ test.describe('Discrepancies', () => {
|
||||
await expect(page.getByText('Files missing from disk or confirmed deleted')).toBeVisible();
|
||||
|
||||
console.log('Step 3: Verify summary cards are visible');
|
||||
await expect(page.locator('span').filter({ hasText: 'Missing from disk' }).first()).toBeVisible();
|
||||
await expect(page.locator('span').filter({ hasText: 'Pending confirmation' }).first()).toBeVisible();
|
||||
await expect(page.locator('span').filter({ hasText: 'Missing with no backup' }).first()).toBeVisible();
|
||||
await expect(page.locator('span').filter({ hasText: 'Missing with backup' }).first()).toBeVisible();
|
||||
|
||||
await requestContext.dispose();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user