package moxio import ( "fmt" "io" "log/slog" "os" "github.com/mjl-/mox/mlog" ) // LinkOrCopy attempts to make a hardlink dst. If that fails, it will try to do a // regular file copy. If srcReaderOpt is not nil, it will be used for reading. If // sync is true and the file is copied, Sync is called on the file after writing to // ensure the file is written on disk. Callers should also sync the directory of // the destination file, but may want to do that after linking/copying multiple // files. If dst was created and an error occurred, it is removed. func LinkOrCopy(log mlog.Log, dst, src string, srcReaderOpt io.Reader, sync bool) (rerr error) { // Try hardlink first. err := os.Link(src, dst) if err == nil { return nil } else if os.IsNotExist(err) { // No point in trying with regular copy, we would fail again. Either src doesn't // exist or dst directory doesn't exist. return err } // File system may not support hardlinks, or link could be crossing file systems. // Do a regular file copy. if srcReaderOpt == nil { sf, err := os.Open(src) if err != nil { return fmt.Errorf("open source file: %w", err) } defer func() { err := sf.Close() log.Check(err, "closing copied source file") }() srcReaderOpt = sf } df, err := os.OpenFile(dst, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0660) if err != nil { return fmt.Errorf("create destination: %w", err) } defer func() { if df != nil { err := df.Close() log.Check(err, "closing partial destination file") err = os.Remove(dst) log.Check(err, "removing partial destination file", slog.String("path", dst)) } }() if _, err := io.Copy(df, srcReaderOpt); err != nil { return fmt.Errorf("copy: %w", err) } if sync { if err := df.Sync(); err != nil { return fmt.Errorf("sync destination: %w", err) } } err = df.Close() df = nil if err != nil { err := os.Remove(dst) log.Check(err, "removing partial destination file", slog.String("path", dst)) return err } return nil }