backups.php

main 153 lines · 6 KB Raw
Alex Alex Commit Initial commit 01/10/2026 09:20
1<?php
2/**
3 * Title: Backups
4 * Icon: fa-archive
5 * Group: Operations
6 * Order: 20
7 * Description: Create, download and manage database backups.
8 */
9
10if (!defined('ACP_ROOT')) {
11 http_response_code(403);
12 die('Direct access denied.');
13}
14
15$retentionKey = 'config:backups.retention';
16$retention = max(1, (int)setting($retentionKey, '10'));
17
18// ---------------------------------------------------------------------------
19// Download - streams the file directly, bypassing the normal page render.
20// ---------------------------------------------------------------------------
21if (($_GET['do'] ?? '') === 'download') {
22 $path = znote_backups_path((string)($_GET['name'] ?? ''));
23 if ($path === null) {
24 http_response_code(404);
25 die('No such backup.');
26 }
27
28 acp_log('backups.download', basename($path));
29
30 while (function_exists('ob_get_level') && ob_get_level() > 0) {
31 ob_end_clean();
32 }
33 header('Content-Type: application/gzip');
34 header('Content-Disposition: attachment; filename="' . basename($path) . '"');
35 header('Content-Length: ' . filesize($path));
36 readfile($path);
37 exit;
38}
39
40if ($_SERVER['REQUEST_METHOD'] === 'POST') {
41 $do = (string)($_POST['do'] ?? '');
42
43 if ($do === 'create') {
44 list($ok, $result) = znote_backups_create();
45 if ($ok) {
46 $removed = znote_backups_prune($retention);
47 acp_log('backups.create', $result, ['pruned' => $removed]);
48 acp_flash_success(t_default('acp.bkp.created', 'Backup created: {name}.', ['name' => h($result)]));
49 } else {
50 acp_flash_error(t_default('acp.bkp.create_failed', 'Backup failed: {error}', ['error' => h($result)]));
51 }
52 acp_redirect('backups');
53 }
54
55 if ($do === 'delete') {
56 $name = (string)($_POST['name'] ?? '');
57 if (znote_backups_delete($name)) {
58 acp_log('backups.delete', $name);
59 acp_flash_success(t_default('acp.bkp.deleted', 'Backup deleted.'));
60 } else {
61 acp_flash_error(t_default('acp.bkp.delete_failed', 'Could not delete that backup.'));
62 }
63 acp_redirect('backups');
64 }
65
66 if ($do === 'retention') {
67 $value = max(1, min(100, intv($_POST['retention'] ?? 10)));
68 setting_set($retentionKey, (string)$value);
69 acp_log('backups.retention', (string)$value);
70 acp_flash_success(t_default('acp.bkp.retention_saved', 'Kept backups set to {n}.', ['n' => $value]));
71 acp_redirect('backups');
72 }
73}
74
75$backups = znote_backups_list();
76$totalSize = array_sum(array_column($backups, 'size'));
77?>
78
79<div class="acp-stats">
80 <?php
81 acp_stat(t_default('acp.bkp.stat_count', 'Backups stored'), count($backups), 'fa-life-ring', null, 'blue');
82 acp_stat(t_default('acp.bkp.stat_size', 'Total size'), serverdata_human_size($totalSize), 'fa-hdd-o', null, 'purple');
83 ?>
84</div>
85
86<section class="acp-card">
87 <header class="acp-card-head">
88 <h2><?= t_default('acp.bkp.create_title', 'Create a backup') ?></h2>
89 <p><?= t_default('acp.bkp.create_sub', 'A full SQL dump of the database, compressed. Restorable with any MySQL client - nothing ZnoteX-specific about the file.') ?></p>
90 </header>
91 <div class="acp-card-body">
92 <form method="post" style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
93 <?= acp_csrf_field() ?>
94 <input type="hidden" name="do" value="create">
95 <button class="acp-btn acp-btn--green" type="submit"><i class="fa fa-plus"></i> <?= t_default('acp.bkp.create_btn', 'Create backup now') ?></button>
96 <span class="acp-hint"><?= t_default('acp.bkp.create_hint', 'A large database can take a while - the page waits for it to finish.') ?></span>
97 </form>
98
99 <form method="post" style="display:flex;align-items:center;gap:10px;margin-top:16px;padding-top:16px;border-top:1px solid var(--acp-line);">
100 <?= acp_csrf_field() ?>
101 <input type="hidden" name="do" value="retention">
102 <label class="acp-label" for="bkp_retention" style="margin:0;"><?= t_default('acp.bkp.retention_label', 'Keep the last') ?></label>
103 <input class="acp-input" id="bkp_retention" name="retention" type="number" min="1" max="100" value="<?= $retention ?>" style="width:80px;">
104 <span class="is-muted"><?= t_default('acp.bkp.retention_suffix', 'backups (older ones are deleted right after a new one is made)') ?></span>
105 <button class="acp-btn acp-btn--sm" type="submit"><i class="fa fa-check"></i> <?= t_default('acp.bkp.retention_save_btn', 'Save') ?></button>
106 </form>
107 </div>
108</section>
109
110<section class="acp-card">
111 <header class="acp-card-head">
112 <h2><?= t_default('acp.bkp.list_title', 'Existing backups') ?></h2>
113 </header>
114 <div class="acp-card-body is-flush">
115 <?php if ($backups): ?>
116 <div class="acp-table-wrap">
117 <table class="acp-table" data-sortable>
118 <thead>
119 <tr>
120 <th><?= t_default('acp.bkp.col_name', 'File') ?></th>
121 <th><?= t_default('acp.bkp.col_date', 'Created') ?></th>
122 <th class="is-num"><?= t_default('acp.bkp.col_size', 'Size') ?></th>
123 <th class="is-num"><?= t_default('acp.bkp.col_actions', 'Actions') ?></th>
124 </tr>
125 </thead>
126 <tbody>
127 <?php foreach ($backups as $backup): ?>
128 <tr>
129 <td><code><?= h($backup['name']) ?></code></td>
130 <td class="is-nowrap is-muted"><?= h(getClock($backup['time'], true)) ?></td>
131 <td class="is-num"><?= h(number_format($backup['size'] / 1048576, 2)) ?> MB</td>
132 <td class="is-nowrap is-num">
133 <a class="acp-btn acp-btn--ghost acp-btn--sm" href="<?= h(acp_url('backups', ['do' => 'download', 'name' => $backup['name']])) ?>">
134 <i class="fa fa-download"></i>
135 </a>
136 <form class="acp-inline-form" method="post" data-confirm="<?= h(t_default('acp.bkp.confirm_delete', 'Delete this backup? This cannot be undone.')) ?>">
137 <?= acp_csrf_field() ?>
138 <input type="hidden" name="do" value="delete">
139 <input type="hidden" name="name" value="<?= h($backup['name']) ?>">
140 <button class="acp-btn acp-btn--red acp-btn--sm" type="submit"><i class="fa fa-trash"></i></button>
141 </form>
142 </td>
143 </tr>
144 <?php endforeach; ?>
145 </tbody>
146 </table>
147 </div>
148 <?php else: ?>
149 <?php acp_empty(t_default('acp.bkp.empty', 'No backups yet.'), 'fa-life-ring'); ?>
150 <?php endif; ?>
151 </div>
152</section>
153
Top