-
Notifications
You must be signed in to change notification settings - Fork 1
/
Dfm.go
486 lines (446 loc) · 14.3 KB
/
Dfm.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
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
package main
import (
"fmt"
"os"
"path"
"sort"
"strings"
"github.com/cevaris/ordered_map"
"github.com/spf13/afero"
)
const (
// OperationAdd means a file was added to a repo.
OperationAdd = "added"
// OperationLink means a file was linked from a repo to the target.
OperationLink = "linked"
// OperationCopy means a file was copied from a repo to the target.
OperationCopy = "copied"
// OperationRemove means a file was removed from the target. If there was an
// error removing the file, reason will describe it.
OperationRemove = "removed"
// OperationSkip means a file was not copied/linked to the target. The
// reason will be the original error, even though the ErrorHandler
// suppressed the error.
OperationSkip = "skipped"
)
// Logger is the type of function that dfm calls whenever it performs a file
// operation.
type Logger func(operation, relative, repo string, reason error)
func noErrorHandler(err *FileError) error {
return err
}
// Dfm is the main controller class for API access to dfm
type Dfm struct {
// The configuration used by this dfm instance
Config Config
// The log function used by this dfm instance
Logger Logger
// When set, don't actually do file operations, only log
DryRun bool
fs afero.Fs
}
// NewDfm creates a new dfm instance with the provided dfm dir.
func NewDfm(dfmDir string) (*Dfm, error) {
return NewDfmFs(afero.NewOsFs(), dfmDir)
}
// NewDfmFs creates a new dfm instance using the provided filesystem driver and
// df mdir.
func NewDfmFs(fs afero.Fs, dfmDir string) (*Dfm, error) {
config := Config{fs: fs}
if err := config.SetDirectory(dfmDir); err != nil {
return nil, err
}
return &Dfm{fs: fs, Config: config}, nil
}
func (dfm *Dfm) log(operation, relative, repo string, reason error) {
if dfm.Logger != nil {
dfm.Logger(operation, relative, repo, reason)
}
}
func (dfm *Dfm) saveConfig() error {
if dfm.DryRun {
return nil
}
if saveErr := dfm.Config.Save(); saveErr != nil {
return saveErr
}
return nil
}
// Init will prepare the configured directory for use with dfm, creating it if
// necessary.
func (dfm *Dfm) Init() error {
return dfm.saveConfig()
}
// IsValidRepo returns true if the given name is a directory in the dfm dir.
func (dfm *Dfm) IsValidRepo(repo string) bool {
fs := dfm.fs
stat, err := fs.Stat(pathJoin(dfm.Config.path, repo))
if err != nil {
return false
}
return stat.IsDir()
}
// HasRepo returns true if the given name is a repository that is currently
// configured to be used.
func (dfm *Dfm) HasRepo(repo string) bool {
for _, test := range dfm.Config.repos {
if test == repo {
return true
}
}
return false
}
func (dfm *Dfm) assertIsActiveRepo(repo string) error {
if !dfm.IsValidRepo(repo) {
return fmt.Errorf("repo %#v does not exist. To create it, run:\nmkdir %s", repo, dfm.RepoPath(repo, ""))
} else if !dfm.HasRepo(repo) {
return fmt.Errorf("repo %#v is not active, cannot add files to it", repo)
}
return nil
}
// RepoPath returns the path to the given file inside of the given repo.
func (dfm *Dfm) RepoPath(repo string, relative string) string {
return pathJoin(dfm.Config.path, repo, relative)
}
// TargetPath returns the path to the given file inside of the target.
func (dfm *Dfm) TargetPath(relative string) string {
return pathJoin(dfm.Config.targetPath, relative)
}
// addFile is the internal implementation of AddFile and AddFiles. Does less
// error checking. Returns the relative path and an error value.
func (dfm *Dfm) addFile(relativePath string, repo string, link bool) (string, error) {
fs := dfm.fs
targetPath := dfm.TargetPath(relativePath)
repoPath := dfm.RepoPath(repo, relativePath)
isRegular, err := IsRegularFile(fs, targetPath)
if err != nil {
return "", WrapFileError(err, targetPath)
} else if !isRegular {
if linked, err := IsLinkedFile(fs, repoPath, targetPath); linked || err != nil {
if err != nil {
return "", err
}
return "", ErrNotNeeded
}
return "", NewFileError(targetPath, "only regular files are supported")
}
if dfm.DryRun {
// do nothing
} else {
if err := MakeDirAll(fs, path.Dir(relativePath), dfm.Config.targetPath, dfm.RepoPath(repo, "")); err != nil {
return "", WrapFileError(err, relativePath)
}
if link {
if err := MoveFile(fs, targetPath, repoPath); err != nil {
return "", WrapFileError(err, repoPath)
}
if err := LinkFile(fs, repoPath, targetPath); err != nil {
return "", WrapFileError(err, targetPath)
}
} else {
if err := CopyFile(fs, targetPath, repoPath); err != nil {
return "", WrapFileError(err, repoPath)
}
}
}
return relativePath, nil
}
// AddFile will copy the provided file into dfm, optionally replacing the
// original with a symlink to the imported file.
func (dfm *Dfm) AddFile(filename string, repo string, link bool) error {
return dfm.AddFiles([]string{filename}, repo, link, noErrorHandler)
}
// AddFiles will copy all of the provided files into dfm, optionally replacing
// the originals with symlinks to the imported ones.
func (dfm *Dfm) AddFiles(inputFilenames []string, repo string, link bool, errorHandler ErrorHandler) error {
if err := dfm.assertIsActiveRepo(repo); err != nil {
return err
}
fileList := ordered_map.NewOrderedMap()
for _, inputFilename := range inputFilenames {
joined := pathJoin(dfm.Config.targetPath, inputFilename)
if !strings.HasPrefix(joined, dfm.Config.targetPath) {
return NewFileErrorf(inputFilename, "not in target path (%s)", dfm.Config.targetPath)
} else if strings.HasPrefix(joined, dfm.Config.path) {
return NewFileError(inputFilename, "cannot add a file already inside the dfm directory")
}
err := populateFileList(dfm.fs, dfm.Config.targetPath, inputFilename, fileList, repo)
if err != nil {
return err
}
}
iter := fileList.IterFunc()
var overallErr error
for kv, ok := iter(); ok; kv, ok = iter() {
filename := kv.Key.(string)
fileOperation := OperationAdd
var relativePath string
skip, abort, fileErr := processWithRetry(errorHandler, func() *FileError {
var rawErr error
relativePath, rawErr = dfm.addFile(filename, repo, link)
if rawErr == nil {
return nil
}
return WrapFileError(rawErr, filename)
})
if abort {
overallErr = fileErr
break
} else if skip {
fileOperation = OperationSkip
} else {
dfm.Config.manifest[relativePath] = true
}
dfm.log(fileOperation, filename, repo, fileErr)
}
if saveErr := dfm.saveConfig(); saveErr != nil {
return saveErr
}
return overallErr
}
// buildFileList scans the given paths in each repo, and returns an OrderedMap
// of relative -> repo. Only the file existing in the last-referenced repo will
// be used.
func (dfm *Dfm) buildFileList(paths []string) (*ordered_map.OrderedMap, error) {
fs := dfm.fs
// Map relative -> repo. Later repos override earlier ones.
fileList := ordered_map.NewOrderedMap()
for _, path := range paths {
found := false
for _, repo := range dfm.Config.repos {
err := populateFileList(fs, dfm.RepoPath(repo, ""), path, fileList, repo)
if err == nil {
found = true
} else if !os.IsNotExist(err) {
return nil, err
}
}
if !found {
return nil, NewFileError(path, "not found in any active repositories")
}
}
return fileList, nil
}
// syncFiles will handle the given list of files and add files to the manifest
// appropriately.
func (dfm *Dfm) syncFiles(
fileList *ordered_map.OrderedMap,
nextManifest map[string]bool,
errorHandler ErrorHandler,
operation string,
handleFile func(s, d string) error,
) error {
iter := fileList.IterFunc()
var overallErr error
for kv, ok := iter(); ok; kv, ok = iter() {
relative := kv.Key.(string)
// Add this file to the manifest now. Even if there is an error, we
// don't want autoclean to remove this file.
nextManifest[relative] = true
repo := kv.Value.(string)
repoPath := dfm.RepoPath(repo, relative)
targetPath := dfm.TargetPath(relative)
fileOperation := operation
skip, abort, fileErr := processWithRetry(errorHandler, func() *FileError {
rawErr := handleFile(repoPath, targetPath)
if rawErr == nil {
return nil
}
return WrapFileError(rawErr, relative)
})
if abort {
overallErr = fileErr
break
} else if skip {
fileOperation = OperationSkip
}
dfm.log(fileOperation, relative, repo, fileErr)
}
return overallErr
}
// runPartialSync is used for syncing specific files. It accepts a list of
// relative filenames to sync, updates the manifest, but does not run the
// cleanup.
func (dfm *Dfm) runPartialSync(
inputFilenames []string,
errorHandler ErrorHandler,
operation string,
handleFile func(s, d string) error,
) error {
fileList, err := dfm.buildFileList(inputFilenames)
if err != nil {
return err
}
err = dfm.syncFiles(fileList, dfm.Config.manifest, errorHandler, operation, handleFile)
if saveErr := dfm.saveConfig(); saveErr != nil {
return saveErr
}
return err
}
// runSync is the main sync function, responsible for listing all files to be
// synced, syncing them, then running the cleanup.
func (dfm *Dfm) runSync(
errorHandler ErrorHandler,
operation string,
handleFile func(s, d string) error,
) error {
fileList, err := dfm.buildFileList([]string{"."})
if err != nil {
return err
}
nextManifest := make(map[string]bool, fileList.Len())
err = dfm.syncFiles(fileList, nextManifest, errorHandler, operation, handleFile)
if err != nil {
// Since there was an error, we will bypass the autoclean. This
// means all existing files plus all new files are presently synced.
// Merge the old and new manifests.
for filename := range dfm.Config.manifest {
nextManifest[filename] = true
}
dfm.Config.manifest = nextManifest
} else {
dfm.autoclean(nextManifest)
}
if saveErr := dfm.saveConfig(); saveErr != nil {
return saveErr
}
return err
}
// handleLink is the workhorse for linking files.
func (dfm *Dfm) handleLink(s, d string) error {
done, err := IsLinkedFile(dfm.fs, s, d)
if err != nil {
return err
} else if done {
return ErrNotNeeded
} else if dfm.DryRun {
return nil
}
relativePath := d[len(dfm.Config.targetPath)+1:]
repoPath := s[:len(s)-len(relativePath)-1]
if err := MakeDirAll(dfm.fs, path.Dir(relativePath), repoPath, dfm.Config.targetPath); err != nil {
return err
}
return LinkFile(dfm.fs, s, d)
}
// handleCopy is the workhorse for copying files.
func (dfm *Dfm) handleCopy(s, d string) error {
// XXX - check if file is identical
if dfm.DryRun {
return nil
}
isLinked, err := IsLinkedFile(dfm.fs, s, d)
if err != nil {
return err
} else if isLinked {
// We allow copy to replace a link to its source file. This should only
// come up when ejecting.
err = RemoveFile(dfm.fs, d)
if err != nil {
return err
}
}
relativePath := d[len(dfm.Config.targetPath)+1:]
repoPath := s[:len(s)-len(relativePath)-1]
if err := MakeDirAll(dfm.fs, path.Dir(relativePath), repoPath, dfm.Config.targetPath); err != nil {
return err
}
return CopyFile(dfm.fs, s, d)
}
// LinkFiles creates symlinks for the given files only. Does not run the
// autoclean, but does update the manifest.
func (dfm *Dfm) LinkFiles(inputFilenames []string, errorHandler ErrorHandler) error {
return dfm.runPartialSync(inputFilenames, errorHandler, OperationLink, dfm.handleLink)
}
// LinkAll creates symlinks for files in all repos in the target directory and
// runs the autoclean.
func (dfm *Dfm) LinkAll(errorHandler ErrorHandler) error {
return dfm.runSync(errorHandler, OperationLink, dfm.handleLink)
}
// CopyFiles copies the given files to the target directory. Does not run the
// autoclean, but does update the manifest.
func (dfm *Dfm) CopyFiles(inputFilenames []string, errorHandler ErrorHandler) error {
return dfm.runPartialSync(inputFilenames, errorHandler, OperationCopy, dfm.handleCopy)
}
// CopyAll copies all files in all report to the target directory and
// runs the autoclean.
func (dfm *Dfm) CopyAll(errorHandler ErrorHandler) error {
return dfm.runSync(errorHandler, OperationCopy, dfm.handleCopy)
}
// RemoveFiles removes the given files from the target directory and from the
// manifest.
func (dfm *Dfm) RemoveFiles(inputFilenames []string) error {
nextManifest := make(map[string]bool, len(dfm.Config.manifest))
for filename := range dfm.Config.manifest {
nextManifest[filename] = true
}
for _, filename := range inputFilenames {
if _, ok := nextManifest[filename]; !ok {
dfm.log(OperationSkip, filename, "", NewFileError(filename, "not tracked by dfm"))
} else {
delete(nextManifest, filename)
}
}
dfm.autoclean(nextManifest)
if saveErr := dfm.saveConfig(); saveErr != nil {
return saveErr
}
return nil
}
// RemoveAll removes all tracked files from the target directory.
func (dfm *Dfm) RemoveAll() error {
nextManifest := map[string]bool{}
dfm.autoclean(nextManifest)
if saveErr := dfm.saveConfig(); saveErr != nil {
return saveErr
}
return nil
}
// EjectFiles copies the given files to the target directory, but removes them
// from the manifest. This results in future operations failing due to an
// existing file, as well as the autoclean never removing the files.
func (dfm *Dfm) EjectFiles(inputFilenames []string, errorHandler ErrorHandler) error {
fileList, err := dfm.buildFileList(inputFilenames)
if err != nil {
return err
}
err = dfm.syncFiles(fileList, dfm.Config.manifest, errorHandler, OperationCopy, dfm.handleCopy)
iter := fileList.IterFunc()
for kv, ok := iter(); ok; kv, ok = iter() {
relative := kv.Key.(string)
// Remove the file from the manifest
delete(dfm.Config.manifest, relative)
}
if saveErr := dfm.saveConfig(); saveErr != nil {
return saveErr
}
return err
}
// autoclean will remove all synced files from the target directory except those
// that are listed in nextManifest. The manifest will be updated but not saved.
func (dfm *Dfm) autoclean(nextManifest map[string]bool) {
var toRemove []string
for filename := range dfm.Config.manifest {
_, needed := nextManifest[filename]
if !needed {
toRemove = append(toRemove, filename)
}
}
sort.Strings(toRemove)
for _, filename := range toRemove {
var err error
if !dfm.DryRun {
err = RemoveFile(dfm.fs, dfm.TargetPath(filename))
if err == nil {
err = CleanDirectories(dfm.fs, path.Dir(dfm.TargetPath(filename)), dfm.Config.targetPath)
}
}
dfm.log(OperationRemove, filename, "", err)
if err == nil || os.IsNotExist(err) {
delete(dfm.Config.manifest, filename)
}
}
for filename := range nextManifest {
dfm.Config.manifest[filename] = true
}
}