diff --git a/backend/app/api/archive.py b/backend/app/api/archive.py index 8a5fef2..13604f3 100644 --- a/backend/app/api/archive.py +++ b/backend/app/api/archive.py @@ -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 diff --git a/backend/app/api/schemas.py b/backend/app/api/schemas.py index 301d1b3..fc5b46a 100644 --- a/backend/app/api/schemas.py +++ b/backend/app/api/schemas.py @@ -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): diff --git a/frontend/src/lib/api/types.gen.ts b/frontend/src/lib/api/types.gen.ts index f576539..54f6c87 100644 --- a/frontend/src/lib/api/types.gen.ts +++ b/frontend/src/lib/api/types.gen.ts @@ -460,6 +460,14 @@ export type ItemMetadataSchema = { versions?: Array<{ [key: string]: unknown; }>; + /** + * Is Partially Archived + */ + is_partially_archived?: boolean; + /** + * Archived Bytes + */ + archived_bytes?: number; }; /** diff --git a/frontend/src/lib/components/file-browser/FileBrowserRowItem.svelte b/frontend/src/lib/components/file-browser/FileBrowserRowItem.svelte index 4d96bf0..6bf5dcc 100644 --- a/frontend/src/lib/components/file-browser/FileBrowserRowItem.svelte +++ b/frontend/src/lib/components/file-browser/FileBrowserRowItem.svelte @@ -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} - {#if mode === "index"} - {#if item.media && item.media.length > 0} -
+ 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. +
+