package fileops import ( "errors" "fmt" "io" "os" "path/filepath" "sort" "strings" "syscall" ) const ( promotedDirectoryMode = 0o755 promotedFileMode = 0o644 ) // PromoteDirectory copies an existing regular-file tree into a new directory // and installs the complete copy atomically. It never removes the source or // replaces an existing destination. func PromoteDirectory(src, dst string) error { return promoteDirectory(src, dst, renameDirectoryNoReplace) } func promoteDirectory(src, dst string, install func(string, string) error) error { if strings.TrimSpace(src) == "" || strings.TrimSpace(dst) == "" { return fmt.Errorf("source and destination directory paths are required") } sourceInfo, err := os.Lstat(src) if err != nil { return fmt.Errorf("inspect source directory: %w", err) } if !sourceInfo.IsDir() { return fmt.Errorf("source path %q is not a directory", src) } if _, err := os.Lstat(dst); err == nil { return fmt.Errorf("destination path %q already exists", dst) } else if !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("inspect destination path: %w", err) } insideSource, err := pathWithin(src, dst) if err != nil { return err } if insideSource { return fmt.Errorf("destination path %q must not be inside source directory %q", dst, src) } destinationParent := filepath.Dir(dst) parentInfo, err := os.Lstat(destinationParent) if err != nil { return fmt.Errorf("inspect destination parent: %w", err) } if !parentInfo.IsDir() { return fmt.Errorf("destination parent %q is not a directory", destinationParent) } temporary, err := os.MkdirTemp(destinationParent, "."+filepath.Base(dst)+".tmp-*") if err != nil { return fmt.Errorf("create temporary destination directory: %w", err) } removeTemporary := true defer func() { if removeTemporary { _ = os.RemoveAll(temporary) } }() if err := copyRegularTree(src, temporary); err != nil { return err } if err := os.Chmod(temporary, promotedDirectoryMode); err != nil { return fmt.Errorf("set temporary root permissions: %w", err) } if err := syncDirectory(temporary); err != nil { return fmt.Errorf("sync temporary root: %w", err) } if err := install(temporary, dst); err != nil { return fmt.Errorf("install promoted directory: %w", err) } removeTemporary = false _ = syncDirectory(destinationParent) return nil } func copyRegularTree(src, dst string) error { entries, err := os.ReadDir(src) if err != nil { return fmt.Errorf("read source directory %q: %w", src, err) } sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() }) for _, entry := range entries { sourcePath := filepath.Join(src, entry.Name()) destinationPath := filepath.Join(dst, entry.Name()) info, err := os.Lstat(sourcePath) if err != nil { return fmt.Errorf("inspect source entry %q: %w", sourcePath, err) } switch { case info.Mode().IsRegular(): if err := copyRegularFile(sourcePath, destinationPath, info); err != nil { return err } case info.IsDir(): if err := os.Mkdir(destinationPath, promotedDirectoryMode); err != nil { return fmt.Errorf("create destination directory %q: %w", destinationPath, err) } if err := copyRegularTree(sourcePath, destinationPath); err != nil { return err } if err := os.Chmod(destinationPath, promotedDirectoryMode); err != nil { return fmt.Errorf("set destination directory permissions %q: %w", destinationPath, err) } if err := syncDirectory(destinationPath); err != nil { return fmt.Errorf("sync destination directory %q: %w", destinationPath, err) } default: return fmt.Errorf("source entry %q has unsupported file type %s", sourcePath, info.Mode().Type()) } } return nil } func copyRegularFile(src, dst string, inspected os.FileInfo) error { in, err := os.Open(src) if err != nil { return fmt.Errorf("open source file %q: %w", src, err) } defer func() { _ = in.Close() }() openedInfo, err := in.Stat() if err != nil { return fmt.Errorf("inspect opened source file %q: %w", src, err) } if !openedInfo.Mode().IsRegular() || !os.SameFile(inspected, openedInfo) { return fmt.Errorf("source file %q changed while being copied", src) } out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, promotedFileMode) if err != nil { return fmt.Errorf("create destination file %q: %w", dst, err) } closed := false defer func() { if !closed { _ = out.Close() } }() if _, err := io.Copy(out, in); err != nil { return fmt.Errorf("copy source file %q: %w", src, err) } if err := out.Chmod(promotedFileMode); err != nil { return fmt.Errorf("set destination file permissions %q: %w", dst, err) } if err := out.Sync(); err != nil { return fmt.Errorf("sync destination file %q: %w", dst, err) } if err := out.Close(); err != nil { return fmt.Errorf("close destination file %q: %w", dst, err) } closed = true return nil } func syncDirectory(path string) error { directory, err := os.Open(path) if err != nil { return err } defer func() { _ = directory.Close() }() err = directory.Sync() if errors.Is(err, syscall.EINVAL) || errors.Is(err, syscall.ENOTSUP) { return nil } return err } func pathWithin(parent, candidate string) (bool, error) { absoluteParent, err := filepath.Abs(parent) if err != nil { return false, fmt.Errorf("resolve source directory: %w", err) } absoluteCandidate, err := filepath.Abs(candidate) if err != nil { return false, fmt.Errorf("resolve destination directory: %w", err) } relative, err := filepath.Rel(absoluteParent, absoluteCandidate) if err != nil { return false, fmt.Errorf("compare source and destination directories: %w", err) } return relative != "." && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator)), nil }