Compare commits
3 Commits
c303e73071
...
2f8e343b6d
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f8e343b6d | |||
| 40db1251e1 | |||
| 1343304c60 |
@@ -1485,6 +1485,8 @@ def get_discrepancies_tree(
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
# Return immediate children of the given path
|
# Return immediate children of the given path
|
||||||
|
if path is None:
|
||||||
|
return []
|
||||||
result = []
|
result = []
|
||||||
for dir_path, node in sorted(dir_nodes.items()):
|
for dir_path, node in sorted(dir_nodes.items()):
|
||||||
if dir_path == path:
|
if dir_path == path:
|
||||||
@@ -1587,16 +1589,19 @@ def browse_discrepancies(
|
|||||||
child_name = file_path
|
child_name = file_path
|
||||||
else:
|
else:
|
||||||
# Check if this file is under the requested path
|
# Check if this file is under the requested path
|
||||||
if file_path != path and not file_path.startswith(path + "/"):
|
if path is None or (
|
||||||
|
file_path != path and not file_path.startswith(path + "/")
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Get immediate child relative to path
|
# Get immediate child relative to path
|
||||||
rel_path = file_path[len(path) :].strip("/")
|
path_str = path or ""
|
||||||
|
rel_path = file_path[len(path_str) :].strip("/")
|
||||||
if "/" in rel_path:
|
if "/" in rel_path:
|
||||||
# It's a subdirectory - get immediate child
|
# It's a subdirectory - get immediate child
|
||||||
child_name = rel_path.split("/")[0]
|
child_name = rel_path.split("/")[0]
|
||||||
child_path = (
|
child_path = (
|
||||||
path + "/" + child_name if path != "/" else "/" + child_name
|
path_str + "/" + child_name if path_str != "/" else "/" + child_name
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# It's a file
|
# It's a file
|
||||||
|
|||||||
@@ -679,8 +679,8 @@ class ArchiverService:
|
|||||||
if chunk_file.startswith("backup_") and chunk_file.endswith(".tar"):
|
if chunk_file.startswith("backup_") and chunk_file.endswith(".tar"):
|
||||||
try:
|
try:
|
||||||
os.remove(os.path.join(self.staging_directory, chunk_file))
|
os.remove(os.path.join(self.staging_directory, chunk_file))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug(f"Failed to remove staging file {chunk_file}: {e}")
|
||||||
|
|
||||||
def run_restore(self, db_session: Session, destination_root: str, job_id: int):
|
def run_restore(self, db_session: Session, destination_root: str, job_id: int):
|
||||||
"""Orchestrates the retrieval and reassembly of data from storage providers."""
|
"""Orchestrates the retrieval and reassembly of data from storage providers."""
|
||||||
@@ -825,8 +825,10 @@ class ArchiverService:
|
|||||||
final_path,
|
final_path,
|
||||||
(v.file_state.mtime, v.file_state.mtime),
|
(v.file_state.mtime, v.file_state.mtime),
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug(
|
||||||
|
f"Failed to restore mtime for {final_path}: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
processed_bytes += v.offset_end - v.offset_start
|
processed_bytes += v.offset_end - v.offset_start
|
||||||
JobManager.update_job(
|
JobManager.update_job(
|
||||||
@@ -900,8 +902,10 @@ class ArchiverService:
|
|||||||
os.chown(
|
os.chown(
|
||||||
final_path, member.uid, member.gid
|
final_path, member.uid, member.gid
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.debug(
|
||||||
|
f"Failed to restore ownership for {final_path}: {e}"
|
||||||
|
)
|
||||||
except Exception as meta_err:
|
except Exception as meta_err:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Failed to apply metadata to {final_path}: {meta_err}"
|
f"Failed to apply metadata to {final_path}: {meta_err}"
|
||||||
|
|||||||
@@ -200,7 +200,11 @@ def _discover_files_fast(
|
|||||||
|
|
||||||
# -printf format: path\tsize\tmtime (tab-separated; split from right for safety)
|
# -printf format: path\tsize\tmtime (tab-separated; split from right for safety)
|
||||||
find_binary = _FAST_FIND_BINARY
|
find_binary = _FAST_FIND_BINARY
|
||||||
assert find_binary is not None
|
if find_binary is None:
|
||||||
|
logger.warning(
|
||||||
|
"Fast file discovery requested but no compatible `find` binary found"
|
||||||
|
)
|
||||||
|
return 0, 0
|
||||||
cmd = [
|
cmd = [
|
||||||
find_binary,
|
find_binary,
|
||||||
root_base,
|
root_base,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
currentPath = $bindable("ROOT"),
|
currentPath = $bindable("ROOT"),
|
||||||
searchQuery = $bindable(""),
|
searchQuery = $bindable(""),
|
||||||
files = [],
|
files = [],
|
||||||
|
selectedPaths = $bindable(new Set<string>()),
|
||||||
onNavigate = (path: string) => {},
|
onNavigate = (path: string) => {},
|
||||||
onToggleTrack = (item: FileItem) => {},
|
onToggleTrack = (item: FileItem) => {},
|
||||||
onSelect = (item: FileItem) => {},
|
onSelect = (item: FileItem) => {},
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
currentPath?: string;
|
currentPath?: string;
|
||||||
searchQuery?: string;
|
searchQuery?: string;
|
||||||
files?: FileItem[];
|
files?: FileItem[];
|
||||||
|
selectedPaths?: Set<string>;
|
||||||
onNavigate?: (path: string) => void;
|
onNavigate?: (path: string) => void;
|
||||||
onToggleTrack?: (item: FileItem) => void;
|
onToggleTrack?: (item: FileItem) => void;
|
||||||
onSelect?: (item: FileItem) => void;
|
onSelect?: (item: FileItem) => void;
|
||||||
@@ -54,7 +56,6 @@
|
|||||||
pendingChanges?: Map<string, boolean>;
|
pendingChanges?: Map<string, boolean>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let selectedPaths = $state<Set<string>>(new Set());
|
|
||||||
let lastSelectedPath = $state<string | null>(null);
|
let lastSelectedPath = $state<string | null>(null);
|
||||||
let sortColumn = $state<"name" | "size" | "mtime" | "type">("name");
|
let sortColumn = $state<"name" | "size" | "mtime" | "type">("name");
|
||||||
let sortDirection = $state<"asc" | "desc">("asc");
|
let sortDirection = $state<"asc" | "desc">("asc");
|
||||||
@@ -284,6 +285,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleRowClick(e: MouseEvent, item: FileItem) {
|
function handleRowClick(e: MouseEvent, item: FileItem) {
|
||||||
|
// For discrepancies mode, row click navigates (dirs) or shows metadata (files)
|
||||||
|
// Checkbox handles selection
|
||||||
|
if (mode === 'discrepancies') {
|
||||||
|
onSelect(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (e.shiftKey && lastSelectedPath) {
|
if (e.shiftKey && lastSelectedPath) {
|
||||||
const lastIndex = filteredFiles.findIndex((f: FileItem) => f.path === lastSelectedPath);
|
const lastIndex = filteredFiles.findIndex((f: FileItem) => f.path === lastSelectedPath);
|
||||||
const currentIndex = filteredFiles.findIndex((f: FileItem) => f.path === item.path);
|
const currentIndex = filteredFiles.findIndex((f: FileItem) => f.path === item.path);
|
||||||
@@ -321,14 +329,51 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recursively add directory and all its children to selection
|
||||||
|
function addItemAndChildren(path: string, newSelection: Set<string>) {
|
||||||
|
newSelection.add(path);
|
||||||
|
// Find all files/dirs that are children of this path
|
||||||
|
for (const f of (files as FileItem[])) {
|
||||||
|
if (f.path.startsWith(path + "/") || f.path === path) {
|
||||||
|
newSelection.add(f.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively remove directory and all its children from selection
|
||||||
|
function removeItemAndChildren(path: string, newSelection: Set<string>) {
|
||||||
|
newSelection.delete(path);
|
||||||
|
for (const f of (files as FileItem[])) {
|
||||||
|
if (f.path.startsWith(path + "/")) {
|
||||||
|
newSelection.delete(f.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleSelectAll(checked: boolean | "indeterminate") {
|
function handleSelectAll(checked: boolean | "indeterminate") {
|
||||||
if (checked === true) {
|
if (checked === true) {
|
||||||
selectedPaths = new Set(filteredFiles.map((f: FileItem) => f.path));
|
const newSelection = new Set<string>();
|
||||||
|
for (const f of (filteredFiles as FileItem[])) {
|
||||||
|
addItemAndChildren(f.path, newSelection);
|
||||||
|
}
|
||||||
|
selectedPaths = newSelection;
|
||||||
} else {
|
} else {
|
||||||
selectedPaths = new Set();
|
selectedPaths = new Set();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleToggleItem(item: FileItem) {
|
||||||
|
const newSelection = new Set(selectedPaths as Set<string>);
|
||||||
|
if (newSelection.has(item.path)) {
|
||||||
|
// Deselecting - remove item and all children if it's a directory
|
||||||
|
removeItemAndChildren(item.path, newSelection);
|
||||||
|
} else {
|
||||||
|
// Selecting - add item and all children if it's a directory
|
||||||
|
addItemAndChildren(item.path, newSelection);
|
||||||
|
}
|
||||||
|
selectedPaths = newSelection;
|
||||||
|
}
|
||||||
|
|
||||||
let isEditingPath = $state(false);
|
let isEditingPath = $state(false);
|
||||||
let pathInputValue = $state("");
|
let pathInputValue = $state("");
|
||||||
|
|
||||||
@@ -605,6 +650,7 @@
|
|||||||
onClick={(e) => handleRowClick(e, item)}
|
onClick={(e) => handleRowClick(e, item)}
|
||||||
onDoubleClick={() => handleRowDoubleClick(item)}
|
onDoubleClick={() => handleRowDoubleClick(item)}
|
||||||
onToggleTrack={() => onToggleTrack(item)}
|
onToggleTrack={() => onToggleTrack(item)}
|
||||||
|
onToggleSelect={() => handleToggleItem(item)}
|
||||||
onAddToCart={() => onAddToCart(item)}
|
onAddToCart={() => onAddToCart(item)}
|
||||||
onDelete={() => onDelete(item)}
|
onDelete={() => onDelete(item)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
onClick = (e: MouseEvent) => {},
|
onClick = (e: MouseEvent) => {},
|
||||||
onDoubleClick = () => {},
|
onDoubleClick = () => {},
|
||||||
onToggleTrack = () => {},
|
onToggleTrack = () => {},
|
||||||
|
onToggleSelect = () => {},
|
||||||
onAddToCart = () => {},
|
onAddToCart = () => {},
|
||||||
onDelete = () => {},
|
onDelete = () => {},
|
||||||
mode = "host",
|
mode = "host",
|
||||||
@@ -39,6 +40,7 @@
|
|||||||
onClick?: (e: MouseEvent) => void;
|
onClick?: (e: MouseEvent) => void;
|
||||||
onDoubleClick?: () => void;
|
onDoubleClick?: () => void;
|
||||||
onToggleTrack?: () => void;
|
onToggleTrack?: () => void;
|
||||||
|
onToggleSelect?: () => void;
|
||||||
onAddToCart?: () => void;
|
onAddToCart?: () => void;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
mode?: "host" | "index" | "live" | "cart" | "discrepancies";
|
mode?: "host" | "index" | "live" | "cart" | "discrepancies";
|
||||||
@@ -122,12 +124,16 @@
|
|||||||
ondblclick={(e) => { e.stopPropagation(); onDoubleClick(); }}
|
ondblclick={(e) => { e.stopPropagation(); onDoubleClick(); }}
|
||||||
onkeydown={(e) => e.key === "Enter" && onDoubleClick()}
|
onkeydown={(e) => e.key === "Enter" && onDoubleClick()}
|
||||||
>
|
>
|
||||||
<!-- TRACKING STATUS / SELECTION -->
|
<!-- SELECTION CHECKBOX -->
|
||||||
<div
|
<div
|
||||||
class="flex h-10 w-12 shrink-0 items-center justify-center border-r border-border-color/10"
|
class="flex h-10 w-12 shrink-0 items-center justify-center border-r border-border-color/10"
|
||||||
onclick={(e) => {
|
onclick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onToggleTrack();
|
if (mode === 'discrepancies') {
|
||||||
|
onToggleSelect();
|
||||||
|
} else {
|
||||||
|
onToggleTrack();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onkeydown={(e) => e.key === " " && e.stopPropagation()}
|
onkeydown={(e) => e.key === " " && e.stopPropagation()}
|
||||||
role="none"
|
role="none"
|
||||||
@@ -150,6 +156,11 @@
|
|||||||
<Square size={16} />
|
<Square size={16} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{:else if mode === 'discrepancies'}
|
||||||
|
<Checkbox
|
||||||
|
checked={isSelected}
|
||||||
|
onCheckedChange={onToggleSelect}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={item.selected}
|
checked={item.selected}
|
||||||
|
|||||||
@@ -25,8 +25,7 @@
|
|||||||
let files = $state<FileItem[]>([]);
|
let files = $state<FileItem[]>([]);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let currentPath = $state("ROOT");
|
let currentPath = $state("ROOT");
|
||||||
let selectedIds = $state<Set<number>>(new Set());
|
let selectedPaths = $state<Set<string>>(new Set());
|
||||||
let batchAction = $state<'acknowledge' | 'recover' | null>(null);
|
|
||||||
let batchLoading = $state(false);
|
let batchLoading = $state(false);
|
||||||
|
|
||||||
async function loadDiscrepancies() {
|
async function loadDiscrepancies() {
|
||||||
@@ -105,6 +104,78 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function batchDismiss() {
|
||||||
|
const ids = getDiscrepancyIdsFromPaths(selectedPaths);
|
||||||
|
if (ids.length === 0) {
|
||||||
|
toast.error("No files selected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
batchLoading = true;
|
||||||
|
try {
|
||||||
|
await batchDismissSystemDiscrepanciesBatchDismissPost({
|
||||||
|
body: { ids }
|
||||||
|
});
|
||||||
|
toast.success(`Dismissed ${ids.length} files`);
|
||||||
|
selectedPaths = new Set();
|
||||||
|
await loadDiscrepancies();
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.body?.detail || "Failed to dismiss files");
|
||||||
|
} finally {
|
||||||
|
batchLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function batchDelete() {
|
||||||
|
const ids = getDiscrepancyIdsFromPaths(selectedPaths);
|
||||||
|
if (ids.length === 0) {
|
||||||
|
toast.error("No files selected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
batchLoading = true;
|
||||||
|
try {
|
||||||
|
await batchHardDeleteSystemDiscrepanciesBatchDeletePost({
|
||||||
|
body: { ids }
|
||||||
|
});
|
||||||
|
toast.success(`Deleted ${ids.length} file records`);
|
||||||
|
selectedPaths = new Set();
|
||||||
|
await loadDiscrepancies();
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.body?.detail || "Failed to delete files");
|
||||||
|
} finally {
|
||||||
|
batchLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function batchAddToCart() {
|
||||||
|
const ids = getDiscrepancyIdsFromPaths(selectedPaths);
|
||||||
|
if (ids.length === 0) {
|
||||||
|
toast.error("No files selected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
batchLoading = true;
|
||||||
|
try {
|
||||||
|
await batchAddToRecoveryQueueRestoresQueueBatchPost({
|
||||||
|
body: { ids }
|
||||||
|
});
|
||||||
|
toast.success(`Added ${ids.length} files to restore cart`);
|
||||||
|
selectedPaths = new Set();
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.body?.detail || "Failed to add files to cart");
|
||||||
|
} finally {
|
||||||
|
batchLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDiscrepancyIdsFromPaths(paths: Set<string>): number[] {
|
||||||
|
const ids: number[] = [];
|
||||||
|
for (const item of files) {
|
||||||
|
if (paths.has(item.path) && item.discrepancy_id) {
|
||||||
|
ids.push(item.discrepancy_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
function navigateTo(path: string) {
|
function navigateTo(path: string) {
|
||||||
currentPath = path;
|
currentPath = path;
|
||||||
loadFiles(path);
|
loadFiles(path);
|
||||||
@@ -176,10 +247,32 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Batch Actions Bar -->
|
||||||
|
{#if selectedPaths.size > 0}
|
||||||
|
<div class="flex items-center gap-3 p-3 bg-bg-tertiary/50 rounded-lg border border-border-color">
|
||||||
|
<span class="text-sm text-text-secondary">
|
||||||
|
{selectedPaths.size} file(s) selected
|
||||||
|
</span>
|
||||||
|
<div class="flex gap-2 ml-auto">
|
||||||
|
<Button size="sm" variant="outline" onclick={batchDismiss} disabled={batchLoading}>
|
||||||
|
Dismiss Selected
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" onclick={batchAddToCart} disabled={batchLoading}>
|
||||||
|
<HardDriveDownload size={14} class="mr-1" />
|
||||||
|
Add to Cart
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="destructive" onclick={batchDelete} disabled={batchLoading}>
|
||||||
|
Delete Records
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- FileBrowser Component in discrepancies mode -->
|
<!-- FileBrowser Component in discrepancies mode -->
|
||||||
<div class="flex-1 min-h-[600px] bg-bg-secondary border border-border-color shadow-2xl rounded-lg flex flex-col relative overflow-hidden">
|
<div class="flex-1 min-h-[600px] bg-bg-secondary border border-border-color shadow-2xl rounded-lg flex flex-col relative overflow-hidden">
|
||||||
<FileBrowser
|
<FileBrowser
|
||||||
bind:currentPath={currentPath}
|
bind:currentPath={currentPath}
|
||||||
|
bind:selectedPaths={selectedPaths}
|
||||||
files={files}
|
files={files}
|
||||||
mode="discrepancies"
|
mode="discrepancies"
|
||||||
onNavigate={navigateTo}
|
onNavigate={navigateTo}
|
||||||
|
|||||||
Reference in New Issue
Block a user