export _archive_pattern="_[0-9]{12}-[[:alnum:]]{40}.t[xg]z" # Archive directories. # All directories by default. # Usage: archive [DIRS] function archive() { local IFS=$'\n' local targets=("${@}") local count=0 local total=${#} local date=$(_archive_date) local failed=0 # Set dafult value to target all directories. if [[ "${targets}" = "" ]]; then targets=($(ls --classify | grep /\$)) total=${#targets[@]} fi # iterate each target. for target in "${targets[@]}"; do # increment counter. ((count++)) # status info. local status="[${count}/${total}] ${target}" echo -e "${color_bwhite}${status}${color_default}" local name=$(parse_camel "${target}") # create archive. local hash=$(tar -c "${target}" | pv -s $(/usr/bin/du -sb "${target}" | awk '{print $1}') | xz -9e | tee "${name}".txz | sha1sum | cut -d\ -f1) # append hash to target name. mv "${name}".txz "${name}"_${date}-${hash}.txz # Show error. if [[ ${?} != 0 ]]; then ((failed++)) echo -e "${color_bred}${status}: Failed.${color_default}" fi done # Show error. if [[ ${failed} != 0 ]]; then echo -e "${color_bred}Failed: ${failed}.${color_default}" false fi } # Archive directories with fast compression. # All directories by default. # Usage: archive_fast [DIRS] function archive_fast() { local IFS=$'\n' local targets=("${@}") local count=0 local total=${#} local date=$(_archive_date) local failed=0 # Set dafult value to target all directories. if [[ "${targets}" = "" ]]; then targets=($(ls --classify | grep /$)) total=${#targets[@]} fi # iterate each target. for target in "${targets[@]}"; do # increment counter. ((count++)) # status info. local status="[${count}/${total}] ${target}" echo -e "${color_bwhite}${status}${color_default}" local name=$(parse_camel "${target}") # create archive. local hash=$(tar -c "${target}" | pv -s $(/usr/bin/du -sb "${target}" | awk '{print $1}') | gzip -1 | tee "${name}".tgz | sha1sum | cut -d\ -f1) # append hash to target name. mv "${name}".tgz "${name}"_${date}-${hash}.tgz # Show error. if [[ $? != 0 ]]; then ((failed++)) echo -e "${color_bred}${status}: Failed.${color_default}" fi done # Show error. if [[ ${failed} != 0 ]]; then echo -e "${color_bred}Failed: ${failed}.${color_default}" false fi } # Check archives integrity. # Checks all archives by default. # Usage: archive_check [FILES] function archive_check() { local IFS=$'\n' local targets=("${@}") local total=${#} local count=0 local failed=0 # set dafult value to target all supported archives. if [[ "${targets}" = "" ]]; then targets=($(ls | grep -E ${_archive_pattern})) total=${#targets[@]} fi # iterate each target. for target in "${targets[@]}"; do # increment counter. ((count++)) # status info. local status="[${count}/${total}] ${target}" echo -e "${color_bwhite}${status}${color_default}" # extract hash from name. local data=($(_archive_parse ${target})) local saved=${data[2]} # calculate actual hash. local actual=$(pv "${target}" | sha1sum | cut -d\ -f1) # compare hashes, show error on mismatch. if [[ "${actual}" != "${saved}" ]]; then ((failed++)) echo -e "${color_bred}${status}: Failed.${color_default}" fi done if [[ ${failed} != 0 ]]; then echo -e "${color_bred}Failed: ${failed}.${color_default}" false fi } # Delete old versions of archives. # All archives by default. # Usage: archive_prune [NAME] function archive_prune() { local IFS=$'\n' local targets=("${@}") local count=0 local total=${#} local failed=0 # All archives by default. if [[ "${targets}" = "" ]]; then targets=($(ls | grep -E ${_archive_pattern})) total=${#targets[@]} fi # Iterate each target. for target in "${targets[@]}"; do # Only work with existing files. [[ -f "${target}" ]] || continue # Iterate counter. ((count++)) local data=($(_archive_parse ${target})) local name="${data[0]}" local time="${data[1]}" local copies=($(ls ${name}_*)) # Iterate each copy. for copy in "${copies[@]}"; do local copy_data=($(_archive_parse ${copy})) local copy_time="${copy_data[1]}" if [[ "${copy_time}" -lt "${time}" ]]; then echo -e "${name}: prune ${copy_time}." rm -- "${copy}" if [[ ${?} != 0 ]]; then echo -e "${color_bred}${target}: Failed.${color_default}" ((failed++)) fi fi done done if [[ ${failed} != 0 ]]; then echo -e "${color_bred}Failed: ${failed}.${color_default}" false fi } # Extract previously created archive with checksum validation. # Usage: unarchive [FILES] function unarchive() { local IFS=$'\n' local targets=("${@}") local count=0 local total=${#} local failed=0 # set dafult value to target all supported archives. if [[ "${targets}" = "" ]]; then targets=($(ls | grep -E ${_archive_pattern})) total=${#targets[@]} fi # iterate each target. for target in "${targets[@]}"; do # increment counter. ((count++)) # status info. local status="[${count}/${total}] ${target}" echo -e "${color_bwhite}${status}${color_default}" # extract hash from name. local data=($(_archive_parse "${target}")) local saved="${data[2]}" # calculate actual hash. local actual=$(pv "${target}" | sha1sum | cut -d\ -f1) # extract if hash matched or show error if not. if [[ "${saved}" = "${actual}" ]]; then # figure out the compression tool. local compressor case "${target##*.}" in "txz") compressor="xz -d" ;; "tgz") compressor="gzip -d" ;; esac # extract. unset IFS pv "${target}" | ${compressor} | tar -xf - if [[ ${?} != 0 ]]; then echo -e "${color_bred}${status}: Failed.${color_default}" ((failed++)) fi else # report validation error & continue. echo -e "${color_bred}${status}: Validation failed.${color_default}" ((failed++)) continue fi done if [[ ${failed} != 0 ]]; then echo -e "${color_bred}Failed: ${failed}.${color_default}" false fi } # Rename archives. # If no name specified, it simplifies archive's name. # If no archives specified, apply to all archives. # Usage: archive_name [ARCHIVE] [NAME] function archive_name() { local IFS=$'\n' local targets="${1}" local name="${2}" local total=1 local count=0 local failed=0 # set dafult value to target all supported archives. if [[ "${targets}" = "" ]]; then targets=($(ls | grep -E ${_archive_pattern})) total=${#targets[@]} fi # iterate each target. for target in "${targets[@]}"; do # iterate counter. ((count++)) # simplify name by default. if [[ "${name}" = "" || ${count} -gt 1 ]]; then name="${target%_*}" name="$(parse_camel ${name})" fi # remove old name. local data="${target##*_}" local new_name="${name}_${data}" # prepare status. local status="[${count}/${total}] ${target} -> ${new_name}" # check for the same name. if [[ "${target}" = "${new_name}" ]]; then echo -e "${color_bwhite}${status}${color_default}" continue fi # check for existing target. if [[ -f "${new_name}" ]]; then echo -e "${color_bred}${status}: Already exists.${color_default}" ((failed++)) continue fi echo -e "${color_bwhite}${status}${color_default}" # rename. mv -- "${target}" "${new_name}" if [[ ${?} != 0 ]]; then echo -e "${color_bred}${status}: Failed.${color_default}" ((failed++)) fi done if [[ ${failed} != 0 ]]; then echo -e "${color_bred}Failed: ${failed}.${color_default}" false fi } # Convert old archives to a new format. *TODO: remove me after some time when there won't be any old archives.* function archive_convert() { local IFS=$'\n' local old_format="_[[:alnum:]]{40}.tar.[xg]z" local targets=($(ls | grep -E ${old_format})) # add timestamp. for target in "${targets[@]}"; do local stamp=$(stat --format '%w' -- "${target}" | sed -e 's/\..*//' -e 's/:..$//' -e 's/-//g' -e 's/://' -e 's/\ //') local name="${target%_*}" local old_data="${target##*_}" local new_name="${name}_${stamp}-${old_data}" echo "${target} -> ${new_name}" mv "${target}" "${new_name}" done # convert tar.xz and tar.gz to .tgz and .txz. old_format="_[0-9]{12}-[[:alnum:]]{40}.tar.[xg]z" targets=($(ls | grep -E ${old_format})) for target in "${targets[@]}"; do local compression="${target##*.}" local new_compression case "${compression}" in "gz") new_compression="tgz" ;; "xz") new_compression="txz" ;; esac local new_name="${target%.tar.*}".${new_compression} echo "${target} -> ${new_name}" mv -- "${target}" "${new_name}" done } # Delete specified or all archive files. # Usage: archive_rm [FILES] function archive_rm() { local IFS=$'\n' local targets=("${@}") local total=${#} local count=0 local failed=0 # set dafult value to target all supported archives. if [[ "${targets}" = "" ]]; then targets=($(ls | grep -E ${_archive_pattern})) total=${#targets[@]} fi # iterate each target. for target in "${targets[@]}"; do # increment counter. ((count++)) # status info. local status="[${count}/${total}] ${target}" echo -e "${color_bwhite}${status}${color_default}" # Delete archive. rm -- "${target}" # Show error. if [[ ${?} != 0 ]]; then ((failed++)) echo -e "${color_bred}${status}: Failed.${color_default}" fi done # Show error. if [[ ${failed} != 0 ]]; then echo -e "${color_bred}Failed: ${failed}.${color_default}" false fi } # Parse archive file name to get: name, date, hash and format. # Usage: _archive_parse function _archive_parse() { local input="${1}" local name="${input%_*}" local format="${input##*.}" local data="${input##*_}"; data="${data%.*}" local date="${data%%-*}" local hash="${data##*-}" echo "${name}" echo "${date}" echo "${hash}" echo "${format}" } # Autocomplete for archive_name function. # First arg is the archives list, second one is selected archive's current name. function _archive_name() { local IFS=$'\n' COMPREPLY=() local cur="${COMP_WORDS[COMP_CWORD]}" local prev="${COMP_WORDS[COMP_CWORD-1]}" local command="${COMP_WORDS[0]}" if [[ "${prev}" = "${command}" ]]; then COMPREPLY=( $(compgen -W "$(ls | grep -E ${_archive_pattern})" -- ${cur}) ) return 0 else local name="${prev%_*}" COMPREPLY=( $(compgen -W "${name}" -- ${cur}) ) return 0 fi } # Autocomplete with archives in current dir. function _archive_grep() { _autocomplete_grep ${_archive_pattern} } # Get date for a new archive. function _archive_date() { date +%Y%m%d%H%M } complete -o filenames -F _archive_grep archive_check unarchive archive_rm complete -o filenames -F _archive_name archive_name