Compare commits
2 Commits
901b17f7cd
...
c303e73071
| Author | SHA1 | Date | |
|---|---|---|---|
| c303e73071 | |||
| 351bc169c5 |
@@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
|||||||
import psutil
|
import psutil
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from sqlalchemy.orm import Session
|
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 import models
|
||||||
from app.db.database import SessionLocal
|
from app.db.database import SessionLocal
|
||||||
@@ -911,6 +911,16 @@ class ScannerService:
|
|||||||
)
|
)
|
||||||
JobManager.complete_job(hashing_job.id)
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Background hashing failed: {e}")
|
logger.error(f"Background hashing failed: {e}")
|
||||||
# Try to report failure, but don't blow up if JobManager fails too
|
# 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."""
|
"""Verifies that the provider prioritizes local config over global settings for passphrases."""
|
||||||
from app.core.config import settings
|
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
|
# Mock global settings
|
||||||
mocker.patch.object(settings, "encryption_passphrase", "global-fallback")
|
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"}
|
config_local = {"bucket_name": "b", "encryption_passphrase": "local-override"}
|
||||||
provider_local = CloudStorageProvider(config_local)
|
provider_local = CloudStorageProvider(config_local)
|
||||||
assert provider_local.passphrase == "local-override"
|
assert provider_local.passphrase == "local-override"
|
||||||
|
|||||||
@@ -143,36 +143,6 @@ def test_scan_sources_mocked(db_session, mocker):
|
|||||||
assert record.size == 500
|
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):
|
def test_hash_file_batch_fast(tmp_path):
|
||||||
"""Tests native sha256sum/shasum batch hashing if available."""
|
"""Tests native sha256sum/shasum batch hashing if available."""
|
||||||
if _FAST_HASH_BINARY is None:
|
if _FAST_HASH_BINARY is None:
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
onNavigate = (path: string) => {},
|
onNavigate = (path: string) => {},
|
||||||
onToggleTrack = (item: FileItem) => {},
|
onToggleTrack = (item: FileItem) => {},
|
||||||
onSelect = (item: FileItem) => {},
|
onSelect = (item: FileItem) => {},
|
||||||
onUndoDismiss = (item: FileItem) => {},
|
onAddToCart = (item: FileItem) => {},
|
||||||
onDelete = (item: FileItem) => {},
|
onDelete = (item: FileItem) => {},
|
||||||
mode = "host",
|
mode = "host",
|
||||||
isSearching = false,
|
isSearching = false,
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
onNavigate?: (path: string) => void;
|
onNavigate?: (path: string) => void;
|
||||||
onToggleTrack?: (item: FileItem) => void;
|
onToggleTrack?: (item: FileItem) => void;
|
||||||
onSelect?: (item: FileItem) => void;
|
onSelect?: (item: FileItem) => void;
|
||||||
onUndoDismiss?: (item: FileItem) => void;
|
onAddToCart?: (item: FileItem) => void;
|
||||||
onDelete?: (item: FileItem) => void;
|
onDelete?: (item: FileItem) => void;
|
||||||
mode?: "host" | "index" | "cart" | "live" | "discrepancies";
|
mode?: "host" | "index" | "cart" | "live" | "discrepancies";
|
||||||
isSearching?: boolean;
|
isSearching?: boolean;
|
||||||
@@ -605,7 +605,7 @@
|
|||||||
onClick={(e) => handleRowClick(e, item)}
|
onClick={(e) => handleRowClick(e, item)}
|
||||||
onDoubleClick={() => handleRowDoubleClick(item)}
|
onDoubleClick={() => handleRowDoubleClick(item)}
|
||||||
onToggleTrack={() => onToggleTrack(item)}
|
onToggleTrack={() => onToggleTrack(item)}
|
||||||
onUndoDismiss={() => onUndoDismiss(item)}
|
onAddToCart={() => onAddToCart(item)}
|
||||||
onDelete={() => onDelete(item)}
|
onDelete={() => onDelete(item)}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
ShieldAlert,
|
ShieldAlert,
|
||||||
Square,
|
Square,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
Undo2,
|
|
||||||
Trash2
|
Trash2
|
||||||
} from "lucide-svelte";
|
} from "lucide-svelte";
|
||||||
import { Checkbox } from "$lib/components/ui/checkbox";
|
import { Checkbox } from "$lib/components/ui/checkbox";
|
||||||
@@ -29,7 +28,7 @@
|
|||||||
onClick = (e: MouseEvent) => {},
|
onClick = (e: MouseEvent) => {},
|
||||||
onDoubleClick = () => {},
|
onDoubleClick = () => {},
|
||||||
onToggleTrack = () => {},
|
onToggleTrack = () => {},
|
||||||
onUndoDismiss = () => {},
|
onAddToCart = () => {},
|
||||||
onDelete = () => {},
|
onDelete = () => {},
|
||||||
mode = "host",
|
mode = "host",
|
||||||
colWidths = { mtime: 200, type: 150, size: 120 }
|
colWidths = { mtime: 200, type: 150, size: 120 }
|
||||||
@@ -40,7 +39,7 @@
|
|||||||
onClick?: (e: MouseEvent) => void;
|
onClick?: (e: MouseEvent) => void;
|
||||||
onDoubleClick?: () => void;
|
onDoubleClick?: () => void;
|
||||||
onToggleTrack?: () => void;
|
onToggleTrack?: () => void;
|
||||||
onUndoDismiss?: () => void;
|
onAddToCart?: () => void;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
mode?: "host" | "index" | "live" | "cart" | "discrepancies";
|
mode?: "host" | "index" | "live" | "cart" | "discrepancies";
|
||||||
colWidths?: { mtime: number; type: number; size: number };
|
colWidths?: { mtime: number; type: number; size: number };
|
||||||
@@ -251,15 +250,15 @@
|
|||||||
<!-- QUICK ACTIONS -->
|
<!-- QUICK ACTIONS -->
|
||||||
<div class="w-24 shrink-0 flex items-center justify-end gap-1 px-2">
|
<div class="w-24 shrink-0 flex items-center justify-end gap-1 px-2">
|
||||||
{#if mode === "discrepancies"}
|
{#if mode === "discrepancies"}
|
||||||
{#if item.discrepancy_id}
|
{#if item.discrepancy_id && item.has_versions && !item.is_deleted}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-7 w-7 text-text-secondary hover:text-blue-400 hover:bg-blue-500/10"
|
class="h-7 w-7 text-text-secondary hover:text-green-400 hover:bg-green-500/10"
|
||||||
onclick={(e: MouseEvent) => { e.stopPropagation(); onUndoDismiss(); }}
|
onclick={(e: MouseEvent) => { e.stopPropagation(); onAddToCart(); }}
|
||||||
title="Undo dismiss"
|
title="Add to restore cart"
|
||||||
>
|
>
|
||||||
<Undo2 size={14} />
|
<CassetteTape size={14} />
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if item.is_deleted}
|
{#if item.is_deleted}
|
||||||
|
|||||||
@@ -80,16 +80,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function undoDismiss(item: FileItem) {
|
async function addToCart(item: FileItem) {
|
||||||
if (!item.discrepancy_id) return;
|
if (!item.discrepancy_id) return;
|
||||||
try {
|
try {
|
||||||
await dismissDiscrepancySystemDiscrepanciesFileIdDismissPost({
|
await addFileToRecoveryQueueRestoresQueueFileFileIdPost({
|
||||||
path: { file_id: item.discrepancy_id }
|
path: { file_id: item.discrepancy_id }
|
||||||
});
|
});
|
||||||
toast.success("Dismissal undone");
|
toast.success("Added to restore cart");
|
||||||
await loadDiscrepancies();
|
|
||||||
} catch (error: any) {
|
} 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}
|
files={files}
|
||||||
mode="discrepancies"
|
mode="discrepancies"
|
||||||
onNavigate={navigateTo}
|
onNavigate={navigateTo}
|
||||||
onUndoDismiss={undoDismiss}
|
onAddToCart={addToCart}
|
||||||
onDelete={deletePermanently}
|
onDelete={deletePermanently}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -330,8 +330,8 @@ test.describe('Discrepancies', () => {
|
|||||||
await expect(page.getByText('Files missing from disk or confirmed deleted')).toBeVisible();
|
await expect(page.getByText('Files missing from disk or confirmed deleted')).toBeVisible();
|
||||||
|
|
||||||
console.log('Step 3: Verify summary cards are visible');
|
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: 'Missing with no backup' }).first()).toBeVisible();
|
||||||
await expect(page.locator('span').filter({ hasText: 'Pending confirmation' }).first()).toBeVisible();
|
await expect(page.locator('span').filter({ hasText: 'Missing with backup' }).first()).toBeVisible();
|
||||||
|
|
||||||
await requestContext.dispose();
|
await requestContext.dispose();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user