-
Notifications
You must be signed in to change notification settings - Fork 0
/
prune.go
258 lines (210 loc) · 7.65 KB
/
prune.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
// Copyright 2020 Adam Chalkley
//
// https://github.com/atc0005/bridge
//
// Licensed under the MIT License. See LICENSE file in the project root for
// full license information.
package main
import (
"encoding/csv"
"fmt"
"io"
"log"
"os"
"path/filepath"
"github.com/atc0005/bridge/internal/config"
"github.com/atc0005/bridge/internal/dupesets"
"github.com/atc0005/bridge/internal/paths"
)
// pruneSubcommand is a wrapper around the "prune" subcommand logic
func pruneSubcommand(appConfig *config.Config) error {
// DEBUG
fmt.Printf("subcommand '%s' called\n", config.PruneSubcommand)
file, err := os.Open(appConfig.InputCSVFile)
if err != nil {
log.Fatal(err)
}
// #nosec G307
// Believed to be a false-positive from recent gosec release
// https://github.com/securego/gosec/issues/714
//
// NOTE: We're not manipulating contents for this file, so relying solely
// on a defer statement to close the file should be sufficient?
defer func() {
if err := file.Close(); err != nil {
log.Printf(
"error occurred closing file %q: %v",
appConfig.InputCSVFile,
err,
)
}
}()
csvReader := csv.NewReader(file)
// Require that the number of fields found matches what we expect to find
csvReader.FieldsPerRecord = config.InputCSVFieldCount
// TODO: Even with this set, we should probably still trim whitespace
// ourselves so that we can be assured that leading AND trailing
// whitespace has been removed
csvReader.TrimLeadingSpace = true
var dfsEntries dupesets.DuplicateFileSetEntries
var rowCounter = 0
for {
// Go ahead and bump the counter to reflect that humans start counting
// CSV rows from 1 and not 0
rowCounter++
record, err := csvReader.Read()
if err == io.EOF {
break
}
if err != nil {
return err
}
// If we are currently evaluating the very first line of the CSV file
// and the user did not override the default option of skipping the
// first row (due to it usually being the header row)
if rowCounter == 1 {
if !appConfig.UseFirstRow {
// DEBUG
log.Println("Skipping first row in input file to avoid processing column headers")
continue
}
log.Println("Attempting to parse row 1 from input CSV file as requested")
}
dfsEntry, err := dupesets.ParseInputRow(record, config.InputCSVFieldCount, rowCounter)
if err != nil {
log.Println("Error encountered parsing CSV file:", err)
if appConfig.IgnoreErrors {
log.Printf("IgnoringErrors set, ignoring input row %d.\n", rowCounter)
continue
}
log.Println("IgnoringErrors NOT set. Exiting.")
return err
}
// validate input row before we consider it OK
if err = dupesets.ValidateInputRow(dfsEntry, rowCounter); err != nil {
log.Println("Error encountered validating CSV row values:", err)
if appConfig.IgnoreErrors {
log.Printf("IgnoringErrors set, ignoring input row %d.\n", rowCounter)
continue
}
log.Println("IgnoringErrors NOT set. Exiting.")
return err
}
// update size details if found missing in CSV row
if err = dfsEntry.UpdateSizeInfo(); err != nil {
log.Println("Error encountered while attempting to update file size info:", err)
if appConfig.IgnoreErrors {
log.Printf("IgnoringErrors set, ignoring input row %d.\n", rowCounter)
continue
}
log.Println("IgnoringErrors NOT set. Exiting.")
return err
}
// Start off with collecting all entries in the CSV file that contain
// all required fields. We'll filter the entries later to just those
// that have been flagged for removal.
dfsEntries = append(dfsEntries, dfsEntry)
}
// at this point we have parsed the CSV file into dfsEntries, validated
// their content, regenerated file size details (if applicable) and are
// now ready to begin work to remove flagged files.
// DEBUG
// fmt.Println("Length of dfsEntries:", len(dfsEntries))
// Print parsed CSV file to the console if user requested it
// NOTE: This contains ALL CSV file entries, not just those flagged for
// removal.
if appConfig.ConsoleReport {
fmt.Println("Parsed CSV contents:")
dfsEntries.Print(appConfig.BlankLineBetweenSets)
}
// if there are no files flagged for removal, say so and exit.
filesToRemove := dfsEntries.FilesToRemove()
if len(filesToRemove) == 0 {
fmt.Printf("0 entries out of %d marked for removal in the %q input CSV file.\n",
len(dfsEntries), appConfig.InputCSVFile)
fmt.Println("Nothing to do, exiting.")
return nil
}
// INFO? DEBUG?
log.Printf("Found %d files to remove in %q", len(filesToRemove), appConfig.InputCSVFile)
// DEBUG
// Is this really debug-level output, or is it useful to print upon
// request via the `console` flag?
if appConfig.ConsoleReport {
fmt.Println("Files marked for removal:")
filesToRemove.Print(appConfig.BlankLineBetweenSets)
}
// Skip backup logic and file removal if running in "dry-run" mode
if !appConfig.DryRun {
// DEBUG? INFO?
fmt.Println("Dry-run not enabled, file removal mode enabled")
if appConfig.BackupDirectory != "" {
// DEBUG
log.Println("Backup directory specified")
if !paths.PathExists(appConfig.BackupDirectory) {
// directory doesn't exist, what about the parent directory? do we
// have permission to create content within the parent directory
// to create the requested directory?
// perhaps we should abort if the target directory doesn't exist?
//
// For example, we could end up trying to create a directory like
// /tmp if the app is run as root. Since /tmp requires special
// permissions, creating it as this application could lead to a
// lot of problems that we cannot reliably anticipate and prevent
return fmt.Errorf(
"backup directory %q specified, but does not exist",
appConfig.BackupDirectory,
)
}
// attempt to backup files that the user marked for removal
for _, file := range filesToRemove {
fullPathToFile := filepath.Join(file.ParentDirectory, file.Filename)
// attempt to backup files if user requested that we do so. if backup
// failure occurs, abort. If file already exists in specified backup
// directory check to see if they're identical. Report identical status
// (yeah, nay) and abort unless an override or force option is given
// (potential future work).
// DEBUG
// fmt.Printf("Calling BackupFile(%s, %s)\n", fullPathToFile, appConfig.BackupDirectory)
err := paths.BackupFile(fullPathToFile, appConfig.BackupDirectory)
if err != nil {
// FIXME: Implement check for appconfig.IgnoreErrors
// extend error message (potentially) to note that the error
// was encountered when creating a backup
return err
}
}
} else {
// DEBUG
log.Println("backup directory not set, not backing up files")
}
// Once backups complete remove original files. Allow IgnoreErrors setting
// to apply, but be very noisy about removal failures
var filesRemovedSuccess int
var filesRemovedFail int
for _, dfsEntry := range filesToRemove {
fullPathToFile := filepath.Join(dfsEntry.ParentDirectory, dfsEntry.Filename)
err = paths.RemoveFile(fullPathToFile, appConfig.DryRun)
if err != nil {
log.Printf("Error encountered while attempting to remove %q: %s\n",
dfsEntry.Filename, err)
if appConfig.IgnoreErrors {
log.Println("IgnoringErrors set, ignoring failed file removal")
filesRemovedFail++
continue
}
log.Println("IgnoringErrors NOT set. Exiting.")
return err
}
// note that we have successfully removed a file
filesRemovedSuccess++
}
// print removal results summary
fmt.Printf("File removal: %d success, %d fail\n",
filesRemovedSuccess, filesRemovedFail)
}
if appConfig.DryRun {
fmt.Println("Dry-run enabled, no files removed")
}
return nil
}