v0.4.5: Add dedicated Backup page (Biztonsági mentés)
New /backups page with full backup system visibility: - Status overview cards (local/remote backup, DB count, repo size) - Schedule section with next-run times and retention policy - Database table with type, size, validation (table count), status - Snapshot history table with per-snapshot stats - Repository info card with paths, integrity status, remote placeholder - "Mentés most" button with auto-refresh polling - Empty state when backup not configured Backend: SnapshotRecord history (ring buffer), DumpValidation, ListDumpFiles, ListSnapshots, GetFullStatus, restic check tracking. Server accepts scheduler for next-run time calculation. Sidebar nav updated with 3rd item, dashboard backup card title clickable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,11 +31,31 @@ type DiscoveredDB struct {
|
||||
|
||||
// DumpResult holds the outcome of a single database dump.
|
||||
type DumpResult struct {
|
||||
DB DiscoveredDB
|
||||
FilePath string
|
||||
Size int64
|
||||
Duration time.Duration
|
||||
Error error
|
||||
DB DiscoveredDB
|
||||
FilePath string
|
||||
Size int64
|
||||
Duration time.Duration
|
||||
Error error
|
||||
Validation DumpValidation
|
||||
}
|
||||
|
||||
// DumpValidation holds the result of a dump file structural check.
|
||||
type DumpValidation struct {
|
||||
Valid bool
|
||||
TableCount int
|
||||
Error string
|
||||
FileSize int64
|
||||
ModTime time.Time
|
||||
}
|
||||
|
||||
// DumpFileInfo holds info about a dump file on disk.
|
||||
type DumpFileInfo struct {
|
||||
FileName string
|
||||
StackName string
|
||||
DBType DBType
|
||||
Size int64
|
||||
ModTime time.Time
|
||||
Validation DumpValidation
|
||||
}
|
||||
|
||||
// DiscoverDatabases finds running database containers via docker ps.
|
||||
@@ -200,12 +220,125 @@ func DumpOne(ctx context.Context, db DiscoveredDB, dumpDir string, logger *log.L
|
||||
result.Size = stat.Size()
|
||||
result.Duration = time.Since(start)
|
||||
|
||||
logger.Printf("[INFO] DB dump: %s → %s (%s, %s)", db.ContainerName, filename,
|
||||
formatBytes(stat.Size()), result.Duration.Round(time.Millisecond))
|
||||
// Run validation on the dump file
|
||||
result.Validation = ValidateDump(finalPath, db.DBType)
|
||||
|
||||
logger.Printf("[INFO] DB dump: %s → %s (%s, %s, %d tables)", db.ContainerName, filename,
|
||||
formatBytes(stat.Size()), result.Duration.Round(time.Millisecond), result.Validation.TableCount)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ValidateDump checks a SQL dump file for basic structural integrity.
|
||||
func ValidateDump(filePath string, dbType DBType) DumpValidation {
|
||||
stat, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return DumpValidation{Error: fmt.Sprintf("stat failed: %v", err)}
|
||||
}
|
||||
|
||||
v := DumpValidation{
|
||||
FileSize: stat.Size(),
|
||||
ModTime: stat.ModTime(),
|
||||
}
|
||||
|
||||
if stat.Size() < 100 {
|
||||
v.Error = "dump file too small (< 100 bytes)"
|
||||
return v
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
v.Error = fmt.Sprintf("read failed: %v", err)
|
||||
return v
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
|
||||
// Count CREATE TABLE statements
|
||||
tableCount := 0
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
upper := strings.ToUpper(strings.TrimSpace(line))
|
||||
if strings.HasPrefix(upper, "CREATE TABLE") {
|
||||
tableCount++
|
||||
}
|
||||
}
|
||||
v.TableCount = tableCount
|
||||
|
||||
// Basic header check
|
||||
switch dbType {
|
||||
case DBTypePostgres:
|
||||
if !strings.HasPrefix(content, "--") {
|
||||
v.Error = "PostgreSQL dump missing comment header"
|
||||
return v
|
||||
}
|
||||
case DBTypeMariaDB:
|
||||
if !strings.HasPrefix(content, "-- ") {
|
||||
v.Error = "MariaDB dump missing comment header"
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
if tableCount == 0 {
|
||||
v.Error = "no CREATE TABLE statements found"
|
||||
return v
|
||||
}
|
||||
|
||||
v.Valid = true
|
||||
return v
|
||||
}
|
||||
|
||||
// ListDumpFiles returns info about SQL dump files on disk.
|
||||
func ListDumpFiles(dumpDir string) ([]DumpFileInfo, error) {
|
||||
entries, err := os.ReadDir(dumpDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("reading dump dir: %w", err)
|
||||
}
|
||||
|
||||
var files []DumpFileInfo
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".sql") {
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(e.Name(), ".tmp") {
|
||||
continue
|
||||
}
|
||||
|
||||
info, err := e.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
f := DumpFileInfo{
|
||||
FileName: e.Name(),
|
||||
Size: info.Size(),
|
||||
ModTime: info.ModTime(),
|
||||
}
|
||||
|
||||
// Parse stack name and DB type from filename: "paperless-ngx-postgres.sql"
|
||||
base := strings.TrimSuffix(e.Name(), ".sql")
|
||||
if strings.HasSuffix(base, "-postgres") {
|
||||
f.StackName = strings.TrimSuffix(base, "-postgres")
|
||||
f.DBType = DBTypePostgres
|
||||
} else if strings.HasSuffix(base, "-mariadb") {
|
||||
f.StackName = strings.TrimSuffix(base, "-mariadb")
|
||||
f.DBType = DBTypeMariaDB
|
||||
} else {
|
||||
f.StackName = base
|
||||
}
|
||||
|
||||
// Run validation on the file
|
||||
fullPath := filepath.Join(dumpDir, e.Name())
|
||||
f.Validation = ValidateDump(fullPath, f.DBType)
|
||||
|
||||
files = append(files, f)
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func populateDBEnv(ctx context.Context, db *DiscoveredDB) error {
|
||||
cmd := exec.CommandContext(ctx, "docker", "inspect", db.ContainerID,
|
||||
"--format", "{{range .Config.Env}}{{println .}}{{end}}")
|
||||
|
||||
Reference in New Issue
Block a user