1package moxio
2
3import (
4 "fmt"
5 "io"
6 "log/slog"
7 "os"
8
9 "github.com/mjl-/mox/mlog"
10)
11
12// LinkOrCopy attempts to make a hardlink dst. If that fails, it will try to do a
13// regular file copy. If srcReaderOpt is not nil, it will be used for reading. If
14// fileSync is true and the file is copied instead of hardlinked, fsync is called
15// on the file after writing to ensure the file is flushed to disk. Callers should
16// also sync the directory of the destination file, but may want to do that after
17// linking/copying multiple files. If dst was created and an error occurred, it is
18// removed.
19func LinkOrCopy(log mlog.Log, dst, src string, srcReaderOpt io.Reader, fileSync bool) (rerr error) {
20
21 // Try hardlink first.
22 err := os.Link(src, dst)
23 if err == nil {
24 return nil
25 } else if os.IsNotExist(err) {
26 // No point in trying with regular copy, we would fail again. Either src doesn't
27 // exist or dst directory doesn't exist.
28 return err
29 }
30
31 // File system may not support hardlinks, or link could be crossing file systems.
32 // Do a regular file copy.
33 if srcReaderOpt == nil {
34 sf, err := os.Open(src)
35 if err != nil {
36 return fmt.Errorf("open source file: %w", err)
37 }
38 defer func() {
39 err := sf.Close()
40 log.Check(err, "closing copied source file")
41 }()
42 srcReaderOpt = sf
43 }
44
45 df, err := os.OpenFile(dst, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0660)
46 if err != nil {
47 return fmt.Errorf("create destination: %w", err)
48 }
49 defer func() {
50 if df != nil {
51 err := df.Close()
52 log.Check(err, "closing partial destination file")
53 }
54 if rerr != nil {
55 err = os.Remove(dst)
56 log.Check(err, "removing partial destination file", slog.String("path", dst))
57 }
58 }()
59
60 if _, err := io.Copy(df, srcReaderOpt); err != nil {
61 return fmt.Errorf("copy: %w", err)
62 }
63 if fileSync {
64 if err := df.Sync(); err != nil {
65 return fmt.Errorf("sync destination: %w", err)
66 }
67 }
68 if err := df.Close(); err != nil {
69 return fmt.Errorf("flush and close destination file: %w", err)
70 }
71 df = nil
72 return nil
73}
74