init
This commit is contained in:
commit
4637861cb9
13 changed files with 740 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
TODO
|
||||||
|
Zmakefile
|
||||||
|
/backups
|
||||||
|
/diffstore
|
||||||
25
Makefile
Normal file
25
Makefile
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
|
||||||
|
var_exclude = DIFF_ENGINE COMPRESSION_TYPE COMPRESSION_LEVEL RETENTION_TYPE RETENTION_AMOUNT DEBUG ENCRYPTION
|
||||||
|
fct_exclude = diff_.* patch_.* cmp_.* dcmp_.* clean_.*
|
||||||
|
|
||||||
|
diffstore: src/*
|
||||||
|
lxsh -o diffstore -M --exclude-var "$(var_exclude)" --exclude-fct "$(fct_exclude)" src/main.sh
|
||||||
|
|
||||||
|
debug: src/*
|
||||||
|
lxsh -o diffstore src/main.sh
|
||||||
|
|
||||||
|
bash: src/*
|
||||||
|
lxsh --bash -o diffstore src/main.sh
|
||||||
|
|
||||||
|
build: diffstore
|
||||||
|
|
||||||
|
install: build
|
||||||
|
mv diffstore /usr/local/bin
|
||||||
|
cp completion/diffstore.bash /etc/bash_completion.d
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
rm /usr/local/bin/diffstore
|
||||||
|
rm /etc/bash_completion.d/diffstore.bash
|
||||||
|
|
||||||
|
clear:
|
||||||
|
rm diffstore
|
||||||
25
README.md
Normal file
25
README.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# diffstore
|
||||||
|
|
||||||
|
Utility to store files using differentials.
|
||||||
|
|
||||||
|
This tool is mostly inteded as a file backup history.
|
||||||
|
It can acheive much higher compression ratios while still being decently fast on compression.
|
||||||
|
However retreiving older backups takes much longer.
|
||||||
|
|
||||||
|
This should only be used to store either very similar files or a history of one file,
|
||||||
|
in the case where files are too different, it can result in more disk usage.
|
||||||
|
This means input files have to be uncompressed and unencrypted.
|
||||||
|
|
||||||
|
### Example configs
|
||||||
|
|
||||||
|
- rdiff + zstd -3 : best compression/time ratio
|
||||||
|
- diff + gzip : best compatibility
|
||||||
|
- rdiff + xz -9 : best compression
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Requires [lxsh](https://git.zawz.net/zawz/lxsh)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make
|
||||||
|
```
|
||||||
36
completion/diffstore.bash
Normal file
36
completion/diffstore.bash
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
#/usr/bin/env bash
|
||||||
|
|
||||||
|
_diffstore_completion()
|
||||||
|
{
|
||||||
|
local _cw2=(init list add store get clean config)
|
||||||
|
local _cw2_val=(get)
|
||||||
|
local _config_
|
||||||
|
local cur=$2
|
||||||
|
local N=0
|
||||||
|
local j I
|
||||||
|
local dir
|
||||||
|
local WORDS=()
|
||||||
|
COMPREPLY=()
|
||||||
|
|
||||||
|
if [ $COMP_CWORD -eq 1 ] ; then
|
||||||
|
_filedir -d
|
||||||
|
elif [ $COMP_CWORD -eq 2 ] ; then
|
||||||
|
WORDS=("${_cw2[@]}")
|
||||||
|
elif { [ "$COMP_CWORD" -eq "3" ] && echo "${_cw2_val[*]}" | grep -qw -- "${COMP_WORDS[2]}" ; } ; then
|
||||||
|
N=0
|
||||||
|
while read -r I ; do
|
||||||
|
WORDS+=("$I")
|
||||||
|
done < <($1 "${COMP_WORDS[1]}" list basic)
|
||||||
|
fi
|
||||||
|
|
||||||
|
N=0
|
||||||
|
if [ -n "$cur" ] ; then
|
||||||
|
for I in "${WORDS[@]}" ; do
|
||||||
|
[ "$I" != "${I#"$cur"}" ] && COMPREPLY[N++]=$I
|
||||||
|
done
|
||||||
|
else
|
||||||
|
COMPREPLY=("${WORDS[@]}")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
complete -o nospace -F _diffstore_completion -o dirnames diffstore
|
||||||
35
src/clean.sh
Normal file
35
src/clean.sh
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
|
||||||
|
# $1 = dir , $2 = type , $3 = amount
|
||||||
|
clean() {
|
||||||
|
dir=$1
|
||||||
|
shift 1
|
||||||
|
type=$RETENTION_TYPE
|
||||||
|
if [ $# -gt 0 ] ; then
|
||||||
|
type=$1
|
||||||
|
shift 1
|
||||||
|
fi
|
||||||
|
clean_$type "$dir" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
## clean_TYPE ##
|
||||||
|
# $1 = dir , $2 = amount
|
||||||
|
|
||||||
|
clean_number() {
|
||||||
|
[ "$LOG" = true ] && rmopts=${rmopts}-v
|
||||||
|
{
|
||||||
|
file_difflist "$STOREDIR" && file_latest "$STOREDIR"
|
||||||
|
} | head -n "-${2-14}" | tr '\n' '\0' | ( cd "$STOREDIR" && xargs -0 rm $rmopts 2>/dev/null)
|
||||||
|
}
|
||||||
|
|
||||||
|
clean_days() {
|
||||||
|
[ "$LOG" = true ] && rmopts=${rmopts}-v
|
||||||
|
(
|
||||||
|
cd "$1"
|
||||||
|
find . -mindepth 1 -maxdepth 1 -mtime +${2-14} -regex '\./[0-9]*-.*' -print0 | xargs -0 rm $rmopts 2>/dev/null || true
|
||||||
|
find . -mindepth 1 -maxdepth 1 -mtime +${2-14} -name 'latest-*' -print0 | xargs -0 rm $rmopts 2>/dev/null || true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
clean_all() {
|
||||||
|
true
|
||||||
|
}
|
||||||
92
src/compression.sh
Normal file
92
src/compression.sh
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
comptypes='
|
||||||
|
gzip:gz
|
||||||
|
xz:xz
|
||||||
|
zstd:zst
|
||||||
|
'
|
||||||
|
|
||||||
|
# $1 = compression
|
||||||
|
compression_extension() {
|
||||||
|
printf "%s\n" "$comptypes" | grep -o "^${1-$COMPRESSION_TYPE}:.*$" | cut -d: -f2-
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# $1 = file
|
||||||
|
compress() {
|
||||||
|
compress_stdout "$1" > "$1.$(compression_extension)$(crypt_extension)" && rm "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 = file
|
||||||
|
compress_stdout() {
|
||||||
|
if [ "$NOBAR" = true ] ; then
|
||||||
|
cat "$1"
|
||||||
|
else
|
||||||
|
pv "$1"
|
||||||
|
fi | cmp_$COMPRESSION_TYPE $COMPRESSION_LEVEL | encrypt
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 = file , $2 = engine , $* = options
|
||||||
|
decompress() {
|
||||||
|
decompress_stdout "$1" > "${1%.$(compression_extension)$(crypt_extension)}" && rm "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 = file , $2 = engine , $* = options
|
||||||
|
decompress_stdout() {
|
||||||
|
if [ "$NOBAR" = true ] ; then
|
||||||
|
cat "$1"
|
||||||
|
else
|
||||||
|
pv "$1"
|
||||||
|
fi | decrypt | dcmp_$COMPRESSION_TYPE $COMPRESSION_LEVEL
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 = dir , $2 = og engine , $3 = new engine , $4 = level
|
||||||
|
recompress_all() {
|
||||||
|
import_crypt
|
||||||
|
{ file_difflist "$STOREDIR" && file_latest "$STOREDIR"; } | while read -r ln ; do
|
||||||
|
basefile=${ln%.$(compression_extension)$(crypt_extension)}
|
||||||
|
outfile=$1/$basefile.$(compression_extension "$3")$(crypt_extension)
|
||||||
|
tmpoutfile=$1/.$basefile.$(compression_extension "$3")$(crypt_extension)
|
||||||
|
echo "recompressing '${basefile%.$DIFF_ENGINE}'"
|
||||||
|
decompress_stdout "$1/$ln" |
|
||||||
|
NOBAR=true COMPRESSION_TYPE=$3 COMPRESSION_LEVEL=${4--6} compress_stdout /dev/stdin > "$tmpoutfile" || return $?
|
||||||
|
mts=$(stat -c "%Y" "$1/$ln")
|
||||||
|
rm "$1/$ln"
|
||||||
|
mv "$tmpoutfile" "$outfile"
|
||||||
|
touch -m -d "@$mts" "$outfile"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
## CMP/DCMP functions
|
||||||
|
|
||||||
|
cmp_gzip() {
|
||||||
|
if which pigz >/dev/null 2>&1; then
|
||||||
|
pigz "$@"
|
||||||
|
else
|
||||||
|
gzip "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
dcmp_gzip() {
|
||||||
|
gzip -d "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmp_xz() {
|
||||||
|
if which pixz >/dev/null 2>&1; then
|
||||||
|
pixz "$@"
|
||||||
|
else
|
||||||
|
xz "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
dcmp_xz() {
|
||||||
|
xz -d "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmp_zstd() {
|
||||||
|
zstd -T0 "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
dcmp_zstd() {
|
||||||
|
zstd -dT0 "$@"
|
||||||
|
}
|
||||||
129
src/config.sh
Normal file
129
src/config.sh
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
export_env() {
|
||||||
|
varlist=$(echo "$CONFIGVARS" | cut -d: -f2- | tr -s ',\n' '\n' | sed '/^$/d;s/^/(/g;s/$/)/g')
|
||||||
|
set | grep -E "^($(printf "%s" "$varlist" | tr '\n' '|'))=" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
configfile=.diffstoreconfig
|
||||||
|
|
||||||
|
CONFIGVARS='
|
||||||
|
engine:DIFF_ENGINE
|
||||||
|
retention:RETENTION_TYPE,RETENTION_AMOUNT
|
||||||
|
compression:COMPRESSION_TYPE,COMPRESSION_LEVEL
|
||||||
|
encryption:ENCRYPTION
|
||||||
|
'
|
||||||
|
|
||||||
|
# $1 = dir , $2 = vars , $@ = values
|
||||||
|
config_set() {
|
||||||
|
local dir=$1 vars=$2
|
||||||
|
shift 2
|
||||||
|
I=1
|
||||||
|
for val
|
||||||
|
do
|
||||||
|
varname=$(echo "$vars" | cut -d, -f$I)
|
||||||
|
export $varname="$val"
|
||||||
|
I=$((I+1))
|
||||||
|
done
|
||||||
|
gen_config > "$dir/$configfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
get_config_var() {
|
||||||
|
echo "$CONFIGVARS" | grep -o "$1:[A-Za-z_,]*" | cut -d: -f2-
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 = dir, $2 = conf op, $@ = values
|
||||||
|
config() {
|
||||||
|
if [ $# -lt 2 ] ; then
|
||||||
|
gen_config_human
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
dir=$1
|
||||||
|
confop=$2
|
||||||
|
shift 2
|
||||||
|
case "$confop" in
|
||||||
|
compression|engine|retention|encryption)
|
||||||
|
case "$confop" in
|
||||||
|
compression)
|
||||||
|
if [ "$COMPRESSION_TYPE$COMPRESSION_LEVEL" != "$1-${2-6}" ] ; then
|
||||||
|
recompress_all "$dir" "$COMPRESSION_TYPE" "$@"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
engine)
|
||||||
|
if [ -n "$(file_difflist)" ] ; then
|
||||||
|
file_difflist
|
||||||
|
echo "Cannot change $confop" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
encryption)
|
||||||
|
if crypt_different_state "$ENCRYPTION" "$@" ; then
|
||||||
|
reencrypt_all "$dir" "$COMPRESSION_TYPE" "$@"
|
||||||
|
fi
|
||||||
|
esac
|
||||||
|
config_set "$dir" "$(get_config_var "$confop")" "$@"
|
||||||
|
;;
|
||||||
|
env) gen_config ;;
|
||||||
|
print) gen_config_human ;;
|
||||||
|
help) config_help ;;
|
||||||
|
*) usage && exit 1;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 = folder
|
||||||
|
import_config() {
|
||||||
|
[ ! -f "$1/$configfile" ] || {
|
||||||
|
tmpenv=$(TMPDIR=${TMPDIR-/tmp} _lxsh_random_tmpfile diffstore-).env
|
||||||
|
# stash current env
|
||||||
|
export_env > "$tmpenv"
|
||||||
|
# load config
|
||||||
|
. "$1/$configfile"
|
||||||
|
# load back env
|
||||||
|
. "$tmpenv"
|
||||||
|
rm -f "$tmpenv"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set_defaults() {
|
||||||
|
if [ -z "$COMPRESSION_TYPE" ] ; then
|
||||||
|
if which zstd >/dev/null 2>&1 ; then
|
||||||
|
COMPRESSION_TYPE=zstd
|
||||||
|
else
|
||||||
|
COMPRESSION_TYPE=gz
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ -z "$DIFF_ENGINE" ] ; then
|
||||||
|
if which rdiff >/dev/null 2>&1 ; then
|
||||||
|
DIFF_ENGINE=rdiff
|
||||||
|
else
|
||||||
|
DIFF_ENGINE=diff
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
RETENTION_TYPE=${RETENTION_TYPE-all}
|
||||||
|
RETENTION_AMOUNT=${RETENTION_AMOUNT-14}
|
||||||
|
COMPRESSION_LEVEL=${COMPRESSION_LEVEL--6}
|
||||||
|
ENCRYPTION=off
|
||||||
|
}
|
||||||
|
|
||||||
|
gen_config() {
|
||||||
|
for I in $(echo "$CONFIGVARS" | cut -d: -f2- | tr ',' ' ') ; do
|
||||||
|
printf "%s=%s\n" "$I" "${!I}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
gen_config_human() {
|
||||||
|
for oneconf in $CONFIGVARS ; do
|
||||||
|
confname=$(echo "$oneconf" | cut -d: -f1)
|
||||||
|
vars=$(echo "$oneconf" | cut -d: -f2-)
|
||||||
|
printf "%-12s:" "$confname"
|
||||||
|
for var in $(echo "$vars" | tr ',' ' ') ; do
|
||||||
|
printf " %s" "${!var}"
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
gen_default_config() {
|
||||||
|
set_defaults
|
||||||
|
gen_config > "$1/$configfile"
|
||||||
|
}
|
||||||
83
src/crypt.sh
Normal file
83
src/crypt.sh
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
_stop() {
|
||||||
|
stty echo
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
crypt_different_state() {
|
||||||
|
if [ "$1" = on ] ; then
|
||||||
|
[ "$2" != on ]
|
||||||
|
else
|
||||||
|
[ "$2" = on ]
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
crypt_extension() {
|
||||||
|
if [ $# -gt 0 ] ; then
|
||||||
|
local ENCRYPTION=$1
|
||||||
|
fi
|
||||||
|
if [ "$ENCRYPTION" = on ] ; then
|
||||||
|
echo ".enc"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 = prompt
|
||||||
|
console_prompt_hidden()
|
||||||
|
{
|
||||||
|
(
|
||||||
|
trap _stop INT
|
||||||
|
local prompt
|
||||||
|
stty -echo
|
||||||
|
read -rp "$1" prompt || { stty echo; return 1; }
|
||||||
|
stty echo
|
||||||
|
printf "\n" >&2
|
||||||
|
echo "$prompt"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
import_crypt() {
|
||||||
|
if [ "$ENCRYPTION" = on ] && [ -z "$CRYPT_PW" ]; then
|
||||||
|
CRYPT_PW=$(console_prompt_hidden "Password: ")
|
||||||
|
export CRYPT_PW
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 = key
|
||||||
|
encrypt() {
|
||||||
|
if [ "$ENCRYPTION" = on ] ; then
|
||||||
|
openssl enc -aes-256-cbc -pbkdf2 -salt -in - -out - -k "$1"
|
||||||
|
else
|
||||||
|
cat
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 = key
|
||||||
|
decrypt() {
|
||||||
|
if [ "$ENCRYPTION" = on ] ; then
|
||||||
|
openssl enc -d -aes-256-cbc -pbkdf2 -in - -out - -k "$1"
|
||||||
|
else
|
||||||
|
cat
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 = dir , $2 = og engine , $3 = new engine
|
||||||
|
reencrypt_all() {
|
||||||
|
if [ -n "$(file_difflist "$STOREDIR")$(file_latest "$STOREDIR")" ] ; then
|
||||||
|
ENCRYPTION=on import_crypt
|
||||||
|
fi
|
||||||
|
{ file_difflist "$STOREDIR" && file_latest "$STOREDIR"; } | while read -r ln ; do
|
||||||
|
basefile=${ln%$(crypt_extension)}
|
||||||
|
outfile=$1/$basefile$(crypt_extension "$3")
|
||||||
|
tmpoutfile=$1/.$basefile$(crypt_extension "$3")
|
||||||
|
adj=Encrypting
|
||||||
|
if [ "$ENCRYPTION" = on ] ; then
|
||||||
|
adj=Decrypting
|
||||||
|
fi
|
||||||
|
echo "$adj '${basefile%.$DIFF_ENGINE.$(compression_extension)}'"
|
||||||
|
decompress_stdout "$1/$ln" |
|
||||||
|
ENCRYPTION=$3 compress_stdout /dev/stdin > "$tmpoutfile" || return $?
|
||||||
|
rm "$1/$ln"
|
||||||
|
mv "$tmpoutfile" "$outfile"
|
||||||
|
done
|
||||||
|
}
|
||||||
72
src/diff.sh
Normal file
72
src/diff.sh
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
### TODO: bsdiff ###
|
||||||
|
|
||||||
|
## with bsdiff
|
||||||
|
# bsdiff older newer patch.bin
|
||||||
|
# [...]
|
||||||
|
# bspatch older newer patch.bin
|
||||||
|
## upside:
|
||||||
|
# - big compress
|
||||||
|
## downside:
|
||||||
|
# - bonkers amount of RAM
|
||||||
|
|
||||||
|
## FCT diff_ENGINE ##
|
||||||
|
# $1 = old file , $2 = new file , $3 = output diff file
|
||||||
|
|
||||||
|
diff_diff() {
|
||||||
|
diff -ua "$1" "$2" > "$3" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
diff_rdiff() {
|
||||||
|
sigfile=$(_lxsh_random_tmpfile diffstore-).sig
|
||||||
|
(
|
||||||
|
set -e
|
||||||
|
rdiff signature "$1" "$sigfile"
|
||||||
|
rdiff delta "$sigfile" "$2" "$3"
|
||||||
|
) || {
|
||||||
|
rm -rf "$sigfile"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
rm -rf "$sigfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
## FCT patch_ENGINE ##
|
||||||
|
# $1 = dir , $2 = file , $3 = output , stdin = patches
|
||||||
|
|
||||||
|
patch_diff() {
|
||||||
|
dir=$1
|
||||||
|
infile=$2
|
||||||
|
outfile=$3
|
||||||
|
shift 3
|
||||||
|
cp "$infile" "$outfile"
|
||||||
|
while read -r filediff ; do
|
||||||
|
decompress_stdout "$dir/$filediff.$DIFF_ENGINE.$(compression_extension)$(crypt_extension)"
|
||||||
|
done | patch -s "$outfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
patch_rdiff() {
|
||||||
|
tmpfile=.diffstoretmp-$(_lxsh_random_string)
|
||||||
|
tmppipe=$(TMPDIR=${XDG_RUNTIME_DIR-/tmp} _lxsh_random_tmpfile "diffstorefifo_")
|
||||||
|
(
|
||||||
|
dir=$1
|
||||||
|
infile=$2
|
||||||
|
realout=$3
|
||||||
|
shift 3
|
||||||
|
outfile=$tmpfile
|
||||||
|
mkfifo "$tmppipe"
|
||||||
|
pv -petls "$1" >/dev/null < "$tmppipe" &
|
||||||
|
while read -r filediff ; do
|
||||||
|
decompress_stdout "$dir/$filediff.$DIFF_ENGINE.$(compression_extension)$(crypt_extension)" | rdiff patch "$infile" /dev/stdin "$outfile"
|
||||||
|
rm "$infile"
|
||||||
|
mv "$outfile" "$infile"
|
||||||
|
echo "dummy line"
|
||||||
|
done > "$tmppipe"
|
||||||
|
mv "$infile" "$realout"
|
||||||
|
rm -f "$tmpfile" "$tmppipe"
|
||||||
|
) || {
|
||||||
|
stat=$?
|
||||||
|
rm -f "$tmpfile" "$tmppipe"
|
||||||
|
return $stat
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/help.sh
Normal file
42
src/help.sh
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
#!/bin/sh
|
||||||
|
usage() {
|
||||||
|
cat << EOF
|
||||||
|
$(basename "$0") <FOLDER> <OPERATION> [ARGS]
|
||||||
|
Script to store files as differential history.
|
||||||
|
Intended purpose is to serve as a heavily compressed backup history.
|
||||||
|
Note that input files need to be unencrypted for it to be effective.
|
||||||
|
|
||||||
|
Operations:
|
||||||
|
list List stored files
|
||||||
|
add <FILE> [NAME] Add given file into diff store
|
||||||
|
store <FILE> [NAME] Alias to add
|
||||||
|
get <FILE> Get original from selected file
|
||||||
|
clean [TYPE] [AMOUNT] Clean according to retention. Can give a retention type in console
|
||||||
|
config <OPERATION> Set config on folder. See '$(basename "$0") config help' for details
|
||||||
|
|
||||||
|
It is recommended to install 'zstd' and 'rdiff' for optimal speed and compression.
|
||||||
|
They will be set as default config if found, otherwise will default to gzip/diff.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
config_help() {
|
||||||
|
cat << EOF
|
||||||
|
$(basename "$0") <FOLDER> config [OPERATION]
|
||||||
|
View or change configuration of FOLDER
|
||||||
|
|
||||||
|
Operations:
|
||||||
|
help Display this message
|
||||||
|
print Print config in human readable format
|
||||||
|
env Print config as environment
|
||||||
|
engine <TYPE> Engine to use for differentials. Supported: diff, rdiff
|
||||||
|
retention <TYPE> <AMOUNT> Configure automatic retention.
|
||||||
|
compression <TYPE> [LEVEL] Configure to use given compression. Supported types: gz, xz, zstd
|
||||||
|
encryption <on/off> Enable or disable password-based encryption.
|
||||||
|
> Use env ENCRYPT_PW for automated password input
|
||||||
|
|
||||||
|
Retention types:
|
||||||
|
all Keep everything
|
||||||
|
number Keep N number of files
|
||||||
|
days Keep only files modified in the N lays days
|
||||||
|
EOF
|
||||||
|
}
|
||||||
22
src/list.sh
Normal file
22
src/list.sh
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
|
||||||
|
# $1 = dir , $2 = op
|
||||||
|
list() {
|
||||||
|
lop=$2
|
||||||
|
case $lop in
|
||||||
|
basic) basic_list "$1" ;;
|
||||||
|
*) default_list "$1" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
basic_list() {
|
||||||
|
{ file_difflist "$1" && file_latest "$1" ; } | filename_pipe
|
||||||
|
}
|
||||||
|
|
||||||
|
default_list() {
|
||||||
|
maxlength=$({ file_difflist "$1" && file_latest "$1" ; } | filename_pipe | wc -L)
|
||||||
|
printf "%-*s %-36s %s\n" "$maxlength" "File" "Store date" "Disk space"
|
||||||
|
{ file_difflist "$1" && file_latest "$1"; } | while read -r ln
|
||||||
|
do
|
||||||
|
printf " %-*s %-36s %s\n" "$maxlength" "$(filename "$ln")" "$(date '+%F %T %z' -d "@$(stat --printf=%Y "$1/$ln")")" "$(du -hs "$1/$ln" | cut -f1)"
|
||||||
|
done
|
||||||
|
}
|
||||||
35
src/main.sh
Executable file
35
src/main.sh
Executable file
|
|
@ -0,0 +1,35 @@
|
||||||
|
#!/usr/bin/env lxsh
|
||||||
|
|
||||||
|
[ "$DEBUG" = true ] && set -x
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
%include *.sh
|
||||||
|
|
||||||
|
if [ $# -lt 2 ] ; then
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
STOREDIR=$1
|
||||||
|
OP=$2
|
||||||
|
shift 2
|
||||||
|
|
||||||
|
if [ "$OP" = init ] || [ ! -f "$STOREDIR/$configfile" ] ; then
|
||||||
|
init_store "$STOREDIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
import_config "$STOREDIR"
|
||||||
|
|
||||||
|
case "$OP" in
|
||||||
|
init) true ;;
|
||||||
|
ls|list) list "$STOREDIR" "$@" ;;
|
||||||
|
store|add) add_file "$STOREDIR" "$@" && clean "$STOREDIR" ;;
|
||||||
|
timeset) timeset_file "$STOREDIR" "$@" ;;
|
||||||
|
get) get_file "$STOREDIR" "$@" ;;
|
||||||
|
clean) LOG=true clean "$STOREDIR" "$@" ;;
|
||||||
|
config)
|
||||||
|
config "$STOREDIR" "$@"
|
||||||
|
;;
|
||||||
|
*) usage && exit 1 ;;
|
||||||
|
esac
|
||||||
140
src/store.sh
Normal file
140
src/store.sh
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# $1 = dir
|
||||||
|
init_store() {
|
||||||
|
mkdir -p "$1"
|
||||||
|
rm -rf "$1/*"
|
||||||
|
gen_default_config "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 = dir
|
||||||
|
file_difflist() {
|
||||||
|
(
|
||||||
|
set -e
|
||||||
|
cd "$1"
|
||||||
|
find . -mindepth 1 -maxdepth 1 -regex '\./[0-9]*-.*' -printf "%f\n" | sort -n
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 = dir
|
||||||
|
file_latest() {
|
||||||
|
(
|
||||||
|
set -e
|
||||||
|
cd "$1"
|
||||||
|
find . -mindepth 1 -maxdepth 1 -name 'latest-*' -printf "%f\n" 2>/dev/null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 = file
|
||||||
|
original_filename() {
|
||||||
|
name=${1%.$(compression_extension)$(crypt_extension)}
|
||||||
|
name=${name%.$DIFF_ENGINE}
|
||||||
|
printf "%s\n" "$name" | cut -d- -f2-
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 = file
|
||||||
|
filename() {
|
||||||
|
name=${1%.$(compression_extension)$(crypt_extension)}
|
||||||
|
name=${name%.$DIFF_ENGINE}
|
||||||
|
printf "%s\n" "$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 = file
|
||||||
|
filename_pipe() {
|
||||||
|
sed "s/$(crypt_extension)$//g;s/\.$(compression_extension)$//g;s/\.$DIFF_ENGINE$//g"
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 = dir
|
||||||
|
getnvalue() {
|
||||||
|
file_difflist "$1" | tail -n1 | cut -d- -f1
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 = dir , $2 = input file name
|
||||||
|
new_difffile() {
|
||||||
|
printf "%s\n" "$(($(getnvalue "$1")+1))-$2.$DIFF_ENGINE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 = dir , $2 = input file
|
||||||
|
get_latestname() {
|
||||||
|
printf "%s\n" "latest-$(basename "$2").$(compression_extension)$(crypt_extension)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 = dir , $2 = file , $3 = name
|
||||||
|
add_file() {
|
||||||
|
import_crypt
|
||||||
|
dir=$1
|
||||||
|
file=$2
|
||||||
|
name=${3-"$2"}
|
||||||
|
tarfile=
|
||||||
|
latest=$(file_latest "$dir")
|
||||||
|
newlatest=$dir/$(get_latestname "$dir" "$name")
|
||||||
|
if [ -d "$file" ] ; then
|
||||||
|
newlatest=$dir/$(get_latestname "$dir" "$name.tar")
|
||||||
|
tarfile="$dir/.$name.tar"
|
||||||
|
tar -cf "$tarfile" "$file"
|
||||||
|
file=$tarfile
|
||||||
|
fi
|
||||||
|
if [ -n "$latest" ] ; then
|
||||||
|
fname=$(original_filename "$latest")
|
||||||
|
destfile=$(new_difffile "$dir" "$fname")
|
||||||
|
echo "> Creating diff"
|
||||||
|
decompress_stdout "$dir/$latest" | diff_$DIFF_ENGINE "$file" /dev/stdin "$dir/$destfile"
|
||||||
|
echo "> Compressing diff file"
|
||||||
|
compress "$dir/$destfile"
|
||||||
|
rm "$dir/$latest"
|
||||||
|
fi
|
||||||
|
echo "> Compressing new file"
|
||||||
|
compress_stdout "$file" > "$newlatest"
|
||||||
|
if [ -n "$tarfile" ] ; then
|
||||||
|
rm "$tarfile"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 = dir , $2 = name
|
||||||
|
resolve_shortname() {
|
||||||
|
local filetest
|
||||||
|
case "$2" in
|
||||||
|
latest|[0-9]*) (
|
||||||
|
cd "$1"
|
||||||
|
filetest=$(printf "%s" "$2"-*)
|
||||||
|
if [ -f "$filetest" ] ; then
|
||||||
|
printf "%s\n" "$filetest" | filename_pipe
|
||||||
|
else
|
||||||
|
printf "%s\n" "$2"
|
||||||
|
fi
|
||||||
|
) ;;
|
||||||
|
*) printf "%s\n" "$2" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
get_file() {
|
||||||
|
import_crypt
|
||||||
|
dir=$1
|
||||||
|
file=$(resolve_shortname "$dir" "$2")
|
||||||
|
dest=${3-$file}
|
||||||
|
latest=$(file_latest "$dir" | filename_pipe)
|
||||||
|
|
||||||
|
if [ "$file" = "$latest" ] ; then
|
||||||
|
echo "> Decompressing"
|
||||||
|
decompress_stdout "$dir/$(file_latest "$dir")" > "$dest"
|
||||||
|
else
|
||||||
|
list=$(file_difflist "$dir" | filename_pipe)
|
||||||
|
list=$(printf "%s\n" "$list" | grep "^$file$" -A$(echo "$list" | wc -l) | tac)
|
||||||
|
if [ -z "$list" ]; then
|
||||||
|
echo "file $file not found" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "> Decompressing"
|
||||||
|
decompress_stdout "$dir/$(file_latest "$dir")" > ".$dest"
|
||||||
|
(
|
||||||
|
set +e
|
||||||
|
echo "> Patching"
|
||||||
|
printf "%s\n" "$list" | NOBAR=true patch_$DIFF_ENGINE "$dir" ".$dest" "$dest" "$(printf "%s\n" "$list" | wc -l)"
|
||||||
|
stat=$?
|
||||||
|
rm -f ".$dest"
|
||||||
|
exit $stat
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue