mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge pull request #1783 from Dokploy/1747-backup-issues-doesnt-list-all-files
Enhance backup restoration UI and API by adding file size formatting,…
This commit is contained in:
@@ -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 flex-col gap-1">
|
||||||
<div className="flex w-full justify-between">
|
<div className="flex w-full justify-between">
|
||||||
<span>{file}</span>
|
<span className="font-medium">
|
||||||
</div>
|
{file.Path}
|
||||||
|
</span>
|
||||||
|
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
file === field.value
|
file.Path === field.value
|
||||||
? "opacity-100"
|
? "opacity-100"
|
||||||
: "opacity-0",
|
: "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>
|
</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:
|
||||||
|
|||||||
Reference in New Issue
Block a user