chiark / gitweb /
git-cache-proxy: housekeeping: use rm -rf, not remove_tree (which is weirdly broken)
[chiark-utils.git] / scripts / expire-iso8601
1 #!/bin/bash
2 set -e
3                         usage () {
4                         cat <<END
5 usage:
6   expire-iso8601 [<options>] <number>x<interval> [<number>x<interval> ...]
7 options:
8    -u<unitlen>  <interval> is measured in units of <unitlen> seconds
9                    (default is 86400, so <interval> is in days)
10    -s<slop>     allow kept items to be <slop> seconds shorter apart than
11                    specified; default is 10% of <unitlen>
12    -n           do not really delete
13    -r           recursive removal (rm -r)
14 example:
15    /home/ian/junk/expire-iso8601 14x1 4x7
16       uses units of 86400s (1 day) with a slop of 8640
17       it keeps 14 daily items
18        (that is 14 items, dated no less than 86400-8640 apart)
19       and 4 weekly items
20        (that is 4 items, dated no less than 7*86400-8640 apart)
21       the 14 daily and 7 weekly items may be the same, or not
22    There is no need to sort the list of <number>x<interval> pairs.
23 exit status:
24    0                   ok
25    4                   rm failed
26    8                   bad usage
27    16                  catastrophic failure
28 END
29                         }
30
31 # Copyright 2006 Ian Jackson <ian@chiark.greenend.org.uk>
32 #
33 # This script and its documentation (if any) are free software; you
34 # can redistribute it and/or modify them under the terms of the GNU
35 # General Public License as published by the Free Software Foundation;
36 # either version 3, or (at your option) any later version.
37
38 # chiark-named-conf and its manpage are distributed in the hope that
39 # it will be useful, but WITHOUT ANY WARRANTY; without even the
40 # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
41 # PURPOSE.  See the GNU General Public License for more details.
42
43 # You should have received a copy of the GNU General Public License along
44 # with this program; if not, consult the Free Software Foundation's
45 # website at www.fsf.org, or the GNU Project website at www.gnu.org.
46
47
48 trap 'exit 16' 0
49 badusage () { echo >&2 "bad usage: $*"; usage >&2; trap '' 0; exit 8; }
50
51 #-------------------- argument parsing --------------------
52
53 alldigits () {
54         [ "x${2##*[^0-9]}" = "x$2" ] || \
55                 badusage "bad $1 \`$2'; must be all digits"
56         [ "$2" ] || badusage "bad $2; must be nonempty"
57         eval $1='$2'
58 }
59
60 rm=rm
61 recurse=''
62 unit=86400
63 slop=''
64
65 while [ $# -ge 1 ]; do
66         arg=$1; shift
67         case "$arg" in
68         --|-)   break ;;
69         --help) usage; exit 0 ;;
70         --*)    badusage "unknown option $arg" ;;
71         -*)
72                 val=${arg#-?}
73                 case "$arg" in
74                 -n*)    rm=: ;;
75                 -r*)    recurse=-r ;;
76                 -u*)    alldigits unit "$val"; arg='' ;;
77                 -s*)    alldigits slop "$val"; arg='' ;;
78                 *)      badusage "unknown option ${1:0:2}" ;;
79                 esac
80                 arg=-${arg#-?}
81                 if test "x$arg" != x-; then set -- "$arg" "$@"; fi
82                 ;;
83         *)      set "$arg" "$@"; break ;;
84         esac
85 done
86
87 [ $# -ge 1 ] || badusage 'too few arguments'
88 [ "$slop" ] || slop=$(( $unit / 10 ))
89
90 for ni in "$@"; do
91         case "$ni" in *x*);; *) badusage "bad <number>x<interval> $ni";; esac
92         alldigits number "${ni%%x*}"
93         alldigits interval "${ni#*x}"
94 done
95
96 #-------------------- scanning the directory ----------
97
98 # We build in $l a list of the relevant filenames and the time_t's
99 # they represent.
100 #
101 # Each entry in $l is $time_t/$filename, and the list is
102 # newline-separated for the benefit of sort(1).
103
104 ls=0
105 for cn in [0-9]*; do
106         case "$cn" in
107         ????-??-??)
108                 conv="$cn";;
109         ????-??-??T[0-2][0-9]+[0-9][0-9][0-9][0-9]|\
110         ????-??-??T[0-2][0-9]:[0-6][0-9]+[0-9][0-9][0-9][0-9]|\
111         ????-??-??T[0-2][0-9]:[0-6][0-9]:[0-6][0-9]+[0-9][0-9][0-9][0-9])
112                 conv="${cn%T*} ${cn#*T}";;
113         *)
114                 echo >&2 "ignoring $cn"
115                 continue;;
116         esac
117         cs=$(date -d "$conv" +%s)
118         l="$cs/$cn
119 $l"
120 done
121
122 #-------------------- main computation --------------------
123
124 # We process each minimum/extent pair, to have it select a bunch of
125 # versions to keep.  We annotate entries in $l: if we are keeping
126 # an entry we prepend a colon; temporarily, if we are keeping an entry
127 # because of this particular minimum/extent, we prepend a comma.
128
129 # For each minimum/extent pair we look at the list from most recent
130 # to least recent,
131 #   ie in order of increasing age
132 #   ie in order of decreasing time_t
133 # and each time we're more than min older than the last item we kept,
134 # we mark the item to keep, until we have as many as we want.
135 #
136 # We build the new list (space-separated) in lnew.
137
138 l=$(sort -nr <<END
139 $l
140 END
141 )
142
143 for ni in "$@"; do
144         wantcount=${ni%x*}
145
146         div=1
147
148         while true; do
149                 min=$(( (${ni#*x} * $unit) / $div - $slop ))
150
151                 ls=''
152                 lnew=''
153                 skipped=0
154                 for ce in $l; do
155                         cn=${ce#*/}; cl=${ce%%/*}
156                         cs=${cl#,}; cs=${cs#:}
157                         case $cl in ,*) ls=$cs; continue;; esac
158                         if [ $wantcount != 0 ]; then
159                                 if ! [ "$ls" ] || \
160                                    [ $(( $ls - $cs )) -ge $min ]; then
161                                         echo "keep (for $ni) $cn"
162                                         ce=,$ce
163                                         ls=$cs
164                                         wantcount=$(( $wantcount - 1 ))
165                                 else
166                                         skipped=$(( $skipped+1 ))
167                                 fi
168                         fi
169                         lnew="$lnew $ce"
170                 done
171                 l=$lnew
172
173                 if [ $wantcount = 0 ]; then break; fi
174                 printf "%s" "insufficient (for $ni) by $wantcount"
175                 if [ $skipped = 0 ]; then echo; break; fi
176                 div=$(( $div * 2 ))
177                 echo " shortening interval ${div}x"
178         done
179
180         # s/([,:]+).*/:\1/g
181         lnew=''
182         for ce in $l; do
183                 case $ce in ,*) ce=:${ce#,};; esac
184                 case $ce in ::*) ce=${ce#:};; esac
185                 lnew="$lnew $ce"
186         done
187         l=$lnew
188 done
189
190 #-------------------- execution --------------------
191
192 trap '' 0
193 exitstatus=0
194
195 nonbroken_echo () { (echo "$@"); }
196 # While we have subprocesses, we have to avoid bash calling write(1,...)
197 # because of a bug in bash (Debian #382798), so we arrange for a subshell
198 # for each echo.
199
200 jobs=''
201 for ce in $l; do
202         case $ce in
203         :*);;
204         *)
205                 cn=${ce#*/}
206                 nonbroken_echo "expire $cn"
207                 $rm $recurse -- $cn &
208                 jobs="$jobs $!"
209                 ;;
210         esac
211 done
212
213 if [ "$jobs" ]; then
214         nonbroken_echo "all running"
215 fi
216
217 for job in $jobs; do
218         wait $job || exitstatus=4
219 done
220
221 if [ $exitstatus = 0 ]; then
222         echo "complete"
223 else
224         echo "complete, but problems deleting"
225 fi
226
227 exit $exitstatus