-
Notifications
You must be signed in to change notification settings - Fork 0
/
heal-bitrots.sh
executable file
·303 lines (250 loc) · 9.78 KB
/
heal-bitrots.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
#!/usr/bin/env bash
#Automatic check and self-healing for bitrot :)
#Requires:
#pip install --user bitrot
#dnf/apt/x install par2cmdline
# dir where will be saved the recovery data (bitrot dbs and par2 files)
# better mirror/backup/sync it also ;)
declare BITROT_BACKUPS=${BITROT_BACKUPS:-~/.bitrot_backups}
declare BITROT_BACKUPS_DEST=${BITROT_BACKUPS_DEST:-$BITROT_BACKUPS/bitrot_dirs}
declare PAR2_NAME=files.par2
declare LOG_CREATE=/tmp/.generate_par2_create.log
declare LOG_REPAIR=/tmp/.generate_par2_repair.log
declare LOG_BITROT=/tmp/.generate_bitrot_db.log
declare DEFAULT_OPTIONS=
[[ -d $BITROT_BACKUPS ]] || mkdir -p "$BITROT_BACKUPS"
hash bitrot &>/dev/null || { echo "Needs bitrot. Install it with: pip install --user bitrot" ; exit 1; }
hash par2 &>/dev/null || { echo "Needs par2. Install it with: dnf/apt/x install par2cmdline" ; exit 1; }
######################
# GLOBAL VARIABLES #
######################
declare -i max_files=32768
declare -i max_block_count=2000
declare -i redundancy=5
###################
# MAIN FUNCTIONS #
###################
set_default_global_options() {
#let's use half free memory
local memgrep=($(grep MemAvailable /proc/meminfo))
local mBFree=$((${memgrep[1]}/1024/2))
DEFAULT_OPTIONS+=" -m${mBFree}"
#set number of threads
#if more than 2 processors, don't use all due performance fall
local cpus=$(getconf _NPROCESSORS_ONLN)
(($cpus > 2)) && DEFAULT_OPTIONS+=" -t$((cpus-2))"
}
generate_par2files() {
local -a target_files=()
local target_dir=$(realpath -P "$1")
local source_dir=$BITROT_BACKUPS/${target_dir:1}
local par2_files=$source_dir/$PAR2_NAME
local local_options=
err() {
echo "$@"
echo "See $LOG_CREATE or $LOG_REPAIR for errors."
exit 1
}
echo "----------------------------------"
echo "Generating par2 files for $target_dir"
# It could be that $target_dir has been deleted or renamed.So not fail
[[ -d $target_dir ]] || { echo "$target_dir not a dir or not found"; return; }
#Create $source_dir
mkdir -p $source_dir || err "Couldn't create $source_dir"
# change local dir
echo "Change local directory to $target_dir"
cd "$target_dir" || err "Couldn't cd into $target_dir "
#check the dir files
local total_files=$(find . -maxdepth 1 -type f | wc -l)
if (( $total_files > $max_files )); then
err "Number of files in $target_dir: $total_files > $max_files"
fi
#check that there aren't more than $max_block_count in $target_dir
if (( $total_files > $max_block_count )); then
local size=($(du -bs $target_dir))
#calculate block size for $redundancy%
local target_dir_tam=$((${size[0]}*$redundancy/100))
#get size in MBs of $target_dir
local block_bytes=$(( $target_dir_tam / $max_block_count ))
#final adjust to be multiple of 4!!
local block_size=$(( $block_bytes * 2**2 ))
echo "Increasing the default block size for $target_dir to $block_size bytes"
local_options+=" -s${block_size}"
fi
for file in $(find . -maxdepth 1 -type f ! -size 0); do
target_files+=("$file")
done
if (( ${#target_files} > 0 )); then
#generate new par2 files
echo "Launching par2create "
if ! par2create $DEFAULT_OPTIONS $local_options -v $PAR2_NAME "${target_files[@]}" &>$LOG_CREATE ; then
\rm -f *.par2
err "Couldn't generate par2 files for $target_dir"
fi
#Copy par2 files and bitrot database to $source_dir
echo "Moving par2 files to $source_dir"
if ! mv *.par2 $source_dir/ ; then
\rm -f *.par2
err "Couldn't copy par2 files and bitrot database to $source_dir/"
fi
fi
echo "Done!"
}
split_dir() {
local line= temp_file= par2_files=
local target_dir=$(realpath -P "$1" 2> /dev/null)
local source_dir=$BITROT_BACKUPS/${target_dir:1}
local regex_bitrot='error: SHA.* mismatch for (.*): expected .*'
local regex_changes='([[:digit:]]* entries in the database. )?([[:digit:]]*) entries (updated|new|missing):'
local regex_moved='([[:digit:]]* entries in the database. )?([[:digit:]]*) entries renamed:'
local regex_dir_changes='(.*)'
local regex_dir_moved='from (.*) to (.*)'
local -a com=(find . -type d)
local -A dirs_bitrot=()
local -A dirs_change=()
local -A dirs_moved=()
local -i count=0
local -i count_moved=0
err() {
echo "$@"
echo "See $LOG_BITROT, $LOG_CREATE or $LOG_REPAIR for errors."
exit 1
}
echo "----------------------------------"
echo "Checking $target_dir for bitrot"
#check if exists after backups dir check
[[ -d $target_dir ]] || err "$target_dir not a dir or not found"
#check if target is just the backups dir
local full_target=$target_dir/
if [[ ${full_target::$((${#BITROT_BACKUPS}+1))} == $BITROT_BACKUPS/ ]]; then
echo "$target_dir is a subdir of $BITROT_BACKUPS. Next!."
return
fi
cd "$target_dir" || err "couldn't cd to $target_dir"
if [[ -f $source_dir/.bitrot.db ]]; then
echo "Copying bitrot db files to ."
if ! cp $source_dir/.bitrot.* .; then
err "Couldn't copy $source_dir/.bitrot.* files to ."
fi
fi
echo "Launching bitrot -v"
bitrot -v &>$LOG_BITROT
while read -r line; do
#no change detected
if (( $count == 0 && $count_moved == 0 )); then
#check log for bitrot errors
if [[ $line =~ $regex_bitrot ]]; then
temp_file="$target_dir/${BASH_REMATCH[1]:2}"
echo "bitrot detected in file:$temp_file"
#add to unique index (associative array)
# and save file just in case
dirs_bitrot["${temp_file%/*}/"]="$temp_file"
fi
if [[ $line =~ $regex_changes ]]; then
count=${BASH_REMATCH[2]}
# echo "->New/updated/missing files detected"
fi
if [[ $line =~ $regex_moved ]]; then
count_moved=${BASH_REMATCH[2]}
# echo "->renamed files detected"
fi
else #change detected
if (( $count > 0 )); then
if [[ $line =~ $regex_dir_changes ]]; then
file=${BASH_REMATCH[1]}
echo "change detected in:$file"
dirs_change["${file%/*}/"]="$file"
((count--))
fi
elif (( $count_moved > 0 )); then
if [[ $line =~ $regex_dir_moved ]]; then
orig=${BASH_REMATCH[1]}
dest=${BASH_REMATCH[2]}
echo "Move detected from:$orig to:$dest"
dirs_moved["${orig%/*}"]=1
dirs_moved["${dest%/*}"]=1
((count_moved--))
fi
fi
fi
done <$LOG_BITROT
for dir in "${!dirs_bitrot[@]}"; do
cd "$dir" || err "Couldn't cd into $dir "
echo "Recovering from bitrot with par2 files in $dir"
par2_files=$BITROT_BACKUPS/${dir:1}/$PAR2_NAME
#if there's a par2 files generated already copy them
if [[ -f $par2_files ]]; then
echo "Copying $par2_files to ."
if ! cp "${par2_files%/*}/"*.par2 .; then
\rm -f *.par2
err "Couldn't copy par2 files to $target_dir"
fi
fi
#Purge recovery par2 files if successfull
echo "Repairing bitrot files with par2repair"
if ! par2repair -p $PAR2_NAME &>$LOG_REPAIR; then
\rm -f *.par2
cd "$target_dir" || err "couldn't cd to $target_dir"
if [[ -e .bitrot.db ]]; then
[[ -d $source_dir ]] || mkdir -p "$source_dir"
echo "Moving bitrot db files to $source_dir"
if ! mv .bitrot.* $source_dir; then
\rm -f .bitrot.*
err "Couldn't move bitrot files to $source_dir"
fi
fi
err "Couldn't repair $dir with par2 files"
else #update the db for the new changes
cd "$target_dir" || err "couldn't cd to $target_dir"
echo "Launching bitrot -v after bitrot"
bitrot -v &>$LOG_BITROT
fi
cd - &>/dev/null
done
#regenerate dir changes
for dir in "${!dirs_change[@]}"; do
cd "$target_dir" || err "couldn't cd to $target_dir"
echo "launching generate_par2files in:$dir"
generate_par2files "${dir}"
done
#regenerate dir moved
for dir in "${!dirs_moved[@]}"; do
#if it doesn't exist it just normal in this case
[[ -d $dir ]] || continue
cd "$target_dir" || err "couldn't cd to $target_dir"
echo "launching generate_par2files in:$dir"
generate_par2files "${dir}"
done
if (( ${#dirs_bitrot} == 0 && ${#dirs_change[@]} == 0 && ${#dirs_moved[@]} )); then
echo "No changes detected."
fi
cd "$target_dir" || err "couldn't cd to $target_dir"
if [[ -e .bitrot.db ]]; then
[[ -d $source_dir ]] || mkdir -p "$source_dir"
echo "Moving bitrot db files to $source_dir"
if ! mv .bitrot.* $source_dir; then
\rm -f .bitrot.*
err "Couldn't move bitrot files to $source_dir"
fi
fi
echo "Done!"
}
#set memory and number of threads
set_default_global_options
cur=$PWD
if [[ -n $1 ]]; then
#For each path
for path; do
cd "$cur"
split_dir "$path"
done
elif [[ -f $BITROT_BACKUPS_DEST ]]; then
while IFS= read -r path
do
cd "$cur"
split_dir "$path"
done < $BITROT_BACKUPS_DEST
else
echo "You must pass the target paths or set $BITROT_BACKUPS_DEST with your absolute target paths in each line"
exit 1
fi