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>;
|
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 = ({
|
export const RestoreBackup = ({
|
||||||
databaseId,
|
databaseId,
|
||||||
databaseType,
|
databaseType,
|
||||||
@ -101,7 +109,7 @@ export const RestoreBackup = ({
|
|||||||
|
|
||||||
const debouncedSetSearch = debounce((value: string) => {
|
const debouncedSetSearch = debounce((value: string) => {
|
||||||
setDebouncedSearchTerm(value);
|
setDebouncedSearchTerm(value);
|
||||||
}, 150);
|
}, 350);
|
||||||
|
|
||||||
const handleSearchChange = (value: string) => {
|
const handleSearchChange = (value: string) => {
|
||||||
setSearch(value);
|
setSearch(value);
|
||||||
@ -271,7 +279,7 @@ export const RestoreBackup = ({
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Popover>
|
<Popover modal>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
@ -308,28 +316,51 @@ export const RestoreBackup = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ScrollArea className="h-64">
|
<ScrollArea className="h-64">
|
||||||
<CommandGroup>
|
<CommandGroup className="w-96">
|
||||||
{files.map((file) => (
|
{files?.map((file) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={file}
|
value={file.Path}
|
||||||
key={file}
|
key={file.Path}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
form.setValue("backupFile", file);
|
form.setValue("backupFile", file.Path);
|
||||||
setSearch(file);
|
if (file.IsDir) {
|
||||||
setDebouncedSearchTerm(file);
|
setSearch(`${file.Path}/`);
|
||||||
|
setDebouncedSearchTerm(`${file.Path}/`);
|
||||||
|
} else {
|
||||||
|
setSearch(file.Path);
|
||||||
|
setDebouncedSearchTerm(file.Path);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex w-full justify-between">
|
<div className="flex w-full flex-col gap-1">
|
||||||
<span>{file}</span>
|
<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>
|
</div>
|
||||||
<CheckIcon
|
|
||||||
className={cn(
|
|
||||||
"ml-auto h-4 w-4",
|
|
||||||
file === field.value
|
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
|
@ -50,6 +50,18 @@ import { TRPCError } from "@trpc/server";
|
|||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
import { z } from "zod";
|
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({
|
export const backupRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(apiCreateBackup)
|
.input(apiCreateBackup)
|
||||||
@ -268,7 +280,7 @@ export const backupRouter = createTRPCRouter({
|
|||||||
: input.search;
|
: input.search;
|
||||||
|
|
||||||
const searchPath = baseDir ? `${bucketPath}/${baseDir}` : bucketPath;
|
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 = "";
|
let stdout = "";
|
||||||
|
|
||||||
@ -280,20 +292,35 @@ export const backupRouter = createTRPCRouter({
|
|||||||
stdout = result.stdout;
|
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
|
const results = baseDir
|
||||||
? files.map((file) => `${baseDir}${file}`)
|
? files.map((file) => ({
|
||||||
|
...file,
|
||||||
|
Path: `${baseDir}${file.Path}`,
|
||||||
|
}))
|
||||||
: files;
|
: files;
|
||||||
|
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
return results.filter((file) =>
|
return results
|
||||||
file.toLowerCase().includes(searchTerm.toLowerCase()),
|
.filter((file) =>
|
||||||
);
|
file.Path.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||||
|
)
|
||||||
|
.slice(0, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results.slice(0, 100);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("Error in listBackupFiles:", error);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message:
|
message:
|
||||||
|
Loading…
Reference in New Issue
Block a user