v0.12.3 — Security & correctness bug fixes (33 bugs)
CRITICAL: 10 data race and security fixes — backup.go mutex coverage (C1-C4), IsSystemDisk 12-bit major/minor (C5), /dev/ path validation (C6), extractName traversal (C7), TargetPath/DestinationPath against registered paths (C8-C9), ParseComposeHDDMounts Clean-before-prefix (C10). HIGH: 17 logic/resource fixes — ValidateDump bufio.Scanner (H1), single appDirSize() with 30s timeout (H2/H3), snapshot ID regex (H4), cross-drive restic prune (H5), temp file order (H6), dirSizeBytes errors (H7), atomic fstab (H8), IsDeviceMounted suffix check (H9), eMMC partition mapping (H10), bytesCopied mutex (H11), separator-aware migrate prefix (H13), DeleteStack error on compose-down (H14), docker 60s timeout (H16), NotificationPrefs deep-copy (H17), wipefs warning (H18), fstab rollback on mount fail (H19). MEDIUM: 7 code quality fixes — formatBytes dedup (M1), .tmp filter order (M2), sizeBytes string type (M3), elapsed in message (M6), LoadLocation fallback (M7), pathCovers separator (M10), cancelEditLabel textContent (M11). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -224,7 +225,7 @@ func DumpOne(ctx context.Context, db DiscoveredDB, dumpDir string, logger *log.L
|
||||
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)
|
||||
humanizeBytes(stat.Size()), result.Duration.Round(time.Millisecond), result.Validation.TableCount)
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -248,18 +249,45 @@ func ValidateDump(filePath string, dbType DBType) DumpValidation {
|
||||
return v
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filePath)
|
||||
// H1: Use bufio.Scanner to read line-by-line instead of loading entire file into memory.
|
||||
// Large dumps (500MB+) would cause massive allocations on every 5-min cache refresh.
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
v.Error = fmt.Sprintf("read failed: %v", err)
|
||||
log.Printf("[WARN] ValidateDump FAIL: %s — %s", filePath, v.Error)
|
||||
return v
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
content := string(data)
|
||||
scanner := bufio.NewScanner(f)
|
||||
// Increase token buffer for very long lines (some SQL lines can be large)
|
||||
scanner.Buffer(make([]byte, 256*1024), 256*1024)
|
||||
|
||||
// Count CREATE TABLE statements
|
||||
lineNum := 0
|
||||
headerFound := false
|
||||
tableCount := 0
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
lineNum++
|
||||
|
||||
// Header check — scan first 10 lines for expected dump header
|
||||
// MariaDB 11.4+ prepends a sandbox comment before the header line
|
||||
if lineNum <= 10 && !headerFound {
|
||||
switch dbType {
|
||||
case DBTypeMariaDB:
|
||||
if strings.HasPrefix(line, "-- MariaDB dump") ||
|
||||
strings.HasPrefix(line, "-- MySQL dump") ||
|
||||
strings.HasPrefix(line, "-- mysqldump") {
|
||||
headerFound = true
|
||||
}
|
||||
case DBTypePostgres:
|
||||
if strings.HasPrefix(line, "-- PostgreSQL database dump") {
|
||||
headerFound = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Count CREATE TABLE statements
|
||||
upper := strings.ToUpper(strings.TrimSpace(line))
|
||||
if strings.HasPrefix(upper, "CREATE TABLE") {
|
||||
tableCount++
|
||||
@@ -267,30 +295,6 @@ func ValidateDump(filePath string, dbType DBType) DumpValidation {
|
||||
}
|
||||
v.TableCount = tableCount
|
||||
|
||||
// Header check — scan first 10 lines for expected dump header
|
||||
// MariaDB 11.4+ prepends a sandbox comment before the header line
|
||||
headerFound := false
|
||||
lines := strings.SplitN(content, "\n", 11) // at most 11 parts = 10 lines
|
||||
for i, line := range lines {
|
||||
if i >= 10 {
|
||||
break
|
||||
}
|
||||
switch dbType {
|
||||
case DBTypeMariaDB:
|
||||
if strings.HasPrefix(line, "-- MariaDB dump") ||
|
||||
strings.HasPrefix(line, "-- MySQL dump") ||
|
||||
strings.HasPrefix(line, "-- mysqldump") {
|
||||
headerFound = true
|
||||
}
|
||||
case DBTypePostgres:
|
||||
if strings.HasPrefix(line, "-- PostgreSQL database dump") {
|
||||
headerFound = true
|
||||
}
|
||||
}
|
||||
if headerFound {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !headerFound {
|
||||
switch dbType {
|
||||
case DBTypeMariaDB:
|
||||
@@ -304,7 +308,7 @@ func ValidateDump(filePath string, dbType DBType) DumpValidation {
|
||||
|
||||
if tableCount == 0 {
|
||||
v.Error = "no CREATE TABLE statements found"
|
||||
log.Printf("[WARN] ValidateDump FAIL: %s — %s (header was found, scanned %d lines)", filePath, v.Error, len(strings.Split(content, "\n")))
|
||||
log.Printf("[WARN] ValidateDump FAIL: %s — %s (header was found, scanned %d lines)", filePath, v.Error, lineNum)
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -325,10 +329,11 @@ func ListDumpFiles(dumpDir string) ([]DumpFileInfo, error) {
|
||||
|
||||
var files []DumpFileInfo
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".sql") {
|
||||
// M2: Check .tmp before .sql to correctly skip ".sql.tmp" temp files (was dead code before).
|
||||
if e.IsDir() || strings.HasSuffix(e.Name(), ".tmp") {
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(e.Name(), ".tmp") {
|
||||
if !strings.HasSuffix(e.Name(), ".sql") {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -464,20 +469,4 @@ func cleanupTmpFiles(dumpDir string, logger *log.Logger) {
|
||||
}
|
||||
}
|
||||
|
||||
func formatBytes(b int64) string {
|
||||
const (
|
||||
kb = 1024
|
||||
mb = 1024 * kb
|
||||
gb = 1024 * mb
|
||||
)
|
||||
switch {
|
||||
case b >= gb:
|
||||
return fmt.Sprintf("%.1f GB", float64(b)/float64(gb))
|
||||
case b >= mb:
|
||||
return fmt.Sprintf("%.1f MB", float64(b)/float64(mb))
|
||||
case b >= kb:
|
||||
return fmt.Sprintf("%.1f KB", float64(b)/float64(kb))
|
||||
default:
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
}
|
||||
// M1: formatBytes removed — use humanizeBytes() from appdata.go (same package, no duplication).
|
||||
|
||||
Reference in New Issue
Block a user