package fileops import ( "crypto/rand" "encoding/hex" "errors" "fmt" "io" "os" "path/filepath" "sort" "strings" ) // ErrAtomicDirectoryPromotionUnsupported indicates that the current operating // system lacks the atomic no-replace primitive required by PromoteDirectory. var ErrAtomicDirectoryPromotionUnsupported = errors.New("atomic no-replace directory promotion is unsupported") // 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 { if err := checkAtomicDirectoryPromotionSupport(); err != nil { return err } parent, destinationName, err := openConfinedParent(dst, false, 0) if err != nil { return fmt.Errorf("open destination parent: %w", err) } defer func() { _ = parent.Close() }() return promoteDirectoryConfined(src, dst, parent, destinationName, sourceTraversalHooks{}) } func promoteDirectoryConfined(src, dst string, parent *os.Root, destinationName string, hooks sourceTraversalHooks) (resultErr 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.Mode()&os.ModeSymlink != 0 || !sourceInfo.IsDir() { return fmt.Errorf("source path %q is not a directory", src) } if _, err := parent.Lstat(destinationName); 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) } temporary, temporaryName, err := createSiblingTempDirectory(parent, destinationName) if err != nil { return err } removeTemporary := true defer func() { var closeErr error if temporary != nil { closeErr = temporary.Close() } if removeTemporary { removeErr := removeConfinedEntry(parent, temporaryName) if removeErr != nil { resultErr = errors.Join(resultErr, fmt.Errorf("remove temporary destination directory: %w", removeErr)) } } if closeErr != nil { resultErr = errors.Join(resultErr, fmt.Errorf("close temporary destination directory: %w", closeErr)) } }() sourceRoot, err := openVerifiedSourceRoot(src, sourceInfo, hooks) if err != nil { return err } defer func() { _ = sourceRoot.Close() }() if err := copyRegularTreeToRoot(sourceRoot, src, temporary, hooks); err != nil { return err } if err := setOpenedDirectoryMode(temporary, WorkspaceDirectoryMode); err != nil { return fmt.Errorf("set temporary root permissions: %w", err) } if err := syncOpenedDirectory(temporary); err != nil { return fmt.Errorf("sync temporary root: %w", err) } if err := temporary.Close(); err != nil { return fmt.Errorf("close temporary root: %w", err) } temporary = nil directory, err := parent.Open(".") if err != nil { return fmt.Errorf("open destination parent for promotion: %w", err) } renameErr := renameDirectoryNoReplaceAt(directory, temporaryName, destinationName) closeErr := directory.Close() if renameErr != nil { return fmt.Errorf("install promoted directory: %w", renameErr) } if closeErr != nil { return fmt.Errorf("close destination parent after promotion: %w", closeErr) } removeTemporary = false if err := syncOpenedDirectory(parent); err != nil { return fmt.Errorf("sync destination parent: %w", err) } return nil } func createSiblingTempDirectory(parent *os.Root, base string) (*os.Root, string, error) { for attempt := 0; attempt < 100; attempt++ { var randomBytes [16]byte if _, err := rand.Read(randomBytes[:]); err != nil { return nil, "", fmt.Errorf("generate temporary directory name: %w", err) } name := "." + base + ".tmp-" + hex.EncodeToString(randomBytes[:]) if err := parent.Mkdir(name, WorkspaceDirectoryMode.Perm()); err != nil { if errors.Is(err, os.ErrExist) { continue } return nil, "", fmt.Errorf("create temporary destination directory: %w", err) } directory, err := openConfinedChild(parent, name, false, 0) if err != nil { _ = parent.Remove(name) return nil, "", err } return directory, name, nil } return nil, "", fmt.Errorf("create unique temporary destination directory") } func copyRegularTreeToRoot(src *os.Root, sourcePath string, dst *os.Root, hooks sourceTraversalHooks) error { directory, err := src.Open(".") if err != nil { return fmt.Errorf("open source directory %q for traversal: %w", sourcePath, err) } entries, readErr := directory.ReadDir(-1) closeErr := directory.Close() if readErr != nil { return fmt.Errorf("read source directory %q: %w", sourcePath, readErr) } if closeErr != nil { return fmt.Errorf("close source directory %q: %w", sourcePath, closeErr) } sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() }) for _, entry := range entries { entryPath := filepath.Join(sourcePath, entry.Name()) info, err := src.Lstat(entry.Name()) if err != nil { return fmt.Errorf("inspect source entry %q: %w", entryPath, err) } if hooks.afterEntryInspect != nil { hooks.afterEntryInspect(entryPath) } switch { case info.Mode().IsRegular(): if err := copyRegularFileToRoot(src, dst, entry.Name(), entryPath, info); err != nil { return err } case info.IsDir(): if err := copyRegularDirectoryToRoot(src, dst, entry.Name(), entryPath, info, hooks); err != nil { return err } default: return fmt.Errorf("source entry %q has unsupported file type %s", entryPath, info.Mode().Type()) } } return nil } func copyRegularDirectoryToRoot(sourceParent, destinationParent *os.Root, name, sourcePath string, inspected os.FileInfo, hooks sourceTraversalHooks) error { source, err := openVerifiedChildDirectory(sourceParent, name, sourcePath, inspected) if err != nil { return err } defer func() { _ = source.Close() }() if err := destinationParent.Mkdir(name, WorkspaceDirectoryMode.Perm()); err != nil { return fmt.Errorf("create destination directory %q: %w", sourcePath, err) } destination, err := openConfinedChild(destinationParent, name, false, 0) if err != nil { return err } err = copyRegularTreeToRoot(source, sourcePath, destination, hooks) if err == nil { err = setOpenedDirectoryMode(destination, WorkspaceDirectoryMode) } if err == nil { err = syncOpenedDirectory(destination) } closeErr := destination.Close() if err != nil { return err } if closeErr != nil { return fmt.Errorf("close destination directory %q: %w", sourcePath, closeErr) } return nil } func copyRegularFileToRoot(sourceRoot, destinationRoot *os.Root, name, sourcePath string, inspected os.FileInfo) error { in, err := openVerifiedSourceFile(sourceRoot, name, sourcePath, inspected) if err != nil { return err } defer func() { _ = in.Close() }() out, err := destinationRoot.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, WorkspaceFileMode) if err != nil { return fmt.Errorf("create destination file %q: %w", sourcePath, err) } if _, err := io.Copy(out, in); err != nil { _ = out.Close() return fmt.Errorf("copy source file %q: %w", sourcePath, err) } if err := out.Chmod(WorkspaceFileMode); err != nil { _ = out.Close() return fmt.Errorf("set destination file permissions %q: %w", sourcePath, err) } if err := out.Sync(); err != nil { _ = out.Close() return fmt.Errorf("sync destination file %q: %w", sourcePath, err) } if err := out.Close(); err != nil { return fmt.Errorf("close destination file %q: %w", sourcePath, err) } return nil } func openVerifiedChildDirectory(parent *os.Root, name, sourcePath string, inspected os.FileInfo) (*os.Root, error) { child, err := parent.OpenRoot(name) if err != nil { return nil, fmt.Errorf("open source directory %q: %w", sourcePath, err) } opened, err := child.Stat(".") if err != nil { _ = child.Close() return nil, fmt.Errorf("inspect opened source directory %q: %w", sourcePath, err) } current, err := parent.Lstat(name) if err != nil || current.Mode()&os.ModeSymlink != 0 || !current.IsDir() || !os.SameFile(opened, current) { _ = child.Close() if err != nil { return nil, fmt.Errorf("reinspect source directory %q: %w", sourcePath, err) } return nil, fmt.Errorf("source directory %q changed while being copied", sourcePath) } return child, nil } func openVerifiedSourceFile(root *os.Root, name, sourcePath string, inspected os.FileInfo) (*os.File, error) { in, err := root.Open(name) if err != nil { return nil, fmt.Errorf("open source file %q: %w", sourcePath, err) } opened, err := in.Stat() if err != nil { _ = in.Close() return nil, fmt.Errorf("inspect opened source file %q: %w", sourcePath, err) } current, err := root.Lstat(name) if err != nil || current.Mode()&os.ModeSymlink != 0 || !current.Mode().IsRegular() || !os.SameFile(opened, current) { _ = in.Close() if err != nil { return nil, fmt.Errorf("reinspect source file %q: %w", sourcePath, err) } return nil, fmt.Errorf("source file %q changed while being copied", sourcePath) } return in, nil } func promoteDirectory(src, dst string, install func(string, string) error) error { return promoteDirectoryWithHooks(src, dst, install, sourceTraversalHooks{}) } type sourceTraversalHooks struct { afterRootInspect func() afterEntryInspect func(string) } func promoteDirectoryWithHooks( src, dst string, install func(string, string) error, hooks sourceTraversalHooks, ) 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 { _ = RemoveAllUnderRoot(destinationParent, temporary) } }() sourceRoot, err := openVerifiedSourceRoot(src, sourceInfo, hooks) if err != nil { return err } defer func() { _ = sourceRoot.Close() }() if err := copyRegularTree(sourceRoot, src, temporary, hooks); err != nil { return err } if err := os.Chmod(temporary, WorkspaceDirectoryMode); 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 if err := syncDirectory(destinationParent); err != nil { return fmt.Errorf("sync destination parent: %w", err) } return nil } func openVerifiedSourceRoot(path string, inspected os.FileInfo, hooks sourceTraversalHooks) (*os.Root, error) { if hooks.afterRootInspect != nil { hooks.afterRootInspect() } root, err := os.OpenRoot(path) if err != nil { return nil, fmt.Errorf("open source directory %q: %w", path, err) } verified := false defer func() { if !verified { _ = root.Close() } }() opened, err := root.Stat(".") if err != nil { return nil, fmt.Errorf("inspect opened source directory %q: %w", path, err) } if !opened.IsDir() || !os.SameFile(inspected, opened) { return nil, fmt.Errorf("source directory %q changed while being opened", path) } current, err := os.Lstat(path) if err != nil { return nil, fmt.Errorf("reinspect source directory %q: %w", path, err) } if current.Mode()&os.ModeSymlink != 0 || !current.IsDir() || !os.SameFile(opened, current) { return nil, fmt.Errorf("source directory %q changed while being opened", path) } verified = true return root, nil } func copyRegularTree(src *os.Root, sourcePath, dst string, hooks sourceTraversalHooks) error { directory, err := src.Open(".") if err != nil { return fmt.Errorf("open source directory %q for traversal: %w", sourcePath, err) } defer func() { _ = directory.Close() }() entries, err := directory.ReadDir(-1) if err != nil { return fmt.Errorf("read source directory %q: %w", sourcePath, err) } sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() }) for _, entry := range entries { entryPath := filepath.Join(sourcePath, entry.Name()) destinationPath := filepath.Join(dst, entry.Name()) info, err := src.Lstat(entry.Name()) if err != nil { return fmt.Errorf("inspect source entry %q: %w", entryPath, err) } switch { case info.Mode().IsRegular(): if hooks.afterEntryInspect != nil { hooks.afterEntryInspect(entryPath) } if err := copyRegularFile(src, entry.Name(), entryPath, destinationPath, info); err != nil { return err } case info.IsDir(): if hooks.afterEntryInspect != nil { hooks.afterEntryInspect(entryPath) } if err := copyRegularDirectory(src, entry.Name(), entryPath, destinationPath, info, hooks); err != nil { return err } default: return fmt.Errorf("source entry %q has unsupported file type %s", entryPath, info.Mode().Type()) } } return nil } func copyRegularDirectory( parent *os.Root, name, sourcePath, dst string, inspected os.FileInfo, hooks sourceTraversalHooks, ) error { child, err := parent.OpenRoot(name) if err != nil { return fmt.Errorf("open source directory %q: %w", sourcePath, err) } defer func() { _ = child.Close() }() opened, err := child.Stat(".") if err != nil { return fmt.Errorf("inspect opened source directory %q: %w", sourcePath, err) } if !opened.IsDir() || !os.SameFile(inspected, opened) { return fmt.Errorf("source directory %q changed while being copied", sourcePath) } current, err := parent.Lstat(name) if err != nil { return fmt.Errorf("reinspect source directory %q: %w", sourcePath, err) } if current.Mode()&os.ModeSymlink != 0 || !current.IsDir() || !os.SameFile(opened, current) { return fmt.Errorf("source directory %q changed while being copied", sourcePath) } if err := os.Mkdir(dst, WorkspaceDirectoryMode); err != nil { return fmt.Errorf("create destination directory %q: %w", dst, err) } if err := copyRegularTree(child, sourcePath, dst, hooks); err != nil { return err } if err := os.Chmod(dst, WorkspaceDirectoryMode); err != nil { return fmt.Errorf("set destination directory permissions %q: %w", dst, err) } if err := syncDirectory(dst); err != nil { return fmt.Errorf("sync destination directory %q: %w", dst, err) } return nil } func copyRegularFile( root *os.Root, name, sourcePath, dst string, inspected os.FileInfo, ) error { in, err := root.Open(name) if err != nil { return fmt.Errorf("open source file %q: %w", sourcePath, err) } defer func() { _ = in.Close() }() opened, err := in.Stat() if err != nil { return fmt.Errorf("inspect opened source file %q: %w", sourcePath, err) } if !opened.Mode().IsRegular() || !os.SameFile(inspected, opened) { return fmt.Errorf("source file %q changed while being copied", sourcePath) } current, err := root.Lstat(name) if err != nil { return fmt.Errorf("reinspect source file %q: %w", sourcePath, err) } if current.Mode()&os.ModeSymlink != 0 || !current.Mode().IsRegular() || !os.SameFile(opened, current) { return fmt.Errorf("source file %q changed while being copied", sourcePath) } out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, WorkspaceFileMode) 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", sourcePath, err) } if err := out.Chmod(WorkspaceFileMode); 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 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 }