Files
tidb/br/tests/utils.go
2025-02-12 23:08:04 +00:00

235 lines
5.8 KiB
Go

// Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0.
package main
import (
"encoding/binary"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/klauspost/compress/zstd"
"github.com/spf13/cobra"
)
const (
cmdValidateBackupFiles = "validateBackupFiles"
extSST = ".sst"
extLOG = ".log"
)
func main() {
rootCmd := &cobra.Command{
Use: "utils",
Short: "Utility commands for backup and restore",
}
validateCmd := &cobra.Command{
Use: cmdValidateBackupFiles,
Short: "Validate backup files",
Run: runValidateBackupFiles,
}
validateCmd.Flags().String("command", "", "Backup or restore command")
validateCmd.Flags().String("encryption", "", "Encryption argument")
rootCmd.AddCommand(validateCmd)
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func runValidateBackupFiles(cmd *cobra.Command, args []string) {
command, _ := cmd.Flags().GetString("command")
encryptionArg, _ := cmd.Flags().GetString("encryption")
if command == "" {
fmt.Println("Please provide the full backup or restore command using --command flag")
err := cmd.Usage()
if err != nil {
fmt.Println("Usage error")
return
}
os.Exit(1)
}
storagePath, found := parseCommand(command)
// doesn't need to validate if it's not doing backup/restore
if !found {
fmt.Println("No need to validate")
return
}
fmt.Printf("Validating files in: %s\n", storagePath)
if !checkCompressionAndEncryption(storagePath, encryptionArg) {
fmt.Println("validation failed")
os.Exit(1)
}
}
// parseCommand parses the command and only returns the storage path if it's a full backup or restore point
// as full backup will have backup files ready in the storage path after returning from the command
// and log backup will not, so we can only use restore point to validate.
func parseCommand(cmd string) (string, bool) {
// not using cobra since it has to define all the possible flags otherwise will report parsing error
args := strings.Fields(cmd)
// check for backup or restore point command
hasBackupOrRestorePoint := false
storagePath := ""
for i := 0; i < len(args); i++ {
arg := args[i]
if arg == "backup" {
hasBackupOrRestorePoint = true
continue
}
if i < len(args)-1 && arg == "restore" && args[i+1] == "point" {
hasBackupOrRestorePoint = true
continue
}
// check for storage path in various formats
if arg == "-s" || arg == "--storage" {
if i+1 < len(args) {
storagePath = args[i+1]
i++ // skip the next arg since we consumed it
}
} else if strings.HasPrefix(arg, "--storage=") {
storagePath = strings.TrimPrefix(arg, "--storage=")
} else if strings.HasPrefix(arg, "-s=") {
storagePath = strings.TrimPrefix(arg, "-s=")
}
}
if strings.HasPrefix(storagePath, "local://") {
storagePath = strings.TrimPrefix(storagePath, "local://")
if hasBackupOrRestorePoint && storagePath != "" {
return storagePath, true
}
}
return "", false
}
func checkCompressionAndEncryption(dir string, encryptionArg string) bool {
allEncrypted := true
allUnencrypted := true
totalFiles := 0
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if strings.HasSuffix(path, extSST) {
totalFiles++
isValidSST, err := isLikelySSTFile(path)
if err != nil {
fmt.Printf("Error checking SST file %s: %v\n", path, err)
return err
}
if isValidSST {
allEncrypted = false
} else {
allUnencrypted = false
}
} else if strings.HasSuffix(path, extLOG) {
totalFiles++
isCompressed, err := isZstdCompressed(path)
if err != nil {
fmt.Printf("Error checking if file is encrypted %s: %v\n", path, err)
return err
}
if isCompressed {
allEncrypted = false
} else {
allUnencrypted = false
}
}
return nil
})
if err != nil {
fmt.Printf("Error walking through directory: %v\n", err)
os.Exit(1)
}
// handle with encryption case
if encryptionArg != "" {
if allEncrypted {
fmt.Printf("All files in %s are encrypted, as expected with encryption\n", dir)
return true
}
fmt.Printf("Error: Some files in %s are not encrypted, which is unexpected with encryption\n", dir)
return false
}
// handle without encryption case
if allUnencrypted {
fmt.Printf("All files in %s are not encrypted, as expected without encryption\n", dir)
return true
} else if allEncrypted {
fmt.Printf("Error: All files in %s are encrypted, which is unexpected without encryption\n", dir)
return false
}
fmt.Printf("Error: Mixed encryption in %s. Some files are encrypted, some are not.\n", dir)
return false
}
func isZstdCompressed(filePath string) (bool, error) {
file, err := os.OpenFile(filePath, os.O_RDONLY, 0) //nolint:gosec
if err != nil {
return false, err
}
defer file.Close()
decoder, err := zstd.NewReader(file)
if err != nil {
return false, nil // Not compressed or error in compression
}
defer decoder.Close()
// Try to read a small amount of data
_, err = decoder.Read(make([]byte, 1))
if err != nil {
return false, nil // Not compressed or error in decompression
}
return true, nil
}
func isLikelySSTFile(filePath string) (bool, error) {
file, err := os.OpenFile(filePath, os.O_RDONLY, 0) //nolint:gosec
if err != nil {
return false, err
}
defer file.Close()
// Seek to 8 bytes from the end of the file
_, err = file.Seek(-8, io.SeekEnd)
if err != nil {
return false, err
}
// Read the last 8 bytes
footer := make([]byte, 8)
_, err = file.Read(footer)
if err != nil {
return false, err
}
// Check for SST magic number (kLegacyBlockBasedTableMagicNumber)
// or (kBlockBasedTableMagicNumber)
magicNumber := binary.LittleEndian.Uint64(footer)
return magicNumber == 0xdb4775248b80fb57 || magicNumber == 0x88e241b785f4cff7, nil
}