-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathbatrash
520 lines (506 loc) · 20.8 KB
/
batrash
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
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
#!/bin/bash
#
# batrash is a bash shell script for moving files and directories to a pre-existing trash can.
# this script relies on bash things
[ $(readlink /proc/$$/exe) = "/bin/bash" ] || {
>&2 echo "batrash : this script really needs bash to work well."
return 1
}
# this script is not tested as source
[[ "$0" = "bash" || -z "${0%%*/bash}" ]] && {
>&2 echo "batrash : sourcing this script is not tested, and therefor unsafe."
return 2
}
getRealparent() {
# Echoes the realpath of the parent of the argument. All terminating '/' characters
# should have been removed from the argument before calling this function
# USAGE: $(getRealparent file|directory)
case "$1" in
# make the function complete, but checkAll() prevents this case
( "" ) # "/", "//" or ... with all terminating '/' stripped off
>&2 echo "Root \"/\" has no parent directory"
return 2
;;
( */* )
realpath -e "${1%/*}"
;;
( * )
realpath -e .
;;
esac || {
>&2 echo "Cannot get real path to parent of \"$1\""
return 1
}
}
getRealmntpt() {
# USAGE: $(getMntPt file|directory)
# NOTE : $(getRealmntpt $(getRealmntpt $1)) = $(getRealmntpt $1)
# is not specified to be canonicalized, so we let realpath do that
local mntpt
# TRICKY separate this from the previous line; 'local' has it's own
# return value, which comes after, and thus overrides, the one
# of the command substitution $()
mntpt=$(stat --format %m "$1") || {
>&2 echo "Cannot get mount point for \"$1\""
return 2
}
mntpt=$(realpath "$mntpt") || {
>&2 echo "Cannot get real path to mount point for \"$1\""
return 1
}
# we always need it ending with a (single) '/'; realpath removes that,
# just make sure it did and put it back there
echo "${mntpt%/}"/
}
isAvailableOrWritable() {
# checks if a path either does not exist (name available), therefor can be
# created valid in a writable parent), or is a valid (traversable, writable)
# directory.
# USAGE : isAvailableOrWritable directory && echo Can write || echo Cannot write
# NOTE '-e' is false for existing but broken symbolic link
if [[ -e "$1" || -L "$1" ]]
then
[[ -d "$1" && -x "$1" && -w "$1" ]] ||
return 1
fi
return 0
}
urlencode() {
# url-encodes a string parameter to stdout
# USAGE: $(urlencode string)
# REMARK: for real url encoding for web purposes, this function might fail,
# because it escapes the '!' character, that has meaning in some urls.
## quickie : check if string contains only chars that do not need escaping
[ "${1//[0-9A-Za-z\)\!\(\'*~._\;\/?:@\&=+\$,-]}" = "" ] && echo -n "$1" && return
( # use subshell with byte-per-byte character encoding, otherwise printf "%X" "'$c"
# would print the unicode code-point, a 16-bit integer
export LANG=C
local c
echo -n "$1" | while read -r -N 1 c
do
case "$c" in
# RFC 2396 2.2. Reserved Characters:
# If the data for a URI component would conflict with
# the reserved purpose, then the conflicting data must be escaped
# before forming the URI.
# reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | ","
# NOTE : As our purpose, file name encoding, does not have the kind
# of conflicts that internet URLs do, we just never escape them
( [\;\/?:@\&=+\$,] )
printf "$c"
;;
# RFC 2396 2.3. Unreserved Characters:
# Data must be escaped if it does not have
# a representation using an unreserved character
# unreserved = alphanum | mark
# mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"
( [0-9A-Za-z\)\(\'*~\!._-] )
printf "$c"
;;
# RFC 2396 2.4.3. Excluded US-ASCII Characters:
# Although they are disallowed within the URI syntax [but not for
# filenames], we include here a description of those US-ASCII
# characters that have been excluded [OMITTED: and the reasons for their
# exclusion.]
# control = <US-ASCII coded characters 00-1F and 7F hexadecimal>
# space = <US-ASCII coded character 20 hexadecimal>
# delims = "<" | ">" | "#" | "%" | <">
# unwise = "{" | "}" | "|" | "\" | "^" | "[" | "]" | "`"
# Data corresponding to excluded characters must be escaped in order to
# be properly represented within a URI
( * )
# RFC 2396 2.4. Escape Sequences
# Data must be escaped if it does not have a representation using an
# unreserved character; this includes data that does not correspond to
# a printable character of the US-ASCII coded character set, or that
# corresponds to any US-ASCII character that is disallowed, as
# explained (below) [above].
# bash builtin printf : if the leading character is a single or double
# quote, the value is the ASCII value of the following character.
# NOTE: not the ASCII value, but the unicode code-point, a 16-bit integer,
# for multibyte LANG
printf "%%%2.2X" "'$c"
;;
esac
done
)
}
findHometrash() {
# Sets the variable batrash_mem_hometrash to the path of the valid and existing
# trash can in the user's home directory, or returns error.
# USAGE: findHometrash && echo HOME trash can is valid || echo no valid HOME trash can
if [ -v batrash_mem_hometrash ]
then # already located, or not found if batrash_mem_hometrash is empty string
[ -n "$batrash_mem_hometrash" ] &&
return 0 ||
return 1
fi
local hometrash
# If $XDG_DATA_HOME is not set or empty, it defaults to $HOME/.local/share
[ -n "$XDG_DATA_HOME" ] &&
hometrash="$XDG_DATA_HOME"/Trash ||
hometrash="$HOME/.local/share"/Trash
# NOTE '-e' is false for existing but broken symbolic link
if [[ -e "$hometrash" || -L "$hometrash" ]]
then # if home trash exists, it must be a directory and valid, else fail
if [[ -d "$hometrash" && -x "$hometrash" && -w "$hometrash" ]]
then # valid trash can; if we cannot write to subdirs, report it and fail
{ isAvailableOrWritable "$hometrash/files" &&
isAvailableOrWritable "$hometrash/info"
} ||
{
>&2 echo "Can not write to directories files and/or info in trash can \"$hometrash\""
return 2
}
batrash_mem_hometrash="$hometrash"
return 0
else # trash can exists, but not valid: report it and fail
>&2 echo -n "Invalid trash can \"$hometrash\" for \"$1\" : "
[ -d "$hometrash" ] || >&2 echo "not a directory."
[ -x "$hometrash" ] || >&2 echo "not traversable."
[ -w "$hometrash" ] || >&2 echo "not writable."
return 3
fi
else # home trash can doesn't exist; remember that
batrash_mem_hometrash=
# no error message; spec says to create a hometrash, we won't:
# instead we look for a fallback trashcan at $topdir
# >&2 echo "local trashcan \"$hometrash\" is not a writable directory."
return 4
fi
}
findTrashcan() {
# Locates an appropriate and valid trash can for the argument, if any. The
# function returns false if no trash can is found, or if the argument is in
# a non-writable directory, and hence can't be moved to a trash can either.
# The result is stored in the variable batrash_findtrashcan; other
# batrash_ variables are set as well
# USAGE: findTrashcan file|directory
# return values -lt 20 are about no or invalid trash can
# return values -gt 30 are about the argument
# NOTE: return values are to be coordinated with checkAll()
local realdir
# TRICKY separate this from the previous line; 'local' has it's own
# return value, which comes after, and thus overrides, the one
# of the command substitution $()
realdir=$(getRealparent "$1") ||
return 31
# don't even try trashing if file can't be removed from parent directory
[[ -d "$realdir" && -x "$realdir" && -w "$realdir" ]] || {
>&2 echo "\"$1\" can not be removed from it's parent \"$realdir\": no write access to directory."
[ -d "$realdir" ] || >&2 echo "not a directory."
[ -x "$realdir" ] || >&2 echo "not traversable."
[ -w "$realdir" ] || >&2 echo "not writable."
return 32
}
batrash_realpath="$realdir/${1##*/}"
# check previous OK'ed result, based on parent directory
[ "$batrash_mem_realdir" = "$realdir" ] && {
batrash_findtrashcan="$batrash_mem_dirtrashcan"
return 0
}
# get mountpoint of batrash_realpath, not realdir; if realdir is e.g. a
# bind-mount point, results of some "stat -c %m" are not consistent
# NOT LOCAL, we use batrash_realpath and batrash_realmntpt in xxx.trashinfo
batrash_realmntpt=$(getRealmntpt "$batrash_realpath") ||
return 33
# NOTE: only if path has the prefix, omitting that prefix has an effect
if [ "$batrash_realpath" != "${batrash_realpath#$batrash_realhome/}" ]
then # argument is sub home path, check mountpoint too
# lazy get mountpoint of $HOME
[ -v batrash_mem_homemntpt ] || batrash_mem_homemntpt=$(getRealmntpt "$HOME") || return 7
if [ "$batrash_mem_homemntpt" = "$batrash_realmntpt" ] && findHometrash
then # use home trash can, and remember it for this parent directory
batrash_mem_realdir="$realdir"
batrash_mem_dirtrashcan="$batrash_mem_hometrash"
batrash_findtrashcan="$batrash_mem_hometrash"
return 0
fi
fi
# home trash not suited, previous OK'ed result may fit $batrash_realmntpt
if [ "$batrash_mem_mntpt" = "$batrash_realmntpt" ]
then # use remembered mount point's trash can, and remember it for
# this parent directory too
batrash_mem_realdir="$realdir"
batrash_mem_dirtrashcan="$batrash_mem_mnttrashcan"
batrash_findtrashcan="$batrash_mem_mnttrashcan"
return 0
fi
# try mount point trash can $topdir/.Trash
local trashcan="$batrash_realmntpt".Trash
# NOTE '-e' is false for existing but broken symbolic link;
# link is not allowed, but must lead to error
if [[ -e "$trashcan" || -L "$trashcan" ]]
then
if [[ -d "$trashcan" && -x "$trashcan" &&
-w "$trashcan" && ! -L "$trashcan" &&
( -k "$trashcan" || -f "$trashcan/.stickybit" ) ]]
then # valid trash can; if we cannot write to subdirs, don't try
# next alternative, but report it and fail
# NOTE '-e' is false for existing but broken symbolic link
[[ ! -e "$trashcan/$batrash_uid" && ! -L "$trashcan/$batrash_uid" ]] || # can create it OK: parent writable
# user dir exists, so isAvailableOrWritable reduces to "isWritable"
{
isAvailableOrWritable "$trashcan/$batrash_uid" &&
isAvailableOrWritable "$trashcan/$batrash_uid/files" &&
isAvailableOrWritable "$trashcan/$batrash_uid/info"
} ||
{
! isAvailableOrWritable "$trashcan/$batrash_uid" &&
# if user dir not writable, subdirs must be and exist
{
[[ -e "$trashcan/$batrash_uid/files" || -L "$trashcan/$batrash_uid/files" ]] &&
isAvailableOrWritable "$trashcan/$batrash_uid/files"
} &&
{
[[ -e "$trashcan/$batrash_uid/info" || -L "$trashcan/$batrash_uid/info" ]] &&
isAvailableOrWritable "$trashcan/$batrash_uid/info"
}
} ||
{
>&2 echo "Can not write to directories \"files\" and/or \"info\" in trash can \"$trashcan/$batrash_uid\""
return 5
}
# remember for both mount point and parent directory
batrash_mem_realdir="$realdir"
batrash_mem_dirtrashcan="$trashcan/$batrash_uid"
batrash_mem_mntpt="$batrash_realmntpt"
batrash_mem_mnttrashcan="$trashcan/$batrash_uid"
batrash_findtrashcan="$trashcan/$batrash_uid" # no need to create until moveTotrashcan()
return 0
else # trash can exists, but not valid: report it and fail
>&2 echo -n "Invalid trash can \"$trashcan\" for \"$1\" : "
[ -d "$trashcan" ] || >&2 echo "not a directory."
[ -x "$trashcan" ] || >&2 echo "not traversable."
[ -w "$trashcan" ] || >&2 echo "not writable."
[ -L "$trashcan" ] && >&2 echo "only a link."
[[ -k "$trashcan" || -f "$trashcan/.stickybit" ]] || >&2 echo "not sticky."
return 4
fi
fi
# try personal mount point trash can $topdir/.Trash-uid
# note: requirements for personal mount point trash can are less
# stringent : may be a symlink, need not be sticky
trashcan="$batrash_realmntpt".Trash-"$batrash_uid"
# NOTE '-e' is false for existing but broken symbolic link
if [[ -e "$trashcan" || -L "$trashcan" ]]
then
if [[ -d "$trashcan" && -x "$trashcan" && -w "$trashcan" ]]
then # valid trash can; if we cannot write to subdirs, report it and fail
{ isAvailableOrWritable "$trashcan/files" &&
isAvailableOrWritable "$trashcan/info"
} ||
{
>&2 echo "Can not write to directories files and/or info in trash can \"$trashcan\""
return 3
}
# remember for both mount point and parent directory
batrash_mem_realdir="$realdir"
batrash_mem_dirtrashcan="$trashcan"
batrash_mem_mntpt="$batrash_realmntpt"
batrash_mem_mnttrashcan="$trashcan"
batrash_findtrashcan="$trashcan"
return 0
else # trash can exists, but not valid: report it and fail
>&2 echo -n "Invalid trash can \"$trashcan\" for \"$1\" : "
[ -d "$trashcan" ] || >&2 echo "not a directory."
[ -x "$trashcan" ] || >&2 echo "not traversable."
[ -w "$trashcan" ] || >&2 echo "not writable."
return 2
fi
fi
>&2 echo "No trash can found for \"$1\""
return 1 # no trash can found
}
moveTotrashcan() {
# USAGE : moveTotrashcan file
# NOTE '-e' is false for existing but broken symbolic link
[[ -e "$1" || -L "$1" ]] || {
# checkAll would halt on this, but now that we are effectively
# trashing, $1 may have been trashed for previous argument,
# via link, directory, or just double occurence
>&2 echo "\"$1\" does not exist anymore. Continuing."
return 0
}
findTrashcan "$1" ||
return 1
local trashcan="$batrash_findtrashcan"
local trashcanmeta="$trashcan/info"
local trashcandata="$trashcan/files"
# - Because we alreay checked that the trash can itself exists, the 1st
# directory is only created effectively when it is the user compartment
# of a global trashcan does not yet exist
# - Trash cans are named per user (/home/$USER/.local/share/Trash,
# .Trash/$batrash_uid, .Trash-$batrash_uid) for a reason, so keep
# them personal and set mode 0700
mkdir -pm=0700 "$trashcan" "$trashcanmeta" "$trashcandata" || {
>&2 echo "Could not create trash can directories \"$trashcandata\" and \"$trashcanmeta\" for \"$1\""
return 2
}
# value of the Path= key in .trashinfo, which is relative to the directory
# where the trash can is located. For the home trash can this is
# "~/.local/share" : don't bother trying to remove that as a prefix,
# other implementations don't, and the standard does allow absolute path
# for the home trash can. For all other trash cans, this is the mount point.
local pathval
[ "$trashcan" = "$batrash_mem_hometrash" ] && pathval="$batrash_realpath" || pathval="${batrash_realpath#$batrash_realmntpt}" # remember we terminate each realmntpt with '/'
pathval=$(urlencode "$pathval")
local basename="${batrash_realpath##*/}"
# make room for appending ".trashinfo"
local trashname=".trashinfo" # just to get it's length (yes I know it's 10)
local maxlen=$((128 - ${#trashname}))
trashname="${basename:0:$maxlen}"
# First create the .trashinfo, as per the _-Standard-_, as atomically as possible.
# Use process id as uniquifier: at some time another process might have had the
# same id, but no other running process on this computer does, so we can
# search for a unique name without fear of racing against another process.
local num=0
local maxnum=1000 # max. retries
while [ $num -lt $maxnum ] # just "true" should be enough, this feels safer
do
{ # with noclobber as set in trashAll(), > fails if target exists
# we encase command in {} to redirect errors to /dev/null outside it
cat <<-trashinfo > "$trashcanmeta/$trashname".trashinfo
[Trash Info]
Path=$pathval
DeletionDate=$(date +%FT%T)
trashinfo
} &> /dev/null && break
# no success; try to find a unique suffix, but first make sure
# that existence of .trashinfo name is the cause of failure
# NOTE '-e' is false for existing but broken symbolic link
[[ -e "$trashcanmeta/$trashname".trashinfo || -L "$trashcanmeta/$trashname".trashinfo ]] || {
>&2 echo "Could not write trashinfo data for \"$1\" in \"$trashcanmeta\""
return 3
}
# do increment the counter first, to avoid never reaching stop condition
# NOTE '-e' is false for existing but broken symbolic link
while [[ $((++num)) -lt $maxnum && ( -e "$trashcanmeta/$trashname".trashinfo || -L "$trashcanmeta/$trashname".trashinfo ) ]]
do
local suffix="~$$~$num"
trashname="${basename:0:(($maxlen - ${#suffix}))}$suffix"
done
[ $num -lt $maxnum ] ||
{ # desperate as we are now, we might also try to generate random
# names, but for now we don't
>&2 echo "Failed to generate a unique trash name for \"$1\" in \"$trashcanmeta\""
return 4
}
done
# on no success, we do not get here, so just go on
mv "$1" "$trashcandata/$trashname" ||
{ # clean up, signal, and return
rm "$trashcanmeta/$trashname".trashinfo
>&2 echo "Could not move \"$1\" to \"$trashcandata/$trashname\""
return 5
}
return 0
}
trashAll() {
# moves all arguments to their respective trash can, continuing with the
# others if one fails
# USAGE : trashAll file...
local retval=0
# use the noclobber shell attribute for creating a unique .trashinfo
# filename. $- is list of 'set' options; if noclobber is set,
# it contains a 'C'. An easy way to dectect that 'C' is to use variable
# expansion with pattern matching; a pattern "*C*" matches and is removed
# if and only if the variable contains a C, and removing it makes the
# variable empty. To distinguish this from a "$-" result that was empty
# to start with, we make sure shellAttrC initially is not empty by
# construction: it contains at least a '_'.
local shellAttrC="_$-"
shellAttrC="${shellAttrC##*C*}" # is now empty if 'C' was set
set -C # -o noclobber
for arg in "$@"
do
moveTotrashcan "$arg" || retval=1
done
# restore noclobber option if needed : if 'C' was set, don't unset
[ -z "$shellAttrC" ] || set +C
return "$retval"
}
checkAll() {
# checks whether all arguments can be moved to a valid trash, halting if
# any one of them fails
# USAGE : checkAll file...
# return values -lt 20 are about no or invalid trash can, others about the argument
# NOTE: return values are to be coordinated with findTrashcan()
for arg in "$@"
do # NOTE '-e' is false for existing but broken symbolic link
[[ -e "$arg" || -L "$arg" ]] || {
>&2 echo "Argument \"$arg\" does not exist."
return 21
}
arg="${arg%%+(/)}"
[ -z "$arg" ] && {
>&2 echo "Cannot trash root \"/\""
return 22
}
# return values are coordinated with those of findTrashcan,
# pass them on to caller
findTrashcan "$arg" || return $?
done
return 0
}
clearBatrash() {
#clears the batrash memory, variables used to minimize disk access
# script constants
unset batrash_realhome batrash_uid batrash_resetextglob
# set for each script argument
unset batrash_realpath batrash_realmntpt
unset batrash_findtrashcan
# memory from one script argument to the next, to
# minimize disk access
unset batrash_mem_realdir batrash_mem_dirtrashcan
unset batrash_mem_mntpt batrash_mem_mnttrashcan
unset batrash_mem_homemntpt batrash_mem_hometrash
}
# NOTE : encapsulating batrash in a function makes it easy to break off
# with return instead of exit. Exit would end the shell if you source
# batrash from it.
main() {
clearBatrash
# home trash can has preference, so every arg to trash must be checked
# against $HOME
batrash_realhome=$(realpath "$HOME") || {
>&2 echo "Cannot get the path to your home directory from the variable \"\$HOME\""
>&2 echo "Batrash really needs to know your home directory to do anything for you."
return 1
}
# all other trash cans need uid
batrash_uid=$(id -u) || {
>&2 echo "Cannot get your used id number from the command \"id\""
>&2 echo "Batrash really needs to know your user id number to do anything for you."
return 2
}
# The easiest method to strip all terminating '/', like Gnome's
# "gio trash"/"gvfs-trash" and trash-cli's "trash-put" do (even for
# file arguments!!), is with extglob ON. For checkAll() we do this per
# argument, in the function itself, so that we can first check that an
# argument with terminating '/' is not a file. Once those checks are done,
# we can strip the terminating '/' off all arguments at once for trashAll().
# NOTE : realpath removes terminating '/' too, and other superfluous '/',
# but as we don't pass last path components to it, because realpath also
# dereferences symlinks, we have to strip a terminating '/' explictely.
batrash_resetextglob=$(shopt -p extglob) # -p : print syntax to set current state
shopt -s extglob # activate extended globbing patterns like +(/)
# can use return value only once, so store $? in a variable for repeated use
# first this, so that return value of the 'local' command isn't mixed in
local check
checkAll "$@"
check=$?
if [ "$check" = 0 ]
then
trashAll "${@%%+(/)}" || >&2 echo "Not all files trashed."
else
[ "$check" -lt 20 ] && {
>&2 echo "Taking no action; create appropriate trash cans first."
} || {
>&2 echo "Taking no action for any argument."
}
fi
$batrash_resetextglob # reset, in case we forget this script should not be sourced
clearBatrash
}
main "$@"