cubefs
413 строк · 11.6 Кб
1#!/usr/bin/env bash
2
3# Purpose: plain text tar format
4# Limitations: - only suitable for text files, directories, and symlinks
5# - stores only filename, content, and mode
6# - not designed for untrusted input
7#
8# Note: must work with bash version 3.2 (macOS)
9
10# Copyright 2017 Roger Luethi
11#
12# Licensed under the Apache License, Version 2.0 (the "License");
13# you may not use this file except in compliance with the License.
14# You may obtain a copy of the License at
15#
16# http://www.apache.org/licenses/LICENSE-2.0
17#
18# Unless required by applicable law or agreed to in writing, software
19# distributed under the License is distributed on an "AS IS" BASIS,
20# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21# See the License for the specific language governing permissions and
22# limitations under the License.
23
24set -o errexit -o nounset25
26# Sanitize environment (for instance, standard sorting of glob matches)
27export LC_ALL=C28
29path=""30CMD=""31ARG_STRING="$*"32
33#------------------------------------------------------------------------------
34# Not all sed implementations can work on null bytes. In order to make ttar
35# work out of the box on macOS, use Python as a stream editor.
36
37USE_PYTHON=038
39PYTHON_CREATE_FILTER=$(cat << 'PCF'40#!/usr/bin/env python
41
42import re
43import sys
44
45for line in sys.stdin:
46line = re.sub(r'EOF', r'\EOF', line)
47line = re.sub(r'NULLBYTE', r'\NULLBYTE', line)
48line = re.sub('\x00', r'NULLBYTE', line)
49sys.stdout.write(line)
50PCF
51)
52
53PYTHON_EXTRACT_FILTER=$(cat << 'PEF'54#!/usr/bin/env python
55
56import re57import sys58
59for line in sys.stdin:60line = re.sub(r'(?<!\\)NULLBYTE', '\x00', line)61line = re.sub(r'\\NULLBYTE', 'NULLBYTE', line)62line = re.sub(r'([^\\])EOF', r'\1', line)63line = re.sub(r'\\EOF', 'EOF', line)64sys.stdout.write(line)65PEF
66)
67
68function test_environment {69if [[ "$(echo "a" | sed 's/a/\x0/' | wc -c)" -ne 2 ]]; then70echo "WARNING sed unable to handle null bytes, using Python (slow)."71if ! which python >/dev/null; then72echo "ERROR Python not found. Aborting."73exit 274fi75USE_PYTHON=176fi77}
78
79#------------------------------------------------------------------------------
80
81function usage {82bname=$(basename "$0")83cat << USAGE84Usage: $bname [-C <DIR>] -c -f <ARCHIVE> <FILE...> (create archive)85$bname -t -f <ARCHIVE> (list archive contents)86$bname [-C <DIR>] -x -f <ARCHIVE> (extract archive)87
88Options:
89-C <DIR> (change directory)
90-v (verbose)
91--recursive-unlink (recursively delete existing directory if path
92collides with file or directory to extract)
93
94Example: Change to sysfs directory, create ttar file from fixtures directory
95$bname -C sysfs -c -f sysfs/fixtures.ttar fixtures/96USAGE
97exit "$1"98}
99
100function vecho {101if [ "${VERBOSE:-}" == "yes" ]; then102echo >&7 "$@"103fi104}
105
106function set_cmd {107if [ -n "$CMD" ]; then108echo "ERROR: more than one command given"109echo110usage 2111fi112CMD=$1113}
114
115unset VERBOSE116unset RECURSIVE_UNLINK117
118while getopts :cf:-:htxvC: opt; do119case $opt in120c)121set_cmd "create"122;;123f)124ARCHIVE=$OPTARG125;;126h)127usage 0128;;129t)130set_cmd "list"131;;132x)133set_cmd "extract"134;;135v)136VERBOSE=yes137exec 7>&1138;;139C)140CDIR=$OPTARG141;;142-)143case $OPTARG in144recursive-unlink)145RECURSIVE_UNLINK="yes"146;;147*)148echo -e "Error: invalid option -$OPTARG"149echo150usage 1151;;152esac153;;154*)155echo >&2 "ERROR: invalid option -$OPTARG"156echo157usage 1158;;159esac160done
161
162# Remove processed options from arguments
163shift $(( OPTIND - 1 ));164
165if [ "${CMD:-}" == "" ]; then166echo >&2 "ERROR: no command given"167echo168usage 1169elif [ "${ARCHIVE:-}" == "" ]; then170echo >&2 "ERROR: no archive name given"171echo172usage 1173fi
174
175function list {176local path=""177local size=0178local line_no=0179local ttar_file=$1180if [ -n "${2:-}" ]; then181echo >&2 "ERROR: too many arguments."182echo183usage 1184fi185if [ ! -e "$ttar_file" ]; then186echo >&2 "ERROR: file not found ($ttar_file)"187echo188usage 1189fi190while read -r line; do191line_no=$(( line_no + 1 ))192if [ $size -gt 0 ]; then193size=$(( size - 1 ))194continue195fi196if [[ $line =~ ^Path:\ (.*)$ ]]; then197path=${BASH_REMATCH[1]}198elif [[ $line =~ ^Lines:\ (.*)$ ]]; then199size=${BASH_REMATCH[1]}200echo "$path"201elif [[ $line =~ ^Directory:\ (.*)$ ]]; then202path=${BASH_REMATCH[1]}203echo "$path/"204elif [[ $line =~ ^SymlinkTo:\ (.*)$ ]]; then205echo "$path -> ${BASH_REMATCH[1]}"206fi207done < "$ttar_file"208}
209
210function extract {211local path=""212local size=0213local line_no=0214local ttar_file=$1215if [ -n "${2:-}" ]; then216echo >&2 "ERROR: too many arguments."217echo218usage 1219fi220if [ ! -e "$ttar_file" ]; then221echo >&2 "ERROR: file not found ($ttar_file)"222echo223usage 1224fi225while IFS= read -r line; do226line_no=$(( line_no + 1 ))227local eof_without_newline228if [ "$size" -gt 0 ]; then229if [[ "$line" =~ [^\\]EOF ]]; then230# An EOF not preceded by a backslash indicates that the line231# does not end with a newline232eof_without_newline=1233else234eof_without_newline=0235fi236# Replace NULLBYTE with null byte if at beginning of line237# Replace NULLBYTE with null byte unless preceded by backslash238# Remove one backslash in front of NULLBYTE (if any)239# Remove EOF unless preceded by backslash240# Remove one backslash in front of EOF241if [ $USE_PYTHON -eq 1 ]; then242echo -n "$line" | python -c "$PYTHON_EXTRACT_FILTER" >> "$path"243else244# The repeated pattern makes up for sed's lack of negative245# lookbehind assertions (for consecutive null bytes).246echo -n "$line" | \247sed -e 's/^NULLBYTE/\x0/g;248s/\([^\\]\)NULLBYTE/\1\x0/g;
249s/\([^\\]\)NULLBYTE/\1\x0/g;
250s/\\NULLBYTE/NULLBYTE/g;
251s/\([^\\]\)EOF/\1/g;
252s/\\EOF/EOF/g;
253' >> "$path"254fi255if [[ "$eof_without_newline" -eq 0 ]]; then256echo >> "$path"257fi258size=$(( size - 1 ))259continue260fi261if [[ $line =~ ^Path:\ (.*)$ ]]; then262path=${BASH_REMATCH[1]}263if [ -L "$path" ]; then264rm "$path"265elif [ -d "$path" ]; then266if [ "${RECURSIVE_UNLINK:-}" == "yes" ]; then267rm -r "$path"268else269# Safe because symlinks to directories are dealt with above270rmdir "$path"271fi272elif [ -e "$path" ]; then273rm "$path"274fi275elif [[ $line =~ ^Lines:\ (.*)$ ]]; then276size=${BASH_REMATCH[1]}277# Create file even if it is zero-length.278touch "$path"279vecho " $path"280elif [[ $line =~ ^Mode:\ (.*)$ ]]; then281mode=${BASH_REMATCH[1]}282chmod "$mode" "$path"283vecho "$mode"284elif [[ $line =~ ^Directory:\ (.*)$ ]]; then285path=${BASH_REMATCH[1]}286mkdir -p "$path"287vecho " $path/"288elif [[ $line =~ ^SymlinkTo:\ (.*)$ ]]; then289ln -s "${BASH_REMATCH[1]}" "$path"290vecho " $path -> ${BASH_REMATCH[1]}"291elif [[ $line =~ ^# ]]; then292# Ignore comments between files293continue294else295echo >&2 "ERROR: Unknown keyword on line $line_no: $line"296exit 1297fi298done < "$ttar_file"299}
300
301function div {302echo "# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -" \303"- - - - - -"304}
305
306function get_mode {307local mfile=$1308if [ -z "${STAT_OPTION:-}" ]; then309if stat -c '%a' "$mfile" >/dev/null 2>&1; then310# GNU stat311STAT_OPTION='-c'312STAT_FORMAT='%a'313else314# BSD stat315STAT_OPTION='-f'316# Octal output, user/group/other (omit file type, sticky bit)317STAT_FORMAT='%OLp'318fi319fi320stat "${STAT_OPTION}" "${STAT_FORMAT}" "$mfile"321}
322
323function _create {324shopt -s nullglob325local mode326local eof_without_newline327while (( "$#" )); do328file=$1329if [ -L "$file" ]; then330echo "Path: $file"331symlinkTo=$(readlink "$file")332echo "SymlinkTo: $symlinkTo"333vecho " $file -> $symlinkTo"334div
335elif [ -d "$file" ]; then336# Strip trailing slash (if there is one)337file=${file%/}338echo "Directory: $file"339mode=$(get_mode "$file")340echo "Mode: $mode"341vecho "$mode $file/"342div
343# Find all files and dirs, including hidden/dot files344for x in "$file/"{*,.[^.]*}; do345_create "$x"346done347elif [ -f "$file" ]; then348echo "Path: $file"349lines=$(wc -l "$file"|awk '{print $1}')350eof_without_newline=0351if [[ "$(wc -c "$file"|awk '{print $1}')" -gt 0 ]] && \352[[ "$(tail -c 1 "$file" | wc -l)" -eq 0 ]]; then353eof_without_newline=1354lines=$((lines+1))355fi356echo "Lines: $lines"357# Add backslash in front of EOF358# Add backslash in front of NULLBYTE359# Replace null byte with NULLBYTE360if [ $USE_PYTHON -eq 1 ]; then361< "$file" python -c "$PYTHON_CREATE_FILTER"362else363< "$file" \364sed 's/EOF/\\EOF/g;365s/NULLBYTE/\\NULLBYTE/g;
366s/\x0/NULLBYTE/g;
367'
368fi369if [[ "$eof_without_newline" -eq 1 ]]; then370# Finish line with EOF to indicate that the original line did371# not end with a linefeed372echo "EOF"373fi374mode=$(get_mode "$file")375echo "Mode: $mode"376vecho "$mode $file"377div
378else379echo >&2 "ERROR: file not found ($file in $(pwd))"380exit 2381fi382shift383done384}
385
386function create {387ttar_file=$1388shift389if [ -z "${1:-}" ]; then390echo >&2 "ERROR: missing arguments."391echo392usage 1393fi394if [ -e "$ttar_file" ]; then395rm "$ttar_file"396fi397exec > "$ttar_file"398echo "# Archive created by ttar $ARG_STRING"399_create "$@"400}
401
402test_environment
403
404if [ -n "${CDIR:-}" ]; then405if [[ "$ARCHIVE" != /* ]]; then406# Relative path: preserve the archive's location before changing407# directory408ARCHIVE="$(pwd)/$ARCHIVE"409fi410cd "$CDIR"411fi
412
413"$CMD" "$ARCHIVE" "$@"414