feat(automatic-backup-to-usb) - done v1

This commit is contained in:
2025-12-19 01:30:27 +05:30
parent adb49a6683
commit 4c030713d7
9 changed files with 517 additions and 5 deletions

View File

@@ -0,0 +1,165 @@
import { useState } from "react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { FolderOpen, Trash2 } from "lucide-react";
import { apiRequest, queryClient } from "@/lib/queryClient";
import { useToast } from "@/hooks/use-toast";
export function BackupDestinationManager() {
const { toast } = useToast();
const [path, setPath] = useState("");
const [deleteId, setDeleteId] = useState<number | null>(null);
// ==============================
// Queries
// ==============================
const { data: destinations = [] } = useQuery({
queryKey: ["/db/destination"],
queryFn: async () => {
const res = await apiRequest(
"GET",
"/api/database-management/destination"
);
return res.json();
},
});
// ==============================
// Mutations
// ==============================
const saveMutation = useMutation({
mutationFn: async () => {
const res = await apiRequest(
"POST",
"/api/database-management/destination",
{ path }
);
if (!res.ok) throw new Error((await res.json()).error);
},
onSuccess: () => {
toast({ title: "Backup destination saved" });
setPath("");
queryClient.invalidateQueries({ queryKey: ["/db/destination"] });
},
});
const deleteMutation = useMutation({
mutationFn: async (id: number) => {
await apiRequest("DELETE", `/api/database-management/destination/${id}`);
},
onSuccess: () => {
toast({ title: "Backup destination deleted" });
queryClient.invalidateQueries({ queryKey: ["/db/destination"] });
setDeleteId(null);
},
});
// ==============================
// Folder picker (browser limitation)
// ==============================
const openFolderPicker = async () => {
// @ts-ignore
if (!window.showDirectoryPicker) {
toast({
title: "Not supported",
description: "Your browser does not support folder picking",
variant: "destructive",
});
return;
}
try {
// @ts-ignore
const dirHandle = await window.showDirectoryPicker();
toast({
title: "Folder selected",
description: `Selected folder: ${dirHandle.name}. Please enter the full path manually.`,
});
} catch {
// user cancelled
}
};
// ==============================
// UI
// ==============================
return (
<Card>
<CardHeader>
<CardTitle>External Backup Destination</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex gap-2">
<Input
placeholder="/media/usb-drive or D:\\Backups"
value={path}
onChange={(e) => setPath(e.target.value)}
/>
<Button variant="outline" onClick={openFolderPicker}>
<FolderOpen className="h-4 w-4" />
</Button>
</div>
<Button
onClick={() => saveMutation.mutate()}
disabled={!path || saveMutation.isPending}
>
Save Destination
</Button>
<div className="space-y-2">
{destinations.map((d: any) => (
<div
key={d.id}
className="flex justify-between items-center border rounded p-2"
>
<span className="text-sm text-gray-700">{d.path}</span>
<Button
size="sm"
variant="destructive"
onClick={() => setDeleteId(d.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
{/* Confirm delete dialog */}
<AlertDialog open={deleteId !== null}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete backup destination?</AlertDialogTitle>
<AlertDialogDescription>
This will remove the destination and stop automatic backups.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setDeleteId(null)}>
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={() => deleteId && deleteMutation.mutate(deleteId)}
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</CardContent>
</Card>
);
}

View File

@@ -12,6 +12,7 @@ import {
import { useMutation, useQuery } from "@tanstack/react-query";
import { apiRequest, queryClient } from "@/lib/queryClient";
import { formatDateToHumanReadable } from "@/utils/dateUtils";
import { BackupDestinationManager } from "@/components/database-management/backup-destination-manager";
export default function DatabaseManagementPage() {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
@@ -205,6 +206,9 @@ export default function DatabaseManagementPage() {
</CardContent>
</Card>
{/* Externa Drive automatic backup manager */}
<BackupDestinationManager />
{/* Database Status Section */}
<Card>
<CardHeader>