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:
sandrodachandClaude Opus 5.5 committed 2026-10-29 15:10:06 +01:00
1 parent cab5a79f6d
commit c97765e08e
9 files changed
+632 -68

No files matched your search

+33 -6
View File
@@ -2,6 +2,7 @@ package storage
import (
"encoding/json"
"strconv"
"strings"
"github.com/sandrodach/mindsift/internal/domain"
@@ -75,6 +76,7 @@ func (s *Store) ExportUserData(userID string) (*domain.FullExport, error) {
docs, _ := s.GetDocuments(userID)
lists, _ := s.GetAllListsForUser(userID)
taskLists, _ := s.GetTaskLists(userID)
themes, _ := s.GetUserThemes(userID)
return &domain.FullExport{
ExportedAt: domain.NowISO(),
@@ -84,6 +86,7 @@ func (s *Store) ExportUserData(userID string) (*domain.FullExport, error) {
Documents: docs,
Lists: lists,
TaskLists: taskLists,
Themes: themes,
}, nil
}
@@ -111,11 +114,13 @@ func (s *Store) SeedSMTPDefaultsFromEnv(host, port, user, pass, from string, tls
func (s *Store) GetSystemSettings() (*domain.SystemSettings, error) {
settings := &domain.SystemSettings{
SharingMode: SharingOrganizations,
AllowRegistration: false,
InstanceName: "MindSift",
SMTPPort: "587",
SMTPTLS: true,
SharingMode: SharingOrganizations,
AllowRegistration: false,
InstanceName: "MindSift",
SMTPPort: "587",
SMTPTLS: true,
BackupTime: DefaultBackupTime,
BackupRetentionDays: DefaultBackupRetentionDays,
}
rows, err := s.db.Query(`SELECT key, value FROM system_settings`)
@@ -152,6 +157,16 @@ func (s *Store) GetSystemSettings() (*domain.SystemSettings, error) {
settings.SMTPSkipVerify = v == "true"
case "allow_private_webhooks":
settings.AllowPrivateWebhooks = v == "true"
case "backup_enabled":
settings.BackupEnabled = v == "true"
case "backup_time":
if validBackupTime(v) {
settings.BackupTime = v
}
case "backup_retention_days":
if n, err := strconv.Atoi(v); err == nil && n > 0 {
settings.BackupRetentionDays = n
}
}
}
}
@@ -182,6 +197,12 @@ func (s *Store) UpdateSystemSettings(settings *domain.SystemSettings) error {
if settings.InstanceName == "" {
settings.InstanceName = "MindSift"
}
if !validBackupTime(settings.BackupTime) {
settings.BackupTime = DefaultBackupTime
}
if settings.BackupRetentionDays < 1 || settings.BackupRetentionDays > 365 {
settings.BackupRetentionDays = DefaultBackupRetentionDays
}
_, err := s.db.Exec(`INSERT INTO system_settings (key, value) VALUES
('sharing_mode', ?),
('allow_registration', ?),
@@ -193,7 +214,10 @@ func (s *Store) UpdateSystemSettings(settings *domain.SystemSettings) error {
('smtp_from', ?),
('smtp_tls', ?),
('smtp_skip_verify', ?),
('allow_private_webhooks', ?)
('allow_private_webhooks', ?),
('backup_enabled', ?),
('backup_time', ?),
('backup_retention_days', ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
settings.SharingMode,
regStr,
@@ -206,6 +230,9 @@ func (s *Store) UpdateSystemSettings(settings *domain.SystemSettings) error {
tlsStr,
skipVerifyStr,
privateHooksStr,
strconv.FormatBool(settings.BackupEnabled),
settings.BackupTime,
strconv.Itoa(settings.BackupRetentionDays),
)
return err
}