Settings > Backups gains a whole-data archive beside the database snapshots, and both lists now fully work from the page. - Data archives: `write-data-backup-archive.ts` tars all of DATA_DIR into BACKUP_ARCHIVE_DIR as cobble-data-<epoch-ms>.tar.gz, every entry under one root folder named after the archive, writing a .part name and renaming on success so a cut-off run is never listed. The newest AppData.DataBackupsKept are kept; db/.backups is left as it is. - Restore: `restore-data-backup.ts` extracts to a temp dir, requires one root folder holding a db folder, takes a safety archive first, then replaces DATA_DIR (the database snapshots included) and `restart-app.ts` restarts the process through the docker socket, since the open SQLite handle points at the replaced file. - Upload and restore: a modal over each list (database file or data archive), replacing the old /admin/settings/backups/restore page. - Delete for both kinds, icon-only row actions shared through RowIconButton, and restore on every row. - One backup or restore of either kind at a time (with-backup-run-lock). - BACKUP_ARCHIVE_DIR is a start-up path (ProcessOnlyEnvKeys); the installer mounts $COBBLE_HOME/backups-archives at /backups-archives and recreates a container that lacks it. The image installs tar and gzip.
97 lines
2.7 KiB
TypeScript
97 lines
2.7 KiB
TypeScript
import FileUpload from "@moduletrace/twui/form/FileUpload";
|
|
import Button from "@moduletrace/twui/layout/Button";
|
|
import Tag from "@moduletrace/twui/elements/Tag";
|
|
import Row from "@moduletrace/twui/layout/Row";
|
|
import Stack from "@moduletrace/twui/layout/Stack";
|
|
import useStatus from "@moduletrace/twui/hooks/useStatus";
|
|
import { useState } from "react";
|
|
import { Upload } from "lucide-react";
|
|
|
|
type Props = {
|
|
/**
|
|
* The file picker's hint, e.g. "Click or drop a SQLite database file".
|
|
*/
|
|
label: string;
|
|
/**
|
|
* What the picker accepts, e.g. `.db,.sqlite,.sqlite3`.
|
|
*/
|
|
accept?: string;
|
|
/**
|
|
* The button's label.
|
|
*/
|
|
action_label: string;
|
|
/**
|
|
* Sends the file and reports the outcome.
|
|
*/
|
|
upload: (params: { file: File }) => Promise<{ success: boolean; msg?: string }>;
|
|
/**
|
|
* Reload the page once it worked (the app restarts after an archive
|
|
* restore, so the caller waits for it first).
|
|
*/
|
|
onDone: () => void;
|
|
};
|
|
|
|
/**
|
|
* Picks one file and sends it for a restore: shared by the database upload
|
|
* and the data archive upload, which differ only in what they accept.
|
|
*/
|
|
export default function UploadRestoreFields({
|
|
label,
|
|
accept,
|
|
action_label,
|
|
upload,
|
|
onDone,
|
|
}: Props) {
|
|
const { loading, setLoading, status, setStatus } = useStatus();
|
|
const [file, setFile] = useState<File>();
|
|
|
|
const send = async () => {
|
|
if (!file) return;
|
|
|
|
setStatus(undefined);
|
|
setLoading(true);
|
|
|
|
const res = await upload({ file });
|
|
|
|
if (res.success) {
|
|
onDone();
|
|
return;
|
|
}
|
|
|
|
setLoading(false);
|
|
|
|
if (res.msg) {
|
|
setStatus({ error: true, msg: res.msg });
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Stack className="w-full items-stretch gap-4">
|
|
<FileUpload
|
|
label={label}
|
|
className="h-[220px]"
|
|
fileInputProps={accept ? { accept } : undefined}
|
|
changeHandler={(file_data) => {
|
|
setStatus(undefined);
|
|
setFile(file_data?.file);
|
|
}}
|
|
/>
|
|
<Row className="w-full justify-end gap-2! flex-nowrap">
|
|
{status?.error && <Tag color="error">{status.msg}</Tag>}
|
|
<Button
|
|
title={action_label}
|
|
color="warning"
|
|
beforeIcon={<Upload />}
|
|
loading={loading}
|
|
disabled={!file || loading}
|
|
onClick={() => {
|
|
send();
|
|
}}
|
|
>
|
|
{action_label}
|
|
</Button>
|
|
</Row>
|
|
</Stack>
|
|
);
|
|
}
|