chiark / gitweb /
new algorithm for expire-iso8601
[chiark-utils.git] / scripts / expire-iso8601
1 #!/bin/bash
2 set -e
3                         usage () {
4                         cat <<END
5 usage:
6   expire-iso8601 [<options>] <unit-in-seconds> <slop-in-seconds>
7      <min-interval-in-units> <number-to-keep>
8     [<min-interval-in-units> <number-to-keep> ...]
9 options:
10    -n    do not really delete
11    -r    recursive removal (rm -r)
12 example:
13    /home/ian/junk/expire-iso8601 86400 10000  1 14  7 4
14       uses units of 86400s (1 day) with a slop of 10ks;
15       it keeps 14 daily items
16        (that is 14 items, dated no less than 86400-10000s apart)
17       and 7 weekly items
18        (that is 7 items, dated no less than 7*86400-10000s apart)
19       the 14 daily and 7 weekly items may be the same, or not
20    There is no need to sort the list of interval/number pairs.
21 exit status:
22    0                   ok
23    4                   rm failed
24    8                   bad usage
25    16                  catastrophic failure
26 END
27                         }
28
29 trap 'exit 16' 0
30 badusage () { echo >&2 "bad usage: $*"; usage >&2; trap '' 0; exit 8; }
31
32 #-------------------- argument parsing --------------------
33
34 rm=rm
35 while [ $# -ge 1 ]; do
36         arg=$1; shift
37         case "$arg" in
38         --|-)   break ;;
39         --help) usage; exit 0 ;;
40         --*)    badusage "unknown option $arg" ;;
41         -*)
42                 case "$arg" in
43                 -n*)    rm=: ;;
44                 -r*)    recurse=-r ;;
45                 *)      badusage "unknown option ${1:0:2}" ;;
46                 esac
47                 arg=-${arg#-?}
48                 if test "x$arg" != x-; then set -- "$arg" "$@"; fi
49                 ;;
50         *)      set "$arg" "$@"; break ;;
51         esac
52 done
53
54 [ $# -ge 4 ] || badusage 'too few arguments'
55
56 unit=$1
57 slop=$2
58 shift;shift
59
60 [ $(($# % 2)) = 0 ] || badusage 'odd keep arguments (need min/extent pairs)'
61 argl="$*"
62
63 alldigits () {
64         [ "x${1##*[^0-9]}" = "x$1" ] || badusage "$2 must be all digits"
65         [ x$1 ] || badusage "$2 must be nonempty"
66 }
67
68 while [ $# -gt 0 ]; do
69         min=$1; shift; extent=$1; shift
70         alldigits $min min
71         alldigits $extent extent
72 done
73
74 #-------------------- scanning the directory ----------
75
76 # We build in $l a list of the relevant filenames and the time_t's
77 # they represent.
78 #
79 # Each entry in $l is $time_t/$filename, and the list is
80 # newline-separated for the benefit of sort(1).
81
82 ls=0
83 for cn in [0-9]*; do
84         case "$cn" in
85         ????-??-??)
86                 conv="$cn";;
87         ????-??-??T[0-2][0-9]+[0-9][0-9][0-9][0-9]|\
88         ????-??-??T[0-2][0-9]:[0-6][0-9]+[0-9][0-9][0-9][0-9]|\
89         ????-??-??T[0-2][0-9]:[0-6][0-9]:[0-6][0-9]+[0-9][0-9][0-9][0-9])
90                 conv="${cn%T*} ${cn#*T}";;
91         *)
92                 echo >&2 "ignoring $cn"
93                 continue;;
94         esac
95         cs=$(date -d "$conv" +%s)
96         l="$cs/$cn
97 $l"
98 done
99
100 #-------------------- main computation --------------------
101
102 # We process each minimum/extent pair, to have it select a bunch of
103 # versions to keep.  We annotate entries in $l: if we are keeping
104 # an entry we prepend a colon.
105
106 # For each minimum/extent pair we look at the list from most recent
107 # to least recent,
108 #   ie in order of increasing age
109 #   ie in order of decreasing time_t
110 # and each time we're more than min older than the last item we kept,
111 # we mark the item to keep, until we have as many as we want.
112 #
113 # We build the new list (space-separated) in lnew.
114
115 l=$(sort -nr <<END
116 $l
117 END
118 )
119
120 set $argl
121 while [ $# != 0 ]; do
122         min=$(( $1 * $unit - $slop ))
123         wantcount=$2
124
125         ls=''
126         lnew=''
127         for ce in $l; do
128                 cn=${ce#*/}; cl=${ce%%/*}; cs=${cl#:}
129                 if [ $wantcount != 0 ]; then
130                         if ! [ "$ls" ] || \
131                            [ $(( $ls - $cs )) -ge $min ]; then
132                                 echo "keep (for $1 $2) $cn"
133                                 ls=$cs
134                                 ce=:$cs/$cn
135                                 wantcount=$(( $wantcount - 1 ))
136                         fi
137                 fi
138                 lnew="$lnew $ce"
139         done
140         if [ $wantcount != 0 ];then
141                 echo "insufficient (for $1 $2) by $wantcount"
142         fi
143         shift;shift
144         l=$lnew
145 done
146
147 #-------------------- execution --------------------
148
149 trap '' 0
150 exitstatus=0
151
152 nonbroken_echo () { (echo "$@"); }
153 # While we have subprocesses, we have to avoid bash calling write(1,...)
154 # because of a bug in bash (Debian #382798), so we arrange for a subshell
155 # for each echo.
156
157 jobs=''
158 for ce in $l; do
159         case $ce in
160         :*);;
161         *)
162                 cn=${ce#*/}
163                 nonbroken_echo "expire $cn"
164                 echo $rm $recurse -- $cn &
165                 jobs="$jobs $!"
166                 ;;
167         esac
168 done
169
170 if [ "$jobs" ]; then
171         nonbroken_echo "all running"
172 fi
173
174 for job in $jobs; do
175         wait $job || exitstatus=4
176 done
177
178 if [ $exitstatus = 0 ]; then
179         echo "complete"
180 else
181         echo "complete, but problems deleting"
182 fi
183
184 exit $exitstatus