Skip to content

Commit 0ec83da

Browse files
committed
enhance: config restore with mount point handling #1419
1 parent 8396599 commit 0ec83da

File tree

2 files changed

+438
-4
lines changed

2 files changed

+438
-4
lines changed

internal/backup/restore.go

Lines changed: 149 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package backup
22

33
import (
44
"archive/zip"
5+
"bufio"
56
"fmt"
67
"io"
78
"os"
89
"path/filepath"
910
"strings"
11+
"syscall"
1012

1113
"github.com/0xJacky/Nginx-UI/internal/nginx"
1214
"github.com/0xJacky/Nginx-UI/settings"
@@ -383,35 +385,178 @@ func restoreNginxConfigs(nginxBackupDir string) error {
383385
return ErrNginxConfigDirEmpty
384386
}
385387

388+
logger.Infof("Starting Nginx config restore from %s to %s", nginxBackupDir, destDir)
389+
386390
// Recursively clean destination directory preserving the directory structure
391+
logger.Info("Cleaning destination directory before restore")
387392
if err := cleanDirectoryPreservingStructure(destDir); err != nil {
393+
logger.Errorf("Failed to clean directory %s: %v", destDir, err)
388394
return cosy.WrapErrorWithParams(ErrCopyNginxConfigDir, "failed to clean directory: "+err.Error())
389395
}
390396

391397
// Copy files from backup to nginx config directory
398+
logger.Infof("Copying backup files to destination: %s", destDir)
392399
if err := copyDirectory(nginxBackupDir, destDir); err != nil {
400+
logger.Errorf("Failed to copy backup files: %v", err)
393401
return err
394402
}
395403

404+
logger.Info("Nginx config restore completed successfully")
396405
return nil
397406
}
398407

399-
// cleanDirectoryPreservingStructure removes all files and symlinks in a directory
400-
// but preserves the directory structure itself
408+
// cleanDirectoryPreservingStructure removes all files and subdirectories in a directory
409+
// but preserves the directory structure itself and handles mount points correctly.
401410
func cleanDirectoryPreservingStructure(dir string) error {
411+
logger.Infof("Cleaning directory: %s", dir)
412+
402413
entries, err := os.ReadDir(dir)
403414
if err != nil {
404415
return err
405416
}
406417

407418
for _, entry := range entries {
408419
path := filepath.Join(dir, entry.Name())
409-
err = os.RemoveAll(path)
410-
if err != nil {
420+
421+
if err := removeOrClearPath(path, entry.IsDir()); err != nil {
411422
return err
412423
}
413424
}
414425

426+
logger.Infof("Successfully cleaned directory: %s", dir)
427+
return nil
428+
}
429+
430+
// removeOrClearPath removes a path or clears it if it's a mount point
431+
func removeOrClearPath(path string, isDir bool) error {
432+
// Try to remove the path first
433+
err := os.RemoveAll(path)
434+
if err == nil {
435+
return nil
436+
}
437+
438+
// Handle removal failures
439+
if !isDeviceBusyError(err) {
440+
return fmt.Errorf("failed to remove %s: %w", path, err)
441+
}
442+
443+
// Device busy - check if it's a mount point or directory
444+
if !isDir {
445+
return fmt.Errorf("file is busy and cannot be removed: %s: %w", path, err)
446+
}
447+
448+
logger.Warnf("Path is busy (mount point): %s, clearing contents only", path)
449+
return clearDirectoryContents(path)
450+
}
451+
452+
// isMountPoint checks if a path is a mount point by comparing device IDs
453+
// or checking /proc/mounts on Linux systems
454+
func isMountPoint(path string) bool {
455+
if isDeviceDifferent(path) {
456+
return true
457+
}
458+
459+
return isInMountTable(path)
460+
}
461+
462+
// isDeviceDifferent checks if path is on a different device than its parent
463+
func isDeviceDifferent(path string) bool {
464+
var pathStat, parentStat syscall.Stat_t
465+
466+
if syscall.Stat(path, &pathStat) != nil {
467+
return false
468+
}
469+
470+
if syscall.Stat(filepath.Dir(path), &parentStat) != nil {
471+
return false
472+
}
473+
474+
return pathStat.Dev != parentStat.Dev
475+
}
476+
477+
// isInMountTable checks if path is listed in /proc/mounts
478+
func isInMountTable(path string) bool {
479+
file, err := os.Open("/proc/mounts")
480+
if err != nil {
481+
return false
482+
}
483+
defer file.Close()
484+
485+
cleanPath := filepath.Clean(path)
486+
scanner := bufio.NewScanner(file)
487+
488+
for scanner.Scan() {
489+
fields := strings.Fields(scanner.Text())
490+
if len(fields) >= 2 && unescapeOctal(fields[1]) == cleanPath {
491+
return true
492+
}
493+
}
494+
495+
return false
496+
}
497+
498+
// unescapeOctal converts octal escape sequences like \040 to their character equivalents
499+
func unescapeOctal(s string) string {
500+
var result strings.Builder
501+
502+
for i := 0; i < len(s); i++ {
503+
if char, skip := tryParseOctal(s, i); skip > 0 {
504+
result.WriteByte(char)
505+
i += skip - 1 // -1 because loop will increment
506+
continue
507+
}
508+
result.WriteByte(s[i])
509+
}
510+
511+
return result.String()
512+
}
513+
514+
// tryParseOctal attempts to parse octal sequence at position i
515+
// returns (char, skip) where skip > 0 if successful
516+
func tryParseOctal(s string, i int) (byte, int) {
517+
if s[i] != '\\' || i+3 >= len(s) {
518+
return 0, 0
519+
}
520+
521+
var char byte
522+
if _, err := fmt.Sscanf(s[i:i+4], "\\%03o", &char); err == nil {
523+
return char, 4
524+
}
525+
526+
return 0, 0
527+
}
528+
529+
// isDeviceBusyError checks if an error is a "device or resource busy" error
530+
func isDeviceBusyError(err error) bool {
531+
if err == nil {
532+
return false
533+
}
534+
535+
if errno, ok := err.(syscall.Errno); ok && errno == syscall.EBUSY {
536+
return true
537+
}
538+
539+
errMsg := err.Error()
540+
return strings.Contains(errMsg, "device or resource busy") ||
541+
strings.Contains(errMsg, "resource busy")
542+
}
543+
544+
// clearDirectoryContents removes all files and subdirectories within a directory
545+
// but preserves the directory itself. This is useful for cleaning mount points.
546+
func clearDirectoryContents(dir string) error {
547+
entries, err := os.ReadDir(dir)
548+
if err != nil {
549+
return err
550+
}
551+
552+
for _, entry := range entries {
553+
path := filepath.Join(dir, entry.Name())
554+
555+
if err := removeOrClearPath(path, entry.IsDir()); err != nil {
556+
logger.Warnf("Failed to clear %s: %v, continuing", path, err)
557+
}
558+
}
559+
415560
return nil
416561
}
417562

0 commit comments

Comments
 (0)