-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathraf.go
363 lines (327 loc) · 10.8 KB
/
raf.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
package main
import (
"encoding/gob"
"fmt"
"os"
"path/filepath"
)
// VarValues stores the values parsed from the original name of the file based on the
// Prop configured.
type VarValues = map[string]string
const (
// RenameWarningTypePropertyValueEmpty is used when the renamer could not extract the
// value for the property from the original file name. The Value property of the
// RenameWarning will be populated with the name of the requested property.
RenameWarningTypePropertyValueEmpty = iota
// RenameWarningtypePropertyMissing is used when the property requested by the output
// name generator was not declared as a property to be extracted from the input file
// name or is not a valid intrinsic property. The Value property of the RenameWarning
// wil be populated with the name of the requested property.
RenameWarningtypePropertyMissing
// RenameWarningTypeFileDoesNotExist is used to report the fact that the original
// file raf has been asked to rename does not exist in the file system. The Value
// property of the RenameWarning will be populated with the original file name
RenameWarningTypeFileDoesNotExist
)
// RenameWarning contains information about potential name generation issues. For example,
// when the output name requires a property that is either not declared or whose value is
// empty.
type RenameWarning struct {
Type int
Value string
}
// String returns the warning message ready to be printed in the log/stdout
func (w *RenameWarning) String(entry RenameLogEntry) string {
switch w.Type {
case RenameWarningTypePropertyValueEmpty:
return fmt.Sprintf("WARNING: Could not extract property %s from original file name: %s ", w.Value, entry.OriginalFileName)
case RenameWarningtypePropertyMissing:
return fmt.Sprintf("WARNING: Output file name asks for property %s which is not delcared", w.Value)
}
return ""
}
// RenameLogEntry records an operation performed in a file and can be used to undo
// the rename
type RenameLogEntry struct {
OriginalFileName string
// OriginalFileChecksum is not used at the moment, we may enable it through a separate
// options since it could have a significant impact on performance
OriginalFileChecksum string
NewFileName string
// Warnings lists potential issues found while renaming the file
Warnings []RenameWarning
// Collisions points to other entries in the log that the new generated name for this
// entry collides with
Collisions []int
}
// RenameLog is a slice of RenameLogEntry objects that record all of the opertaions
// performed by the RenameAllFiles function
type RenameLog = []RenameLogEntry
// RenameAllFiles iterates over the files passed as input and for each one, extracts the
// property values, populates the intrinsic properties, and calls the GenerateName function.
// The output RenameLog file can be passed to the Apply() function to perform the changes.
func RenameAllFiles(p []Prop, tokens TokenStream, files []string, opts Opts) (RenameLog, error) {
rlog := make([]RenameLogEntry, len(files))
collisions := make(map[string][]int)
for idx, f := range files {
_, err := filepath.Abs(f)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not determine absolute path for %s: %s\n", f, err)
return rlog[:idx], err
}
//baseDir := filepath.Dir(absPath)
fileName := filepath.Base(f)
state := renamerState{
idx: idx,
fileName: fileName,
extension: filepath.Ext(fileName),
}
varValues := extractVarValues(fileName, p, opts)
for k, v := range ReservedVarNames {
varValues[k] = v(state)
}
outName, warnings, err := GenerateName(varValues, tokens, state, opts)
if err != nil {
return rlog[:idx], err
}
if opts.Verbose {
fmt.Fprintf(os.Stderr, "Renaming \"%s\" to \"%s\"\n", fileName, outName)
}
c, ok := collisions[outName]
if !ok {
collisions[outName] = make([]int, 1)
collisions[outName][0] = idx
} else {
collisions[outName] = append(c, idx)
}
rlog[idx] = RenameLogEntry{
OriginalFileName: fileName,
NewFileName: outName,
Warnings: warnings,
// we'll append the collisions at teh end, once we have a fully populated map
}
}
// populate collisions
// TODO: Validate output file name
for _, v := range collisions {
if len(v) > 1 {
for _, idx := range v {
rlog[idx].Collisions = v
}
}
}
return rlog, nil
}
// GenerateName uses the variable values to generate a string based on the input TokenStream
func GenerateName(varValues VarValues, out TokenStream, rstate renamerState, opts Opts) (string, []RenameWarning, error) {
outName := ""
warnings := make([]RenameWarning, 0)
for _, t := range out {
if t.Type == TokenTypeLiteral {
outName += t.Value
continue
}
if t.Type == TokenTypeProperty {
propValue, ok := varValues[t.Value]
if !ok {
fmt.Fprintf(os.Stderr, "WARNING: Output asks for value %s that is not declared as a property\n", t.Value)
warnings = append(warnings, RenameWarning{
Type: RenameWarningtypePropertyMissing,
Value: t.Value,
})
continue
}
if propValue == "" {
if opts.Verbose {
fmt.Fprintf(os.Stderr, "WARNING: Value for property %s is empty\n", t.Value)
}
warnings = append(warnings, RenameWarning{
Type: RenameWarningTypePropertyValueEmpty,
Value: t.Value,
})
}
if t.Formatter != nil {
formattedValue := propValue
for _, f := range t.Formatter {
fout, err := f.Format(formattedValue, rstate)
if err != nil {
return "", warnings, err
}
formattedValue = fout
}
outName += formattedValue
} else {
outName += propValue
}
}
}
return outName, warnings, nil
}
// Undo looks for a rename log file in the given folder and reverses the change to the files listed in the log.
// Returns a flipped RenameLog that can be passed to the Apply() function
func Undo(cwd string, opts Opts) (RenameLog, error) {
abs, err := filepath.Abs(cwd)
if err != nil {
return nil, err
}
rafPath, err := os.Stat(abs)
if err != nil {
return nil, err
}
if !rafPath.IsDir() {
return nil, fmt.Errorf("%s is not a valid directory. The undo command receives the path to a directory containing a %s file", rafPath, rafStatusFile)
}
rafFilePath := abs + string(os.PathSeparator) + rafStatusFile
if _, err = os.Stat(rafFilePath); os.IsNotExist(err) {
return nil, fmt.Errorf("The directory %s does not contain a valid raf status file (%s)", abs, rafStatusFile)
}
rlog, err := ReadRenameLog(rafFilePath)
if err != nil {
return nil, err
}
if len(rlog) == 0 {
return nil, fmt.Errorf("The raf status file %s does not contain any log entries", rafFilePath)
}
if opts.Verbose {
fmt.Fprintf(os.Stderr, "Beginning raf undo in folder %s", abs)
}
flipRlog := make([]RenameLogEntry, len(rlog))
collisions := make(map[string][]int)
for idx, entry := range rlog {
warnings := make([]RenameWarning, 0)
curFilePath := abs + string(os.PathSeparator) + entry.NewFileName
newFilePath := abs + string(os.PathSeparator) + entry.OriginalFileName
// new file must exists
if _, err = os.Stat(curFilePath); os.IsNotExist(err) {
if opts.Verbose {
fmt.Fprintf(os.Stderr, "WARNING: File %s from raf log not found", entry.NewFileName)
}
warnings = append(warnings, RenameWarning{
Type: RenameWarningTypeFileDoesNotExist,
Value: entry.NewFileName,
})
continue
}
// original file must not
if _, err = os.Stat(newFilePath); !os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "WARNING: Another file is already using the name %s preventing raf from resting %s to its original name", entry.OriginalFileName, entry.NewFileName)
continue
}
c, ok := collisions[entry.OriginalFileName]
if !ok {
collisions[entry.OriginalFileName] = make([]int, 1)
collisions[entry.OriginalFileName][0] = idx
} else {
collisions[entry.OriginalFileName] = append(c, idx)
}
flipRlog[idx] = RenameLogEntry{
OriginalFileName: entry.NewFileName,
NewFileName: entry.OriginalFileName,
Warnings: warnings,
}
}
// populate collisions
// TODO: Validate output file name
for _, v := range collisions {
if len(v) > 1 {
for _, idx := range v {
flipRlog[idx].Collisions = v
}
}
}
return flipRlog, nil
}
// Apply makes the changes outlined by the given RenameLog in the given path. Apply will not handle
// collisions or warnings in the log, the function will attempt to use the os.Rename method to
// perform the action and if an error is thrown return the os error.
func Apply(rlog RenameLog, path string, opts Opts) error {
absPath, err := filepath.Abs(path)
if err != nil {
return err
}
pathStat, err := os.Stat(absPath)
if err != nil {
return err
}
if !pathStat.IsDir() {
return fmt.Errorf("The given path %s is not a directory", path)
}
for idx, e := range rlog {
origFile := absPath + string(os.PathSeparator) + e.OriginalFileName
newFile := absPath + string(os.PathSeparator) + e.NewFileName
err = os.Rename(origFile, newFile)
if err != nil {
if writeErr := writeRenameLog(rlog[:idx], absPath); writeErr != nil {
fmt.Fprintf(os.Stderr, "FATAL: Could not write rename log after rename error: %s", writeErr)
}
return err
}
fmt.Println(e.NewFileName)
}
// write new log file
return writeRenameLog(rlog, absPath)
}
// ReadRenameLog parses a RenameLog file at the given path and unmarshals it into a
// RenameLog object (slice of RenameLogEntry)
func ReadRenameLog(path string) (RenameLog, error) {
reader, err := os.Open(path)
if err != nil {
return nil, err
}
defer reader.Close()
decoder := gob.NewDecoder(reader)
var rlog RenameLog
err = decoder.Decode(&rlog)
if err != nil {
return nil, err
}
return rlog, nil
}
type renamerState struct {
idx int
fileName string
extension string
}
func extractVarValues(fname string, p []Prop, opts Opts) VarValues {
varValues := make(map[string]string)
for _, prop := range p {
matches := prop.Regex.FindAllStringSubmatch(fname, -1) //prop.Regex.FindAllString(fname, -1)
if matches == nil {
if opts.Verbose {
fmt.Fprintf(os.Stderr, "WARNING: the matcher %s does not match any string on file %s\n", prop.Matcher, fname)
}
varValues["$"+prop.Name] = ""
continue
}
if len(matches) > 1 && opts.Verbose {
fmt.Fprintf(os.Stderr, "WARNING: the matcher %s matches multiple parts on file %s, only the leftmost is available\n", prop.Matcher, fname)
}
varValues["$"+prop.Name] = matches[0][len(matches[0])-1]
}
return varValues
}
func writeRenameLog(rlog RenameLog, absPath string) error {
statusFile := absPath + string(os.PathSeparator) + rafStatusFile
_, err := os.Stat(statusFile)
if err != nil {
if !os.IsNotExist(err) {
return err
}
} else {
err = os.Remove(statusFile)
if err != nil {
return err
}
}
statusFileWriter, err := os.Create(statusFile)
if err != nil {
return err
}
defer statusFileWriter.Close()
gobEncoder := gob.NewEncoder(statusFileWriter)
err = gobEncoder.Encode(rlog)
if err != nil {
return err
}
return nil
}