Skip to content

Commit 315b538

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

File tree

4 files changed

+469
-4
lines changed

4 files changed

+469
-4
lines changed

internal/backup/restore.go

Lines changed: 116 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"path/filepath"
99
"strings"
10+
"syscall"
1011

1112
"github.com/0xJacky/Nginx-UI/internal/nginx"
1213
"github.com/0xJacky/Nginx-UI/settings"
@@ -383,35 +384,146 @@ func restoreNginxConfigs(nginxBackupDir string) error {
383384
return ErrNginxConfigDirEmpty
384385
}
385386

387+
logger.Infof("Starting Nginx config restore from %s to %s", nginxBackupDir, destDir)
388+
386389
// Recursively clean destination directory preserving the directory structure
390+
logger.Info("Cleaning destination directory before restore")
387391
if err := cleanDirectoryPreservingStructure(destDir); err != nil {
392+
logger.Errorf("Failed to clean directory %s: %v", destDir, err)
388393
return cosy.WrapErrorWithParams(ErrCopyNginxConfigDir, "failed to clean directory: "+err.Error())
389394
}
390395

391396
// Copy files from backup to nginx config directory
397+
logger.Infof("Copying backup files to destination: %s", destDir)
392398
if err := copyDirectory(nginxBackupDir, destDir); err != nil {
399+
logger.Errorf("Failed to copy backup files: %v", err)
393400
return err
394401
}
395402

403+
logger.Info("Nginx config restore completed successfully")
396404
return nil
397405
}
398406

399-
// cleanDirectoryPreservingStructure removes all files and symlinks in a directory
400-
// but preserves the directory structure itself
407+
// cleanDirectoryPreservingStructure removes all files and subdirectories in a directory
408+
// but preserves the directory structure itself and handles mount points correctly.
401409
func cleanDirectoryPreservingStructure(dir string) error {
410+
logger.Infof("Cleaning directory: %s", dir)
411+
402412
entries, err := os.ReadDir(dir)
403413
if err != nil {
404414
return err
405415
}
406416

407417
for _, entry := range entries {
408418
path := filepath.Join(dir, entry.Name())
409-
err = os.RemoveAll(path)
410-
if err != nil {
419+
420+
if err := removeOrClearPath(path, entry.IsDir()); err != nil {
411421
return err
412422
}
413423
}
414424

425+
logger.Infof("Successfully cleaned directory: %s", dir)
426+
return nil
427+
}
428+
429+
// removeOrClearPath removes a path or clears it if it's a mount point
430+
func removeOrClearPath(path string, isDir bool) error {
431+
// Try to remove the path first
432+
err := os.RemoveAll(path)
433+
if err == nil {
434+
return nil
435+
}
436+
437+
// Handle removal failures
438+
if !isDeviceBusyError(err) {
439+
return fmt.Errorf("failed to remove %s: %w", path, err)
440+
}
441+
442+
// Device busy - check if it's a mount point or directory
443+
if !isDir {
444+
return fmt.Errorf("file is busy and cannot be removed: %s: %w", path, err)
445+
}
446+
447+
logger.Warnf("Path is busy (mount point): %s, clearing contents only", path)
448+
return clearDirectoryContents(path)
449+
}
450+
451+
// isMountPoint checks if a path is a mount point by comparing device IDs
452+
// or checking /proc/mounts on Linux systems
453+
func isMountPoint(path string) bool {
454+
if isDeviceDifferent(path) {
455+
return true
456+
}
457+
458+
return isInMountTable(path)
459+
}
460+
461+
// isDeviceDifferent and isInMountTable are implemented in platform-specific files:
462+
// - restore_unix.go for Linux/Unix systems
463+
// - restore_windows.go for Windows systems
464+
465+
// unescapeOctal converts octal escape sequences like \040 to their character equivalents
466+
func unescapeOctal(s string) string {
467+
var result strings.Builder
468+
469+
for i := 0; i < len(s); i++ {
470+
if char, skip := tryParseOctal(s, i); skip > 0 {
471+
result.WriteByte(char)
472+
i += skip - 1 // -1 because loop will increment
473+
continue
474+
}
475+
result.WriteByte(s[i])
476+
}
477+
478+
return result.String()
479+
}
480+
481+
// tryParseOctal attempts to parse octal sequence at position i
482+
// returns (char, skip) where skip > 0 if successful
483+
func tryParseOctal(s string, i int) (byte, int) {
484+
if s[i] != '\\' || i+3 >= len(s) {
485+
return 0, 0
486+
}
487+
488+
var char byte
489+
if _, err := fmt.Sscanf(s[i:i+4], "\\%03o", &char); err == nil {
490+
return char, 4
491+
}
492+
493+
return 0, 0
494+
}
495+
496+
// isDeviceBusyError checks if an error is a "device or resource busy" error
497+
func isDeviceBusyError(err error) bool {
498+
if err == nil {
499+
return false
500+
}
501+
502+
if errno, ok := err.(syscall.Errno); ok && errno == syscall.EBUSY {
503+
return true
504+
}
505+
506+
errMsg := err.Error()
507+
return strings.Contains(errMsg, "device or resource busy") ||
508+
strings.Contains(errMsg, "resource busy")
509+
}
510+
511+
// clearDirectoryContents removes all files and subdirectories within a directory
512+
// but preserves the directory itself. This is useful for cleaning mount points.
513+
func clearDirectoryContents(dir string) error {
514+
entries, err := os.ReadDir(dir)
515+
if err != nil {
516+
return err
517+
}
518+
519+
for _, entry := range entries {
520+
path := filepath.Join(dir, entry.Name())
521+
522+
if err := removeOrClearPath(path, entry.IsDir()); err != nil {
523+
logger.Warnf("Failed to clear %s: %v, continuing", path, err)
524+
}
525+
}
526+
415527
return nil
416528
}
417529

0 commit comments

Comments
 (0)