show partial archival in archive browser

This commit is contained in:
2026-05-05 10:02:26 -04:00
parent 40c56f8301
commit 06c0b1631b
7 changed files with 106 additions and 44 deletions
+42 -17
View File
@@ -121,7 +121,7 @@ def browse(path: str = "ROOT", db_session: Session = Depends(get_db)):
dir_sql, {"prefix": query_path, "prefix_wildcard": f"{query_path}%"}
).fetchall()
# Find files (immediate children) with their media locations
# Find files (immediate children) with their media locations and archive coverage
file_sql = text("""
SELECT
fs.id, fs.file_path, fs.size, fs.mtime,
@@ -130,7 +130,10 @@ def browse(path: str = "ROOT", db_session: Session = Depends(get_db)):
FROM file_versions fv
JOIN storage_media sm ON sm.id = fv.media_id
WHERE fv.filesystem_state_id = fs.id) as media_list,
EXISTS(SELECT 1 FROM restore_cart rc WHERE rc.filesystem_state_id = fs.id) as is_selected
EXISTS(SELECT 1 FROM restore_cart rc WHERE rc.filesystem_state_id = fs.id) as is_selected,
COALESCE((SELECT SUM(fv.offset_end - fv.offset_start)
FROM file_versions fv
WHERE fv.filesystem_state_id = fs.id), 0) as archived_bytes
FROM filesystem_state fs
WHERE fs.file_path LIKE :prefix_wildcard
AND fs.file_path != :prefix
@@ -175,6 +178,10 @@ def browse(path: str = "ROOT", db_session: Session = Depends(get_db)):
if not f[4]: # f[4] is has_version
continue
archived_bytes = f[7] or 0
file_size = f[2] or 0
is_partially_archived = archived_bytes > 0 and archived_bytes < file_size
results.append(
{
"name": os.path.basename(f[1]),
@@ -185,6 +192,8 @@ def browse(path: str = "ROOT", db_session: Session = Depends(get_db)):
"vulnerable": False,
"selected": bool(f[6]),
"media": f[5].split(",") if f[5] else [],
"is_partially_archived": is_partially_archived,
"archived_bytes": archived_bytes,
}
)
@@ -215,7 +224,10 @@ def search(q: str, path: Optional[str] = None, db_session: Session = Depends(get
FROM file_versions fv
JOIN storage_media sm ON sm.id = fv.media_id
WHERE fv.filesystem_state_id = fs.id) as media_list,
EXISTS(SELECT 1 FROM restore_cart rc WHERE rc.filesystem_state_id = fs.id) as is_selected
EXISTS(SELECT 1 FROM restore_cart rc WHERE rc.filesystem_state_id = fs.id) as is_selected,
COALESCE((SELECT SUM(fv.offset_end - fv.offset_start)
FROM file_versions fv
WHERE fv.filesystem_state_id = fs.id), 0) as archived_bytes
FROM filesystem_fts fts
JOIN filesystem_state fs ON fs.id = fts.rowid
WHERE filesystem_fts MATCH :query
@@ -229,20 +241,28 @@ def search(q: str, path: Optional[str] = None, db_session: Session = Depends(get
query_params = {"query": q, "path_prefix": path_prefix}
rows = db_session.execute(search_sql, query_params).fetchall()
return [
{
"name": os.path.basename(r[1]),
"path": r[1],
"type": "file",
"size": r[2],
"mtime": datetime.fromtimestamp(r[3], tz=timezone.utc),
"vulnerable": False,
"selected": bool(r[6]),
"media": r[5].split(",") if r[5] else [],
}
for r in rows
if r[4] # Only show if has_version is True
]
results = []
for r in rows:
if not r[4]: # Only show if has_version is True
continue
archived_bytes = r[7] or 0
file_size = r[2] or 0
is_partially_archived = archived_bytes > 0 and archived_bytes < file_size
results.append(
{
"name": os.path.basename(r[1]),
"path": r[1],
"type": "file",
"size": r[2],
"mtime": datetime.fromtimestamp(r[3], tz=timezone.utc),
"vulnerable": False,
"selected": bool(r[6]),
"media": r[5].split(",") if r[5] else [],
"is_partially_archived": is_partially_archived,
"archived_bytes": archived_bytes,
}
)
return results
@router.get("/tree", response_model=List[TreeNodeSchema], operation_id="archive_tree")
@@ -323,6 +343,9 @@ def metadata(path: str, db_session: Session = Depends(get_db)):
}
)
archived_bytes = sum((v.offset_end - v.offset_start) for v in item.versions)
is_partially_archived = archived_bytes > 0 and archived_bytes < item.size
return ItemMetadataSchema(
id=item.id,
path=item.file_path,
@@ -333,6 +356,8 @@ def metadata(path: str, db_session: Session = Depends(get_db)):
sha256_hash=item.sha256_hash,
is_ignored=item.is_ignored,
versions=versions,
is_partially_archived=is_partially_archived,
archived_bytes=archived_bytes,
)
# No exact match — check if this is a directory with archived children
+2
View File
@@ -25,6 +25,8 @@ class ItemMetadataSchema(BaseModel):
child_count: Optional[int] = 0
selected: bool = False
versions: List[Dict[str, Any]] = []
is_partially_archived: bool = False
archived_bytes: int = 0
class DiscrepancySchema(BaseModel):
+8
View File
@@ -460,6 +460,14 @@ export type ItemMetadataSchema = {
versions?: Array<{
[key: string]: unknown;
}>;
/**
* Is Partially Archived
*/
is_partially_archived?: boolean;
/**
* Archived Bytes
*/
archived_bytes?: number;
};
/**
@@ -10,11 +10,12 @@
MoreVertical,
ExternalLink,
CassetteTape,
ShieldCheck,
ShieldAlert,
Square,
EyeOff,
Trash2
ShieldCheck,
ShieldAlert,
Square,
EyeOff,
Trash2,
AlertTriangle
} from "lucide-svelte";
import { Checkbox } from "$lib/components/ui/checkbox";
import { Button } from "$lib/components/ui/button";
@@ -205,18 +206,24 @@
>
{item.name}
</span>
{#if mode === "index"}
{#if item.media && item.media.length > 0}
<div class="flex gap-1 overflow-hidden shrink-0">
{#each item.media as m}
<span class="inline-flex items-center gap-1 bg-blue-500/10 text-blue-400 text-[10px] px-1.5 py-0.5 rounded border border-blue-500/20 font-medium">
<CassetteTape size={10} />
{m}
</span>
{/each}
</div>
{/if}
{/if}
{#if mode === "index"}
{#if item.media && item.media.length > 0}
<div class="flex gap-1 overflow-hidden shrink-0">
{#each item.media as m}
<span class="inline-flex items-center gap-1 bg-blue-500/10 text-blue-400 text-[10px] px-1.5 py-0.5 rounded border border-blue-500/20 font-medium">
<CassetteTape size={10} />
{m}
</span>
{/each}
</div>
{/if}
{#if item.is_partially_archived}
<span class="inline-flex items-center gap-1 bg-orange-500/10 text-orange-400 text-[10px] px-1.5 py-0.5 rounded border border-orange-500/20 font-medium" title="Only {formatSize(item.archived_bytes)} of {formatSize(item.size)} archived">
<AlertTriangle size={10} />
Partial
</span>
{/if}
{/if}
{#if mode === "discrepancies"}
{#if item.is_deleted}
<span class="inline-flex items-center gap-1 bg-red-500/10 text-red-400 text-[10px] px-1.5 py-0.5 rounded border border-red-500/20 font-medium">
@@ -256,7 +263,7 @@
class="shrink-0 px-4 h-full flex items-center justify-end text-xs text-text-secondary mono text-right tabular-nums font-medium border-r border-border-color/10"
style="width: {colWidths.size}px"
>
{formatSize(item.size)}
{formatSize(item.archived_bytes !== undefined ? item.archived_bytes : item.size)}
</div>
<!-- QUICK ACTIONS -->
+3
View File
@@ -11,6 +11,9 @@ export interface FileItem {
sha256_hash?: string | null;
vulnerable?: boolean;
indeterminate?: boolean;
// Partial archive indicator
is_partially_archived?: boolean;
archived_bytes?: number;
// Discrepancy fields
discrepancy_id?: number;
is_deleted?: boolean;
+22 -5
View File
@@ -12,7 +12,8 @@
ListPlus,
FolderTree,
Clock,
ArrowRight
ArrowRight,
AlertTriangle
} from 'lucide-svelte';
import { Button } from '$lib/components/ui/button';
import PageHeader from '$lib/components/ui/PageHeader.svelte';
@@ -89,7 +90,9 @@
media: f.media ?? [],
vulnerable: f.vulnerable,
selected: f.selected,
indeterminate: f.indeterminate
indeterminate: f.indeterminate,
is_partially_archived: f.is_partially_archived ?? false,
archived_bytes: f.archived_bytes ?? undefined
}));
}
} catch (error) {
@@ -116,7 +119,9 @@
media: f.media ?? [],
vulnerable: f.vulnerable,
selected: f.selected,
indeterminate: f.indeterminate
indeterminate: f.indeterminate,
is_partially_archived: f.is_partially_archived ?? false,
archived_bytes: f.archived_bytes ?? undefined
}));
}
} catch (error) {
@@ -315,9 +320,9 @@
<div class="grid grid-cols-2 gap-4">
<div class="space-y-1">
<span class="text-xs font-medium text-text-secondary opacity-60 block">
{selectedItemMetadata.type === 'directory' ? 'Aggregate Size' : 'File Size'}
{selectedItemMetadata.type === 'directory' ? 'Aggregate Size' : 'Archived Size'}
</span>
<span class="text-xs font-semibold text-text-primary mono">{formatSize(selectedItemMetadata.size)}</span>
<span class="text-xs font-semibold text-text-primary mono">{formatSize(selectedItemMetadata.type === 'file' ? (selectedItemMetadata.archived_bytes || 0) : selectedItemMetadata.size)}</span>
</div>
<div class="space-y-1">
<span class="text-xs font-medium text-text-secondary opacity-60 block">Last Indexed</span>
@@ -332,6 +337,18 @@
</div>
{#if selectedItemMetadata.type === 'file'}
{#if selectedItemMetadata.is_partially_archived}
<div class="p-3 bg-orange-500/5 border border-orange-500/20 rounded-lg space-y-1">
<div class="flex items-center gap-2">
<AlertTriangle size={14} class="text-orange-400" />
<span class="text-xs font-semibold text-orange-400">Partially Archived</span>
</div>
<p class="text-5xs text-text-secondary opacity-60 leading-relaxed">
Only {formatSize(selectedItemMetadata.archived_bytes || 0)} of {formatSize(selectedItemMetadata.size)} has been written to archive media. The remaining {formatSize(selectedItemMetadata.size - (selectedItemMetadata.archived_bytes || 0))} was not archived because the target media became full.
</p>
</div>
{/if}
<!-- Hash -->
<div class="space-y-2">
<span class="text-xs font-medium text-text-secondary opacity-60 block">SHA-256 Fingerprint</span>
+4 -4
View File
@@ -626,12 +626,12 @@
{#if showAddSecret}
<div class="space-y-3 p-4 bg-bg-primary/30 rounded-lg border border-border-color">
<div class="space-y-2">
<label class="text-xs font-medium text-text-secondary ml-1">Secret Name</label>
<Input bind:value={newSecretName} placeholder="e.g., aws-production-key" class="h-10 bg-bg-primary border-border-color text-sm" />
<label class="text-xs font-medium text-text-secondary ml-1" for="new-secret-name">Secret Name</label>
<Input id="new-secret-name" bind:value={newSecretName} placeholder="e.g., aws-production-key" class="h-10 bg-bg-primary border-border-color text-sm" />
</div>
<div class="space-y-2">
<label class="text-xs font-medium text-text-secondary ml-1">Secret Value</label>
<Input bind:value={newSecretValue} type="password" placeholder="Enter secret value" class="h-10 bg-bg-primary border-border-color font-mono text-sm" />
<label class="text-xs font-medium text-text-secondary ml-1" for="new-secret-value">Secret Value</label>
<Input id="new-secret-value" bind:value={newSecretValue} type="password" placeholder="Enter secret value" class="h-10 bg-bg-primary border-border-color font-mono text-sm" />
</div>
<div class="flex gap-2">
<Button variant="outline" class="flex-1 h-10" onclick={() => { showAddSecret = false; newSecretName = ''; newSecretValue = ''; }}>Cancel</Button>