@@ -2,11 +2,13 @@ package backup
22
33import (
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.
401410func 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