Enhance backup restoration UI and API by adding file size formatting, improving search debounce timing, and updating file listing to include additional metadata. Refactor file handling to ensure proper path resolution and error handling during JSON parsing.

This commit is contained in:
Mauricio Siu 2025-04-26 23:23:51 -06:00
parent c0b35efaca
commit ffcdbcf046
2 changed files with 84 additions and 26 deletions

View File

@ -77,6 +77,14 @@ const RestoreBackupSchema = z.object({
type RestoreBackup = z.infer<typeof RestoreBackupSchema>;
const formatBytes = (bytes: number): string => {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
};
export const RestoreBackup = ({
databaseId,
databaseType,
@ -101,7 +109,7 @@ export const RestoreBackup = ({
const debouncedSetSearch = debounce((value: string) => {
setDebouncedSearchTerm(value);
}, 150);
}, 350);
const handleSearchChange = (value: string) => {
setSearch(value);
@ -271,7 +279,7 @@ export const RestoreBackup = ({
</Badge>
)}
</FormLabel>
<Popover>
<Popover modal>
<PopoverTrigger asChild>
<FormControl>
<Button
@ -308,28 +316,51 @@ export const RestoreBackup = ({
</div>
) : (
<ScrollArea className="h-64">
<CommandGroup>
{files.map((file) => (
<CommandGroup className="w-96">
{files?.map((file) => (
<CommandItem
value={file}
key={file}
value={file.Path}
key={file.Path}
onSelect={() => {
form.setValue("backupFile", file);
setSearch(file);
setDebouncedSearchTerm(file);
form.setValue("backupFile", file.Path);
if (file.IsDir) {
setSearch(`${file.Path}/`);
setDebouncedSearchTerm(`${file.Path}/`);
} else {
setSearch(file.Path);
setDebouncedSearchTerm(file.Path);
}
}}
>
<div className="flex w-full justify-between">
<span>{file}</span>
<div className="flex w-full flex-col gap-1">
<div className="flex w-full justify-between">
<span className="font-medium">
{file.Path}
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
file.Path === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</div>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span>
Size: {formatBytes(file.Size)}
</span>
{file.IsDir && (
<span className="text-blue-500">
Directory
</span>
)}
{file.Hashes?.MD5 && (
<span>MD5: {file.Hashes.MD5}</span>
)}
</div>
</div>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
file === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>

View File

@ -50,6 +50,18 @@ import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { z } from "zod";
interface RcloneFile {
Path: string;
Name: string;
Size: number;
IsDir: boolean;
Tier?: string;
Hashes?: {
MD5?: string;
SHA1?: string;
};
}
export const backupRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateBackup)
@ -268,7 +280,7 @@ export const backupRouter = createTRPCRouter({
: input.search;
const searchPath = baseDir ? `${bucketPath}/${baseDir}` : bucketPath;
const listCommand = `rclone lsf ${rcloneFlags.join(" ")} "${searchPath}" | head -n 100`;
const listCommand = `rclone lsjson ${rcloneFlags.join(" ")} "${searchPath}" --no-mimetype --no-modtime 2>/dev/null`;
let stdout = "";
@ -280,20 +292,35 @@ export const backupRouter = createTRPCRouter({
stdout = result.stdout;
}
const files = stdout.split("\n").filter(Boolean);
let files: RcloneFile[] = [];
try {
files = JSON.parse(stdout) as RcloneFile[];
} catch (error) {
console.error("Error parsing JSON response:", error);
console.error("Raw stdout:", stdout);
throw new Error("Failed to parse backup files list");
}
// Limit to first 100 files
const results = baseDir
? files.map((file) => `${baseDir}${file}`)
? files.map((file) => ({
...file,
Path: `${baseDir}${file.Path}`,
}))
: files;
if (searchTerm) {
return results.filter((file) =>
file.toLowerCase().includes(searchTerm.toLowerCase()),
);
return results
.filter((file) =>
file.Path.toLowerCase().includes(searchTerm.toLowerCase()),
)
.slice(0, 100);
}
return results;
return results.slice(0, 100);
} catch (error) {
console.error("Error in listBackupFiles:", error);
throw new TRPCError({
code: "BAD_REQUEST",
message: