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
@@ -28,12 +28,34 @@ const (
|
||||
// ImportPlan is everything an import would create. It is built completely (and validated) before
|
||||
// anything is written, so the preview shows exactly what the import does.
|
||||
type ImportPlan struct {
|
||||
Folders []string `json:"-"`
|
||||
Docs []importDoc `json:"-"`
|
||||
TaskLists []importTaskList `json:"-"`
|
||||
Lists []importList `json:"-"`
|
||||
Inbox []importTask `json:"-"`
|
||||
Skipped []ImportSkip `json:"skipped"`
|
||||
Folders []string `json:"-"`
|
||||
Docs []importDoc `json:"-"`
|
||||
TaskLists []importTaskList `json:"-"`
|
||||
Lists []importList `json:"-"`
|
||||
Inbox []importTask `json:"-"`
|
||||
Themes []domain.UserTheme `json:"-"`
|
||||
Skipped []ImportSkip `json:"skipped"`
|
||||
}
|
||||
|
||||
// Only keeps the chosen sections of the plan (see NormalizeSections).
|
||||
func (p *ImportPlan) Only(sections map[string]bool) *ImportPlan {
|
||||
out := &ImportPlan{Skipped: p.Skipped}
|
||||
if sections[SectionNotes] {
|
||||
out.Folders, out.Docs = p.Folders, p.Docs
|
||||
}
|
||||
if sections[SectionTasks] {
|
||||
out.TaskLists = p.TaskLists
|
||||
}
|
||||
if sections[SectionLists] {
|
||||
out.Lists = p.Lists
|
||||
}
|
||||
if sections[SectionInbox] {
|
||||
out.Inbox = p.Inbox
|
||||
}
|
||||
if sections[SectionThemes] {
|
||||
out.Themes = p.Themes
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type ImportSkip struct {
|
||||
@@ -50,6 +72,7 @@ type ImportSummary struct {
|
||||
Lists int `json:"lists"`
|
||||
ListItems int `json:"listItems"`
|
||||
Inbox int `json:"inbox"`
|
||||
Themes int `json:"themes"`
|
||||
Skipped []ImportSkip `json:"skipped"`
|
||||
Applied bool `json:"applied"`
|
||||
}
|
||||
@@ -85,7 +108,7 @@ func (p *ImportPlan) skip(path, reason string) {
|
||||
}
|
||||
|
||||
func (p *ImportPlan) Summary(applied bool) ImportSummary {
|
||||
sum := ImportSummary{Folders: len(p.Folders), Notes: len(p.Docs), Lists: len(p.Lists), Inbox: len(p.Inbox), Skipped: p.Skipped, Applied: applied}
|
||||
sum := ImportSummary{Folders: len(p.Folders), Notes: len(p.Docs), Lists: len(p.Lists), Inbox: len(p.Inbox), Themes: len(p.Themes), Skipped: p.Skipped, Applied: applied}
|
||||
for _, tl := range p.TaskLists {
|
||||
if tl.title != "" {
|
||||
sum.TaskLists++
|
||||
@@ -416,6 +439,7 @@ func PlanMindSiftJSONImport(data []byte) (*ImportPlan, error) {
|
||||
}
|
||||
plan.Lists = append(plan.Lists, il)
|
||||
}
|
||||
plan.Themes = exp.Themes
|
||||
plan.Folders = sortedKeys(folders)
|
||||
return plan, nil
|
||||
}
|
||||
@@ -484,6 +508,11 @@ func (s *Store) ApplyImport(userID string, plan *ImportPlan) (ImportSummary, err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, th := range plan.Themes {
|
||||
if _, err := s.SaveUserTheme(userID, "", th.Name, th.CSS); err != nil {
|
||||
plan.skip(th.Name, "Theme: "+err.Error())
|
||||
}
|
||||
}
|
||||
return plan.Summary(true), nil
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user