mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
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:
parent
c0b35efaca
commit
ffcdbcf046
@ -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 flex-col gap-1">
|
||||
<div className="flex w-full justify-between">
|
||||
<span>{file}</span>
|
||||
</div>
|
||||
<span className="font-medium">
|
||||
{file.Path}
|
||||
</span>
|
||||
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
file === field.value
|
||||
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>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user