From d6f5c91d7892bd1fc9009deba973bf1ee978b901 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Tue, 13 May 2025 15:07:55 +0100 Subject: [PATCH] backups: add more info, keep 1 of each version opt backup's filename format has changed, and includes the commit too now. a Backup struct for going to/from the filename has been added, and the option "keep 1 backup from each version" has been added, leaving the most recent backup from each version always. All pre-this-commit backups are considered the same "old" version. --- api-backups.go | 20 ++--- backups.go | 157 +++++++++++++++++++++++++++++++++---- backups_test.go | 57 ++++++++++++++ config.go | 1 + config/config-base.yaml | 6 ++ discord.go | 2 +- email.go | 2 +- go.mod | 12 +-- go.sum | 20 ++--- html/admin.html | 1 + logmessages/logmessages.go | 1 + main.go | 2 +- models.go | 13 +-- telegram.go | 2 +- ts/modules/settings.ts | 2 + 15 files changed, 248 insertions(+), 50 deletions(-) create mode 100644 backups_test.go diff --git a/api-backups.go b/api-backups.go index 109082e..ca7e28b 100644 --- a/api-backups.go +++ b/api-backups.go @@ -4,8 +4,6 @@ import ( "os" "path/filepath" "sort" - "strings" - "time" "github.com/gin-gonic/gin" lm "github.com/hrfee/jfa-go/logmessages" @@ -33,9 +31,9 @@ func (app *appContext) CreateBackup(gc *gin.Context) { func (app *appContext) GetBackup(gc *gin.Context) { fname := gc.Param("fname") // Hopefully this is enough to ensure the path isn't malicious. Hidden behind bearer auth anyway so shouldn't matter too much I guess. - ok := (strings.HasPrefix(fname, BACKUP_PREFIX) || strings.HasPrefix(fname, BACKUP_UPLOAD_PREFIX+BACKUP_PREFIX)) && strings.HasSuffix(fname, BACKUP_SUFFIX) - t, err := time.Parse(BACKUP_DATEFMT, strings.TrimSuffix(strings.TrimPrefix(strings.TrimPrefix(fname, BACKUP_UPLOAD_PREFIX), BACKUP_PREFIX), BACKUP_SUFFIX)) - if !ok || err != nil || t.IsZero() { + b := Backup{} + err := b.FromString(fname) + if err != nil || b.Date.IsZero() { app.debug.Printf(lm.IgnoreInvalidFilename, fname, err) respondBool(400, false, gc) return @@ -62,7 +60,8 @@ func (app *appContext) GetBackups(gc *gin.Context) { resp.Backups[i].Name = item.Name() fullpath := filepath.Join(path, item.Name()) resp.Backups[i].Path = fullpath - resp.Backups[i].Date = backups.dates[i].Unix() + resp.Backups[i].Date = backups.info[i].Date.Unix() + resp.Backups[i].Commit = backups.info[i].Commit fstat, err := os.Stat(fullpath) if err == nil { resp.Backups[i].Size = fileSize(fstat.Size()) @@ -81,9 +80,9 @@ func (app *appContext) GetBackups(gc *gin.Context) { func (app *appContext) RestoreLocalBackup(gc *gin.Context) { fname := gc.Param("fname") // Hopefully this is enough to ensure the path isn't malicious. Hidden behind bearer auth anyway so shouldn't matter too much I guess. - ok := strings.HasPrefix(fname, BACKUP_PREFIX) && strings.HasSuffix(fname, BACKUP_SUFFIX) - t, err := time.Parse(BACKUP_DATEFMT, strings.TrimSuffix(strings.TrimPrefix(fname, BACKUP_PREFIX), BACKUP_SUFFIX)) - if !ok || err != nil || t.IsZero() { + b := Backup{} + err := b.FromString(fname) + if err != nil || b.Date.IsZero() { app.debug.Printf(lm.IgnoreInvalidFilename, fname, err) respondBool(400, false, gc) return @@ -110,7 +109,8 @@ func (app *appContext) RestoreBackup(gc *gin.Context) { } app.debug.Printf(lm.GetUpload, file.Filename) path := app.config.Section("backups").Key("path").String() - fullpath := filepath.Join(path, BACKUP_UPLOAD_PREFIX+BACKUP_PREFIX+time.Now().Local().Format(BACKUP_DATEFMT)+BACKUP_SUFFIX) + b := Backup{Upload: true} + fullpath := filepath.Join(path, b.String()) gc.SaveUploadedFile(file, fullpath) app.debug.Printf(lm.Write, fullpath) LOADBAK = fullpath diff --git a/backups.go b/backups.go index 6690454..0a1ae57 100644 --- a/backups.go +++ b/backups.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "sort" + "strconv" "strings" "time" @@ -12,35 +13,126 @@ import ( ) const ( - BACKUP_PREFIX = "jfa-go-db-" + BACKUP_PREFIX = "jfa-go-db" + BACKUP_COMMIT_PREFIX = "-c-" + BACKUP_DATE_PREFIX = "-d-" BACKUP_UPLOAD_PREFIX = "upload-" BACKUP_DATEFMT = "2006-01-02T15-04-05" BACKUP_SUFFIX = ".bak" ) +type Backup struct { + Date time.Time + Commit string + Upload bool +} + +func (b Backup) IsZero() bool { return b.Date.IsZero() && b.Commit == "" && b.Upload == false } + +func (b Backup) Equals(a Backup) bool { + return a.Date.Equal(b.Date) && a.Commit == b.Commit && a.Upload == b.Upload +} + +// Pre 21/03/25 format: "{BACKUP_PREFIX}{date in BACKUP_DATEFMT}{BACKUP_SUFFIX}" = "jfa-go-db-2006-01-02T15-04-05.bak" +// Post 21/03/25 format: "{BACKUP_PREFIX}-c-{commit}-d-{date in BACKUP_DATEFMT}{BACKUP_SUFFIX}" = "jfa-go-db-c-0b92060-d-2006-01-02T15-04-05.bak" + +func (b Backup) String() string { + t := b.Date + if t.IsZero() { + t = time.Now() + } + out := BACKUP_PREFIX + if b.Upload { + out = BACKUP_UPLOAD_PREFIX + out + } + if b.Commit != "" { + out += BACKUP_COMMIT_PREFIX + b.Commit + } + out += BACKUP_DATE_PREFIX + t.Local().Format(BACKUP_DATEFMT) + BACKUP_SUFFIX + return out +} + +func (b *Backup) FromString(f string) error { + of := f + if strings.HasPrefix(f, BACKUP_UPLOAD_PREFIX) { + b.Upload = true + f = f[len(BACKUP_UPLOAD_PREFIX):] + } + if !strings.HasPrefix(f, BACKUP_PREFIX) { + return fmt.Errorf("file doesn't have correct prefix (\"%s\")", BACKUP_PREFIX) + } + f = f[len(BACKUP_PREFIX):] + if !strings.HasSuffix(f, BACKUP_SUFFIX) { + return fmt.Errorf("file doesn't have correct suffix (\"%s\")", BACKUP_SUFFIX) + } + for range 2 { + if strings.HasPrefix(f, BACKUP_COMMIT_PREFIX) { + f = f[len(BACKUP_COMMIT_PREFIX):] + commitEnd := strings.Index(f, BACKUP_DATE_PREFIX) + if commitEnd == -1 { + commitEnd = strings.Index(f, BACKUP_SUFFIX) + } + if commitEnd == -1 { + return fmt.Errorf("end of commit (\"%s\" or \"%s\") not found in \"%s\"", BACKUP_DATE_PREFIX, BACKUP_PREFIX, f) + } + b.Commit = f[:commitEnd] + f = f[commitEnd:] + } else if strings.HasPrefix(f, BACKUP_DATE_PREFIX) { + f = f[len(BACKUP_DATE_PREFIX):] + dateEnd := strings.Index(f, BACKUP_COMMIT_PREFIX) + if dateEnd == -1 { + dateEnd = strings.Index(f, BACKUP_SUFFIX) + } + if dateEnd == -1 { + return fmt.Errorf("end of date (\"%s\" or \"%s\") not found in \"%s\"", BACKUP_COMMIT_PREFIX, BACKUP_PREFIX, f) + } + t, err := time.Parse(BACKUP_DATEFMT, f[:dateEnd]) + if err != nil { + return err + } + b.Date = t + f = f[dateEnd:] + } + } + if b.Date.IsZero() { + return b.FromOldString(of) + } + return nil +} + +func (b *Backup) FromOldString(f string) error { + t, err := time.Parse(BACKUP_DATEFMT, strings.TrimSuffix(strings.TrimPrefix(strings.TrimPrefix(f, BACKUP_UPLOAD_PREFIX), BACKUP_PREFIX+"-"), BACKUP_SUFFIX)) + if err != nil { + return fmt.Errorf(lm.FailedParseTime, err) + } + b.Date = t + return nil + +} + type BackupList struct { files []os.DirEntry - dates []time.Time + info []Backup count int } func (bl BackupList) Len() int { return len(bl.files) } func (bl BackupList) Swap(i, j int) { bl.files[i], bl.files[j] = bl.files[j], bl.files[i] - bl.dates[i], bl.dates[j] = bl.dates[j], bl.dates[i] + bl.info[i], bl.info[j] = bl.info[j], bl.info[i] } func (bl BackupList) Less(i, j int) bool { // Push non-backup files to the end of the array, // Since they didn't have a date parsed. - if bl.dates[i].IsZero() { + if bl.info[i].Date.IsZero() { return false } - if bl.dates[j].IsZero() { + if bl.info[j].Date.IsZero() { return true } // Sort by oldest first - return bl.dates[j].After(bl.dates[i]) + return bl.info[j].Date.After(bl.info[i].Date) } // Get human-readable file size from f.Size() result. @@ -72,18 +164,19 @@ func (app *appContext) getBackups() *BackupList { } backups := &BackupList{} backups.files = items - backups.dates = make([]time.Time, len(items)) + backups.info = make([]Backup, len(items)) backups.count = 0 for i, item := range items { + // Even though Backup{} can parse and check validity, still check if the file ends in .bak, we don't need to print an error if a file isn't a .bak. if item.IsDir() || !(strings.HasSuffix(item.Name(), BACKUP_SUFFIX)) { continue } - t, err := time.Parse(BACKUP_DATEFMT, strings.TrimSuffix(strings.TrimPrefix(strings.TrimPrefix(item.Name(), BACKUP_UPLOAD_PREFIX), BACKUP_PREFIX), BACKUP_SUFFIX)) - if err != nil { - app.debug.Printf(lm.FailedParseTime, err) + b := Backup{} + if err := b.FromString(item.Name()); err != nil { + app.debug.Printf(lm.FailedParseBackup, item.Name(), err) continue } - backups.dates[i] = t + backups.info[i] = b backups.count++ } return backups @@ -91,17 +184,47 @@ func (app *appContext) getBackups() *BackupList { func (app *appContext) makeBackup() (fileDetails CreateBackupDTO) { toKeep := app.config.Section("backups").Key("keep_n_backups").MustInt(20) - fname := BACKUP_PREFIX + time.Now().Local().Format(BACKUP_DATEFMT) + BACKUP_SUFFIX + keepPreviousVersions := app.config.Section("backups").Key("keep_previous_version_backup").MustBool(true) + + b := Backup{Commit: commit} + fname := b.String() path := app.config.Section("backups").Key("path").String() backups := app.getBackups() if backups == nil { return } toDelete := backups.count + 1 - toKeep + if toDelete > 0 || keepPreviousVersions { + sort.Sort(backups) + } + backupsByCommit := map[string]int{} + if keepPreviousVersions { + // Count backups by commit + for _, b := range backups.info { + if b.IsZero() { + continue + } + // If b.Commit is empty, the backup is pre-versions-in-backup-names. + // Still use the empty string as a key, considering these as a single version. + count, ok := backupsByCommit[b.Commit] + if !ok { + count = 0 + } + count += 1 + backupsByCommit[b.Commit] = count + } + fmt.Printf("remaining:%+v\n", backupsByCommit) + } // fmt.Printf("toDelete: %d, backCount: %d, keep: %d, length: %d\n", toDelete, backups.count, toKeep, len(backups.files)) if toDelete > 0 && toDelete <= backups.count { - sort.Sort(backups) - for _, item := range backups.files[:toDelete] { + for i := range toDelete { + backupsRemaining, ok := backupsByCommit[backups.info[i].Commit] + app.debug.Println("item", backups.files[i], "remaining", backupsRemaining) + if keepPreviousVersions && ok && backupsRemaining <= 1 { + continue + } + + item := backups.files[i] fullpath := filepath.Join(path, item.Name()) err := os.Remove(fullpath) if err != nil { @@ -109,6 +232,10 @@ func (app *appContext) makeBackup() (fileDetails CreateBackupDTO) { return } app.debug.Printf(lm.DeleteOldBackup, fullpath) + if keepPreviousVersions && ok { + backupsRemaining -= 1 + backupsByCommit[backups.info[i].Commit] = backupsRemaining + } } } fullpath := filepath.Join(path, fname) @@ -140,7 +267,7 @@ func (app *appContext) loadPendingBackup() { if LOADBAK == "" { return } - oldPath := filepath.Join(app.dataPath, "db-"+string(time.Now().Unix())+"-pre-"+filepath.Base(LOADBAK)) + oldPath := filepath.Join(app.dataPath, "db-"+strconv.FormatInt(time.Now().Unix(), 10)+"-pre-"+filepath.Base(LOADBAK)) err := os.Rename(app.storage.db_path, oldPath) if err != nil { app.err.Fatalf(lm.FailedMoveOldDB, oldPath, err) diff --git a/backups_test.go b/backups_test.go new file mode 100644 index 0000000..abdcda8 --- /dev/null +++ b/backups_test.go @@ -0,0 +1,57 @@ +package main + +import ( + "testing" + "time" +) + +func testBackupParse(f string, a Backup, t *testing.T) { + b := Backup{} + err := b.FromString(f) + if err != nil { + t.Fatalf("error: %+v", err) + } + if !b.Equals(a) { + t.Fatalf("not equal: %+v != %+v", b, a) + } +} + +func TestBackupParserOld(t *testing.T) { + Q1 := BACKUP_PREFIX + "2023-12-21T21-08-00" + BACKUP_SUFFIX + A1 := Backup{} + A1.Date, _ = time.Parse(BACKUP_DATEFMT, "2023-12-21T21-08-00") + testBackupParse(Q1, A1, t) +} +func TestBackupParserOldUpload(t *testing.T) { + Q2 := BACKUP_UPLOAD_PREFIX + BACKUP_PREFIX + "2023-12-21T21-08-00" + BACKUP_SUFFIX + A2 := Backup{ + Upload: true, + } + A2.Date, _ = time.Parse(BACKUP_DATEFMT, "2023-12-21T21-08-00") + testBackupParse(Q2, A2, t) +} +func TestBackupParserUploadDate(t *testing.T) { + Q3 := BACKUP_UPLOAD_PREFIX + BACKUP_PREFIX + BACKUP_DATE_PREFIX + "2023-12-21T21-08-00" + BACKUP_SUFFIX + A3 := Backup{ + Upload: true, + } + A3.Date, _ = time.Parse(BACKUP_DATEFMT, "2023-12-21T21-08-00") + testBackupParse(Q3, A3, t) +} +func TestBackupParserUploadCommitDate(t *testing.T) { + Q4 := BACKUP_UPLOAD_PREFIX + BACKUP_PREFIX + BACKUP_COMMIT_PREFIX + "testcommit" + BACKUP_DATE_PREFIX + "2023-12-21T21-08-00" + BACKUP_SUFFIX + A4 := Backup{ + Commit: "testcommit", + Upload: true, + } + A4.Date, _ = time.Parse(BACKUP_DATEFMT, "2023-12-21T21-08-00") + testBackupParse(Q4, A4, t) +} +func TestBackupParserDateCommit(t *testing.T) { + Q5 := BACKUP_PREFIX + BACKUP_DATE_PREFIX + "2023-12-21T21-08-00" + BACKUP_COMMIT_PREFIX + "testcommit" + BACKUP_SUFFIX + A5 := Backup{ + Commit: "testcommit", + } + A5.Date, _ = time.Parse(BACKUP_DATEFMT, "2023-12-21T21-08-00") + testBackupParse(Q5, A5, t) +} diff --git a/config.go b/config.go index c58f425..c149084 100644 --- a/config.go +++ b/config.go @@ -141,6 +141,7 @@ func (app *appContext) loadConfig() error { app.MustSetValue("backups", "every_n_minutes", "1440") app.MustSetValue("backups", "path", filepath.Join(app.dataPath, "backups")) app.MustSetValue("backups", "keep_n_backups", "20") + app.MustSetValue("backups", "keep_previous_version_backup", "true") app.config.Section("jellyfin").Key("version").SetValue(version) app.config.Section("jellyfin").Key("device").SetValue("jfa-go") diff --git a/config/config-base.yaml b/config/config-base.yaml index ff427d7..49e4392 100644 --- a/config/config-base.yaml +++ b/config/config-base.yaml @@ -1315,6 +1315,12 @@ sections: value: 20 description: Number of most recent backups to keep. Once this is hit, the oldest backup will be deleted before doing a new one. + - setting: keep_previous_version_backup + name: Keep 1 backup from each previous version + requires_restart: true + type: bool + value: true + description: Always keep the most recent backup for each jfa-go version, incase updates mess things up. If enabled, these aren't counted by the "Number of backups to keep" setting. - section: welcome_email meta: name: Welcome Message diff --git a/discord.go b/discord.go index df102da..96fa4bc 100644 --- a/discord.go +++ b/discord.go @@ -196,7 +196,7 @@ func (d *DiscordDaemon) NewTempInvite(ageSeconds, maxUses int) (inviteURL, iconU var inv *dg.Invite var err error if d.InviteChannel.Name == "" { - d.app.err.Println(lm.FailedCreateDiscordInviteChannel, lm.InviteChannelEmpty) + d.app.err.Printf(lm.FailedCreateDiscordInviteChannel, lm.InviteChannelEmpty) return } if d.InviteChannel.ID == "" { diff --git a/email.go b/email.go index 6fb0fac..82da3f2 100644 --- a/email.go +++ b/email.go @@ -576,7 +576,7 @@ func (emailer *Emailer) resetValues(pwr PasswordReset, app *appContext, noSub bo // Only used in html email. template["pin_code"] = pwr.Pin } else { - app.info.Println(lm.FailedGeneratePWRLink, err) + app.info.Printf(lm.FailedGeneratePWRLink, err) template["pin"] = pwr.Pin } } else { diff --git a/go.mod b/go.mod index 08cbe88..6cd8c7a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/hrfee/jfa-go -go 1.22.0 +go 1.23.0 + +toolchain go1.24.0 replace github.com/hrfee/jfa-go/docs => ./docs @@ -131,12 +133,12 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/arch v0.11.0 // indirect - golang.org/x/crypto v0.28.0 // indirect + golang.org/x/crypto v0.35.0 // indirect golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect golang.org/x/image v0.21.0 // indirect - golang.org/x/net v0.30.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.19.0 // indirect + golang.org/x/net v0.36.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect golang.org/x/tools v0.26.0 // indirect google.golang.org/protobuf v1.35.1 // indirect ) diff --git a/go.sum b/go.sum index 2dfb31f..c263bc6 100644 --- a/go.sum +++ b/go.sum @@ -401,8 +401,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= @@ -441,8 +441,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= +golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -454,8 +454,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -483,8 +483,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -496,8 +496,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= diff --git a/html/admin.html b/html/admin.html index ef75925..bb10c4c 100644 --- a/html/admin.html +++ b/html/admin.html @@ -372,6 +372,7 @@ {{ .strings.name }} {{ .strings.date }} + {{ .strings.version }} {{ .strings.backupDownloadRestore }} diff --git a/logmessages/logmessages.go b/logmessages/logmessages.go index f2b9ea2..fbde1e1 100644 --- a/logmessages/logmessages.go +++ b/logmessages/logmessages.go @@ -200,6 +200,7 @@ const ( DeleteOldBackup = "Deleted old backup \"%s\"" FailedDeleteOldBackup = "Failed to delete old backup \"%s\": %v" CreateBackup = "Created database backup \"%+v\"" + FailedParseBackup = "Failed to parse backup \"%s\": %v" FailedCreateBackup = "Faled to create database backup: %v" MoveOldDB = "Moved existing database to \"%s\"" FailedMoveOldDB = "Failed to move existing database to \"%s\": %v" diff --git a/main.go b/main.go index 04d408f..d2f881c 100644 --- a/main.go +++ b/main.go @@ -171,7 +171,7 @@ func test(app *appContext) { var username string fmt.Scanln(&username) user, err := app.jf.UserByName(username, false) - fmt.Printf("UserByName (%s): code %d err %s", username, err) + fmt.Printf("UserByName (%s): err %v", username, err) out, _ := json.MarshalIndent(user, "", " ") fmt.Print(string(out)) } diff --git a/models.go b/models.go index fd24e2a..97e669b 100644 --- a/models.go +++ b/models.go @@ -39,13 +39,13 @@ type newUserResponse struct { } type deleteUserDTO struct { - Users []string `json:"users" binding:"required"` // List of usernames to delete + Users []string `json:"users" binding:"required"` // List of user IDs. Notify bool `json:"notify"` // Whether to notify users of deletion Reason string `json:"reason"` // Account deletion reason (for notification) } type enableDisableUserDTO struct { - Users []string `json:"users" binding:"required"` // List of usernames to delete + Users []string `json:"users" binding:"required"` // List of userIDs. Enabled bool `json:"enabled"` // True = enable users, False = disable. Notify bool `json:"notify"` // Whether to notify users of deletion Reason string `json:"reason"` // Account deletion reason (for notification) @@ -446,10 +446,11 @@ type GetActivityCountDTO struct { } type CreateBackupDTO struct { - Size string `json:"size"` - Name string `json:"name"` - Path string `json:"path"` - Date int64 `json:"date"` + Size string `json:"size"` + Name string `json:"name"` + Path string `json:"path"` + Date int64 `json:"date"` + Commit string `json:"commit"` } type GetBackupsDTO struct { diff --git a/telegram.go b/telegram.go index b68e91c..c7cf5fb 100644 --- a/telegram.go +++ b/telegram.go @@ -121,7 +121,7 @@ func (t *TelegramDaemon) NewAssignedAuthToken(id string) string { } func (t *TelegramDaemon) run() { - t.app.info.Println(lm.StartDaemon, lm.Telegram) + t.app.info.Printf(lm.StartDaemon, lm.Telegram) u := tg.NewUpdate(0) u.Timeout = 60 updates, err := t.bot.GetUpdatesChan(u) diff --git a/ts/modules/settings.ts b/ts/modules/settings.ts index 01aa5df..c9d1459 100644 --- a/ts/modules/settings.ts +++ b/ts/modules/settings.ts @@ -13,6 +13,7 @@ interface BackupDTO { name: string; path: string; date: number; + commit: string; } interface settingsChangedEvent extends Event { @@ -731,6 +732,7 @@ export class settingsList { tr.innerHTML = ` ${b.name} ${toDateString(new Date(b.date*1000))} + ${b.commit || "?"}