add kvpairs2td
[hband-tools.git] / user-tools / takeown
blob587ab4f34ce6c65d5d539562499696a7db8bd97e
1 #!/bin/bash
3 . /usr/lib/tool/bash-utils || exit 1
5 verbose()
7 local cmd=$1
8 shift
9 command "$cmd" ${verbose:+-v} "$@"
12 run()
14 if [ "$dryrun" = yes ]
15 then
16 [ "$1" != verbose ] || shift
17 echo "$@" >&2
18 else
19 "$@"
23 debug()
25 [ "$debug" != yes ] || warnx "$@"
28 warnx()
30 echo "$PROGNAME: $*" >&2
33 yesno()
35 "$@" && echo yes || echo no
38 no_err_trap()
40 trap - ERR
41 "$@"
44 echo_status()
46 "$@"
47 echo $?
50 is_empty_directory()
52 local content=`no_err_trap find "$1" -mindepth 1 -printf 1 -quit`
53 if [ -z "$content" ]
54 then
55 return 0
56 else
57 return 1
61 is_writable_dir()
63 local access_code=`echo_status no_err_trap [ -w "$1" ]`
65 if [ "$dryrun" != yes ]
66 then
67 if [ "$activewritetestonly" = yes -o \( $access_code = 0 -a "$activewritetest" = yes \) ]
68 then
69 local testfile=''
70 while [ -z "$testfile" -o -e "$testfile" ]
72 testfile=$1/takeown-writetest.$RANDOM
73 done
74 access_code=`echo_status no_err_trap touch "$testfile"`
75 [ ! -e "$testfile" ] || rm "$testfile" || true
78 return $access_code
81 is_recursive()
83 return `echo_status no_err_trap [ "$recursive" = yes ]`
86 besteffort_mode()
88 return `echo_status no_err_trap [ "$besteffort" = yes ]`
91 strip_trailing_slashes()
93 local str=$1
94 while [ "${str: -1}" = / ]
96 str=${str%/}
97 done
98 echo "$str"
101 strict()
103 return `echo_status no_err_trap [ "${strict[$1]}" = yes ]`
106 no_strict()
108 if [ $besteffort = yes ]
109 then
110 return 0
112 if [ $silent_strictness_check != yes ]
113 then
114 warnx "Can not clone file's $1: $2"
116 return `echo_status no_err_trap [ "${strict[$1]}" != yes ]`
119 sort_by_length_desc()
121 local x
122 local y
123 local index
124 local shiftindex
125 declare -a -g sort_by_length_desc_result
126 sort_by_length_desc_result=()
128 for x in "$@"
130 index=0
131 for y in "${sort_by_length_desc_result[@]}"
133 if [ ${#y} -le ${#x} ]
134 then
135 shiftindex=${#sort_by_length_desc_result[@]}
136 while [ $shiftindex -ge $index ]
138 sort_by_length_desc_result[$shiftindex]=${sort_by_length_desc_result[shiftindex - 1]}
139 shiftindex=$[shiftindex - 1]
140 done
141 break
143 index=$[index + 1]
144 done
145 sort_by_length_desc_result[$index]=$x
146 done
149 main()
151 local argv
152 argv=("$@")
154 if ! is_recursive
155 then
156 # Sort CLI arguments to make "dir1/file1" files processed earlier than "dir1/".
157 sort_by_length_desc "${argv[@]}"
158 declare -a argv
159 argv=("${sort_by_length_desc_result[@]}")
162 local file
163 for file in "${argv[@]}"
165 file=`no_err_trap strip_trailing_slashes "$file"`
167 local owner=`no_err_trap stat -c %u "$file"`
169 if [ "$owner" != "$UID" ]
170 then
171 if [ -L "$file" ]
172 then
173 takeown symlink "$file"
174 elif [ -d "$file" ]
175 then
176 takeown directory "$file"
177 else
178 takeown file "$file"
181 done
184 subs_file_comment()
186 # Replace filename in lines like: `# file: ...´
187 sed -e "s@^# file:.*@# file: ${1//@/\@}@"
190 takeown()
192 # the file/directory to be taken own
193 local TAKEOWN_FILE=$2
194 # the file/directory to be a clone of TAKEOWN_FILE (and supersede thereafter)
195 local TAKEOWN_COPY=$2.takeown
196 # the file/directory TAKEOWN_FILE will be renamed to (can be used as backup)
197 local TAKEOWN_BACK=$2.tookown
198 local errstatus
200 debug "${FUNCNAME[0]}-$1 $2"
202 # clear tracking arrays
203 files_copied_out_src=()
204 files_copied_out_trg=()
205 files_moved_out_src=()
206 files_moved_out_trg=()
207 dirs_created=()
209 # Run individual takeown procedures in subshell to avoid cascade exception.
211 case "$1" in
212 symlink)
213 takeown_symlink
215 file)
216 takeown_file
218 directory)
219 takeown_directory
221 esac
223 errstatus=$?
224 if [ $errstatus != 0 ]
225 then
226 if [ "$ignoreerrors" = yes ]
227 then
228 Error=$errstatus
229 else
230 exit $errstatus
235 cleanup()
237 if [ $do_cleanup = yes ]
238 then
239 warnx "Error happened. Reverting changes..."
241 case "$1" in
242 symlink)
243 run verbose rm --force "$TAKEOWN_COPY"
245 file)
246 run verbose rm --force "$TAKEOWN_COPY"
248 directory)
249 local idx
251 warnx "Removing copies..."
252 idx=$[ ${#files_copied_out_trg[@]} - 1 ]
253 for ((; idx>=0; idx--))
255 run verbose rm --force "${files_copied_out_trg[idx]}"
256 done
258 warnx "Moving back moved files..."
259 idx=$[ ${#files_moved_out_trg[@]} - 1 ]
260 for ((; idx>=0; idx--))
262 run verbose mv --force --no-target-directory "${files_moved_out_trg[idx]}" "${files_moved_out_src[idx]}"
263 done
265 warnx "Removing created directories..."
266 idx=$[ ${#dirs_created[@]} - 1 ]
267 for ((; idx>=0; idx--))
269 run verbose rmdir "${dirs_created[idx]}"
270 done
272 # the last step of taking own a directory is to replace TAKEOWN_FILE with TAKEOWN_COPY.
273 # until this step we're working on and in TAKEOWN_COPY,
274 # so it's save to remove it.
275 if [ -e "$TAKEOWN_COPY" ]
276 then
277 run verbose rmdir "$TAKEOWN_COPY"
280 if [ -e "$TAKEOWN_BACK" ]
281 then
282 run verbose mv --force --no-target-directory "$TAKEOWN_BACK" "$TAKEOWN_FILE"
285 esac
286 else
287 warnx "Error happened. Keep files."
291 takeown_symlink()
293 try --except "cleanup symlink"
294 copy_out_symlink "$TAKEOWN_FILE" "$TAKEOWN_COPY"
295 run verbose mv --force --no-target-directory "$TAKEOWN_COPY" "$TAKEOWN_FILE"
296 untry
299 takeown_file()
301 try --except "cleanup file"
302 copy_out_file "$TAKEOWN_FILE" "$TAKEOWN_COPY"
303 run verbose mv --force --no-target-directory "$TAKEOWN_COPY" "$TAKEOWN_FILE"
304 untry
307 register_moved_file()
309 files_moved_out_src+=("$1")
310 files_moved_out_trg+=("$2")
313 register_copied_file()
315 files_copied_out_src+=("$1")
316 files_copied_out_trg+=("$2")
319 register_created_dir()
321 dirs_created+=("$1")
324 copy_attributes()
326 if [ "$do_chgrp" = yes ]
327 then
328 run verbose chgrp --quiet --reference="$1" --no-dereference "$2" || no_strict group "$1"
330 # First chgrp then chmod to prevent to clear setgid bit.
331 run verbose chmod --quiet --reference="$1" "$2" || no_strict mode "$1"
332 getfacl --skip-base "$1" | subs_file_comment "$2" | run setfacl --restore=- || no_strict acl "$1"
333 getfattr --no-dereference --physical --dump "$1" | subs_file_comment "$2" | run setfattr --restore=- || no_strict xattr "$1"
334 # TODO support ext2 attributes
337 copy_out()
339 local filetype=$1
340 local errstatus
341 shift
342 COPYOUT_SRC=$1
343 COPYOUT_TRG=$2
345 # run in subshell to able to check best-effort mode.
346 # enclose in try/untry to clear "cleanup" ERR trap.
348 set +e
350 set -e
351 case "$filetype" in
352 symlink)
353 copy_out_symlink "$@"
355 file)
356 copy_out_file "$@"
359 warnx "internal error: copy_out $filetype $*"
360 exit 1
362 esac
364 errstatus=$?
365 untry
367 if [ $errstatus = 0 ]
368 then
369 register_copied_file "$1" "$2"
370 else
371 if besteffort_mode
372 then
373 true
374 else
375 if [ $do_cleanup = yes ]
376 then
377 # delete file failed to copy (at all or properly)
378 if [ -e "$COPYOUT_TRG" ]
379 then
380 run verbose rm --force "$COPYOUT_TRG"
383 false
387 # return success. errors handled earlier.
388 return 0
391 copy_out_file()
393 local src=$1
394 local trg=$2
396 run touch --reference="$src" "$trg"
397 # setup a mode just enough to able to write. clone all attributes later.
398 run chmod 0600 "$trg"
399 run verbose cp --no-dereference --preserve=all --force --no-target-directory "$src" "$trg"
400 copy_attributes "$src" "$trg"
403 copy_out_symlink()
405 local src=$1
406 local trg=$2
408 local symlink_target=`no_err_trap readlink "$src"`
409 run verbose ln --symbolic --no-target-directory --force "$symlink_target" "$trg"
410 run touch --no-dereference --reference="$src" "$trg"
411 # symlinks does not seem to capable to carry ACLs on my system.
412 # I get EPERM on lsetxattr(2) on symlinks too.
415 takeown_directory_recursive()
417 local src=$1
418 local trg=$2
419 local src_back=$3
420 local do_supersede=$4
421 local subfile
422 local owner
423 local is_writable_src
425 run verbose install -m 0700 -d "$trg"
426 # delay cloning attibutes to prevent unlucky cases
427 # such as "u-w" permissions on the directory
429 register_created_dir "$trg"
431 if is_recursive
432 then
433 for subfile in "$src"/*
435 subfile=${subfile##*/}
436 owner=`no_err_trap stat -c %u "$src/$subfile"`
437 is_writable_src=`no_err_trap yesno is_writable_dir "$src"`
439 if [ -L "$src/$subfile" ]
440 then
441 if [ "$owner" = "$UID" -a $is_writable_src = yes ]
442 then
443 if run verbose mv --force --no-target-directory "$src/$subfile" "$trg/$subfile"
444 then
445 register_moved_file "$src/$subfile" "$trg/$subfile"
446 else
447 # either 'mv' should succeed or we should be in best-effort mode
448 besteffort_mode
450 else
451 copy_out symlink "$src/$subfile" "$trg/$subfile"
452 # best-effort mode checked and exceptions handled within copy_out func
455 elif [ -d "$src/$subfile" ]
456 then
457 # it's a directory ($subfile) within that directory
458 # which has to be taken own ($src).
460 # now, it's either ours ($subfile) and parent ($src) is writable,
461 # then it can be moved almost directly;
462 # or else it has be taken own as well.
464 if [ "$owner" = "$UID" -a $is_writable_src = yes ]
465 then
466 # directories must be writable in order to be moved
467 # into an other parent directory.
468 # so temporary allow the current user to write her
469 # own directory ($subfile) to able to move it.
471 local modes=`no_err_trap stat -c %a "$src/$subfile"`
472 run chmod u+w "$src/$subfile" || [ -w "$src/$subfile" ]
473 local moved
474 if run verbose mv --force --no-target-directory "$src/$subfile" "$trg/$subfile"
475 then
476 moved=yes
477 # restore permission modes on $subfile (now it's in $trg directory)
478 run chmod 0$modes "$trg/$subfile"
479 register_moved_file "$src/$subfile" "$trg/$subfile"
480 else
481 moved=no
482 # restore permission modes on $subfile (stayd in $src directory)
483 run chmod 0$modes "$src/$subfile"
486 # assert the move was successful
487 test $moved = yes -o $besteffort = yes
488 else
489 # this directory ($subfile) can not be moved,
490 # let's takeown.
491 takeown_directory_recursive "$src/$subfile" "$trg/$subfile" "" no
492 # best-effort mode is checked recursively
495 elif [ "$owner" = "$UID" -a $is_writable_src = yes ]
496 then
497 if run verbose mv --force --no-target-directory "$src/$subfile" "$trg/$subfile"
498 then
499 register_moved_file "$src/$subfile" "$trg/$subfile"
500 else
501 # either 'mv' should succeed or we should be in best-effort mode
502 besteffort_mode
504 else
505 copy_out file "$src/$subfile" "$trg/$subfile"
506 # best-effort mode checked and exceptions handled within copy_out func
508 done
511 # clone attributes of the directory taken own just now
512 # as we finished writing into it.
513 copy_attributes "$src" "$trg"
514 # best-effort mode checked and exceptions handled within copy_attributes func
516 # actually rename the owned directory
517 # to the one which is wanted to be taken own.
518 if [ "$do_supersede" = yes ]
519 then
520 # make a backup
521 if [ -n "$src_back" ]
522 then
523 if ! is_empty_directory "$src"
524 then
525 run verbose mv --force --no-target-directory "$src" "$src_back"
529 # final step to takeown
530 run verbose mv --force --no-target-directory "$trg" "$src"
534 takeown_directory()
536 local do_supersede=yes
538 try --except "cleanup directory"
539 takeown_directory_recursive "$TAKEOWN_FILE" "$TAKEOWN_COPY" "$TAKEOWN_BACK" $do_supersede
540 untry
545 shopt -s dotglob
546 shopt -s nullglob
548 PROGNAME=takeown
549 verbose=''
550 debug=''
551 dryrun=''
552 recursive=''
553 declare -A strict
554 strict=([mode]='' [group]='' [acl]='' [xattr]='')
555 do_chgrp=yes
556 ignoreerrors=''
557 activewritetest=''
558 activewritetestonly=''
559 silent_strictness_check=no
560 do_cleanup=yes
561 besteffort=no
564 while [ -n "$1" ]
566 case "$1" in
567 -h|--help)
568 echo "Usage: takeown [options] <files>
569 Options:
570 -D, --debug debug
571 -v, --verbose verbose
572 -n, --dry-run dry run
573 -R, --recursive recurse into directories
574 -w, --active-write-test active write test if access(2) reports writable
575 -W, --active-write-test-only active write test regardless of access(2), default is access(2) only
577 -i, --ignore ignore failures on files given in command arguments
578 -b, -I, --best-effort ignore any failure while recursing (force -i -T)
579 -C, --no-cleanup keep *.takeown files on failure
581 -T, --no-strict attempt but do not requisite file attribute preservation (default)
582 -M, --no-strict-chmod ignore unpreserved permission modes
583 -G, --no-strict-chgrp ignore unpreserved group owner
584 -A, --no-strict-setfacl ignore unpreserved ACL
585 -X, --no-strict-setfattr ignore unpreserved extended attributes
587 -t, --strict fail if can not preserve file attributes
588 -m, --strict-chmod requisite preserved permission modes
589 -g, --strict-chgrp requisite preserved group owner
590 -a, --strict-setfacl requisite preserved ACL
591 -x, --strict-setfattr requisite preserved extended attributes
592 --no-chgrp do not chgrp at all
594 Exit code:
595 0 all operations succeeded
596 1+ error happended during progress, even it was ignored"
597 exit 0
599 -v|--verbose)
600 verbose=yes
602 -D|--debug)
603 debug=yes
605 -n|--dry-run)
606 dryrun=yes
608 -C|--no-cleanup)
609 do_cleanup=no
611 -R|--recursive|--recurse)
612 recursive=yes
614 --no-group|--no-chgrp)
615 do_chgrp=no
617 -i|--ignore)
618 ignoreerrors=yes
620 -b|-I|--best-effort)
621 besteffort=yes
623 -t|--strict)
624 for s in ${!strict[@]}
626 [ -z "${strict[$s]}" ] && strict[$s]=yes
627 done
629 -T|--no-strict)
630 for s in ${!strict[@]}
632 [ -z "${strict[$s]}" ] && strict[$s]=no
633 done
635 -M|--no-strict-mode|--no-strict-modes|--no-strict-chmod)
636 strict[mode]=no
638 -G|--no-strict-group|--no-strict-chgrp)
639 strict[group]=no
641 -A|--no-strict-acl|--no-strict-setfacl)
642 strict[acl]=no
644 -X|--no-strict-xattr|--no-strict-setfattr)
645 strict[xattr]=no
647 -M|--strict-mode|--strict-modes|--strict-chmod)
648 strict[mode]=yes
650 --strict-group|--strict-chgrp)
651 strict[group]=yes
653 -A|--strict-acl|--strict-setfacl)
654 strict[acl]=yes
656 -X|--strict-xattr|--strict-setfattr)
657 strict[xattr]=yes
659 -w|--active-write-test)
660 activewritetest=yes
662 -w|--active-write-test-only)
663 activewritetestonly=yes
666 errx 1 "Unknown option: $1"
669 shift
670 break
673 break
675 esac
676 shift
677 done
679 declare -g Error=0
680 # these arrays keep track of recursive directory takeown process.
681 declare -a files_copied_out_src
682 declare -a files_copied_out_trg
683 declare -a files_moved_out_src
684 declare -a files_moved_out_trg
685 declare -a dirs_created
687 main "$@"
688 exit $Error
692 true <<EOF
694 =pod
696 =encoding utf8
698 =head1 NAME
700 takeown - Take ownership on files, even for unprivileged users
702 =head1 SYNOPSIS
704 takeown [I<options>] <I<files> and I<directories>>
706 =head1 DESCRIPTION
708 Command chown(1) or chown(2) is permitted only for root (and processes with CAP_CHOWN),
709 but normal users can imitate this behavior.
710 You can copy other users' file to your own in a directory writable by you,
711 and then replace the original file with your copy.
712 It is quite tricky and maybe expensive (copying huge files), but gives you an option.
713 Say, when somebody forgot to use the right user account when saving files directly to your folders.
715 takeown(1) uses B<*.takeown> and B<*.tookown> filename extensions to create new files
716 and to rename existing files to respectively.
718 See C<takeown --help> for option list.
720 =head1 TECH REFERENCE
722 =head2 Call stack
724 script --> main --> takeown
725 / | \
726 / | \
727 / | \
728 takeown takeown takeown
729 _file _symlink _directory
730 | | |
731 - - - - - - | - - - - | - - - - - | - - - - - - - - - - - - - -
732 error | | |
733 handler | | V ,---> register_created_dir
734 function: | | ,--> takeown |
735 cleanup | | | _directory ---+---> register_moved_file
736 | | | _recursive |
737 | | | | | \ `---> register_
738 | | `------´ | \ ,-> copied_file
739 | V V \ |
740 | copy_out <-- copy_out --\--'
741 | _symlink / \
742 V / |
743 copy_out <------------´ |
744 _file |
746 `-------> copy_attributes <-----´
748 =cut