chiark / gitweb /
ce7e7cb74babdc2026f9993a15d6af3c3329d983
[distorted-backup] / bkpadmin.in
1 #! /bin/sh
2 ###
3 ### Manage the backup archive structure
4 ###
5 ### (c) 2011 Mark Wooding
6 ###
7
8 ###----- Licensing notice ---------------------------------------------------
9 ###
10 ### This program is free software; you can redistribute it and/or modify
11 ### it under the terms of the GNU General Public License as published by
12 ### the Free Software Foundation; either version 2 of the License, or
13 ### (at your option) any later version.
14 ###
15 ### This program is distributed in the hope that it will be useful,
16 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 ### GNU General Public License for more details.
19 ###
20 ### You should have received a copy of the GNU General Public License
21 ### along with this program; if not, write to the Free Software Foundation,
22 ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
23
24 set -e
25
26 ## Configuration and testing.
27 : ${BKP=/mnt/bkp} ${META=/mnt/bkpmeta}
28 : ${KEYS=/etc/keys}
29
30 case $(id -u) in 0) ;; *) exec userv root bkpadmin "$@" ;; esac
31
32 ###--------------------------------------------------------------------------
33 ### Common utilities.
34
35 quis=${0##*/}
36 version="@VERSION@"
37
38 moan () {
39   ## Print a complaint to standard error.
40
41   echo >&2 "$quis: $*"
42 }
43
44 die () {
45   ## Print a complaint and exit.
46
47   moan "$*"
48   exit 1
49 }
50
51 cleanups=""
52 addcleanup () {
53   cmd=$1
54   ## Add a cleanup command CMD to the list.
55
56   case "$cleanups" in
57     ?*)
58       ;;
59     *)
60       trap 'rc=$?; for c in $cleanups; do $c; done; exit $rc' \
61         EXIT INT TERM
62       ;;
63   esac
64   cleanups=${cleanups+$cleanups }$cmd
65 }
66
67 rmtmp () { case ${tmpdir+t} in t) rm -rf "$tmpdir" ;; esac }
68 addcleanup rmtmp
69 mktmp () {
70   ## Make a temporary directory and output its name.
71
72   case "${tmpdir+t}" in
73     t)
74       ;;
75     *)
76       i=0
77       while :; do
78         r=$(openssl rand -base64 12)
79         tmpdir=${TMPDIR-/tmp}/$quis.$$.$r
80         if mkdir -m700 "$tmpdir" >/dev/null 2>&1; then break; fi
81         case $i in ???) die "failed to create temporary directory" ;; esac
82         i=$(( $i + 1 ))
83       done
84       ;;
85   esac
86   echo "$tmpdir"
87 }
88
89 ###--------------------------------------------------------------------------
90 ### Command dispatch.
91
92 case "${USERV_USER+t}" in t) uservp=t ;; *) uservp=nil ;; esac
93
94 USAGE="COMMAND [ARGUMENT ...]"
95 cmdname=""
96 cmdargs=$USAGE
97 cmds=""
98 _defcmd () {
99   name=$1; shift; args=$*
100   ## Define a command unconditionally.
101
102   cmds="${cmds:+$cmds
103 }$name $args"
104 }
105
106 defcmd () {
107   ## Define a command for privileged users only.
108
109   case $uservp in nil) _defcmd "$@" ;; esac
110 }
111
112 defucmd () {
113   ## Define a command usable via userv.
114
115   _defcmd "$@"
116 }
117
118 usage () {
119   ## Write a usage message for the current command.
120
121   echo "usage: $quis${cmdname:+ $cmdname}${cmdargs:+ $cmdargs}"
122 }
123
124 usage_err () {
125   ## Fail with a usage error.
126
127   usage >&2
128   exit 1
129 }
130
131 lookupcmd () {
132   cmd=$1
133   ## Try to loop up the command CMD.
134
135   while read cmdname cmdargs; do
136     case $cmdname in "$cmd") return ;; esac
137   done <<EOF
138 $cmds
139 EOF
140   die "unknown command \`$cmd'"
141 }
142
143 defucmd help
144 cmd_help () {
145   case $# in 0) ;; *) usage_err ;; esac
146
147   cat <<EOF
148 $quis, version $version
149
150 usage: $quis $USAGE
151
152 Commands provided:
153 EOF
154   while read cmd args; do
155     echo "      $cmd${args:+ $args}"
156   done <<EOF
157 $cmds
158 EOF
159 }
160
161 ###--------------------------------------------------------------------------
162 ### Utility functions.
163
164 sign () {
165   file=$1
166   ## Sign the named FILE, producing a signature FILE.sig.
167
168   seccure-sign -F$KEYS/priv/backup-auth -cp256 -s"$file.sig" <"$file"
169 }
170
171 checkhost () {
172   ## Check that a host is defined.
173
174   case "${host+t}" in
175     t) ;; *) die "no host defined (use \`-H')" ;;
176   esac
177 }
178
179 checkthing () {
180   thing=$1 good=$2 what=$3 string=$4
181   ## Check that STRING is a valid THING -- i.e., it only consists of GOOD
182   ## characters.
183
184   case "$string" in
185     *[!$good]*)
186       die "bad $thing \`$string' given for $what"
187       ;;
188   esac
189 }
190
191 checknum () {
192   what=$1 string=$2
193   ## Check that STRING is at least plausibly numeric.
194
195   checkthing number "0-9" "$what" "$string"
196 }
197
198 checkpath () {
199   what=$1 string=$2
200   ## Check that STRING is a plausible pathname.
201
202   case "$string" in
203     .* | */.* | *[!-a-zA-Z0-9.,_#!%^+=@/:]*)
204       die "bad pathname \`$string' given for $what"
205       ;;
206   esac
207 }
208
209 checkword () {
210   what=$1 thing=$2
211   ## Check that THING doesn't need shell quoting, and doesn't interfere with
212   ## other common delimiter characters.  (Colons aren't allowed because they
213   ## mess up /etc/passwd; slashes aren't allowed because they're directory
214   ## separators.  Leading dots aren't allowed either.  Hashes seem OK.)
215
216   checkthing word "-a-zA-Z0-9.,_#!%^+=@" "$what" "$string"
217 }
218
219 domkdir () {
220   dir=$1 owner=$2 mode=$3
221   ## Make a directory and set permissions on it.
222
223   mkdir -m755 "$dir"
224   chown $owner "$dir"
225   chmod $mode "$dir"
226 }
227
228 ###--------------------------------------------------------------------------
229 ### Volume and volume group maintenance.
230
231 currenttag () {
232   ## Output the tag of the mounted backup volume group.
233
234   dev=$(mntdev $BKP)
235   case "$dev" in
236     /dev/mapper/cbkp-*) echo "${dev#*-}"; return ;;
237     *) die "failed to parse tag from device name \`$dev'" ;;
238   esac
239 }
240
241 guesstag () {
242   ## Guess and print the tag of the available backup volume group.  If there
243   ## is not exactly one volume group available, print an error and fail.
244
245   LVM_SUPPRESS_FD_WARNINGS=t vgs @backup --noheadings -o name,attr | {
246     match=""
247     while read name attr; do
248       case "$name" in bkp-*) ;; *) continue ;; esac
249       case "$attr" in ??x*) continue ;; esac
250       match="$match${match:+ }${name#bkp-}"
251     done
252     case "x$match" in
253       x) die "no backup volume groups available" ;;
254       x*\ *) die "multiple backup volume groups available: $match" ;;
255     esac
256     echo "$match"
257   }
258 }
259
260 mntdev () {
261   dir=$1
262   ## Output a device name for the filesystem mounted on DIR.
263
264   dev=$(mountpoint -d "$dir")
265   devname=$(udevadm info --query=name --path="/dev/block/$dev")
266   case "$devname" in
267     dm-*)
268       devname=mapper/$(dmsetup info -c --noheadings -oname "/dev/$devname")
269       ;;
270   esac
271   echo "/dev/$devname"
272 }
273
274 mntmeta () {
275   tag=$1
276   ## Mount the metadata volume of the backup volume group named TAG.
277
278   if ! mountpoint -q $META; then
279     mount "/dev/bkp-$tag/meta" $META
280   fi
281 }
282
283 cryptkey () {
284   ## Decrypt and output the key for the encrypted volume.  This assumes that
285   ## the metadata volume is already mounted on /mnt/bkpmeta.
286
287   seccure-decrypt -q -m128 -cp256 -F$KEYS/priv/backup-disk <$META/cur/blob
288 }
289
290 decrypt () {
291   tag=$1
292   ## Decrypt but don't mount the encrypted volume of the backup volume group
293   ## named TAG.
294
295   mntmeta "$tag"
296   if [ ! -b "/dev/mapper/cbkp-$tag" ]; then
297     cryptkey | cryptsetup luksOpen --key-file=- \
298       "/dev/bkp-$tag/crypt" "cbkp-$tag"
299   fi
300 }
301
302 mntcrypt () {
303   tag=$1
304   ## Mount the encrypted subvolume of the backup volume group named TAG.  The
305   ## metadata volume will be mounted if necessary.
306
307   decrypt "$tag"
308   if ! mountpoint -q $BKP; then
309     mount "/dev/mapper/cbkp-$tag" $BKP
310   fi
311 }
312
313 umnt () {
314   ## Unmounts a backup volume group: both the encrypted and metadata volumes
315   ## are unmounted.
316
317   if mountpoint -q $BKP; then
318     tag=$(currenttag) cryptclosep=t
319   else
320     cryptclosep=nil
321   fi
322   for i in bkp bkpmeta; do
323     if mountpoint -q /mnt/$i; then umount /mnt/$i; fi
324   done
325   case $cryptclosep in
326     t)
327       if [ -b "/dev/mapper/cbkp-$tag" ]; then
328         cryptsetup luksClose "cbkp-$tag"
329       fi
330   esac
331 }
332
333 defcmd initvol TAG DEVICE
334 cmd_initvol () {
335   case $# in 2) ;; *) usage_err ;; esac
336   tag=$1 dev=$2
337
338   vgcreate --addtag @backup "bkp-$tag" "$dev"
339
340   lvcreate -L4M -nmeta "bkp-$tag"
341   mkfs -text2 -Lmeta "/dev/bkp-$tag/meta"
342   mntmeta "$tag"
343
344   mkdir -m755 $META/new
345   dd if=/dev/random bs=1 count=512 |
346   seccure-encrypt -m128 $(cat $KEYS/pub/backup-disk.pub) >$META/new/blob
347   mv $META/new $META/cur
348
349   lvcreate -l100%FREE -ncrypt "bkp-$tag"
350   cryptkey | cryptsetup luksFormat \
351     --cipher=twofish-xts-benbi:sha256 --hash=sha256 \
352     "/dev/bkp-$tag/crypt" -
353   decrypt "$tag"
354   mkfs -text2 -Lbackup -i1048576 "/dev/mapper/cbkp-$tag"
355   mntcrypt "$tag"
356 }
357
358 defucmd mount "[TAG]"
359 cmd_mount () {
360   case $# in
361     0) tag=$(guesstag) check=nil ;;
362     1) tag=$1 check=t ;;
363     *) usage_err ;;
364   esac
365
366   if mountpoint -q $BKP; then
367     curtag=$(currenttag)
368     case "$check,$curtag" in "t,$tag") ;; t*) exit 1 ;; esac
369   else
370     mntcrypt "$tag"
371   fi
372 }
373
374 defcmd umount
375 cmd_umount () {
376   case $# in 0) ;; *) usage_err ;; esac
377   mntp=nil
378   for i in bkp bkpmeta; do
379     if mountpoint -q /mnt/$i; then mntp=t; fi
380   done
381   case $mntp in
382     nil) die "backup volume not mounted" ;;
383   esac
384   umnt
385 }
386
387 ###--------------------------------------------------------------------------
388 ### Archive maintenance.
389
390 checkdir () {
391   key=$1 dir=$2
392   ## Check a directory which has `hashes' and `hashes.sig' files.
393
394   if ! seccure-verify -q -i"$dir/hashes" -- \
395     $(cat "$KEYS/$key") $(cat "$dir/hashes.sig")
396   then
397     die "failed to verify signature for \`$dir'"
398   fi
399
400   cd "$dir"
401   sha256sum --quiet -c hashes
402
403   tmpdir=$(mktmp)
404   find . -type f -print | sed 's:^\./::' | sort >"$tmpdir/present"
405   { echo hashes
406     echo hashes.sig
407     sed 's/^[a-f0-9]*[* ] //' hashes
408   } | sort >"$tmpdir/checked"
409   cd "$tmpdir"
410   diff -u checked present
411 }
412
413 fixperms () {
414   dir=$1 owner=$2 fmode=$3 dmode=$4
415   ## Fix the directory tree DIR so that everything is owned by OWNER (a
416   ## USER:GROUP pair) and has modes FMODE for files and DMODE for
417   ## directories.
418
419   ## Change all of the ownerships.  This will prevent anyone else from
420   ## changing the permissions on the files.  This assumes that chown(1) is
421   ## secure in recursive mode; I've checked that GNU chown seems correct.
422   chown -R $owner "$dir"
423
424   ## Paranoia: check that we correctly changed all of the files.
425   u=${owner%:*} g=${owner#*:}
426   (cd "$dir"; find . ! \( -user $u -group $g \) -ls) |
427   if read line; then
428     moan "failed to fix permssions on \`$dir'"
429     { echo $line; cat; } | sed 's/^/    /'
430     exit 1
431   fi
432
433   ## Now get to work on the file and directory permissions.
434   find "$dir" -type d -print0 | xargs -0r chmod $dmode
435   find "$dir" ! -type d -print0 | xargs -0r chmod $fmode
436 }
437
438 commitdir () {
439   dir=$1 target=$2
440   ## Commit an `prepare' directory DIR, moving its `incoming' files to
441   ## TARGET.  This will choose the correct name for the directory, but
442   ## assumes that it's already correctly laid out.  We assume that the
443   ## permissions on this directory are safe (e.g., they've already been fixed
444   ## using `fixperms').  On successful exit, DIR won't exist any more.  The
445   ## shell variable `label' is set to the resulting archive name.
446
447   ## If there's no `incoming' directory, then there's nothing to do.  Just
448   ## zap the directory and move on.
449   if [ ! -d "$dir/incoming" ]; then
450     rm -rf "$dir"
451     return
452   fi
453
454   ## Find the datestamp and level numbers to use for this directory.  These
455   ## are created before the `incoming' directory, so they ought to exist.
456   read level date time tz <"$dir/meta"
457
458   ## Find a suitable sequence number for the target.  This is rather ugly;
459   ## sorry.
460   seq=1
461   while :; do
462     anyp=nil
463     for i in "$target"/"$date#$seq".*; do
464       if [ -e "$i" ]; then anyp=t; break; fi
465     done
466     case $anyp in nil) break ;; esac
467     seq=$(( $seq + 1 ))
468   done
469
470   ## Move the directory.
471   label="$date#$seq.$level"
472   mv "$dir/incoming" "$target/$label"
473   rm -rf "$dir"
474
475   ## Update the catalogue.  Replace an existing dump at the same level.
476   ## Assume that dates are monotonically increasing: add the new entry at the
477   ## end.
478   { found=nil
479     while read lab l d t; do
480       if [ $l -ne $level ]; then echo $label $l $d $t; fi
481     done <"$target"/CATALOGUE
482     echo $level $date $time $tz
483   } >"$target"/CATALOGUE.new
484   mv "$target"/CATALOGUE.new "$target"/CATALOGUE
485 }
486
487 defcmd initmeta
488 cmd_initmeta () {
489   case $# in 0) ;; *) usage_err ;; esac
490
491   ## Make a `new' directory and start recording our files.
492   cd $META
493   rm -rf new
494   mkdir -m755 new
495   f=""
496
497   ## Copy the blob from the existing metadata.
498   cp cur/blob new/
499   f="$f blob"
500
501   ## Archive the key recovery information.
502   cd $KEYS
503   tar cfz $META/new/keys.tgz pub/ recov/
504   f="$f keys.tgz"
505
506   ## Copy user and group information.
507   cd $META/new
508   for i in passwd group; do
509     grep -E '^(root|backup|bkp-[[:alnum:]]+):' /etc/$i >$i
510   done
511   f="$f passwd group"
512
513   ## Build the hashes file, and sign it.
514   chown root:root $f
515   chmod 644 $f
516   sha256sum $f >hashes
517   sign hashes
518
519   ## Replace the old metadata.
520   cd $META
521   mv cur old
522   mv new cur
523   rm -rf old
524 }
525
526 defcmd chkmeta
527 cmd_chkmeta () {
528   case $# in 0) ;; *) usage_err ;; esac
529
530   checkdir pub/backup-auth.pub $META/cur
531 }
532
533 today () {
534   ## Report the current date, as ISO8601.  Allow an override.
535
536   case "${forceday+t}" in t) echo "$forceday" ;; *) date +%Y-%m-%d ;; esac
537 }
538
539 defucmd prep ASSET LEVEL \[DATE TIME TZ]
540 cmd_prep () {
541   case $# in
542     2) set -- "$@" $(today) $(date +%H:%M:%S) $(date +%z) ;;
543     5) ;;
544     *) usage_err ;;
545   esac
546   asset=$1 level=$2 date=$3 time=$4 tz=$5
547   checkhost
548   checkword asset "$asset"
549   checknum level "$level"
550   checkthing date -0-9 date "$date"
551   checkthing time :0-9 time "$time"
552   checkthing timezone -+0-9 tz "$tz"
553
554   ## Make the host and asset directories if necessary.
555   cd $BKP
556   for i in $host $asset; do
557     if [ ! -d $i ]; then domkdir $i root:root 755; fi
558     cd $i
559   done
560   if [ ! -d failed ]; then domkdir failed root:root 755; fi
561   for i in . failed; do
562     if [ ! -f $i/CATALOGUE ]; then
563       touch $i/CATALOGUE
564       chown root:root $i/CATALOGUE
565       chmod 644 $i/CATALOGUE
566     fi
567   done
568
569   ## If an existing dump is in progress then archive it as a failure.
570   if [ -d prepare ]; then
571     if [ -d prepare/incoming ]; then
572       fixperms prepare/incoming root:root 640 755
573     fi
574     commitdir prepare failed/
575   fi
576
577   ## Make a new preparation directory.
578   domkdir prepare root:bkp-$host 755
579   echo $level $date $time $tz >prepare/meta
580   domkdir prepare/incoming bkp-$host:bkp-$host 2775
581
582   ## Print the directory name.
583   echo $BKP/$host/$asset/prepare/incoming
584 }
585
586 defucmd abort ASSET
587 cmd_abort () {
588   case $# in 1) ;; *) usage_err ;; esac
589   asset=$1
590   checkhost
591   checkword asset "$asset"
592
593   ## Check that there's something to abort.
594   cd $BKP
595   if [ ! -d $host/$asset/prepare ]; then
596     die "no dump in progress for $host/$asset"
597   fi
598
599   ## Just throw it away.
600   rm -rf $host/$asset/prepare
601 }
602
603 defucmd fail ASSET
604 cmd_fail () {
605   case $# in 1) ;; *) usage_err ;; esac
606   asset=$1
607   checkhost
608   checkword asset "$asset"
609
610   ## Check that there's something to fail.
611   cd $BKP
612   if [ ! -d $host/$asset/prepare ]; then
613     die "no dump in progress for $host/$asset"
614   fi
615
616   ## Archive the failure.  This shouldn't be used to determine dump levels or
617   ## we'll have gaps when things get sorted out.
618   cd $host/$asset
619   if [ -d prepare/incoming ]; then
620     fixperms prepare/incoming root:root 640 755
621   fi
622   commitdir prepare failed/
623 }
624
625 julian () {
626   date=$1
627   ## Convert an ISO8601 DATE to a Julian Day Number.
628
629   ## Extract the components of the date and trim leading zeros (which will
630   ## cause things to be interpreted as octal and fail).
631   year=${date%%-*} rest=${date#*-}; month=${rest%%-*} day=${rest#*-}
632   year=${year#0} month=${month#0} day=${day#0}
633
634   ## The actual calculation: convert a (proleptic) Gregorian calendar date
635   ## into a Julian day number.  This is taken from Wikipedia's page
636   ## http://en.wikipedia.org/wiki/Julian_day#Calculation but the commentary
637   ## is mine.  The epoch is 4713BC-01-01 (proleptic) Julian, or 4714BC-11-24
638   ## proleptic Gregorian.
639
640   ## If the MONTH is January or February then set a = 1, otherwise set a = 0.
641   a=$(( (14 - $month)/12 ))
642
643   ## Compute a year offset relative to 4799BC-03-01.  This puts the leap day
644   ## as the very last day in a year, which is very convenient.  The offset
645   ## here is sufficient to make all y values positive (within the range of
646   ## the JDN calendar), and is a multiple of 400, which is the Gregorian
647   ## cycle length.
648   y=$(( $year + 4800 - $a ))
649
650   ## Compute the offset month number in that year.  These months count from
651   ## zero, not one.
652   m=$(( $month + 12*$a - 3 ))
653
654   ## Now for the main event.  The (153 m + 2)/5 term is a surprising but
655   ## correct trick for obtaining the number of days in the first m months of
656   ## the (shifted) year).  The magic offset 32045 is what you get when you
657   ## plug the proper JDN epoch (year = -4713, month = 11, day = 24) into the
658   ## above machinery.
659   jdn=$(( $day + (153*$m + 2)/5 + 365*$y + $y/4 - $y/100 + $y/400 - 32045 ))
660
661   echo $jdn
662 }
663
664 dumplevel () {
665   fulldate=$1 lastdate=$2
666   ## Return the dump level, given that the most recent full dump occurred on
667   ## FULLDATE and the most revent dump of any kind occurred on LASTDATE.
668
669   ## Actually, we're much more interested in the day difference between these
670   ## two times.
671   fulljdn=$(julian $fulldate)
672   lastjdn=$(julian $lastdate)
673   now=$(today); nowjdn=$(julian $now)
674   lastday=$(( $lastjdn - $fulljdn ))
675   today=$(( $nowjdn - $fulljdn ))
676
677   ## If the difference is greater than 512 then we know we should do a full
678   ## dump.  (This provides an upper bound for the search below.  It should
679   ## never happen in practice, of course.)
680   if [ $(( $today - $lastday )) -ge 512 ]; then echo 0; return; fi
681
682   ## Now we work out the correct dump level.  This will assume that the
683   ## previous dump had a sensible level.  If dumps are omitted, then we will
684   ## choose a lower (more comprehensive) dump level than the schedule calls
685   ## for; such an overestimation will mean that we will probably end up
686   ## dumping too much again.  This is the right error to make.
687   ##
688   ## We use a Towers of Hanoi schedule.  If we're doing dumps every day, then
689   ## on day n since the last full dump, we work out the dump level as
690   ## follows: write n = 2^s t where t is odd (i.e., s is the number of
691   ## trailing zero bits in the binary representation of n); then the dump
692   ## level on day n is 9 - s.  This is enough for 512 days without a full
693   ## dump, and it fails gracefully anyway.
694   ##
695   ## Now we have to deal with the problem of skipping dumps.  Suppose the
696   ## last dump was on day m = 2^u v, and it's now day n = 2^s t.  We ought to
697   ## take the lowest dump level of any intervening day, i.e., the dump level
698   ## is 9 - a for the largest a such that there exists b with m < l = 2^a b
699   ## <= n.  We claim that such an l is unique.  Suppose, to the contrary,
700   ## that m < 2^a b < 2^a b' <= n, with both b and b' odd.  Then m < 2^{a+1}
701   ## (b + 1)/2 <= n, contradicting maximality of a.
702   ##
703   ## How does this help?  Observe that n = 2^s t = 2^a b + o, for some o <
704   ## 2^a: if o >= 2^a then 2^a (b + 1) <= n contradicting uniqueness of l.
705   ## Similarly, m = 2^u v = 2^a b - r, for some r <= 2^a (otherwise m <
706   ## 2^a (b - 1), again contradicting uniqueness).  Therefore, m and n are
707   ## identical from bit a + 1 onwards, and differ at bit a.  In other words,
708   ## a is the position of the most significant set bit in m XOR n.
709   diff=$(( lastday ^ today ))
710
711   ## We know that the bit position must be less than 16.
712   t=16 n=0
713   while [ $diff -gt 1 ]; do
714     xx=$(( $diff >> $t ))
715     if [ $xx -gt 0 ]; then
716       diff=$xx n=$(( $n + $t ))
717     fi
718     t=$(( $t >> 1 ))
719   done
720
721   echo $(( 9 - $n ))
722 }
723
724 defucmd level ASSET
725 cmd_level () {
726   case $# in 1) ;; *) usage_err ;; esac
727   asset=$1
728   checkhost
729   checkword asset "$asset"
730
731   ## Set the correct directory.  If it doesn't exist then we obviously need a
732   ## level-0 dump.
733   cd $BKP
734   full="0 1970-01-01 00:00:00 +0000"
735   if [ ! -d $host/$asset ]; then echo $full; return; fi
736   cd $host/$asset
737
738   ## We need the time of the most recent dump of any kind, and the most
739   ## recent level-zero dump.
740   fulldate=none lastdate=none
741   while read label level date time tz; do
742     if [ $level -eq 0 ]; then fulldate=$date; fi
743     lastdate=$date
744   done <CATALOGUE
745   case $fulldate in none) echo $full; return ;; esac
746   level=$(dumplevel $fulldate $lastdate)
747
748   ## Determine the time of the most recent dump of the same or more inclusive
749   ## level.
750   date=none
751   while read lab l d t; do
752     if [ $l -le $level ]; then date=$d time=$t; fi
753   done <CATALOGUE
754   echo $level $date $time $tz
755 }
756
757 defucmd hash ASSET FILE HASH
758 cmd_hash () {
759   case $# in 3) ;; *) usage_err ;; esac
760   asset=$1 file=$2 hash=$3
761   checkword asset "$asset"
762   checkpath file "$file"
763   checkword hash "$hash"
764
765   cd $BKP/$host/$asset/prepare
766
767   if [ -f hashes ]; then
768     while read h f; do
769       case "$f" in "$file") die "file \`$file' already hashed" ;; esac
770     done <hashes
771     cp hashes hashes.new
772   fi
773   echo "$hash  $file" >>hashes.new
774   mv hashes.new hashes
775 }
776
777 defucmd commit ASSET
778 cmd_commit () {
779   case $# in 1) ;; *) usage_err ;; esac
780   asset=$1
781   checkhost
782   checkword asset "$asset"
783
784   cd $BKP/$host/$asset/prepare
785   fixperms incoming root:bkp-$host 640 755
786   findargs=""
787
788   if [ -f hashes ]; then
789     while read hash name; do
790       if [ ! -f "incoming/$name" ]; then
791         die "precomputed hash for nonexistent or non-file \`$name'"
792       fi
793       findargs="$findargs ! -path incoming/$name"
794     done <hashes
795     cp hashes hashes.calc
796   fi
797
798   find incoming -type f $findargs -print0 | \
799     xargs -0r sha256sum | \
800     sed 's:  incoming/:  :' \
801     >>hashes.calc
802   sort -k2 hashes.calc >incoming/hashes
803   sign incoming/hashes
804   chmod 640 incoming/hashes incoming/hashes.sig
805   chown root:bkp-$host incoming/hashes incoming/hashes.sig
806
807   cd ..
808   commitdir prepare .
809   echo "$label"
810 }
811
812 defucmd check ASSET LABEL
813 cmd_check () {
814   case $# in 2) ;; *) usage_err ;; esac
815   asset=$1 label=$2
816   checkhost
817   checkword asset "$asset"
818   checkword label "$label"
819
820   checkdir pub/backup-auth.pub $BKP/$host/$asset/$label
821 }
822
823 defucmd catalogue ASSET
824 cmd_catalogue () {
825   case $# in 1) ;; *) usage_err ;; esac
826   asset=$1
827   checkhost
828   checkword asset "$asset"
829
830   cat $BKP/$host/$asset/CATALOGUE
831 }
832
833 defucmd outdated ASSET
834 cmd_outdated () {
835   case $# in 1) ;; *) usage_err ;; esac
836   asset=$1
837   checkhost
838   checkword asset "$asset"
839
840   cd $BKP/$host/$asset
841   for i in [0-9]*#*.*; do
842     if [ -d "$i" ]; then echo "$i"; fi
843   done |
844   sort -rn |
845   { best=10
846     while read tag; do
847       date=${tag%%#*} level=${tag##*.}
848       if [ $level -le $best ]
849       then best=$level
850       else echo "$tag"
851       fi
852     done
853   }
854 }
855
856 ###--------------------------------------------------------------------------
857 ### Main program.
858
859 defcmd test CMD '[ARGS ...]'
860 cmd_test () { "$@"; }
861
862 case $uservp in
863   t)
864     host=${USERV_USER#bkp-}
865     opts="h"
866     ;;
867   nil)
868     unset host
869     opts="hH:D:"
870     ;;
871 esac
872
873 while getopts "$opts" opt; do
874   case "$opt" in
875     h) cmd_help; exit ;;
876     H) host=$OPTARG ;;
877     D) forceday=$OPTARG ;;
878     *) usage_err ;;
879   esac
880 done
881 shift $(( $OPTIND - 1 ))
882
883 case $# in 0) usage_err ;; esac
884 lookupcmd "$1"; shift
885 cmd_$cmdname "$@"
886
887 ###----- That's all, folks --------------------------------------------------