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