refactoring of backup/restore functionality
This commit is contained in:
@@ -39,15 +39,25 @@ export default function SystemSettings({}: SystemSettingsProps) {
|
||||
success?: boolean;
|
||||
jobId?: string;
|
||||
progress?: number;
|
||||
downloadReady?: boolean;
|
||||
};
|
||||
completeRestore: { loading: boolean; message: string; success?: boolean };
|
||||
completeClear: { loading: boolean; message: string; success?: boolean };
|
||||
}>({
|
||||
completeBackup: { loading: false, message: '', progress: 0 },
|
||||
completeRestore: { loading: false, message: '' },
|
||||
completeClear: { loading: false, message: '' }
|
||||
});
|
||||
|
||||
const [backupList, setBackupList] = useState<{
|
||||
loading: boolean;
|
||||
backups: Array<{ filename: string; sizeBytes: number; createdAt: string; origin: 'MANUAL' | 'AUTO' | 'UNKNOWN' }>;
|
||||
error?: string;
|
||||
}>({ loading: false, backups: [] });
|
||||
|
||||
const [restoreStatus, setRestoreStatus] = useState<{
|
||||
loading: boolean;
|
||||
filename?: string;
|
||||
message: string;
|
||||
success?: boolean;
|
||||
}>({ loading: false, message: '' });
|
||||
const [cleanupStatus, setCleanupStatus] = useState<{
|
||||
preview: { loading: boolean; message: string; success?: boolean; data?: any };
|
||||
execute: { loading: boolean; message: string; success?: boolean };
|
||||
@@ -67,6 +77,20 @@ export default function SystemSettings({}: SystemSettingsProps) {
|
||||
const [hoveredImage, setHoveredImage] = useState<{ src: string; alt: string } | null>(null);
|
||||
const [mousePosition, setMousePosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||
|
||||
const loadBackups = async () => {
|
||||
setBackupList(prev => ({ ...prev, loading: true, error: undefined }));
|
||||
try {
|
||||
const result = await databaseApi.listBackupFiles();
|
||||
setBackupList({ loading: false, backups: result.backups });
|
||||
} catch (error: any) {
|
||||
setBackupList({ loading: false, backups: [], error: error.message || 'Failed to load backups' });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadBackups();
|
||||
}, []);
|
||||
|
||||
const handleImageHover = (filePath: string, fileName: string, event: React.MouseEvent) => {
|
||||
// Convert backend file path to frontend image URL
|
||||
const imageUrl = filePath.replace(/^.*\/images\//, '/images/');
|
||||
@@ -88,11 +112,10 @@ export default function SystemSettings({}: SystemSettingsProps) {
|
||||
const handleCompleteBackup = async () => {
|
||||
setDatabaseStatus(prev => ({
|
||||
...prev,
|
||||
completeBackup: { loading: true, message: 'Starting backup...', success: undefined, progress: 0, downloadReady: false }
|
||||
completeBackup: { loading: true, message: 'Starting backup...', success: undefined, progress: 0 }
|
||||
}));
|
||||
|
||||
try {
|
||||
// Start the async backup job
|
||||
const startResponse = await databaseApi.backupComplete();
|
||||
const jobId = startResponse.jobId;
|
||||
|
||||
@@ -101,7 +124,6 @@ export default function SystemSettings({}: SystemSettingsProps) {
|
||||
completeBackup: { ...prev.completeBackup, jobId, message: 'Backup in progress...' }
|
||||
}));
|
||||
|
||||
// Poll for progress
|
||||
const pollInterval = setInterval(async () => {
|
||||
try {
|
||||
const status = await databaseApi.getBackupStatus(jobId);
|
||||
@@ -110,147 +132,79 @@ export default function SystemSettings({}: SystemSettingsProps) {
|
||||
clearInterval(pollInterval);
|
||||
setDatabaseStatus(prev => ({
|
||||
...prev,
|
||||
completeBackup: {
|
||||
loading: false,
|
||||
message: 'Backup completed! Ready to download.',
|
||||
success: true,
|
||||
jobId,
|
||||
progress: 100,
|
||||
downloadReady: true
|
||||
}
|
||||
completeBackup: { loading: false, message: 'Backup created successfully.', success: true, jobId, progress: 100 }
|
||||
}));
|
||||
|
||||
// Clear message after 30 seconds (keep download button visible)
|
||||
loadBackups();
|
||||
setTimeout(() => {
|
||||
setDatabaseStatus(prev => ({
|
||||
...prev,
|
||||
completeBackup: { ...prev.completeBackup, message: '' }
|
||||
}));
|
||||
}, 30000);
|
||||
setDatabaseStatus(prev => ({ ...prev, completeBackup: { ...prev.completeBackup, message: '' } }));
|
||||
}, 8000);
|
||||
} else if (status.status === 'FAILED') {
|
||||
clearInterval(pollInterval);
|
||||
setDatabaseStatus(prev => ({
|
||||
...prev,
|
||||
completeBackup: {
|
||||
loading: false,
|
||||
message: `Backup failed: ${status.errorMessage}`,
|
||||
success: false,
|
||||
progress: 0,
|
||||
downloadReady: false
|
||||
}
|
||||
completeBackup: { loading: false, message: `Backup failed: ${status.errorMessage}`, success: false, progress: 0 }
|
||||
}));
|
||||
} else {
|
||||
// Update progress
|
||||
setDatabaseStatus(prev => ({
|
||||
...prev,
|
||||
completeBackup: {
|
||||
...prev.completeBackup,
|
||||
progress: status.progress,
|
||||
message: `Creating backup... ${status.progress}%`
|
||||
}
|
||||
completeBackup: { ...prev.completeBackup, progress: status.progress, message: `Creating backup... ${status.progress}%` }
|
||||
}));
|
||||
}
|
||||
} catch (pollError: any) {
|
||||
clearInterval(pollInterval);
|
||||
setDatabaseStatus(prev => ({
|
||||
...prev,
|
||||
completeBackup: {
|
||||
loading: false,
|
||||
message: `Failed to check backup status: ${pollError.message}`,
|
||||
success: false,
|
||||
progress: 0,
|
||||
downloadReady: false
|
||||
}
|
||||
completeBackup: { loading: false, message: `Failed to check backup status: ${pollError.message}`, success: false, progress: 0 }
|
||||
}));
|
||||
}
|
||||
}, 2000); // Poll every 2 seconds
|
||||
}, 2000);
|
||||
|
||||
} catch (error: any) {
|
||||
setDatabaseStatus(prev => ({
|
||||
...prev,
|
||||
completeBackup: {
|
||||
loading: false,
|
||||
message: error.message || 'Failed to start backup',
|
||||
success: false,
|
||||
progress: 0,
|
||||
downloadReady: false
|
||||
}
|
||||
completeBackup: { loading: false, message: error.message || 'Failed to start backup', success: false, progress: 0 }
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadBackup = (jobId: string) => {
|
||||
const downloadUrl = databaseApi.downloadBackup(jobId);
|
||||
const handleRestoreFromFile = async (filename: string) => {
|
||||
const confirmed = window.confirm(
|
||||
`Are you sure you want to restore "${filename}"? This will PERMANENTLY DELETE all current data AND files (cover images, avatars) and replace them with the backup data. This action cannot be undone!`
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
setRestoreStatus({ loading: true, filename, message: 'Restoring backup...' });
|
||||
|
||||
try {
|
||||
const result = await databaseApi.restoreFromFile(filename);
|
||||
setRestoreStatus({ loading: false, filename, message: result.message, success: result.success });
|
||||
} catch (error: any) {
|
||||
setRestoreStatus({ loading: false, filename, message: error.message || 'Restore failed', success: false });
|
||||
}
|
||||
|
||||
setTimeout(() => setRestoreStatus({ loading: false, message: '' }), 10000);
|
||||
};
|
||||
|
||||
const handleDownloadBackupFile = (filename: string) => {
|
||||
const url = databaseApi.downloadBackupFile(filename);
|
||||
const link = document.createElement('a');
|
||||
link.href = downloadUrl;
|
||||
link.download = ''; // Filename will be set by server
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
// Clear the download ready state after download
|
||||
setDatabaseStatus(prev => ({
|
||||
...prev,
|
||||
completeBackup: {
|
||||
loading: false,
|
||||
message: 'Backup downloaded successfully',
|
||||
success: true,
|
||||
progress: 100,
|
||||
downloadReady: false
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const handleCompleteRestore = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
// Reset the input so the same file can be selected again
|
||||
event.target.value = '';
|
||||
|
||||
if (!file.name.endsWith('.zip')) {
|
||||
setDatabaseStatus(prev => ({
|
||||
...prev,
|
||||
completeRestore: { loading: false, message: 'Please select a .zip file', success: false }
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = window.confirm(
|
||||
'Are you sure you want to restore the complete backup? This will PERMANENTLY DELETE all current data AND files (cover images, avatars) and replace them with the backup data. This action cannot be undone!'
|
||||
);
|
||||
|
||||
const handleDeleteBackupFile = async (filename: string) => {
|
||||
const confirmed = window.confirm(`Delete backup "${filename}"? This cannot be undone.`);
|
||||
if (!confirmed) return;
|
||||
|
||||
setDatabaseStatus(prev => ({
|
||||
...prev,
|
||||
completeRestore: { loading: true, message: 'Restoring complete backup...', success: undefined }
|
||||
}));
|
||||
|
||||
try {
|
||||
const result = await databaseApi.restoreComplete(file);
|
||||
setDatabaseStatus(prev => ({
|
||||
...prev,
|
||||
completeRestore: {
|
||||
loading: false,
|
||||
message: result.success ? result.message : result.message,
|
||||
success: result.success
|
||||
}
|
||||
}));
|
||||
await databaseApi.deleteBackupFile(filename);
|
||||
loadBackups();
|
||||
} catch (error: any) {
|
||||
setDatabaseStatus(prev => ({
|
||||
...prev,
|
||||
completeRestore: { loading: false, message: error.message || 'Complete restore failed', success: false }
|
||||
}));
|
||||
alert(`Failed to delete backup: ${error.message}`);
|
||||
}
|
||||
|
||||
// Clear message after 10 seconds for restore (longer because it's important)
|
||||
setTimeout(() => {
|
||||
setDatabaseStatus(prev => ({
|
||||
...prev,
|
||||
completeRestore: { loading: false, message: '', success: undefined }
|
||||
}));
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
const handleCompleteClear = async () => {
|
||||
@@ -1129,29 +1083,17 @@ export default function SystemSettings({}: SystemSettingsProps) {
|
||||
<div className="border theme-border rounded-lg p-4 border-blue-200 dark:border-blue-800">
|
||||
<h3 className="text-lg font-semibold theme-header mb-3">📦 Create Backup</h3>
|
||||
<p className="text-sm theme-text mb-3">
|
||||
Download a complete backup as a ZIP file. This includes your database AND all uploaded files (cover images, avatars). This is a comprehensive backup of your entire StoryCove installation.
|
||||
Creates a complete backup saved on the server. The backup includes your database AND all uploaded files (cover images, avatars).
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
onClick={handleCompleteBackup}
|
||||
disabled={databaseStatus.completeBackup.loading || databaseStatus.completeBackup.downloadReady}
|
||||
loading={databaseStatus.completeBackup.loading}
|
||||
variant="primary"
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{databaseStatus.completeBackup.loading ? 'Creating Backup...' : 'Create Backup'}
|
||||
</Button>
|
||||
|
||||
{databaseStatus.completeBackup.downloadReady && databaseStatus.completeBackup.jobId && (
|
||||
<Button
|
||||
onClick={() => handleDownloadBackup(databaseStatus.completeBackup.jobId!)}
|
||||
variant="primary"
|
||||
className="w-full sm:w-auto ml-0 sm:ml-3 bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
⬇️ Download Backup
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleCompleteBackup}
|
||||
disabled={databaseStatus.completeBackup.loading}
|
||||
loading={databaseStatus.completeBackup.loading}
|
||||
variant="primary"
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{databaseStatus.completeBackup.loading ? 'Creating Backup...' : 'Create Backup'}
|
||||
</Button>
|
||||
|
||||
{databaseStatus.completeBackup.loading && databaseStatus.completeBackup.progress !== undefined && (
|
||||
<div className="mt-3">
|
||||
@@ -1181,34 +1123,117 @@ export default function SystemSettings({}: SystemSettingsProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Restore Section */}
|
||||
<div className="border theme-border rounded-lg p-4 border-orange-200 dark:border-orange-800">
|
||||
<h3 className="text-lg font-semibold theme-header mb-3">📥 Restore Backup</h3>
|
||||
<p className="text-sm theme-text mb-3">
|
||||
<strong className="text-orange-600 dark:text-orange-400">⚠️ Warning:</strong> This will completely replace your current database AND all files with the backup. All existing data and uploaded files will be permanently deleted.
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="file"
|
||||
accept=".zip"
|
||||
onChange={handleCompleteRestore}
|
||||
disabled={databaseStatus.completeRestore.loading}
|
||||
className="flex-1 text-sm theme-text file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:theme-accent-bg file:text-white hover:file:bg-opacity-90 file:cursor-pointer"
|
||||
/>
|
||||
{/* Backup List / Restore Section */}
|
||||
<div className="border theme-border rounded-lg p-4 border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-lg font-semibold theme-header">📋 Backups</h3>
|
||||
<button
|
||||
onClick={loadBackups}
|
||||
disabled={backupList.loading}
|
||||
className="text-sm theme-text opacity-70 hover:opacity-100 flex items-center gap-1"
|
||||
title="Refresh backup list"
|
||||
>
|
||||
<span className={backupList.loading ? 'animate-spin' : ''}>↻</span>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
{databaseStatus.completeRestore.message && (
|
||||
<div className={`text-sm p-2 rounded mt-3 ${
|
||||
databaseStatus.completeRestore.success
|
||||
<p className="text-sm theme-text mb-4">
|
||||
Backups are stored on the server. To transfer a backup to another server, download it and place it in the server's backup directory — it will appear here automatically.
|
||||
</p>
|
||||
|
||||
{restoreStatus.message && (
|
||||
<div className={`text-sm p-2 rounded mb-3 ${
|
||||
restoreStatus.success
|
||||
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200'
|
||||
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'
|
||||
: restoreStatus.success === false
|
||||
? 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'
|
||||
: 'bg-blue-50 dark:bg-blue-900/20 text-blue-800 dark:text-blue-200'
|
||||
}`}>
|
||||
{databaseStatus.completeRestore.message}
|
||||
{restoreStatus.loading && (
|
||||
<span className="inline-block animate-spin mr-2">↻</span>
|
||||
)}
|
||||
{restoreStatus.message}
|
||||
</div>
|
||||
)}
|
||||
{databaseStatus.completeRestore.loading && (
|
||||
<div className="text-sm theme-text mt-3 flex items-center gap-2">
|
||||
|
||||
{backupList.error && (
|
||||
<div className="text-sm p-2 rounded mb-3 bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200">
|
||||
{backupList.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{backupList.loading && backupList.backups.length === 0 ? (
|
||||
<div className="text-sm theme-text flex items-center gap-2 py-4">
|
||||
<div className="animate-spin w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full"></div>
|
||||
Restoring backup...
|
||||
Loading backups...
|
||||
</div>
|
||||
) : backupList.backups.length === 0 ? (
|
||||
<p className="text-sm theme-text opacity-60 py-4 text-center">No backups found. Create your first backup above.</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b theme-border text-left">
|
||||
<th className="pb-2 pr-4 font-medium theme-text">Date</th>
|
||||
<th className="pb-2 pr-4 font-medium theme-text">Origin</th>
|
||||
<th className="pb-2 pr-4 font-medium theme-text">Size</th>
|
||||
<th className="pb-2 font-medium theme-text text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{backupList.backups.map(backup => (
|
||||
<tr key={backup.filename} className="border-b theme-border last:border-0">
|
||||
<td className="py-2 pr-4 theme-text">
|
||||
<div>{new Date(backup.createdAt).toLocaleDateString()}</div>
|
||||
<div className="text-xs opacity-60">{new Date(backup.createdAt).toLocaleTimeString()}</div>
|
||||
</td>
|
||||
<td className="py-2 pr-4">
|
||||
<span className={`inline-block text-xs px-2 py-0.5 rounded-full font-medium ${
|
||||
backup.origin === 'MANUAL'
|
||||
? 'bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300'
|
||||
: backup.origin === 'AUTO'
|
||||
? 'bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300'
|
||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400'
|
||||
}`}>
|
||||
{backup.origin === 'MANUAL' ? 'Manual' : backup.origin === 'AUTO' ? 'Automatic' : 'Unknown'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2 pr-4 theme-text opacity-70 text-xs">
|
||||
{backup.sizeBytes >= 1024 * 1024
|
||||
? `${(backup.sizeBytes / 1024 / 1024).toFixed(1)} MB`
|
||||
: `${(backup.sizeBytes / 1024).toFixed(0)} KB`}
|
||||
</td>
|
||||
<td className="py-2">
|
||||
<div className="flex items-center gap-2 justify-end">
|
||||
<button
|
||||
onClick={() => handleRestoreFromFile(backup.filename)}
|
||||
disabled={restoreStatus.loading}
|
||||
className="text-xs px-2 py-1 rounded border theme-border theme-text hover:bg-orange-50 dark:hover:bg-orange-900/20 hover:border-orange-300 dark:hover:border-orange-700 disabled:opacity-40"
|
||||
title="Restore this backup"
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDownloadBackupFile(backup.filename)}
|
||||
className="text-xs px-2 py-1 rounded border theme-border theme-text hover:bg-blue-50 dark:hover:bg-blue-900/20 hover:border-blue-300 dark:hover:border-blue-700"
|
||||
title="Download this backup"
|
||||
>
|
||||
⬇
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteBackupFile(backup.filename)}
|
||||
disabled={restoreStatus.loading}
|
||||
className="text-xs px-2 py-1 rounded border border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 disabled:opacity-40"
|
||||
title="Delete this backup"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1243,10 +1268,9 @@ export default function SystemSettings({}: SystemSettingsProps) {
|
||||
<p className="font-medium mb-1">💡 Best Practices:</p>
|
||||
<ul className="text-xs space-y-1 ml-4">
|
||||
<li>• <strong>Always backup</strong> before performing restore or clear operations</li>
|
||||
<li>• <strong>Store backups safely</strong> in multiple locations for important data</li>
|
||||
<li>• <strong>Test restores</strong> in a development environment when possible</li>
|
||||
<li>• <strong>Cross-server transfer:</strong> download a backup and place it in the backup directory of the other server — it appears in this list automatically</li>
|
||||
<li>• <strong>Backup files (.zip)</strong> contain both database and all uploaded files</li>
|
||||
<li>• <strong>Verify backup files</strong> are complete before relying on them</li>
|
||||
<li>• <strong>Automatic backups</strong> are created daily at 4 AM when content has changed (up to 10 kept)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1043,27 +1043,12 @@ export const collectionApi = {
|
||||
|
||||
// Database management endpoints
|
||||
export const databaseApi = {
|
||||
backup: async (): Promise<Blob> => {
|
||||
const response = await api.post('/database/backup', {}, {
|
||||
responseType: 'blob'
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
restore: async (file: File): Promise<{ success: boolean; message: string }> => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const response = await api.post('/database/restore', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
clear: async (): Promise<{ success: boolean; message: string; deletedRecords?: number }> => {
|
||||
const response = await api.post('/database/clear');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Async backup creation
|
||||
backupComplete: async (): Promise<{ success: boolean; jobId: string; status: string; message: string }> => {
|
||||
const response = await api.post('/database/backup-complete');
|
||||
return response.data;
|
||||
@@ -1083,37 +1068,31 @@ export const databaseApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
downloadBackup: (jobId: string): string => {
|
||||
return `/api/database/backup-download/${jobId}`;
|
||||
},
|
||||
|
||||
listBackups: async (): Promise<{
|
||||
// Filesystem-based backup management
|
||||
listBackupFiles: async (): Promise<{
|
||||
success: boolean;
|
||||
backups: Array<{
|
||||
jobId: string;
|
||||
type: string;
|
||||
status: string;
|
||||
progress: number;
|
||||
fileSizeBytes: number;
|
||||
filename: string;
|
||||
sizeBytes: number;
|
||||
createdAt: string;
|
||||
completedAt: string;
|
||||
origin: 'MANUAL' | 'AUTO' | 'UNKNOWN';
|
||||
}>;
|
||||
}> => {
|
||||
const response = await api.get('/database/backup-list');
|
||||
const response = await api.get('/database/backups');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
deleteBackup: async (jobId: string): Promise<{ success: boolean; message: string }> => {
|
||||
const response = await api.delete(`/database/backup/${jobId}`);
|
||||
restoreFromFile: async (filename: string): Promise<{ success: boolean; message: string }> => {
|
||||
const response = await api.post(`/database/backups/${encodeURIComponent(filename)}/restore`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
restoreComplete: async (file: File): Promise<{ success: boolean; message: string }> => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const response = await api.post('/database/restore-complete', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
downloadBackupFile: (filename: string): string => {
|
||||
return `/api/database/backups/${encodeURIComponent(filename)}/download`;
|
||||
},
|
||||
|
||||
deleteBackupFile: async (filename: string): Promise<{ success: boolean; message: string }> => {
|
||||
const response = await api.delete(`/database/backups/${encodeURIComponent(filename)}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user