blob: 88179b3f15bd161fb2bd83ca5235a7dd36fd7f60 [file] [log] [blame]
// Copyright 2023 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Package fsutil implements common file operations.
package fsutil
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"syscall"
"golang.org/x/sys/unix"
"go.chromium.org/tast/core/errors"
)
// CopyFile copies the regular file at path src to dst.
// dst is atomically replaced if it already exists and inherits src's mode.
// Ownership will also be preserved if the EUID is 0.
func CopyFile(src, dst string) error {
sf, err := os.Open(src)
if err != nil {
return errors.Wrap(err, "failed to open src file")
}
defer sf.Close()
fi, err := sf.Stat()
if err != nil {
return errors.Wrap(err, "failed to stat src file")
} else if !fi.Mode().IsRegular() {
return fmt.Errorf("source not regular file (mode %s)", fi.Mode())
}
// Copy to a temp file.
df, err := ioutil.TempFile(filepath.Dir(dst), "."+filepath.Base(dst)+".")
if err != nil {
return errors.Wrap(err, "failed to create tmp file")
}
if _, err := io.Copy(df, sf); err != nil {
df.Close()
os.Remove(df.Name())
return errors.Wrap(err, "failed to copy data from src file to tmp file")
}
if err := df.Close(); err != nil {
os.Remove(df.Name())
return errors.Wrap(err, "failed to close tmp file")
}
// Finally, set the mode and ownership and rename to the requested name.
if err := os.Chmod(df.Name(), fi.Mode()); err != nil {
os.Remove(df.Name())
return errors.Wrap(err, "failed to change permissions of tmp file")
}
if os.Geteuid() == 0 {
st := fi.Sys().(*syscall.Stat_t)
if err := os.Chown(df.Name(), int(st.Uid), int(st.Gid)); err != nil {
os.Remove(df.Name())
return errors.Wrap(err, "failed to change owner of tmp file")
}
}
if err := os.Rename(df.Name(), dst); err != nil {
os.Remove(df.Name())
return errors.Wrap(err, "failed to rename tmp file to dst file")
}
return nil
}
// MoveFile moves the file at src to dst.
// The source and destination paths may be on different filesystems.
// The mode will be preserved, and ownership will also be preserved if possible
// (i.e. if called with an EUID of 0 or if moving the file within a filesystem).
func MoveFile(src, dst string) error {
fi, err := os.Stat(src)
if err != nil {
return errors.Wrap(err, "failed to open src file")
} else if !fi.Mode().IsRegular() {
return fmt.Errorf("source not regular file (mode %s)", fi.Mode())
}
// Try to do a rename first. This only works if we're not moving across filesystems.
if err := os.Rename(src, dst); err == nil {
return nil
} else if lerr, ok := err.(*os.LinkError); !ok || lerr.Err != unix.EXDEV {
return errors.Wrap(err, "failed to rename src to dst file")
}
// Otherwise, copy before deleting src.
if err := CopyFile(src, dst); err != nil {
return errors.Wrap(err, "failed to copy src to dst file")
}
return os.Remove(src)
}
// CopyDir copies a directory from srcDir to dstDir. Target dir must not exist.
// The mode is preserved. The owner is also preserved if the EUID is 0.
func CopyDir(srcDir, dstDir string) error {
// Create target dir or return error if already present.
srcStat, err := os.Stat(srcDir)
if err != nil {
return errors.Errorf("failed to stat %s", srcDir)
}
if _, err := os.Stat(dstDir); os.IsNotExist(err) {
if err := os.MkdirAll(dstDir, srcStat.Mode()); err != nil {
return errors.Wrapf(err, "failed to create %s", dstDir)
}
} else {
return errors.Errorf("target dir %s exists", dstDir)
}
// Attempt to remove target dir if copying failed.
success := false
defer func() {
if !success {
os.RemoveAll(dstDir)
}
}()
// Preserve owner attributes.
if os.Geteuid() == 0 {
st := srcStat.Sys().(*syscall.Stat_t)
if err := os.Chown(dstDir, int(st.Uid), int(st.Gid)); err != nil {
return errors.Wrapf(err, "failed to change owner of dir %s", dstDir)
}
}
// Copy dir content.
entries, err := ioutil.ReadDir(srcDir)
if err != nil {
return errors.Wrapf(err, "failed to read dir %s", srcDir)
}
for _, entry := range entries {
srcPath := filepath.Join(srcDir, entry.Name())
dstPath := filepath.Join(dstDir, entry.Name())
stat, err := os.Stat(srcPath)
if err != nil {
return errors.Errorf("failed to stat %s", srcPath)
}
switch stat.Mode() & os.ModeType {
case os.ModeDir:
if err := CopyDir(srcPath, dstPath); err != nil {
return errors.Wrapf(err, "failed to copy dir %s", srcPath)
}
case os.ModeSymlink:
if link, err := os.Readlink(srcPath); err != nil {
return errors.Wrapf(err, "failed to read symlink %s", srcPath)
} else if err := os.Symlink(link, dstPath); err != nil {
return errors.Wrapf(err, "failed to create symlink %s", dstPath)
}
default:
if err := CopyFile(srcPath, dstPath); err != nil {
return errors.Wrapf(err, "failed to copy file %s", srcPath)
}
}
}
success = true
return nil
}