-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathprune.sh
399 lines (351 loc) · 10.4 KB
/
prune.sh
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
#!/bin/bash
# Version 1.0
# Author: Taylor Flatt
# A prune script that will remove files in a directory who have existed for longer than
# a specified number of days.
#
# -d, --directory: Directory in which files are subject to be pruned.
# -n, --days: Files older than this number of days are subject to be pruned.
# -a, --automated: Automated flag. This will run the script without user interaction and will take
# the safest option in deleting files. It leaves at least the newest backup file.
#
# Note: Sudoers and the root user cannot run this script. Limit the amount of possible damage.
#
# Usage: prune -d DIRECTORY -n NUM_DAYS [-a]
# Prevent Root and Sudo
if [[ $EUID -eq 0 ]] || [[ ! -z $SUDO_USER ]]; then
echo "This script cannot be invoked as an elevated user to prevent potentially undesired effects."
exit 1
fi
# Displays the usage information.
function print_usage()
{
echo "Usage: $0 -d DIRECTORY -n NUM_DAYS [-a]"
}
# Displays the help information to a user.
function print_help()
{
print_usage
echo "Prunes files in DIRECTORY older tha NUM_DAYS."
echo ""
echo -e "\t -d DIRECTORY, --directory \t\t Directory whose contents are subject to prune."
echo -e "\t -n NUM_DAYS, --days \t\t Files older than this will be subject to prune."
echo -e "\t -a, --automated \t\t\t Runs without user input and will at least keep the most recent file."
echo ""
echo "Notes:"
echo "-When -a is not supplied, user interaction is default. User interaction requires confirmation on each removal."
echo "-This script cannot be run as sudo or root user. This is to reduce as much undefined behavior as possible."
echo ""
echo "Examples:"
echo -e "\t prune -d /opt/myBackup -n 20 \t Prunes files older than 20 days from the /opt/myBackup directory."
echo -e "\t prune -d /mnt/share/backup -n 160 -a \t Prunes files older than 180 days from the /mnt/share/backup directory without user input."
echo ""
echo ""
echo "Full documentation and source code can be found at: <www.github.com/taylorflatt/bash-scripts> under the appropriate repository."
}
# Check arguments
if [[ $# < 1 || $# > 5 ]]; then
print_usage
exit 1;
fi
# Global variables
directory=
numDays=
automated=
newestFile=
numFiles=
numFilesDeleted=0
removedFileNames=()
# Global error variables
numFilesError=0
fileErrorNames=()
# Font colors for error/success messages.
RED=`tput setaf 1`
GREEN=`tput setaf 2`
END_COLOR=`tput sgr0`
# Parse arguments
while :; do
case $1 in
-h|-\?|--help)
print_help
exit 0
;;
-d|--directory)
directory=("${2-}")
shift
;;
-n|--days)
numDays=("${2-}")
shift
;;
-a|--automated)
echo "Prune is running in automated mode."
automated="TRUE"
;;
-?*)
echo "Error, invalid parameter $1"
exit 1
;;
--) # End of parameters
shift
break
;;
*) # Breaking loop.
break
;;
esac
shift
done
# Make sure the inputs are not empty and assigned.
if [[ -z $directory ]] || [[ -z $numDays ]]; then
echo "First"
print_usage
exit 1
fi
# Make sure the input is a valid directory.
if [[ ! -d $directory ]]; then
echo "${RED}DIRECTORY must be a real and accessible directory.${END_COLOR}"
print_usage
exit 1
fi
# Only want a positive integer, compare bitwise.
expr='^[1-9][0-9]*'
if [[ ! $numDays =~ $expr ]]; then
echo "${RED}NUM_DAYS must be a positive non-zero integer!${END_COLOR}"
print_usage
exit 1
fi
# Author: pjh
# Print the newest file, if any, matching the given pattern
# Parameter 1: A regex pattern that will be used to match files in the cwd.
#
# Usage: newest_matching_file PATTERN
# Example: newest_matching_file 'file*'
#
# WARNING: Files whose names begin with a dot will not be checked
function newest_matching_file()
{
# Use ${1-} instead of $1 in case 'nounset' is set
local -r globPattern=${1-}
if (( $# != 1 )) ; then
echo 'Usage: newest_matching_file GLOB_PATTERN' >&2
return 1
fi
# To avoid printing garbage if no files match the pattern, set
# 'nullglob' if necessary
local -i unsetNullglob=0
if [[ ":$BASHOPTS:" != *:nullglob:* ]] ; then
shopt -s nullglob
unsetNullglob=1
fi
for file in $globPattern ; do
[[ -z $newestFile || $file -nt $newestFile ]] \
&& newestFile=$file
done
# To avoid unexpected behaviour elsewhere, unset nullglob if it was
# set by this function
(( unsetNullglob )) && shopt -u nullglob
return 0
}
# Prompt for the user to determine if a file is to be removed.
#
# Usage: prunte_file_prompt
#
# WARNING: This will also actually remove files if the choice is to remove a file.
function prune_file_prompt()
{
local rmChoice=
while [[ "$rmChoice" != "y" ]] && [[ "$rmChoice" != "n" ]]; do
echo -n "Would you like to remove $file? (y/n):"
read rmChoice
case "$rmChoice" in
"y")
rm -rf "$file"
((numFilesDeleted++))
;;
"n")
echo "Skipping $file..."
;;
*)
echo "Error: Please choose a valid option of y or n."
;;
esac
done
}
# Removes all files except the newest file as determined by the newest_matching_file function.
# Parameter 1: The directory from which files will be removed.
#
# Usage: keep_newest_file DIRECTORY
#
# Note: It will NOT remove the CWD.
function keep_newest_file()
{
if [[ $# -ne 1 ]]; then
echo "Usage: keep_newest_file DIRECTORY"
return 1
fi
local dir=$1
# Make sure the input is a valid directory.
if [[ ! -d $dir ]]; then
echo "${RED}DIRECTORY must be a real and accessible directory.${END_COLOR}"
return 1
fi
newest_matching_file '*'
# Remove all files except for the newest file.
for file in *; do
if [[ "$file" == "$newestFile" ]]; then
echo "Skipping $file since it is the newest file..."
else
# Don't remove the root (backup) directory.
if [[ "$file" == "$dir" ]]; then
echo "Root directory, we should skip."
echo "Skipping $dir..."
# Prune files (Interactive).
elif [[ -z $automated ]]; then
prune_file_prompt
# Prune files (Non-interactive).
else
# Force the removal in case the file is write protected.
if rm -rf "$file"; then
echo "Deleting file: $file"
((numFilesDeleted++))
else
echo "Error deleting $file..."
((numFilesError++))
fileErrorNames+=("$file")
fi
fi
fi
done
return 0
}
# Prunes files depending on user input.
# Parameter 1: The directory from which files will be removed.
# Parameter 2: The number of days from the current date in which files prior
# to that date will be removed.
# Parameter 3: Whether or not the script should run in an automated fashion.
# This will suppress any user interaction and will only perform safe deletes.
#
# Usage: prune_file DIRECTORY DAYS [AUTOMATED]
#
# Example: prune_files /home/user1/directory7/backupDir 20
# Example: prune_files /home/user1/directory7/backupDir 20 TRUE
#
# Note: ANY input for a third parameter will be read as an automated task. Typically
# passing in the value set for $automated in the main script parameter works just as well.
# Note: In no case will it remove the CWD.
function prune_files()
{
if [[ $# < 2 ]] || [[ $# > 3 ]]; then
echo "Usage: $0 DIRECTORY DAYS [AUTOMATED]"
fi
local dir=$1
local days=$2
# Check if the script should run w/o user interaction.
if [[ $# -eq 3 ]]; then
local automated=$3
fi
# Make sure the input is a valid directory.
if [[ ! -d "$dir" ]]; then
echo "${RED}DIRECTORY must be a real and accessible directory.${END_COLOR}"
exit 1
fi
# Move to the backup directory.
if ! cd "$dir"; then
echo "${RED}Error changing directories to $dir ${END_COLOR}"
return 1
fi
# This way of processing is a bit more costly but allows a safety check.
local totalFiles=
local date=$(date -d "$days days ago" +%s)
# Check how many total files there are in the directory and find how many
# are candidates for removal.
for file in *; do
local fileLastMod=$(date -r "$file" +%s)
echo "Found: $file"
# File is marked as needing to be removed.
if [[ "$date" -ge "$fileLastMod" ]]; then
echo "File: $file is set to be removed."
((numFiles++))
fi
((totalFiles++))
done
# If numFiles=totalFiles, then this determines if all files in dir will be removed.
local deleteAll=
# Case: Number of files to delete outnumbers the files in the directory. (Did we catch . or ..?)
if [[ numFiles > totalFiles ]]; then
echo "{RED}Cannot delete more files than actually exist. Something went wrong.{END_COLOR}."
exit 1
# Case: All files in the directory are marked for pruning. Check with the user if interactive, otherwise keep newest file.
elif [[ numFiles -eq totalFiles ]]; then
if [[ -z "$automated" ]]; then
delPrompt=
while [[ "$delPrompt" != "y" ]] && [[ "$delPrompt" != "n" ]]; do
echo -n "All files in $dir are over $days days old. Would you like to save the NEWEST file in the directory and delete the rest? ${RED}Warning: This is not recoverable: ${END_COLOR} (y/n/q (quit)):"
read delPrompt
echo ""
case "$delPrompt" in
"y")
deleteAll="FALSE"
;;
"q")
echo "Exiting without making any changes..."
exit 0
;;
"n")
deleteAll="TRUE"
;;
*)
echo "${RED}Error: Please choose a valid option of y or n or q.${END_COLOR}"
echo ""
;;
esac
done
else
deleteAll="FALSE"
fi
fi
# Remove files (Interactive)
if [[ -z "$automated" ]]; then
if [[ "$deleteAll" == "TRUE" ]]; then
for file in *; do
prune_file_prompt
done
elif [[ "$deleteAll" == "FALSE" ]]; then
keep_newest_file "$dir"
else # Typical removal case.
for file in *; do
local fileLastMod=$(date -r "$file" +%s)
if [[ "$date" -ge "$fileLastMod" ]]; then
# Don't remove the root (backup) directory.
if [[ "$file" == "$dir" ]]; then
echo "Root directory, we should skip."
echo "Skipping $dir..."
else
prune_file_prompt
fi
fi
done
fi
# Remove files (Non-interactive)
else
keep_newest_file "$dir"
fi
}
# Prune (Non-interactive).
if [[ -z "$automated" ]]; then
prune_files "$directory" "$numDays"
# Prune (Interactive).
else
prune_files "$directory" "$numDays" "$automated"
fi
echo "${GREEN}Successfully removed $numFilesDeleted files from $directory! ${END_COLOR}"
# If there is an error (or more), indicate the files that failed to be removed.
if [[ $numFilesError -ne 0 ]]; then
echo ""
echo ""
echo "${RED} The following $numFilesError files failed to delete:{END_COLOR}"
printf 'Removed %s...\n' "${fileErrorNames[@]}"
fi
exit 0
#EOF