show partial archival in archive browser
This commit is contained in:
+42
-17
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user