-
Notifications
You must be signed in to change notification settings - Fork 11
/
copy.go
374 lines (328 loc) · 10.2 KB
/
copy.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
// Package flop implements file operations, taking most queues from GNU cp while trying to be
// more programmatically friendly.
package flop
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strconv"
"github.com/pkg/errors"
)
// numberedBackupFile matches files that looks like file.ext.~1~ and uses a capture group to grab the number
var numberedBackupFile = regexp.MustCompile(`^.*\.~([0-9]{1,5})~$`)
// File describes a file on the filesystem.
type File struct {
// Path is the path to the src file.
Path string
// fileInfoOnInit is os.FileInfo for file when initialized.
fileInfoOnInit os.FileInfo
// existOnInit is true if the file exists when initialized.
existOnInit bool
// isDir is true if the file object is a directory.
isDir bool
}
// NewFile creates a new File.
func NewFile(path string) *File {
return &File{Path: path}
}
// setInfo will collect information about a File and populate the necessary fields.
func (f *File) setInfo() error {
info, err := os.Lstat(f.Path)
f.fileInfoOnInit = info
if err != nil {
if !os.IsNotExist(err) {
// if we are here then we have an error, but not one indicating the file does not exist
return err
}
} else {
f.existOnInit = true
if f.fileInfoOnInit.IsDir() {
f.isDir = true
}
}
return nil
}
func (f *File) isSymlink() bool {
if f.fileInfoOnInit.Mode()&os.ModeSymlink != 0 {
return true
}
return false
}
// shouldMakeParents returns true if we should make parent directories up to the dst
func (f *File) shouldMakeParents(opts Options) bool {
if opts.MkdirAll || opts.mkdirAll {
return true
}
if opts.Parents {
return true
}
if f.existOnInit {
return false
}
parent := filepath.Dir(filepath.Clean(f.Path))
if _, err := os.Stat(parent); !os.IsNotExist(err) {
// dst does not exist but the direct parent does. make the target dir.
return true
}
return false
}
// shouldCopyParents returns true if parent directories from src should be copied into dst.
func (f *File) shouldCopyParents(opts Options) bool {
if !opts.Parents {
return false
}
return true
}
// SimpleCopy will src to dst with default Options.
func SimpleCopy(src, dst string) error {
return Copy(src, dst, Options{})
}
// Copy will copy src to dst. Behavior is determined by the given Options.
func Copy(src, dst string, opts Options) (err error) {
opts.setLoggers()
srcFile, dstFile := NewFile(src), NewFile(dst)
// set src attributes
if err := srcFile.setInfo(); err != nil {
return errors.Wrapf(ErrCannotStatFile, "source file %s: %s", srcFile.Path, err)
}
if !srcFile.existOnInit {
return errors.Wrapf(ErrFileNotExist, "source file %s", srcFile.Path)
}
opts.logDebug("src %s existOnInit: %t", srcFile.Path, srcFile.existOnInit)
// stat dst attributes. handle errors later
_ = dstFile.setInfo()
opts.logDebug("dst %s existOnInit: %t", dstFile.Path, dstFile.existOnInit)
if dstFile.shouldMakeParents(opts) {
opts.mkdirAll = true
opts.DebugLogFunc("dst mkdirAll: true")
}
if opts.Parents {
if dstFile.existOnInit && !dstFile.isDir {
return ErrWithParentsDstMustBeDir
}
// TODO: figure out how to handle windows paths where they reference the full path like c:/dir
dstFile.Path = filepath.Join(dstFile.Path, srcFile.Path)
opts.logDebug("because of Parents option, setting dst Path to %s", dstFile.Path)
dstFile.isDir = srcFile.isDir
opts.Parents = false // ensure we don't keep creating parents on recursive calls
}
// copying src directory requires dst is also a directory, if it existOnInit
if srcFile.isDir && dstFile.existOnInit && !dstFile.isDir {
return errors.Wrapf(
ErrCannotOverwriteNonDir, "source directory %s, destination file %s", srcFile.Path, dstFile.Path)
}
// divide and conquer
switch {
case opts.Link:
return hardLink(srcFile, dstFile, opts.logDebug)
case srcFile.isSymlink():
// FIXME: we really need to copy the pass through dest unless they specify otherwise...check the docs
return copyLink(srcFile, dstFile, opts.logDebug)
case srcFile.isDir:
return copyDir(srcFile, dstFile, opts)
default:
return copyFile(srcFile, dstFile, opts)
}
}
// hardLink creates a hard link to src at dst.
func hardLink(src, dst *File, logFunc func(format string, a ...interface{})) error {
logFunc("creating hard link to src %s at dst %s", src.Path, dst.Path)
return os.Link(src.Path, dst.Path)
}
// copyLink copies a symbolic link from src to dst.
func copyLink(src, dst *File, logFunc func(format string, a ...interface{})) error {
logFunc("copying sym link %s to %s", src.Path, dst.Path)
linkSrc, err := os.Readlink(src.Path)
if err != nil {
return err
}
return os.Symlink(linkSrc, dst.Path)
}
func copyDir(srcFile, dstFile *File, opts Options) error {
if !opts.Recursive {
return errors.Wrapf(ErrOmittingDir, "source directory %s", srcFile.Path)
}
if opts.mkdirAll {
opts.logDebug("making all dirs up to %s", dstFile.Path)
if err := os.MkdirAll(dstFile.Path, srcFile.fileInfoOnInit.Mode()); err != nil {
return err
}
}
srcDirEntries, err := ioutil.ReadDir(srcFile.Path)
if err != nil {
return errors.Wrapf(ErrReadingSrcDir, "source directory %s: %s", srcFile.Path, err)
}
for _, entry := range srcDirEntries {
newSrc := filepath.Join(srcFile.Path, entry.Name())
newDst := filepath.Join(dstFile.Path, entry.Name())
opts.logDebug("recursive cp with src %s and dst %s", newSrc, newDst)
if err := Copy(
newSrc,
newDst,
opts,
); err != nil {
return err
}
}
return nil
}
func copyFile(srcFile, dstFile *File, opts Options) (err error) {
// shortcut if files are the same file
if os.SameFile(srcFile.fileInfoOnInit, dstFile.fileInfoOnInit) {
opts.logDebug("src %s is same file as dst %s", srcFile.Path, dstFile.Path)
return nil
}
// optionally make dst parent directories
if dstFile.shouldMakeParents(opts) {
// TODO: permissive perms here to ensure tmp file can write on nix.. ensure we are setting these correctly down the line or fix here
if err := os.MkdirAll(filepath.Dir(dstFile.Path), 0777); err != nil {
return err
}
}
if dstFile.existOnInit {
if dstFile.isDir {
// optionally append src file name to dst dir like cp does
if opts.AppendNameToPath {
dstFile.Path = filepath.Join(dstFile.Path, filepath.Base(srcFile.Path))
opts.logDebug("because of AppendNameToPath option, setting dst path to %s", dstFile.Path)
} else {
return errors.Wrapf(ErrWritingFileToExistingDir, "destination directory %s", dstFile.Path)
}
}
// optionally do not clobber existing dst file
if opts.NoClobber {
opts.logDebug("dst %s exists, will not clobber", dstFile.Path)
return nil
}
if opts.Backup != "" {
if err := backupFile(dstFile, opts.Backup, opts); err != nil {
return err
}
}
}
srcFD, err := os.Open(srcFile.Path)
if err != nil {
return errors.Wrapf(ErrCannotOpenSrc, "source file %s: %s", srcFile.Path, err)
}
defer func() {
if closeErr := srcFD.Close(); closeErr != nil {
err = closeErr
}
}()
if opts.Atomic {
dstDir := filepath.Dir(dstFile.Path)
tmpFD, err := ioutil.TempFile(dstDir, "copyfile-")
defer closeAndRemove(tmpFD, opts.logDebug)
if err != nil {
return errors.Wrapf(ErrCannotCreateTmpFile, "destination directory %s: %s", dstDir, err)
}
opts.logDebug("created tmp file %s", tmpFD.Name())
//copy src to tmp and cleanup on any error
opts.logInfo("copying src file %s to tmp file %s", srcFD.Name(), tmpFD.Name())
if _, err := io.Copy(tmpFD, srcFD); err != nil {
return err
}
if err := tmpFD.Sync(); err != nil {
return err
}
if err := tmpFD.Close(); err != nil {
return err
}
// move tmp to dst
opts.logInfo("renaming tmp file %s to dst %s", tmpFD.Name(), dstFile.Path)
if err := os.Rename(tmpFD.Name(), dstFile.Path); err != nil {
return errors.Wrapf(ErrCannotRenameTempFile, "attempted to rename temp transfer file %s to %s", tmpFD.Name(), dstFile.Path)
}
} else {
dstFD, err := os.Create(dstFile.Path)
if err != nil {
return errors.Wrapf(ErrCannotOpenOrCreateDstFile, "destination file %s: %s", dstFile.Path, err)
}
defer func() {
if closeErr := dstFD.Close(); closeErr != nil {
err = closeErr
}
}()
opts.logInfo("copying src file %s to dst file %s", srcFD.Name(), dstFD.Name())
if _, err = io.Copy(dstFD, srcFD); err != nil {
return err
}
if err := dstFD.Sync(); err != nil {
return err
}
}
return setPermissions(dstFile, srcFile.fileInfoOnInit.Mode(), opts)
}
// backupFile will create a backup of the file using the chosen control method. See Options.Backup.
func backupFile(file *File, control string, opts Options) error {
// TODO: this func could be more efficient if it used file instead of the path but right now this causes panic
// do not copy if the file did not exist
if !file.existOnInit {
return nil
}
// simple backup
simple := func() error {
bkp := file.Path + "~"
opts.logDebug("creating simple backup file %s", bkp)
return Copy(file.Path, bkp, opts)
}
// next gives the next unused backup file number, 1 above the current highest
next := func() (int, error) {
// find general matches that look like numbered backup files
m, err := filepath.Glob(file.Path + ".~[0-9]*~")
if err != nil {
return -1, err
}
// get each backup file num substring, convert to int, track highest num
var highest int
for _, f := range m {
subs := numberedBackupFile.FindStringSubmatch(filepath.Base(f))
if len(subs) > 1 {
if i, _ := strconv.Atoi(string(subs[1])); i > highest {
highest = i
}
}
}
return highest + 1, nil
}
// numbered backup
numbered := func(n int) error {
return Copy(file.Path, fmt.Sprintf("%s.~%d~", file.Path, n), opts)
}
switch control {
default:
return errors.Wrapf(ErrInvalidBackupControlValue, "backup value '%s'", control)
case "off":
return nil
case "simple":
return simple()
case "numbered":
i, err := next()
if err != nil {
return err
}
return numbered(i)
case "existing":
i, err := next()
if err != nil {
return err
}
if i > 1 {
return numbered(i)
}
return simple()
}
}
func closeAndRemove(file *os.File, logFunc func(format string, a ...interface{})) {
if file != nil {
if err := file.Close(); err != nil {
logFunc("err closing file %s: %s", file.Name(), err)
}
if err := os.Remove(file.Name()); err != nil {
logFunc("err removing file %s: %s", file.Name(), err)
}
}
}