feat(admin): back the data up, and put it back, from the admin tool
A new BACKUP tab downloads the deployment's whole state — the SQLite file and both media folders, photos included — as one .tar.gz, and takes the same file back. That one artefact therefore does both jobs: the operator's backup and the data package that moves an install onto another box. The database is snapshotted through SQLite's own backup rather than copied, because the file is written to while the archive streams; the media folders are tarred straight off the volume, so no second copy of them is made. A restore replaces the data on disk and then exits — the container's restart policy brings the API back on the restored files, which is the only moment the open handle can be dropped. The state being replaced is tarred aside first, and the archive is checked for `..` entries before anything is unpacked. The API authenticates that route before it reads a byte, and nginx lets that one path past the body cap which holds everywhere else.
This commit is contained in:
@@ -282,6 +282,15 @@ export const en: Dict = {
|
||||
'adm.deleteSelected': 'DELETE SELECTED ({n})',
|
||||
'adm.userDeleteSelectedConfirm': 'Permanently delete the {n} selected accounts with all of their photos and recipes? This cannot be undone.',
|
||||
'adm.tabStats': 'STATS',
|
||||
'adm.tabBackup': 'BACKUP',
|
||||
'adm.backupHint': 'Download the whole system state — the accounts/recipes database and both media folders — as one .tar.gz. That same file is the data package which moves this install to another machine: restore with it.',
|
||||
'adm.backupSave': 'DOWNLOAD BACKUP',
|
||||
'adm.restorePick': 'RESTORE FROM FILE…',
|
||||
'adm.restoreHint': 'A restore replaces all current data with the file’s, then the API restarts itself. The data being replaced is archived aside in the data folder first.',
|
||||
'adm.restoreConfirm': 'Replace all current data with {file}? This cannot be undone. A safety archive is still kept in the data folder.',
|
||||
'adm.restoreRunning': 'Restoring — the API is restarting…',
|
||||
'adm.restoreDone': 'Restore finished.',
|
||||
'adm.restoreSlow': 'The API never answered. Check the container log, then reload.',
|
||||
|
||||
'stats.days': '{n} DAYS',
|
||||
'stats.hint': 'Traffic over the selected range.',
|
||||
|
||||
@@ -286,6 +286,15 @@ export const vi = {
|
||||
'adm.deleteSelected': 'XOÁ ĐÃ CHỌN ({n})',
|
||||
'adm.userDeleteSelectedConfirm': 'Xoá vĩnh viễn {n} tài khoản đã chọn cùng toàn bộ ảnh và công thức của chúng? Không hoàn tác được.',
|
||||
'adm.tabStats': 'THỐNG KÊ',
|
||||
'adm.tabBackup': 'SAO LƯU',
|
||||
'adm.backupHint': 'Tải toàn bộ dữ liệu của hệ thống — cơ sở dữ liệu tài khoản/công thức và hai thư mục ảnh — thành một tệp .tar.gz. Chính tệp này là gói dữ liệu để chuyển sang máy khác: khôi phục bằng nó.',
|
||||
'adm.backupSave': 'TẢI BẢN SAO LƯU',
|
||||
'adm.restorePick': 'KHÔI PHỤC TỪ TỆP…',
|
||||
'adm.restoreHint': 'Khôi phục thay toàn bộ dữ liệu hiện tại bằng dữ liệu trong tệp, sau đó API tự khởi động lại. Dữ liệu đang chạy được nén lại một bản an toàn trong thư mục data trước khi thay.',
|
||||
'adm.restoreConfirm': 'Thay toàn bộ dữ liệu hiện tại bằng tệp {file}? Không hoàn tác được. Một bản an toàn vẫn được giữ trong thư mục data.',
|
||||
'adm.restoreRunning': 'Đang khôi phục — API đang khởi động lại…',
|
||||
'adm.restoreDone': 'Đã khôi phục xong.',
|
||||
'adm.restoreSlow': 'API không trả lời lại. Kiểm tra nhật ký container rồi tải lại trang.',
|
||||
|
||||
// The traffic screen. Bucket names ('/app', a control's data-key, a browser
|
||||
// version) are machine values and stay as they are.
|
||||
|
||||
Reference in New Issue
Block a user