feat(backup): daily server and user backups, restore by section (backend)
Admins switch automatic backups on, pick the time and the retention (default 03:00, 3 days). Each run writes a gzip snapshot of the whole database (VACUUM INTO, consistent while others write) and one MindSift backup ZIP per active user below $DATA_DIR/backups, then deletes files past the retention. A restart after the time catches up on that day. Admins list, download and trigger runs; users list, download and restore their own backups. Import and restore can be limited to sections (notes, tasks, lists, inbox, themes): restore only empties the chosen sections. Export and import now carry custom themes. File names are checked against a fixed pattern, so no other file can be fetched through the API. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cab5a79f6d
commit
c97765e08e
9 files changed
+632
-68
No files matched your search
@@ -82,15 +82,21 @@ func (s *Server) runImport(w http.ResponseWriter, r *http.Request, plan *storage
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
// sections=notes,tasks,…: only these parts of a MindSift backup (all when not given)
|
||||
sections := storage.NormalizeSections(strings.Split(r.URL.Query().Get("sections"), ","))
|
||||
plan = plan.Only(sections)
|
||||
if r.URL.Query().Get("preview") == "1" {
|
||||
writeJSON(w, http.StatusOK, plan.Summary(false))
|
||||
return
|
||||
}
|
||||
userID := auth.GetUserID(r.Context())
|
||||
// mode=restore (MindSift data only): replace everything instead of adding next to it. The plan is
|
||||
// fully read and validated at this point, so a broken file never wipes anything.
|
||||
if r.URL.Query().Get("mode") == "restore" && strings.HasSuffix(r.URL.Path, "/mindsift") {
|
||||
if err := s.store.PurgeUserData(userID); err != nil {
|
||||
s.applyImport(w, auth.GetUserID(r.Context()), plan, sections, r.URL.Query().Get("mode") == "restore" && strings.HasSuffix(r.URL.Path, "/mindsift"))
|
||||
}
|
||||
|
||||
// applyImport adds the plan to the user's data; with restore the chosen sections are emptied first.
|
||||
// The plan is fully read and validated at this point, so a broken file never wipes anything.
|
||||
func (s *Server) applyImport(w http.ResponseWriter, userID string, plan *storage.ImportPlan, sections map[string]bool, restore bool) {
|
||||
if restore {
|
||||
if err := s.store.PurgeUserSections(userID, sections); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Wiederherstellen abgebrochen: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user