-
Notifications
You must be signed in to change notification settings - Fork 29
/
nicotine.bash
326 lines (275 loc) · 8.94 KB
/
nicotine.bash
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
#!/usr/bin/env bash
# ___ ___ ___ ___
# /\__\ /\ \ /\ \ /\__\
# /:/ / \:\ \ \:\ \ /::| |
# /:/__/ \:\ \ \:\ \ /:|:| |
# /::\ \ ___ /::\ \ /::\ \ /:/|:|__|__
# /:/\:\ /\__\ /:/\:\__\ /:/\:\__\ /:/ |::::\__\
# \/__\:\/:/ / /:/ \/__/ /:/ \/__/ \/__/~~/:/ /
# \::/ / /:/ / /:/ / /:/ /
# /:/ / \/__/ \/__/ /:/ /
# /:/ / /:/ /
# \/__/ \/__/
#
# Copyright (c) 2023, Robert Swinford <robert.swinford<...at...>gmail.com>
#
# For the full copyright and license information, please view the LICENSE file
# that was distributed with this source code.
set -euf -o pipefail
#set -x
print_version() {
printf "\
nicotine $(httm --version | cut -f2 -d' ')
" 1>&2
exit 0
}
print_usage() {
local nicotine="\e[31mnicotine\e[0m"
local httm="\e[31mhttm\e[0m"
local git="\e[31mgit\e[0m"
local tar="\e[31mtar\e[0m"
printf "\
$nicotine is a wrapper script for $httm which converts unique snapshot file versions to a $git archive.
USAGE:
nicotine [OPTIONS]... [file1 file2...]
OPTIONS:
--output-dir:
Select the output directory. Default is the current working directory.
--no-archive
Disable archive creation. Create a new $git respository (e.g. named \"\$file1-git\")
in the output directory.
--debug:
Show $git and $tar command output. Default is to complete silence both.
--help:
Display this dialog.
--version:
Display script version.
" 1>&2
exit 1
}
print_err_exit() {
print_err "$@"
exit 1
}
print_err() {
printf "%s\n" "Error: $*" 1>&2
}
prep_exec() {
[[ -n "$(
command -v find
exit 0
)" ]] || print_err_exit "'find' is required to execute 'nicotine'. Please check that 'find' is in your path."
[[ -n "$(
command -v readlink
exit 0
)" ]] || print_err_exit "'readlink' is required to execute 'nicotine'. Please check that 'readlink' is in your path."
[[ -n "$(
command -v git
exit 0
)" ]] || print_err_exit "'git' is required to execute 'nicotine'. Please check that 'git' is in your path."
[[ -n "$(
command -v tar
exit 0
)" ]] || print_err_exit "'tar' is required to execute 'nicotine'. Please check that 'targit' is in your path."
[[ -n "$(
command -v mktemp
exit 0
)" ]] || print_err_exit "'mktemp' is required to execute 'nicotine'. Please check that 'mktemp' is in your path."
[[ -n "$(
command -v mkdir
exit 0
)" ]] || print_err_exit "'mkdir' is required to execute 'nicotine'. Please check that 'mkdir' is in your path."
[[ -n "$(
command -v httm
exit 0
)" ]] || print_err_exit "'httm' is required to execute 'nicotine'. Please check that 'httm' is in your path."
}
function copy_add_commit {
local debug=$1
shift
local path="$1"
shift
local dest_dir="$1"
shift
if [[ -d "$path" ]]; then
cp -a "$path" "$dest_dir/"
# return early -- only commit files not directories
return 0
else
cp -a "$path" "$dest_dir"
fi
if [[ $debug = true ]]; then
git add --all "$dest_dir"
git commit -m "httm commit from ZFS snapshot" --date "$(date -d "$(stat -c %y $path)")" || true
else
git add --all "$dest_dir" > /dev/null
git commit -q -m "httm commit from ZFS snapshot" --date "$(date -d "$(stat -c %y $path)")" > /dev/null || true
fi
}
function get_unique_versions {
local debug=$1
shift
local path="$1"
shift
local dest_dir="$1"
shift
local -a version_list
# all we care about is unique file versions, unique directory versions aren't useful
# in this context, as we are recursing and commiting every file we find, we can skip
# commiting all directory versions we find
[[ -d "$path" ]] || while read -r line; do
version_list+=("$line")
done <<<"$(httm -n --omit-ditto "$path")"
# see above, one/zero version indicates $path has no snaps
if [[ -d "$path" ]] || [[ ${#version_list[@]} -eq 0 ]] || [[ ${#version_list[@]} -eq 1 ]]; then
copy_add_commit $debug "$path" "$dest_dir"
else
for version in "${version_list[@]}"; do
copy_add_commit $debug "$version" "$dest_dir"
done
fi
}
function traverse {
local debug=$1
shift
local path="$1"
shift
local dest_dir="$1"
shift
get_unique_versions $debug "$path" "$dest_dir"
# return early - if is file, can't traverse
[[ -d "$path" ]] || return 0
local -a dir_entries=()
local basename="$(basename "$path")"
while read -r line; do
[[ -z "$line" ]] || dir_entries+=("$line")
done <<<"$(find "$path" -mindepth 1 -maxdepth 1)"
# return early - empty dir
[[ ${#dir_entries[@]} -ne 0 ]] || return 0
for entry in "${dir_entries[@]}"; do
if [[ -d "$entry" ]]; then
traverse $debug "$entry" "$dest_dir/$basename"
else
get_unique_versions $debug "$entry" "$dest_dir/$basename"
fi
done
}
function convert_to_git {
local debug=$1
shift
local no_archive=$1
shift
local working_dir="$1"
shift
local tmp_dir="$1"
shift
local output_dir="$1"
shift
local path="$1"
shift
local archive_dir=""
local basename=""
# create dir for file
raw="$(basename "$path")"
# Use parameter expansion to remove leading dot: "${FILENAME#.}"
basename="${raw#.}"
# git requires a dir to init
archive_dir="$tmp_dir/$basename"
mkdir "$archive_dir" || print_err_exit "nicotine could not create a temporary directory. Check you have permissions to create."
# ... and we must enter the dir to have git work
cd "$archive_dir" || print_err_exit "nicotine could not enter a temporary directory: $archive_dir. Check you have permissions to enter."
# create git repo
if [[ $debug = true ]]; then
git init || print_err_exit "git could not initialize directory"
else
git init -q >/dev/null || print_err_exit "git could not initialize directory"
fi
# copy, add, and commit to git repo in loop
# why branch? because git requires a dir and
# for files we create a dir specifically for the file
if [[ -d "$path" ]]; then
traverse $debug "$path" "$tmp_dir"
else
traverse $debug "$path" "$archive_dir"
fi
if [[ $no_archive = true ]]; then
cp -ra "$archive_dir" "$output_dir/$basename-git"
else
# tar works with relative paths, so make certain we are in our base tmp_dir
cd "$tmp_dir"
# create archive
local output_file="$output_dir/$basename-git.tar.gz"
if [[ $debug = true ]]; then
tar -zcvf "$output_file" "./$basename" || print_err_exit "Archive creation failed. Quitting."
else
tar -zcvf "$output_file" "./$basename" > /dev/null || print_err_exit "Archive creation failed. Quitting."
fi
fi
# cleanup safely
[[ ! -e "$tmp_dir/$basename" ]] || rm -rf "$tmp_dir/$basename"
[[ -e "$tmp_dir/$basename" ]] || rm -rf "$tmp_dir/*"
if [[ $no_archive = true ]]; then
printf "nicotine git repository created successfully: $basename-git\n"
else
printf "nicotine git archive created successfully: $output_file\n"
fi
}
function nicotine {
# do we have commands to execute?
prep_exec
local debug=false
local no_archive=false
local working_dir
local output_dir
working_dir="$(realpath "$( pwd )" 2>/dev/null || true)"
[[ -e "$working_dir" ]] || print_err_exit "Could not obtain current working directory. Quitting."
output_dir="$working_dir"
[[ $# -ge 1 ]] || print_usage
[[ "$1" != "-h" && "$1" != "--help" ]] || print_usage
[[ "$1" != "-V" && "$1" != "--version" ]] || print_version
while [[ $# -ge 1 ]]; do
if [[ "$1" == "--output-dir" ]]; then
shift
[[ $# -ge 1 ]] || print_err_exit "output-dir argument is empty"
output_dir="$(realpath "$1" 2>/dev/null || true)"
[[ -e "$output_dir" ]] || print_err_exit "Could not obtain output directory from path given. Quitting."
shift
elif [[ "$1" == "--debug" ]]; then
shift
debug=true
elif [[ "$1" == "--no-archive" ]]; then
shift
no_archive=true
else
break
fi
done
local tmp_dir="$( mktemp -d )"
trap "[[ ! -d "$tmp_dir" ]] || rm -rf "$tmp_dir"" EXIT
[[ -e "$tmp_dir" ]] || print_err_exit "Could not create a temporary directory for scratch work. Quitting."
[[ $# -ne 0 ]] || print_err_exit "User must specify at least one input file. Quitting."
for a; do
canonical_path="$(
realpath "$a" 2>/dev/null
[[ $? -eq 0 ]] || print_err "Could not determine canonical path for: $a"
)"
[[ -n "$canonical_path" ]] || continue
# check if file exists
if [[ ! -e "$canonical_path" ]]; then
printf "$canonical_path does not exist. Skipping.\n"
continue
fi
# ... and tar will not create an archive for an empty dir
if [[ -d "$canonical_path" ]] && [[ -z "$(find "$canonical_path" -mindepth 1 -maxdepth 1)" ]]; then
printf "$canonical_path is an empty directory. Skipping.\n"
continue
fi
# if not a file, directory, or symlink, we can't use so also skip
if [[ ! -f "$canonical_path" && ! -d "$canonical_path" && ! -L "$canonical_path" ]]; then
printf "$canonical_path is not a file, directory, or symlink. Skipping.\n"
continue
fi
convert_to_git $debug $no_archive "$working_dir" "$tmp_dir" "$output_dir" "$canonical_path"
done
}
nicotine "$@"