Selaa lähdekoodia

feat(backup): prefix backup filenames with date and time (#5606)

* feat(backup): add YYYY-MM-DD_ date prefix to backup filenames

Refs #5584

* feat(backup): prefix backup filenames with date and time

* fix(backup): put host before date in backup filename

Backup filenames now read {host}_{date}{ext} (e.g. panel.example.com_2026-06-27_000000.db) instead of {date}_{host}{ext}, so files group by server first then sort chronologically within each server.
Nikan Zeyaei 15 tuntia sitten
vanhempi
sitoutus
1bad2fcba1
2 muutettua tiedostoa jossa 47 lisäystä ja 7 poistoa
  1. 30 0
      internal/web/service/backup_filename_test.go
  2. 17 7
      internal/web/service/server.go

+ 30 - 0
internal/web/service/backup_filename_test.go

@@ -3,6 +3,7 @@ package service
 import (
 	"regexp"
 	"testing"
+	"time"
 )
 
 // getDb (controller) only accepts a Content-Disposition filename matching this
@@ -36,3 +37,32 @@ func TestSanitizeBackupHost(t *testing.T) {
 		})
 	}
 }
+
+// dateSuffixRegex narrows backupFilenameRegex to the exact _YYYY-MM-DD_HHMMSS shape.
+var dateSuffixRegex = regexp.MustCompile(`^_\d{4}-\d{2}-\d{2}_\d{6}$`)
+
+func TestBackupDateSuffix(t *testing.T) {
+	cases := []struct {
+		name string
+		now  time.Time
+		want string
+	}{
+		{"utc midnight", time.Date(2026, 6, 27, 0, 0, 0, 0, time.UTC), "_2026-06-27_000000"},
+		{"end of year", time.Date(2025, 12, 31, 23, 59, 59, 0, time.UTC), "_2025-12-31_235959"},
+		{"single digit month/day padded", time.Date(2026, 1, 5, 9, 4, 0, 0, time.UTC), "_2026-01-05_090400"},
+	}
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := backupDateSuffix(tc.now)
+			if got != tc.want {
+				t.Errorf("backupDateSuffix(%v) = %q, want %q", tc.now, got, tc.want)
+			}
+			if !dateSuffixRegex.MatchString(got) {
+				t.Errorf("backupDateSuffix(%v) = %q, not a valid date suffix", tc.now, got)
+			}
+			if !backupFilenameRegex.MatchString(got) {
+				t.Errorf("backupDateSuffix(%v) = %q, not a valid download filename char", tc.now, got)
+			}
+		})
+	}
+}

+ 17 - 7
internal/web/service/server.go

@@ -1298,18 +1298,28 @@ func (s *ServerService) GetDb() ([]byte, error) {
 
 // BackupFilename returns the filename for a database backup, named after the
 // panel's address so a downloaded or Telegram-sent backup identifies the server
-// it came from. requestHost is the browser's address: the getDb handler passes
-// c.Request.Host so a panel download is named after whatever address the user
-// reached the panel with, no Listen Domain needed. The Telegram bot has no
-// request and passes "", falling back to the configured Listen Domain (webDomain)
-// and then the public IP. The extension is .dump on PostgreSQL and .db on SQLite;
-// the base falls back to "x-ui" when no address is known.
+// it came from, followed by the current date and time (_YYYY-MM-DD_HHMMSS) so
+// files accumulated in Telegram chat history group by server then sort
+// chronologically and same-day backups stay distinct. requestHost is the
+// browser's address: the getDb handler passes c.Request.Host so a panel download
+// is named after whatever address the user reached the panel with, no Listen
+// Domain needed. The Telegram bot has no request and passes "", falling back to
+// the configured Listen Domain (webDomain) and then the public IP. The extension
+// is .dump on PostgreSQL and .db on SQLite; the base falls back to "x-ui" when
+// no address is known.
 func (s *ServerService) BackupFilename(requestHost string) string {
 	ext := ".db"
 	if database.IsPostgres() {
 		ext = ".dump"
 	}
-	return s.backupHost(requestHost) + ext
+	return s.backupHost(requestHost) + backupDateSuffix(time.Now()) + ext
+}
+
+// backupDateSuffix returns the _YYYY-MM-DD_HHMMSS chronological suffix appended
+// after the host in backup filenames. Uses server-local time for consistency
+// with the timestamp printed in the Telegram backup message body.
+func backupDateSuffix(now time.Time) string {
+	return "_" + now.Format("2006-01-02_150405")
 }
 
 // backupHost picks the address used to name backup files: the browser's request