mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-11 10:25:52 +02:00
feat: backup and restore
This commit is contained in:
parent
60f35ef863
commit
4cb4695e7b
52 changed files with 9270 additions and 1439 deletions
169
internal/backup/backup.go
Normal file
169
internal/backup/backup.go
Normal file
|
@ -0,0 +1,169 @@
|
|||
package backup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/0xJacky/Nginx-UI/internal/version"
|
||||
"github.com/uozi-tech/cosy"
|
||||
"github.com/uozi-tech/cosy/logger"
|
||||
)
|
||||
|
||||
// Directory and file names
|
||||
const (
|
||||
BackupDirPrefix = "nginx-ui-backup-"
|
||||
NginxUIDir = "nginx-ui"
|
||||
NginxDir = "nginx"
|
||||
HashInfoFile = "hash_info.txt"
|
||||
NginxUIZipName = "nginx-ui.zip"
|
||||
NginxZipName = "nginx.zip"
|
||||
)
|
||||
|
||||
// BackupResult contains the results of a backup operation
|
||||
type BackupResult struct {
|
||||
BackupContent []byte `json:"-"` // Backup content as byte array
|
||||
BackupName string `json:"name"` // Backup file name
|
||||
AESKey string `json:"aes_key"` // Base64 encoded AES key
|
||||
AESIv string `json:"aes_iv"` // Base64 encoded AES IV
|
||||
}
|
||||
|
||||
// HashInfo contains hash information for verification
|
||||
type HashInfo struct {
|
||||
NginxUIHash string `json:"nginx_ui_hash"`
|
||||
NginxHash string `json:"nginx_hash"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// Backup creates a backup of nginx-ui configuration and database files,
|
||||
// and nginx configuration directory, compressed into an encrypted archive
|
||||
func Backup() (BackupResult, error) {
|
||||
// Generate timestamps for filenames
|
||||
timestamp := time.Now().Format("20060102-150405")
|
||||
backupName := fmt.Sprintf("backup-%s.zip", timestamp)
|
||||
|
||||
// Generate AES key and IV
|
||||
key, err := GenerateAESKey()
|
||||
if err != nil {
|
||||
return BackupResult{}, cosy.WrapErrorWithParams(ErrGenerateAESKey, err.Error())
|
||||
}
|
||||
|
||||
iv, err := GenerateIV()
|
||||
if err != nil {
|
||||
return BackupResult{}, cosy.WrapErrorWithParams(ErrGenerateIV, err.Error())
|
||||
}
|
||||
|
||||
// Create temporary directory for files to be archived
|
||||
tempDir, err := os.MkdirTemp("", "nginx-ui-backup-*")
|
||||
if err != nil {
|
||||
return BackupResult{}, cosy.WrapErrorWithParams(ErrCreateTempDir, err.Error())
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create directories in temp
|
||||
nginxUITempDir := filepath.Join(tempDir, NginxUIDir)
|
||||
nginxTempDir := filepath.Join(tempDir, NginxDir)
|
||||
if err := os.MkdirAll(nginxUITempDir, 0755); err != nil {
|
||||
return BackupResult{}, cosy.WrapErrorWithParams(ErrCreateTempSubDir, err.Error())
|
||||
}
|
||||
if err := os.MkdirAll(nginxTempDir, 0755); err != nil {
|
||||
return BackupResult{}, cosy.WrapErrorWithParams(ErrCreateTempSubDir, err.Error())
|
||||
}
|
||||
|
||||
// Backup nginx-ui config and database to a directory
|
||||
if err := backupNginxUIFiles(nginxUITempDir); err != nil {
|
||||
return BackupResult{}, cosy.WrapErrorWithParams(ErrBackupNginxUI, err.Error())
|
||||
}
|
||||
|
||||
// Backup nginx configs to a directory
|
||||
if err := backupNginxFiles(nginxTempDir); err != nil {
|
||||
return BackupResult{}, cosy.WrapErrorWithParams(ErrBackupNginx, err.Error())
|
||||
}
|
||||
|
||||
// Create individual zip files for nginx-ui and nginx directories
|
||||
nginxUIZipPath := filepath.Join(tempDir, NginxUIZipName)
|
||||
nginxZipPath := filepath.Join(tempDir, NginxZipName)
|
||||
|
||||
// Create zip archives for each directory
|
||||
if err := createZipArchive(nginxUIZipPath, nginxUITempDir); err != nil {
|
||||
return BackupResult{}, cosy.WrapErrorWithParams(ErrCreateZipArchive, err.Error())
|
||||
}
|
||||
|
||||
if err := createZipArchive(nginxZipPath, nginxTempDir); err != nil {
|
||||
return BackupResult{}, cosy.WrapErrorWithParams(ErrCreateZipArchive, err.Error())
|
||||
}
|
||||
|
||||
// Calculate hashes for the zip files
|
||||
nginxUIHash, err := calculateFileHash(nginxUIZipPath)
|
||||
if err != nil {
|
||||
return BackupResult{}, cosy.WrapErrorWithParams(ErrCalculateHash, err.Error())
|
||||
}
|
||||
|
||||
nginxHash, err := calculateFileHash(nginxZipPath)
|
||||
if err != nil {
|
||||
return BackupResult{}, cosy.WrapErrorWithParams(ErrCalculateHash, err.Error())
|
||||
}
|
||||
|
||||
// Get current version information
|
||||
versionInfo := version.GetVersionInfo()
|
||||
|
||||
// Create hash info file
|
||||
hashInfo := HashInfo{
|
||||
NginxUIHash: nginxUIHash,
|
||||
NginxHash: nginxHash,
|
||||
Timestamp: timestamp,
|
||||
Version: versionInfo.Version,
|
||||
}
|
||||
|
||||
// Write hash info to file
|
||||
hashInfoPath := filepath.Join(tempDir, HashInfoFile)
|
||||
if err := writeHashInfoFile(hashInfoPath, hashInfo); err != nil {
|
||||
return BackupResult{}, cosy.WrapErrorWithParams(ErrCreateHashFile, err.Error())
|
||||
}
|
||||
|
||||
// Encrypt the individual files
|
||||
if err := encryptFile(hashInfoPath, key, iv); err != nil {
|
||||
return BackupResult{}, cosy.WrapErrorWithParams(ErrEncryptFile, HashInfoFile)
|
||||
}
|
||||
|
||||
if err := encryptFile(nginxUIZipPath, key, iv); err != nil {
|
||||
return BackupResult{}, cosy.WrapErrorWithParams(ErrEncryptNginxUIDir, err.Error())
|
||||
}
|
||||
|
||||
if err := encryptFile(nginxZipPath, key, iv); err != nil {
|
||||
return BackupResult{}, cosy.WrapErrorWithParams(ErrEncryptNginxDir, err.Error())
|
||||
}
|
||||
|
||||
// Remove the original directories to avoid duplicating them in the final archive
|
||||
if err := os.RemoveAll(nginxUITempDir); err != nil {
|
||||
return BackupResult{}, cosy.WrapErrorWithParams(ErrCleanupTempDir, err.Error())
|
||||
}
|
||||
if err := os.RemoveAll(nginxTempDir); err != nil {
|
||||
return BackupResult{}, cosy.WrapErrorWithParams(ErrCleanupTempDir, err.Error())
|
||||
}
|
||||
|
||||
// Create final zip file to memory buffer
|
||||
var buffer bytes.Buffer
|
||||
if err := createZipArchiveToBuffer(&buffer, tempDir); err != nil {
|
||||
return BackupResult{}, cosy.WrapErrorWithParams(ErrCreateZipArchive, err.Error())
|
||||
}
|
||||
|
||||
// Convert AES key and IV to base64 encoded strings
|
||||
keyBase64 := base64.StdEncoding.EncodeToString(key)
|
||||
ivBase64 := base64.StdEncoding.EncodeToString(iv)
|
||||
|
||||
// Return result
|
||||
result := BackupResult{
|
||||
BackupContent: buffer.Bytes(),
|
||||
BackupName: backupName,
|
||||
AESKey: keyBase64,
|
||||
AESIv: ivBase64,
|
||||
}
|
||||
|
||||
logger.Infof("Backup created successfully: %s", backupName)
|
||||
return result, nil
|
||||
}
|
128
internal/backup/backup_crypto.go
Normal file
128
internal/backup/backup_crypto.go
Normal file
|
@ -0,0 +1,128 @@
|
|||
package backup
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/uozi-tech/cosy"
|
||||
)
|
||||
|
||||
// AESEncrypt encrypts data using AES-256-CBC
|
||||
func AESEncrypt(data []byte, key []byte, iv []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, cosy.WrapErrorWithParams(ErrEncryptData, err.Error())
|
||||
}
|
||||
|
||||
// Pad data to be a multiple of block size
|
||||
padding := aes.BlockSize - (len(data) % aes.BlockSize)
|
||||
padtext := make([]byte, len(data)+padding)
|
||||
copy(padtext, data)
|
||||
// PKCS#7 padding
|
||||
for i := len(data); i < len(padtext); i++ {
|
||||
padtext[i] = byte(padding)
|
||||
}
|
||||
|
||||
// Create CBC encrypter
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
encrypted := make([]byte, len(padtext))
|
||||
mode.CryptBlocks(encrypted, padtext)
|
||||
|
||||
return encrypted, nil
|
||||
}
|
||||
|
||||
// AESDecrypt decrypts data using AES-256-CBC
|
||||
func AESDecrypt(encrypted []byte, key []byte, iv []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, cosy.WrapErrorWithParams(ErrDecryptData, err.Error())
|
||||
}
|
||||
|
||||
// Create CBC decrypter
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
decrypted := make([]byte, len(encrypted))
|
||||
mode.CryptBlocks(decrypted, encrypted)
|
||||
|
||||
// Remove padding
|
||||
padding := int(decrypted[len(decrypted)-1])
|
||||
if padding < 1 || padding > aes.BlockSize {
|
||||
return nil, ErrInvalidPadding
|
||||
}
|
||||
return decrypted[:len(decrypted)-padding], nil
|
||||
}
|
||||
|
||||
// GenerateAESKey generates a random 32-byte AES key
|
||||
func GenerateAESKey() ([]byte, error) {
|
||||
key := make([]byte, 32) // 256-bit key
|
||||
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
||||
return nil, cosy.WrapErrorWithParams(ErrGenerateAESKey, err.Error())
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// GenerateIV generates a random 16-byte initialization vector
|
||||
func GenerateIV() ([]byte, error) {
|
||||
iv := make([]byte, aes.BlockSize)
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return nil, cosy.WrapErrorWithParams(ErrGenerateIV, err.Error())
|
||||
}
|
||||
return iv, nil
|
||||
}
|
||||
|
||||
// encryptFile encrypts a single file using AES encryption
|
||||
func encryptFile(filePath string, key []byte, iv []byte) error {
|
||||
// Read file content
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrReadFile, filePath)
|
||||
}
|
||||
|
||||
// Encrypt file content
|
||||
encrypted, err := AESEncrypt(data, key, iv)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrEncryptFile, filePath)
|
||||
}
|
||||
|
||||
// Write encrypted content back
|
||||
if err := os.WriteFile(filePath, encrypted, 0644); err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrWriteEncryptedFile, filePath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// decryptFile decrypts a single file using AES decryption
|
||||
func decryptFile(filePath string, key []byte, iv []byte) error {
|
||||
// Read encrypted file content
|
||||
encryptedData, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrReadEncryptedFile, filePath)
|
||||
}
|
||||
|
||||
// Decrypt file content
|
||||
decryptedData, err := AESDecrypt(encryptedData, key, iv)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrDecryptFile, filePath)
|
||||
}
|
||||
|
||||
// Write decrypted content back
|
||||
if err := os.WriteFile(filePath, decryptedData, 0644); err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrWriteDecryptedFile, filePath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodeToBase64 encodes byte slice to base64 string
|
||||
func EncodeToBase64(data []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(data)
|
||||
}
|
||||
|
||||
// DecodeFromBase64 decodes base64 string to byte slice
|
||||
func DecodeFromBase64(encoded string) ([]byte, error) {
|
||||
return base64.StdEncoding.DecodeString(encoded)
|
||||
}
|
76
internal/backup/backup_nginx_ui.go
Normal file
76
internal/backup/backup_nginx_ui.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package backup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/0xJacky/Nginx-UI/settings"
|
||||
"github.com/uozi-tech/cosy"
|
||||
"github.com/uozi-tech/cosy/logger"
|
||||
cosysettings "github.com/uozi-tech/cosy/settings"
|
||||
)
|
||||
|
||||
// backupNginxUIFiles backs up the nginx-ui configuration and database files
|
||||
func backupNginxUIFiles(destDir string) error {
|
||||
// Get config file path
|
||||
configPath := cosysettings.ConfPath
|
||||
if configPath == "" {
|
||||
return ErrConfigPathEmpty
|
||||
}
|
||||
|
||||
// Always save the config file as app.ini, regardless of its original name
|
||||
destConfigPath := filepath.Join(destDir, "app.ini")
|
||||
if err := copyFile(configPath, destConfigPath); err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCopyConfigFile, err.Error())
|
||||
}
|
||||
|
||||
// Get database file name and path
|
||||
dbName := settings.DatabaseSettings.GetName()
|
||||
dbFile := dbName + ".db"
|
||||
|
||||
// Database directory is the same as config file directory
|
||||
dbDir := filepath.Dir(configPath)
|
||||
dbPath := filepath.Join(dbDir, dbFile)
|
||||
|
||||
// Copy database file
|
||||
if _, err := os.Stat(dbPath); err == nil {
|
||||
// Database exists as file
|
||||
destDBPath := filepath.Join(destDir, dbFile)
|
||||
if err := copyFile(dbPath, destDBPath); err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCopyDBFile, err.Error())
|
||||
}
|
||||
} else {
|
||||
logger.Warn("Database file not found: %s", dbPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// backupNginxFiles backs up the nginx configuration directory
|
||||
func backupNginxFiles(destDir string) error {
|
||||
// Get nginx config directory
|
||||
nginxConfigDir := settings.NginxSettings.ConfigDir
|
||||
if nginxConfigDir == "" {
|
||||
return ErrNginxConfigDirEmpty
|
||||
}
|
||||
|
||||
// Copy nginx config directory
|
||||
if err := copyDirectory(nginxConfigDir, destDir); err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCopyNginxConfigDir, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeHashInfoFile creates a hash information file for verification
|
||||
func writeHashInfoFile(hashFilePath string, info HashInfo) error {
|
||||
content := fmt.Sprintf("nginx-ui_hash: %s\nnginx_hash: %s\ntimestamp: %s\nversion: %s\n",
|
||||
info.NginxUIHash, info.NginxHash, info.Timestamp, info.Version)
|
||||
|
||||
if err := os.WriteFile(hashFilePath, []byte(content), 0644); err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCreateHashFile, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
466
internal/backup/backup_test.go
Normal file
466
internal/backup/backup_test.go
Normal file
|
@ -0,0 +1,466 @@
|
|||
package backup
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/0xJacky/Nginx-UI/settings"
|
||||
"github.com/stretchr/testify/assert"
|
||||
cosylogger "github.com/uozi-tech/cosy/logger"
|
||||
cosysettings "github.com/uozi-tech/cosy/settings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Initialize logging system to avoid nil pointer exceptions during tests
|
||||
cosylogger.Init("debug")
|
||||
|
||||
// Clean up backup files at the start of tests
|
||||
cleanupBackupFiles()
|
||||
}
|
||||
|
||||
// cleanupBackupFiles removes all backup files in the current directory
|
||||
func cleanupBackupFiles() {
|
||||
// Get current directory
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete all backup files
|
||||
matches, err := filepath.Glob(filepath.Join(dir, "backup-*.zip"))
|
||||
if err == nil {
|
||||
for _, file := range matches {
|
||||
os.Remove(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setupTestEnvironment creates a temporary environment for testing
|
||||
func setupTestEnvironment(t *testing.T) (string, func()) {
|
||||
// Create temporary test directory
|
||||
tempDir, err := os.MkdirTemp("", "backup-test-*")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Set up necessary directories
|
||||
nginxDir := filepath.Join(tempDir, "nginx")
|
||||
nginxUIDir := filepath.Join(tempDir, "nginx-ui")
|
||||
configDir := filepath.Join(tempDir, "config")
|
||||
backupDir := filepath.Join(tempDir, "backup")
|
||||
|
||||
// Create directories
|
||||
for _, dir := range []string{nginxDir, nginxUIDir, configDir, backupDir} {
|
||||
err = os.MkdirAll(dir, 0755)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// Create some test files
|
||||
testFiles := map[string]string{
|
||||
filepath.Join(nginxDir, "nginx.conf"): "user nginx;\nworker_processes auto;\n",
|
||||
filepath.Join(nginxUIDir, "config.json"): `{"version": "1.0", "settings": {"theme": "dark"}}`,
|
||||
}
|
||||
|
||||
for file, content := range testFiles {
|
||||
err = os.WriteFile(file, []byte(content), 0644)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// Save original configuration
|
||||
origNginxConfigDir := settings.NginxSettings.ConfigDir
|
||||
origNginxUIConfigPath := cosysettings.ConfPath
|
||||
|
||||
// Set test configuration
|
||||
settings.NginxSettings.ConfigDir = nginxDir
|
||||
cosysettings.ConfPath = filepath.Join(configDir, "config.ini")
|
||||
|
||||
// Return cleanup function
|
||||
cleanup := func() {
|
||||
// Restore original configuration
|
||||
settings.NginxSettings.ConfigDir = origNginxConfigDir
|
||||
cosysettings.ConfPath = origNginxUIConfigPath
|
||||
|
||||
// Delete temporary directory
|
||||
os.RemoveAll(tempDir)
|
||||
}
|
||||
|
||||
return tempDir, cleanup
|
||||
}
|
||||
|
||||
// Test backup and restore functionality
|
||||
func TestBackupAndRestore(t *testing.T) {
|
||||
// Make sure backup files are cleaned up at the start and end of the test
|
||||
cleanupBackupFiles()
|
||||
defer cleanupBackupFiles()
|
||||
|
||||
// Create test configuration
|
||||
tempDir, err := os.MkdirTemp("", "nginx-ui-backup-test-*")
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create config file
|
||||
configPath := filepath.Join(tempDir, "config.ini")
|
||||
testConfig := []byte("[app]\nName = Nginx UI Test\n")
|
||||
err = os.WriteFile(configPath, testConfig, 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create database file
|
||||
dbName := settings.DatabaseSettings.GetName()
|
||||
dbFile := dbName + ".db"
|
||||
dbPath := filepath.Join(tempDir, dbFile)
|
||||
testDB := []byte("CREATE TABLE users (id INT, name TEXT);")
|
||||
err = os.WriteFile(dbPath, testDB, 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create nginx directory
|
||||
nginxConfigDir := filepath.Join(tempDir, "nginx")
|
||||
err = os.MkdirAll(nginxConfigDir, 0755)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create test nginx config
|
||||
testNginxContent := []byte("server {\n listen 80;\n server_name example.com;\n}\n")
|
||||
err = os.WriteFile(filepath.Join(nginxConfigDir, "nginx.conf"), testNginxContent, 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Setup settings for testing
|
||||
originalConfPath := cosysettings.ConfPath
|
||||
originalNginxConfigDir := settings.NginxSettings.ConfigDir
|
||||
|
||||
cosysettings.ConfPath = configPath
|
||||
settings.NginxSettings.ConfigDir = nginxConfigDir
|
||||
|
||||
// Restore original settings after test
|
||||
defer func() {
|
||||
cosysettings.ConfPath = originalConfPath
|
||||
settings.NginxSettings.ConfigDir = originalNginxConfigDir
|
||||
}()
|
||||
|
||||
// Run backup
|
||||
result, err := Backup()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, result.BackupContent)
|
||||
assert.NotEmpty(t, result.BackupName)
|
||||
assert.NotEmpty(t, result.AESKey)
|
||||
assert.NotEmpty(t, result.AESIv)
|
||||
|
||||
// Save backup content to a temporary file for restore testing
|
||||
backupPath := filepath.Join(tempDir, result.BackupName)
|
||||
err = os.WriteFile(backupPath, result.BackupContent, 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test restore functionality
|
||||
restoreDir, err := os.MkdirTemp("", "nginx-ui-restore-test-*")
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(restoreDir)
|
||||
|
||||
// Decode AES key and IV
|
||||
aesKey, err := DecodeFromBase64(result.AESKey)
|
||||
assert.NoError(t, err)
|
||||
aesIv, err := DecodeFromBase64(result.AESIv)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Perform restore
|
||||
restoreResult, err := Restore(RestoreOptions{
|
||||
BackupPath: backupPath,
|
||||
AESKey: aesKey,
|
||||
AESIv: aesIv,
|
||||
RestoreDir: restoreDir,
|
||||
RestoreNginx: true,
|
||||
RestoreNginxUI: true,
|
||||
VerifyHash: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, restoreResult.RestoreDir)
|
||||
|
||||
// Verify restored directories
|
||||
nginxUIDir := filepath.Join(restoreDir, NginxUIDir)
|
||||
nginxDir := filepath.Join(restoreDir, NginxDir)
|
||||
|
||||
_, err = os.Stat(nginxUIDir)
|
||||
assert.NoError(t, err)
|
||||
_, err = os.Stat(nginxDir)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify hash info exists
|
||||
_, err = os.Stat(filepath.Join(restoreDir, HashInfoFile))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// Test AES encryption/decryption
|
||||
func TestEncryptionDecryption(t *testing.T) {
|
||||
// Test data
|
||||
testData := []byte("This is a test message to encrypt and decrypt")
|
||||
|
||||
// Create temp dir for testing
|
||||
testDir, err := os.MkdirTemp("", "nginx-ui-crypto-test-*")
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(testDir)
|
||||
|
||||
// Create test file
|
||||
testFile := filepath.Join(testDir, "test.txt")
|
||||
err = os.WriteFile(testFile, testData, 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Generate AES key and IV
|
||||
key, err := GenerateAESKey()
|
||||
assert.NoError(t, err)
|
||||
iv, err := GenerateIV()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test encrypt file
|
||||
err = encryptFile(testFile, key, iv)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Read encrypted data
|
||||
encryptedData, err := os.ReadFile(testFile)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, string(testData), string(encryptedData))
|
||||
|
||||
// Test decrypt file
|
||||
err = decryptFile(testFile, key, iv)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Read decrypted data
|
||||
decryptedData, err := os.ReadFile(testFile)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, string(testData), string(decryptedData))
|
||||
}
|
||||
|
||||
// Test AES direct encryption/decryption
|
||||
func TestAESEncryptDecrypt(t *testing.T) {
|
||||
// Generate key and IV
|
||||
key, err := GenerateAESKey()
|
||||
assert.NoError(t, err)
|
||||
|
||||
iv, err := GenerateIV()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test data
|
||||
original := []byte("This is a test message for encryption and decryption")
|
||||
|
||||
// Encrypt
|
||||
encrypted, err := AESEncrypt(original, key, iv)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, original, encrypted)
|
||||
|
||||
// Decrypt
|
||||
decrypted, err := AESDecrypt(encrypted, key, iv)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, original, decrypted)
|
||||
}
|
||||
|
||||
// Test Base64 encoding/decoding
|
||||
func TestEncodeDecodeBase64(t *testing.T) {
|
||||
original := []byte("Test data for base64 encoding")
|
||||
|
||||
// Encode
|
||||
encoded := EncodeToBase64(original)
|
||||
|
||||
// Decode
|
||||
decoded, err := DecodeFromBase64(encoded)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, original, decoded)
|
||||
}
|
||||
|
||||
func TestGenerateAESKey(t *testing.T) {
|
||||
key, err := GenerateAESKey()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 32, len(key))
|
||||
}
|
||||
|
||||
func TestGenerateIV(t *testing.T) {
|
||||
iv, err := GenerateIV()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 16, len(iv))
|
||||
}
|
||||
|
||||
func TestEncryptDecryptFile(t *testing.T) {
|
||||
// Create temp directory
|
||||
tempDir, err := os.MkdirTemp("", "encrypt-file-test-*")
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create test file
|
||||
testFile := filepath.Join(tempDir, "test.txt")
|
||||
testContent := []byte("This is test content for file encryption")
|
||||
err = os.WriteFile(testFile, testContent, 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Generate key and IV
|
||||
key, err := GenerateAESKey()
|
||||
assert.NoError(t, err)
|
||||
|
||||
iv, err := GenerateIV()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Encrypt file
|
||||
err = encryptFile(testFile, key, iv)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Read encrypted content
|
||||
encryptedContent, err := os.ReadFile(testFile)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, testContent, encryptedContent)
|
||||
|
||||
// Decrypt file
|
||||
err = decryptFile(testFile, key, iv)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Read decrypted content
|
||||
decryptedContent, err := os.ReadFile(testFile)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testContent, decryptedContent)
|
||||
}
|
||||
|
||||
func TestBackupRestore(t *testing.T) {
|
||||
// Set up test environment
|
||||
tempDir, cleanup := setupTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create a config.ini file since it's required for the test
|
||||
configDir := filepath.Join(tempDir, "config")
|
||||
configPath := filepath.Join(configDir, "config.ini")
|
||||
err := os.WriteFile(configPath, []byte("[app]\nName = Nginx UI Test\n"), 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Update Cosy settings path
|
||||
originalConfPath := cosysettings.ConfPath
|
||||
cosysettings.ConfPath = configPath
|
||||
defer func() {
|
||||
cosysettings.ConfPath = originalConfPath
|
||||
}()
|
||||
|
||||
// Create backup
|
||||
backupResult, err := Backup()
|
||||
// If there's an error, log it but continue testing
|
||||
if err != nil {
|
||||
t.Logf("Backup failed with error: %v", err)
|
||||
t.Fail()
|
||||
return
|
||||
}
|
||||
|
||||
assert.NotNil(t, backupResult.BackupContent)
|
||||
assert.NotEmpty(t, backupResult.BackupName)
|
||||
assert.NotEmpty(t, backupResult.AESKey)
|
||||
assert.NotEmpty(t, backupResult.AESIv)
|
||||
|
||||
// Create temporary file for restore testing
|
||||
backupPath := filepath.Join(tempDir, backupResult.BackupName)
|
||||
err = os.WriteFile(backupPath, backupResult.BackupContent, 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Decode key and IV
|
||||
key, err := DecodeFromBase64(backupResult.AESKey)
|
||||
assert.NoError(t, err)
|
||||
|
||||
iv, err := DecodeFromBase64(backupResult.AESIv)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create restore directory
|
||||
restoreDir := filepath.Join(tempDir, "restore")
|
||||
err = os.MkdirAll(restoreDir, 0755)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create restore options
|
||||
options := RestoreOptions{
|
||||
BackupPath: backupPath,
|
||||
AESKey: key,
|
||||
AESIv: iv,
|
||||
RestoreDir: restoreDir,
|
||||
VerifyHash: true,
|
||||
// Avoid modifying the system
|
||||
RestoreNginx: false,
|
||||
RestoreNginxUI: false,
|
||||
}
|
||||
|
||||
// Test restore
|
||||
result, err := Restore(options)
|
||||
if err != nil {
|
||||
t.Logf("Restore failed with error: %v", err)
|
||||
t.Fail()
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, restoreDir, result.RestoreDir)
|
||||
// If hash verification is enabled, check the result
|
||||
if options.VerifyHash {
|
||||
assert.True(t, result.HashMatch, "Hash verification should pass")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateZipArchive(t *testing.T) {
|
||||
// Create temp directories
|
||||
tempSourceDir, err := os.MkdirTemp("", "zip-source-test-*")
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(tempSourceDir)
|
||||
|
||||
// Create some test files
|
||||
testFiles := []string{"file1.txt", "file2.txt", "subdir/file3.txt"}
|
||||
testContent := []byte("Test content")
|
||||
|
||||
for _, file := range testFiles {
|
||||
filePath := filepath.Join(tempSourceDir, file)
|
||||
dirPath := filepath.Dir(filePath)
|
||||
|
||||
err = os.MkdirAll(dirPath, 0755)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = os.WriteFile(filePath, testContent, 0644)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// Create zip file
|
||||
zipPath := filepath.Join(tempSourceDir, "test.zip")
|
||||
err = createZipArchive(zipPath, tempSourceDir)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify zip file was created
|
||||
_, err = os.Stat(zipPath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Extract to new directory to verify contents
|
||||
extractDir := filepath.Join(tempSourceDir, "extract")
|
||||
err = os.MkdirAll(extractDir, 0755)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = extractZipArchive(zipPath, extractDir)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify extracted files
|
||||
for _, file := range testFiles {
|
||||
extractedPath := filepath.Join(extractDir, file)
|
||||
content, err := os.ReadFile(extractedPath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testContent, content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashCalculation(t *testing.T) {
|
||||
// Create temp file
|
||||
tempFile, err := os.CreateTemp("", "hash-test-*.txt")
|
||||
assert.NoError(t, err)
|
||||
defer os.Remove(tempFile.Name())
|
||||
|
||||
// Write content
|
||||
testContent := []byte("Test content for hash calculation")
|
||||
_, err = tempFile.Write(testContent)
|
||||
assert.NoError(t, err)
|
||||
tempFile.Close()
|
||||
|
||||
// Calculate hash
|
||||
hash, err := calculateFileHash(tempFile.Name())
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, hash)
|
||||
|
||||
// Calculate again to verify consistency
|
||||
hash2, err := calculateFileHash(tempFile.Name())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, hash, hash2)
|
||||
|
||||
// Modify file and check hash changes
|
||||
err = os.WriteFile(tempFile.Name(), []byte("Modified content"), 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
hash3, err := calculateFileHash(tempFile.Name())
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, hash, hash3)
|
||||
}
|
290
internal/backup/backup_zip.go
Normal file
290
internal/backup/backup_zip.go
Normal file
|
@ -0,0 +1,290 @@
|
|||
package backup
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/uozi-tech/cosy"
|
||||
)
|
||||
|
||||
// createZipArchive creates a zip archive from a directory
|
||||
func createZipArchive(zipPath, srcDir string) error {
|
||||
// Create a new zip file
|
||||
zipFile, err := os.Create(zipPath)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCreateZipFile, err.Error())
|
||||
}
|
||||
defer zipFile.Close()
|
||||
|
||||
// Create a new zip writer
|
||||
zipWriter := zip.NewWriter(zipFile)
|
||||
defer zipWriter.Close()
|
||||
|
||||
// Walk through all files in the source directory
|
||||
err = filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get relative path
|
||||
relPath, err := filepath.Rel(srcDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Skip if it's the source directory itself
|
||||
if relPath == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if it's a symlink
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
// Get target of symlink
|
||||
linkTarget, err := os.Readlink(path)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrReadSymlink, err.Error())
|
||||
}
|
||||
|
||||
// Create symlink entry in zip
|
||||
header := &zip.FileHeader{
|
||||
Name: relPath,
|
||||
Method: zip.Deflate,
|
||||
}
|
||||
header.SetMode(info.Mode())
|
||||
|
||||
writer, err := zipWriter.CreateHeader(header)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCreateZipEntry, err.Error())
|
||||
}
|
||||
|
||||
// Write link target as content (common way to store symlinks in zip)
|
||||
_, err = writer.Write([]byte(linkTarget))
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCopyContent, relPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create zip header
|
||||
header, err := zip.FileInfoHeader(info)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCreateZipHeader, err.Error())
|
||||
}
|
||||
|
||||
// Set relative path as name
|
||||
header.Name = relPath
|
||||
if info.IsDir() {
|
||||
header.Name += "/"
|
||||
}
|
||||
|
||||
// Set compression method
|
||||
header.Method = zip.Deflate
|
||||
|
||||
// Create zip entry writer
|
||||
writer, err := zipWriter.CreateHeader(header)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCreateZipEntry, err.Error())
|
||||
}
|
||||
|
||||
// Skip if it's a directory
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Open source file
|
||||
source, err := os.Open(path)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrOpenSourceFile, err.Error())
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
// Copy to zip
|
||||
_, err = io.Copy(writer, source)
|
||||
return err
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// createZipArchiveFromFiles creates a zip archive from a list of files
|
||||
func createZipArchiveFromFiles(zipPath string, files []string) error {
|
||||
// Create a new zip file
|
||||
zipFile, err := os.Create(zipPath)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCreateZipFile, err.Error())
|
||||
}
|
||||
defer zipFile.Close()
|
||||
|
||||
// Create a new zip writer
|
||||
zipWriter := zip.NewWriter(zipFile)
|
||||
defer zipWriter.Close()
|
||||
|
||||
// Add each file to the zip
|
||||
for _, file := range files {
|
||||
// Get file info
|
||||
info, err := os.Stat(file)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrOpenSourceFile, err.Error())
|
||||
}
|
||||
|
||||
// Create zip header
|
||||
header, err := zip.FileInfoHeader(info)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCreateZipHeader, err.Error())
|
||||
}
|
||||
|
||||
// Set base name as header name
|
||||
header.Name = filepath.Base(file)
|
||||
|
||||
// Set compression method
|
||||
header.Method = zip.Deflate
|
||||
|
||||
// Create zip entry writer
|
||||
writer, err := zipWriter.CreateHeader(header)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCreateZipEntry, err.Error())
|
||||
}
|
||||
|
||||
// Open source file
|
||||
source, err := os.Open(file)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrOpenSourceFile, err.Error())
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
// Copy to zip
|
||||
_, err = io.Copy(writer, source)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCopyContent, file)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculateFileHash calculates the SHA-256 hash of a file
|
||||
func calculateFileHash(filePath string) (string, error) {
|
||||
// Open file
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return "", cosy.WrapErrorWithParams(ErrReadFile, filePath)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Create hash
|
||||
hash := sha256.New()
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
return "", cosy.WrapErrorWithParams(ErrCalculateHash, err.Error())
|
||||
}
|
||||
|
||||
// Return hex hash
|
||||
return hex.EncodeToString(hash.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// createZipArchiveToBuffer creates a zip archive of files in the specified directory
|
||||
// and writes the zip content to the provided buffer
|
||||
func createZipArchiveToBuffer(buffer *bytes.Buffer, sourceDir string) error {
|
||||
// Create a zip writer that writes to the buffer
|
||||
zipWriter := zip.NewWriter(buffer)
|
||||
defer zipWriter.Close()
|
||||
|
||||
// Walk through all files in the source directory
|
||||
err := filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Skip the source directory itself
|
||||
if path == sourceDir {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the relative path to the source directory
|
||||
relPath, err := filepath.Rel(sourceDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if it's a symlink
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
// Get target of symlink
|
||||
linkTarget, err := os.Readlink(path)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrReadSymlink, err.Error())
|
||||
}
|
||||
|
||||
// Create symlink entry in zip
|
||||
header := &zip.FileHeader{
|
||||
Name: relPath,
|
||||
Method: zip.Deflate,
|
||||
}
|
||||
header.SetMode(info.Mode())
|
||||
|
||||
writer, err := zipWriter.CreateHeader(header)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCreateZipEntry, err.Error())
|
||||
}
|
||||
|
||||
// Write link target as content
|
||||
_, err = writer.Write([]byte(linkTarget))
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCopyContent, relPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a zip header from the file info
|
||||
header, err := zip.FileInfoHeader(info)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCreateZipHeader, err.Error())
|
||||
}
|
||||
|
||||
// Set the name to be relative to the source directory
|
||||
header.Name = relPath
|
||||
|
||||
// Set the compression method
|
||||
if !info.IsDir() {
|
||||
header.Method = zip.Deflate
|
||||
}
|
||||
|
||||
// Create the entry in the zip file
|
||||
writer, err := zipWriter.CreateHeader(header)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCreateZipEntry, err.Error())
|
||||
}
|
||||
|
||||
// If it's a directory, we're done
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Open the source file
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrOpenSourceFile, err.Error())
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Copy the file contents to the zip entry
|
||||
_, err = io.Copy(writer, file)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCopyContent, relPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Close the zip writer to ensure all data is written
|
||||
return zipWriter.Close()
|
||||
}
|
83
internal/backup/errors.go
Normal file
83
internal/backup/errors.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
package backup
|
||||
|
||||
import (
|
||||
"github.com/uozi-tech/cosy"
|
||||
)
|
||||
|
||||
var (
|
||||
errScope = cosy.NewErrorScope("backup")
|
||||
|
||||
// Backup errors
|
||||
ErrCreateTempDir = errScope.New(4002, "Failed to create temporary directory")
|
||||
ErrCreateTempSubDir = errScope.New(4003, "Failed to create temporary subdirectory")
|
||||
ErrBackupNginxUI = errScope.New(4004, "Failed to backup Nginx UI files: {0}")
|
||||
ErrBackupNginx = errScope.New(4005, "Failed to backup Nginx config files: {0}")
|
||||
ErrCreateHashFile = errScope.New(4006, "Failed to create hash info file: {0}")
|
||||
ErrEncryptNginxUIDir = errScope.New(4007, "Failed to encrypt Nginx UI directory: {0}")
|
||||
ErrEncryptNginxDir = errScope.New(4008, "Failed to encrypt Nginx directory: {0}")
|
||||
ErrCreateZipArchive = errScope.New(4009, "Failed to create zip archive: {0}")
|
||||
ErrGenerateAESKey = errScope.New(4011, "Failed to generate AES key: {0}")
|
||||
ErrGenerateIV = errScope.New(4012, "Failed to generate initialization vector: {0}")
|
||||
ErrCreateBackupFile = errScope.New(4013, "Failed to create backup file: {0}")
|
||||
ErrCleanupTempDir = errScope.New(4014, "Failed to cleanup temporary directory: {0}")
|
||||
|
||||
// Config and file errors
|
||||
ErrConfigPathEmpty = errScope.New(4101, "Config path is empty")
|
||||
ErrCopyConfigFile = errScope.New(4102, "Failed to copy config file: {0}")
|
||||
ErrCopyDBDir = errScope.New(4103, "Failed to copy database directory: {0}")
|
||||
ErrCopyDBFile = errScope.New(4104, "Failed to copy database file: {0}")
|
||||
ErrCalculateHash = errScope.New(4105, "Failed to calculate hash: {0}")
|
||||
ErrNginxConfigDirEmpty = errScope.New(4106, "Nginx config directory is not set")
|
||||
ErrCopyNginxConfigDir = errScope.New(4107, "Failed to copy Nginx config directory: {0}")
|
||||
ErrReadSymlink = errScope.New(4108, "Failed to read symlink: {0}")
|
||||
|
||||
// Encryption and decryption errors
|
||||
ErrReadFile = errScope.New(4201, "Failed to read file: {0}")
|
||||
ErrEncryptFile = errScope.New(4202, "Failed to encrypt file: {0}")
|
||||
ErrWriteEncryptedFile = errScope.New(4203, "Failed to write encrypted file: {0}")
|
||||
ErrEncryptData = errScope.New(4204, "Failed to encrypt data: {0}")
|
||||
ErrDecryptData = errScope.New(4205, "Failed to decrypt data: {0}")
|
||||
ErrInvalidPadding = errScope.New(4206, "Invalid padding in decrypted data")
|
||||
|
||||
// Zip file errors
|
||||
ErrCreateZipFile = errScope.New(4301, "Failed to create zip file: {0}")
|
||||
ErrCreateZipEntry = errScope.New(4302, "Failed to create zip entry: {0}")
|
||||
ErrOpenSourceFile = errScope.New(4303, "Failed to open source file: {0}")
|
||||
ErrCreateZipHeader = errScope.New(4304, "Failed to create zip header: {0}")
|
||||
ErrCopyContent = errScope.New(4305, "Failed to copy file content: {0}")
|
||||
ErrWriteZipBuffer = errScope.New(4306, "Failed to write to zip buffer: {0}")
|
||||
|
||||
// Restore errors
|
||||
ErrCreateRestoreDir = errScope.New(4501, "Failed to create restore directory: {0}")
|
||||
ErrExtractArchive = errScope.New(4505, "Failed to extract archive: {0}")
|
||||
ErrDecryptNginxUIDir = errScope.New(4506, "Failed to decrypt Nginx UI directory: {0}")
|
||||
ErrDecryptNginxDir = errScope.New(4507, "Failed to decrypt Nginx directory: {0}")
|
||||
ErrVerifyHashes = errScope.New(4508, "Failed to verify hashes: {0}")
|
||||
ErrRestoreNginxConfigs = errScope.New(4509, "Failed to restore Nginx configs: {0}")
|
||||
ErrRestoreNginxUIFiles = errScope.New(4510, "Failed to restore Nginx UI files: {0}")
|
||||
ErrBackupFileNotFound = errScope.New(4511, "Backup file not found: {0}")
|
||||
ErrInvalidSecurityToken = errScope.New(4512, "Invalid security token format")
|
||||
ErrInvalidAESKey = errScope.New(4513, "Invalid AES key format: {0}")
|
||||
ErrInvalidAESIV = errScope.New(4514, "Invalid AES IV format: {0}")
|
||||
|
||||
// Zip extraction errors
|
||||
ErrOpenZipFile = errScope.New(4601, "Failed to open zip file: {0}")
|
||||
ErrCreateDir = errScope.New(4602, "Failed to create directory: {0}")
|
||||
ErrCreateParentDir = errScope.New(4603, "Failed to create parent directory: {0}")
|
||||
ErrCreateFile = errScope.New(4604, "Failed to create file: {0}")
|
||||
ErrOpenZipEntry = errScope.New(4605, "Failed to open zip entry: {0}")
|
||||
ErrCreateSymlink = errScope.New(4606, "Failed to create symbolic link: {0}")
|
||||
ErrInvalidFilePath = errScope.New(4607, "Invalid file path: {0}")
|
||||
ErrEvalSymlinks = errScope.New(4608, "Failed to evaluate symbolic links: {0}")
|
||||
|
||||
// Decryption errors
|
||||
ErrReadEncryptedFile = errScope.New(4701, "Failed to read encrypted file: {0}")
|
||||
ErrDecryptFile = errScope.New(4702, "Failed to decrypt file: {0}")
|
||||
ErrWriteDecryptedFile = errScope.New(4703, "Failed to write decrypted file: {0}")
|
||||
|
||||
// Hash verification errors
|
||||
ErrReadHashFile = errScope.New(4801, "Failed to read hash info file: {0}")
|
||||
ErrCalculateUIHash = errScope.New(4802, "Failed to calculate Nginx UI hash: {0}")
|
||||
ErrCalculateNginxHash = errScope.New(4803, "Failed to calculate Nginx hash: {0}")
|
||||
ErrHashMismatch = errScope.New(4804, "Hash verification failed: file integrity compromised")
|
||||
)
|
369
internal/backup/restore.go
Normal file
369
internal/backup/restore.go
Normal file
|
@ -0,0 +1,369 @@
|
|||
package backup
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
||||
"github.com/0xJacky/Nginx-UI/settings"
|
||||
"github.com/uozi-tech/cosy"
|
||||
cosysettings "github.com/uozi-tech/cosy/settings"
|
||||
)
|
||||
|
||||
// RestoreResult contains the results of a restore operation
|
||||
type RestoreResult struct {
|
||||
RestoreDir string
|
||||
NginxUIRestored bool
|
||||
NginxRestored bool
|
||||
HashMatch bool
|
||||
}
|
||||
|
||||
// RestoreOptions contains options for restore operation
|
||||
type RestoreOptions struct {
|
||||
BackupPath string
|
||||
AESKey []byte
|
||||
AESIv []byte
|
||||
RestoreDir string
|
||||
RestoreNginx bool
|
||||
VerifyHash bool
|
||||
RestoreNginxUI bool
|
||||
}
|
||||
|
||||
// Restore restores data from a backup archive
|
||||
func Restore(options RestoreOptions) (RestoreResult, error) {
|
||||
// Create restore directory if it doesn't exist
|
||||
if err := os.MkdirAll(options.RestoreDir, 0755); err != nil {
|
||||
return RestoreResult{}, cosy.WrapErrorWithParams(ErrCreateRestoreDir, err.Error())
|
||||
}
|
||||
|
||||
// Extract main archive to restore directory
|
||||
if err := extractZipArchive(options.BackupPath, options.RestoreDir); err != nil {
|
||||
return RestoreResult{}, cosy.WrapErrorWithParams(ErrExtractArchive, err.Error())
|
||||
}
|
||||
|
||||
// Decrypt the extracted files
|
||||
hashInfoPath := filepath.Join(options.RestoreDir, HashInfoFile)
|
||||
nginxUIZipPath := filepath.Join(options.RestoreDir, NginxUIZipName)
|
||||
nginxZipPath := filepath.Join(options.RestoreDir, NginxZipName)
|
||||
|
||||
// Decrypt hash info file
|
||||
if err := decryptFile(hashInfoPath, options.AESKey, options.AESIv); err != nil {
|
||||
return RestoreResult{}, cosy.WrapErrorWithParams(ErrDecryptFile, HashInfoFile)
|
||||
}
|
||||
|
||||
// Decrypt nginx-ui.zip
|
||||
if err := decryptFile(nginxUIZipPath, options.AESKey, options.AESIv); err != nil {
|
||||
return RestoreResult{}, cosy.WrapErrorWithParams(ErrDecryptNginxUIDir, err.Error())
|
||||
}
|
||||
|
||||
// Decrypt nginx.zip
|
||||
if err := decryptFile(nginxZipPath, options.AESKey, options.AESIv); err != nil {
|
||||
return RestoreResult{}, cosy.WrapErrorWithParams(ErrDecryptNginxDir, err.Error())
|
||||
}
|
||||
|
||||
// Extract zip files to subdirectories
|
||||
nginxUIDir := filepath.Join(options.RestoreDir, NginxUIDir)
|
||||
nginxDir := filepath.Join(options.RestoreDir, NginxDir)
|
||||
|
||||
if err := os.MkdirAll(nginxUIDir, 0755); err != nil {
|
||||
return RestoreResult{}, cosy.WrapErrorWithParams(ErrCreateDir, nginxUIDir)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(nginxDir, 0755); err != nil {
|
||||
return RestoreResult{}, cosy.WrapErrorWithParams(ErrCreateDir, nginxDir)
|
||||
}
|
||||
|
||||
// Extract nginx-ui.zip to nginx-ui directory
|
||||
if err := extractZipArchive(nginxUIZipPath, nginxUIDir); err != nil {
|
||||
return RestoreResult{}, cosy.WrapErrorWithParams(ErrExtractArchive, "nginx-ui.zip")
|
||||
}
|
||||
|
||||
// Extract nginx.zip to nginx directory
|
||||
if err := extractZipArchive(nginxZipPath, nginxDir); err != nil {
|
||||
return RestoreResult{}, cosy.WrapErrorWithParams(ErrExtractArchive, "nginx.zip")
|
||||
}
|
||||
|
||||
result := RestoreResult{
|
||||
RestoreDir: options.RestoreDir,
|
||||
NginxUIRestored: false,
|
||||
NginxRestored: false,
|
||||
HashMatch: false,
|
||||
}
|
||||
|
||||
// Verify hashes if requested
|
||||
if options.VerifyHash {
|
||||
hashMatch, err := verifyHashes(options.RestoreDir, nginxUIZipPath, nginxZipPath)
|
||||
if err != nil {
|
||||
return result, cosy.WrapErrorWithParams(ErrVerifyHashes, err.Error())
|
||||
}
|
||||
result.HashMatch = hashMatch
|
||||
}
|
||||
|
||||
// Restore nginx configs if requested
|
||||
if options.RestoreNginx {
|
||||
if err := restoreNginxConfigs(nginxDir); err != nil {
|
||||
return result, cosy.WrapErrorWithParams(ErrRestoreNginxConfigs, err.Error())
|
||||
}
|
||||
result.NginxRestored = true
|
||||
}
|
||||
|
||||
// Restore nginx-ui config if requested
|
||||
if options.RestoreNginxUI {
|
||||
if err := restoreNginxUIConfig(nginxUIDir); err != nil {
|
||||
return result, cosy.WrapErrorWithParams(ErrBackupNginxUI, err.Error())
|
||||
}
|
||||
result.NginxUIRestored = true
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// extractZipArchive extracts a zip archive to the specified directory
|
||||
func extractZipArchive(zipPath, destDir string) error {
|
||||
reader, err := zip.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrOpenZipFile, err.Error())
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
for _, file := range reader.File {
|
||||
err := extractZipFile(file, destDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractZipFile extracts a single file from a zip archive
|
||||
func extractZipFile(file *zip.File, destDir string) error {
|
||||
// Check for directory traversal elements in the file name
|
||||
if strings.Contains(file.Name, "..") {
|
||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, file.Name)
|
||||
}
|
||||
|
||||
// Create directory path if needed
|
||||
filePath := filepath.Join(destDir, file.Name)
|
||||
|
||||
// Ensure the resulting file path is within the destination directory
|
||||
destDirAbs, err := filepath.Abs(destDir)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, "cannot resolve destination path")
|
||||
}
|
||||
|
||||
filePathAbs, err := filepath.Abs(filePath)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, file.Name)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(filePathAbs, destDirAbs+string(os.PathSeparator)) {
|
||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, file.Name)
|
||||
}
|
||||
|
||||
if file.FileInfo().IsDir() {
|
||||
if err := os.MkdirAll(filePath, file.Mode()); err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCreateDir, filePath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create parent directory if needed
|
||||
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCreateParentDir, filePath)
|
||||
}
|
||||
|
||||
// Check if this is a symlink by examining mode bits
|
||||
if file.Mode()&os.ModeSymlink != 0 {
|
||||
// Open source file in zip to read the link target
|
||||
srcFile, err := file.Open()
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrOpenZipEntry, file.Name)
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
// Read the link target
|
||||
linkTargetBytes, err := io.ReadAll(srcFile)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrReadSymlink, file.Name)
|
||||
}
|
||||
linkTarget := string(linkTargetBytes)
|
||||
|
||||
// Verify the link target doesn't escape the destination directory
|
||||
absLinkTarget := filepath.Clean(filepath.Join(filepath.Dir(filePath), linkTarget))
|
||||
if !strings.HasPrefix(absLinkTarget, destDirAbs+string(os.PathSeparator)) {
|
||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, linkTarget)
|
||||
}
|
||||
|
||||
// Remove any existing file/link at the target path
|
||||
_ = os.Remove(filePath)
|
||||
|
||||
// Create the symlink
|
||||
if err := os.Symlink(linkTarget, filePath); err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCreateSymlink, file.Name)
|
||||
}
|
||||
|
||||
// Verify the resolved symlink path is within destination directory
|
||||
resolvedPath, err := filepath.EvalSymlinks(filePath)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrEvalSymlinks, filePath)
|
||||
}
|
||||
|
||||
resolvedPathAbs, err := filepath.Abs(resolvedPath)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, resolvedPath)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(resolvedPathAbs, destDirAbs+string(os.PathSeparator)) {
|
||||
// Remove the symlink if it points outside the destination directory
|
||||
_ = os.Remove(filePath)
|
||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, resolvedPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create file
|
||||
destFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCreateFile, filePath)
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
// Open source file in zip
|
||||
srcFile, err := file.Open()
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrOpenZipEntry, file.Name)
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
// Copy content
|
||||
if _, err := io.Copy(destFile, srcFile); err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCopyContent, file.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// verifyHashes verifies the hashes of the extracted zip files
|
||||
func verifyHashes(restoreDir, nginxUIZipPath, nginxZipPath string) (bool, error) {
|
||||
hashFile := filepath.Join(restoreDir, HashInfoFile)
|
||||
hashContent, err := os.ReadFile(hashFile)
|
||||
if err != nil {
|
||||
return false, cosy.WrapErrorWithParams(ErrReadHashFile, err.Error())
|
||||
}
|
||||
|
||||
hashInfo := parseHashInfo(string(hashContent))
|
||||
|
||||
// Calculate hash for nginx-ui.zip
|
||||
nginxUIHash, err := calculateFileHash(nginxUIZipPath)
|
||||
if err != nil {
|
||||
return false, cosy.WrapErrorWithParams(ErrCalculateUIHash, err.Error())
|
||||
}
|
||||
|
||||
// Calculate hash for nginx.zip
|
||||
nginxHash, err := calculateFileHash(nginxZipPath)
|
||||
if err != nil {
|
||||
return false, cosy.WrapErrorWithParams(ErrCalculateNginxHash, err.Error())
|
||||
}
|
||||
|
||||
// Verify hashes
|
||||
return (hashInfo.NginxUIHash == nginxUIHash && hashInfo.NginxHash == nginxHash), nil
|
||||
}
|
||||
|
||||
// parseHashInfo parses hash info from content string
|
||||
func parseHashInfo(content string) HashInfo {
|
||||
info := HashInfo{}
|
||||
lines := strings.Split(content, "\n")
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
switch key {
|
||||
case "nginx-ui_hash":
|
||||
info.NginxUIHash = value
|
||||
case "nginx_hash":
|
||||
info.NginxHash = value
|
||||
case "timestamp":
|
||||
info.Timestamp = value
|
||||
case "version":
|
||||
info.Version = value
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
// restoreNginxConfigs restores nginx configuration files
|
||||
func restoreNginxConfigs(nginxBackupDir string) error {
|
||||
destDir := nginx.GetConfPath()
|
||||
if destDir == "" {
|
||||
return ErrNginxConfigDirEmpty
|
||||
}
|
||||
|
||||
// Remove all contents in the destination directory first
|
||||
// Read directory entries
|
||||
entries, err := os.ReadDir(destDir)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCopyNginxConfigDir, "failed to read directory: "+err.Error())
|
||||
}
|
||||
|
||||
// Remove each entry
|
||||
for _, entry := range entries {
|
||||
entryPath := filepath.Join(destDir, entry.Name())
|
||||
err := os.RemoveAll(entryPath)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCopyNginxConfigDir, "failed to remove: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Copy files from backup to nginx config directory
|
||||
if err := copyDirectory(nginxBackupDir, destDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// restoreNginxUIConfig restores nginx-ui configuration files
|
||||
func restoreNginxUIConfig(nginxUIBackupDir string) error {
|
||||
// Get config directory
|
||||
configDir := filepath.Dir(cosysettings.ConfPath)
|
||||
if configDir == "" {
|
||||
return ErrConfigPathEmpty
|
||||
}
|
||||
|
||||
// Restore app.ini to the configured location
|
||||
srcConfigPath := filepath.Join(nginxUIBackupDir, "app.ini")
|
||||
if err := copyFile(srcConfigPath, cosysettings.ConfPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Restore database file if exists
|
||||
dbName := settings.DatabaseSettings.GetName()
|
||||
srcDBPath := filepath.Join(nginxUIBackupDir, dbName+".db")
|
||||
destDBPath := filepath.Join(configDir, dbName+".db")
|
||||
|
||||
// Only attempt to copy if database file exists in backup
|
||||
if _, err := os.Stat(srcDBPath); err == nil {
|
||||
if err := copyFile(srcDBPath, destDBPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
85
internal/backup/utils.go
Normal file
85
internal/backup/utils.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
package backup
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/uozi-tech/cosy"
|
||||
)
|
||||
|
||||
// copyFile copies a file from src to dst
|
||||
func copyFile(src, dst string) error {
|
||||
// Open source file
|
||||
source, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
// Create destination file
|
||||
destination, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer destination.Close()
|
||||
|
||||
// Copy content
|
||||
_, err = io.Copy(destination, source)
|
||||
return err
|
||||
}
|
||||
|
||||
// copyDirectory copies a directory recursively from src to dst
|
||||
func copyDirectory(src, dst string) error {
|
||||
// Check if source is a directory
|
||||
srcInfo, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !srcInfo.IsDir() {
|
||||
return cosy.WrapErrorWithParams(ErrCopyNginxConfigDir, "%s is not a directory", src)
|
||||
}
|
||||
|
||||
// Create destination directory
|
||||
if err := os.MkdirAll(dst, srcInfo.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Walk through source directory
|
||||
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Calculate relative path
|
||||
relPath, err := filepath.Rel(src, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if relPath == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create target path
|
||||
targetPath := filepath.Join(dst, relPath)
|
||||
|
||||
// Check if it's a symlink
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
// Read the link
|
||||
linkTarget, err := os.Readlink(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Create symlink at target path
|
||||
return os.Symlink(linkTarget, targetPath)
|
||||
}
|
||||
|
||||
// If it's a directory, create it
|
||||
if info.IsDir() {
|
||||
return os.MkdirAll(targetPath, info.Mode())
|
||||
}
|
||||
|
||||
// If it's a file, copy it
|
||||
return copyFile(path, targetPath)
|
||||
})
|
||||
}
|
117
internal/backup/version_test.go
Normal file
117
internal/backup/version_test.go
Normal file
|
@ -0,0 +1,117 @@
|
|||
package backup
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/0xJacky/Nginx-UI/internal/version"
|
||||
"github.com/0xJacky/Nginx-UI/settings"
|
||||
"github.com/stretchr/testify/assert"
|
||||
cosysettings "github.com/uozi-tech/cosy/settings"
|
||||
)
|
||||
|
||||
// TestBackupVersion verifies that the backup file contains correct version information
|
||||
func TestBackupVersion(t *testing.T) {
|
||||
// Make sure backup files are cleaned up at the start and end of the test
|
||||
cleanupBackupFiles()
|
||||
defer cleanupBackupFiles()
|
||||
|
||||
// Create test configuration
|
||||
tempDir, err := os.MkdirTemp("", "nginx-ui-backup-version-test-*")
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create config file
|
||||
configPath := filepath.Join(tempDir, "config.ini")
|
||||
testConfig := []byte("[app]\nName = Nginx UI Test\n")
|
||||
err = os.WriteFile(configPath, testConfig, 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create database file
|
||||
dbName := settings.DatabaseSettings.GetName()
|
||||
dbFile := dbName + ".db"
|
||||
dbPath := filepath.Join(tempDir, dbFile)
|
||||
testDB := []byte("CREATE TABLE users (id INT, name TEXT);")
|
||||
err = os.WriteFile(dbPath, testDB, 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create nginx directory
|
||||
nginxConfigDir := filepath.Join(tempDir, "nginx")
|
||||
err = os.MkdirAll(nginxConfigDir, 0755)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create nginx config
|
||||
testNginxContent := []byte("server {\n listen 80;\n server_name example.com;\n}\n")
|
||||
err = os.WriteFile(filepath.Join(nginxConfigDir, "nginx.conf"), testNginxContent, 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Setup test environment
|
||||
originalConfPath := cosysettings.ConfPath
|
||||
originalNginxConfigDir := settings.NginxSettings.ConfigDir
|
||||
|
||||
cosysettings.ConfPath = configPath
|
||||
settings.NginxSettings.ConfigDir = nginxConfigDir
|
||||
|
||||
// Restore original settings after test
|
||||
defer func() {
|
||||
cosysettings.ConfPath = originalConfPath
|
||||
settings.NginxSettings.ConfigDir = originalNginxConfigDir
|
||||
}()
|
||||
|
||||
// Run backup
|
||||
result, err := Backup()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, result.BackupContent)
|
||||
assert.NotEmpty(t, result.BackupName)
|
||||
assert.NotEmpty(t, result.AESKey)
|
||||
assert.NotEmpty(t, result.AESIv)
|
||||
|
||||
// Save backup content to temporary file for restore testing
|
||||
backupFile := filepath.Join(tempDir, result.BackupName)
|
||||
err = os.WriteFile(backupFile, result.BackupContent, 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Decode AES key and IV
|
||||
key, err := DecodeFromBase64(result.AESKey)
|
||||
assert.NoError(t, err)
|
||||
iv, err := DecodeFromBase64(result.AESIv)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Use the Restore function to extract and verify
|
||||
restoreDir, err := os.MkdirTemp("", "nginx-ui-restore-version-test-*")
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(restoreDir)
|
||||
|
||||
restoreResult, err := Restore(RestoreOptions{
|
||||
BackupPath: backupFile,
|
||||
AESKey: key,
|
||||
AESIv: iv,
|
||||
RestoreDir: restoreDir,
|
||||
VerifyHash: true,
|
||||
RestoreNginx: false,
|
||||
RestoreNginxUI: false,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, restoreResult.HashMatch, "Hash should match")
|
||||
|
||||
// Check hash_info.txt file
|
||||
hashInfoPath := filepath.Join(restoreDir, HashInfoFile)
|
||||
hashInfoContent, err := os.ReadFile(hashInfoPath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify version information
|
||||
versionInfo := version.GetVersionInfo()
|
||||
expectedVersion := versionInfo.Version
|
||||
|
||||
// Check if hash_info.txt contains version info
|
||||
hashInfoStr := string(hashInfoContent)
|
||||
t.Logf("Hash info content: %s", hashInfoStr)
|
||||
|
||||
assert.True(t, strings.Contains(hashInfoStr, "version: "), "Hash info should contain version field")
|
||||
|
||||
// Parse hash_info.txt content
|
||||
info := parseHashInfo(hashInfoStr)
|
||||
assert.Equal(t, expectedVersion, info.Version, "Backup version should match current version")
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue