Add scheduled and manual backups
Backups are .tar.gz archives of a consistent database snapshot (VACUUM INTO) plus the images folder. They run on a cron schedule, daily at 3:00 by default, and the newest N scheduled ones are kept (7 by default). Manual backups are never pruned. Settings → Backups offers schedule presets or a custom cron expression, retention, "Back up now", and download/delete for each backup. A small built-in parser handles standard 5-field cron syntax with names, ranges, steps and @daily-style shorthands. Backups go to CANON_BACKUP_DIR (default data/backups). Compose mounts a separate canon-backups volume at /backups, overridable with CANON_BACKUP_PATH. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
72b49d709a
commit
f3185ba7bd
18 files changed
+940
-10
No files matched your search
@@ -0,0 +1,50 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNext(t *testing.T) {
|
||||
utc := func(s string) time.Time {
|
||||
v, err := time.Parse("2006-01-02 15:04", s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
cases := []struct{ expr, from, want string }{
|
||||
{"0 3 * * *", "2026-09-30 02:59", "2026-09-30 03:00"},
|
||||
{"0 3 * * *", "2026-09-30 03:00", "2026-10-01 03:00"},
|
||||
{"@daily", "2026-12-31 23:30", "2027-01-01 00:00"},
|
||||
{"*/15 * * * *", "2026-09-30 10:07", "2026-09-30 10:15"},
|
||||
{"30 4 * * sun", "2026-09-30 00:00", "2026-10-04 04:30"}, // Wed → Sun
|
||||
{"30 4 * * 7", "2026-09-30 00:00", "2026-10-04 04:30"},
|
||||
{"0 0 1,15 * *", "2026-09-02 00:00", "2026-09-15 00:00"},
|
||||
{"0 9 * * mon-fri", "2026-10-02 10:00", "2026-10-05 09:00"}, // Fri after 9 → Mon
|
||||
{"0 0 29 2 *", "2026-03-01 00:00", "2028-02-29 00:00"},
|
||||
{"0 12 13 * 5", "2026-09-30 00:00", "2026-10-02 12:00"}, // Friday OR the 13th
|
||||
{"5/20 * * * *", "2026-09-30 10:26", "2026-09-30 10:45"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
s, err := ParseCron(c.expr)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", c.expr, err)
|
||||
}
|
||||
if got := s.Next(utc(c.from)); !got.Equal(utc(c.want)) {
|
||||
t.Errorf("%s from %s = %s, want %s", c.expr, c.from, got.Format("2006-01-02 15:04 Mon"), c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCronErrors(t *testing.T) {
|
||||
for _, expr := range []string{"", "* * * *", "60 * * * *", "* 24 * * *", "* * 0 * *", "5-1 * * * *", "*/0 * * * *", "a * * * *"} {
|
||||
if _, err := ParseCron(expr); err == nil {
|
||||
t.Errorf("%q: expected an error", expr)
|
||||
}
|
||||
}
|
||||
s, _ := ParseCron("0 0 30 2 *")
|
||||
if !s.Next(time.Now()).IsZero() {
|
||||
t.Error("Feb 30 should never run")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user