]> CyberLeo.Net >> Repos - FreeBSD/releng/9.2.git/blob - usr.sbin/portsnap/portsnap/portsnap.sh
- Copy stable/9 to releng/9.2 as part of the 9.2-RELEASE cycle.
[FreeBSD/releng/9.2.git] / usr.sbin / portsnap / portsnap / portsnap.sh
1 #!/bin/sh
2
3 #-
4 # Copyright 2004-2005 Colin Percival
5 # All rights reserved
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted providing that the following conditions 
9 # are met:
10 # 1. Redistributions of source code must retain the above copyright
11 #    notice, this list of conditions and the following disclaimer.
12 # 2. Redistributions in binary form must reproduce the above copyright
13 #    notice, this list of conditions and the following disclaimer in the
14 #    documentation and/or other materials provided with the distribution.
15 #
16 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
17 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19 # ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
20 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
22 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
23 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
24 # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
25 # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26 # POSSIBILITY OF SUCH DAMAGE.
27
28 # $FreeBSD$
29
30 #### Usage function -- called from command-line handling code.
31
32 # Usage instructions.  Options not listed:
33 # --debug       -- don't filter output from utilities
34 # --no-stats    -- don't show progress statistics while fetching files
35 usage() {
36         cat <<EOF
37 usage: `basename $0` [options] command ... [path]
38
39 Options:
40   -d workdir   -- Store working files in workdir
41                   (default: /var/db/portsnap/)
42   -f conffile  -- Read configuration options from conffile
43                   (default: /etc/portsnap.conf)
44   -I           -- Update INDEX only. (update command only)
45   -k KEY       -- Trust an RSA key with SHA256 hash of KEY
46   -l descfile  -- Merge the specified local describes file into the INDEX.
47   -p portsdir  -- Location of uncompressed ports tree
48                   (default: /usr/ports/)
49   -s server    -- Server from which to fetch updates.
50                   (default: portsnap.FreeBSD.org)
51   path         -- Extract only parts of the tree starting with the given
52                   string.  (extract command only)
53 Commands:
54   fetch        -- Fetch a compressed snapshot of the ports tree,
55                   or update an existing snapshot.
56   cron         -- Sleep rand(3600) seconds, and then fetch updates.
57   extract      -- Extract snapshot of ports tree, replacing existing
58                   files and directories.
59   update       -- Update ports tree to match current snapshot, replacing
60                   files and directories which have changed.
61 EOF
62         exit 0
63 }
64
65 #### Parameter handling functions.
66
67 # Initialize parameters to null, just in case they're
68 # set in the environment.
69 init_params() {
70         KEYPRINT=""
71         EXTRACTPATH=""
72         WORKDIR=""
73         PORTSDIR=""
74         CONFFILE=""
75         COMMAND=""
76         COMMANDS=""
77         QUIETREDIR=""
78         QUIETFLAG=""
79         STATSREDIR=""
80         XARGST=""
81         NDEBUG=""
82         DDSTATS=""
83         INDEXONLY=""
84         SERVERNAME=""
85         REFUSE=""
86         LOCALDESC=""
87 }
88
89 # Parse the command line
90 parse_cmdline() {
91         while [ $# -gt 0 ]; do
92                 case "$1" in
93                 -d)
94                         if [ $# -eq 1 ]; then usage; fi
95                         if [ ! -z "${WORKDIR}" ]; then usage; fi
96                         shift; WORKDIR="$1"
97                         ;;
98                 --debug)
99                         QUIETREDIR="/dev/stderr"
100                         STATSREDIR="/dev/stderr"
101                         QUIETFLAG=" "
102                         NDEBUG=" "
103                         XARGST="-t"
104                         DDSTATS=".."
105                         ;;
106                 -f)
107                         if [ $# -eq 1 ]; then usage; fi
108                         if [ ! -z "${CONFFILE}" ]; then usage; fi
109                         shift; CONFFILE="$1"
110                         ;;
111                 -h | --help | help)
112                         usage
113                         ;;
114                 -I)
115                         INDEXONLY="YES"
116                         ;;
117                 -k)
118                         if [ $# -eq 1 ]; then usage; fi
119                         if [ ! -z "${KEYPRINT}" ]; then usage; fi
120                         shift; KEYPRINT="$1"
121                         ;;
122                 -l)
123                         if [ $# -eq 1 ]; then usage; fi
124                         if [ ! -z "${LOCALDESC}" ]; then usage; fi
125                         shift; LOCALDESC="$1"
126                         ;;
127                 --no-stats)
128                         if [ -z "${STATSREDIR}" ]; then
129                                 STATSREDIR="/dev/null"
130                                 DDSTATS=".. "
131                         fi
132                         ;;
133                 -p)
134                         if [ $# -eq 1 ]; then usage; fi
135                         if [ ! -z "${PORTSDIR}" ]; then usage; fi
136                         shift; PORTSDIR="$1"
137                         ;;
138                 -s)
139                         if [ $# -eq 1 ]; then usage; fi
140                         if [ ! -z "${SERVERNAME}" ]; then usage; fi
141                         shift; SERVERNAME="$1"
142                         ;;
143                 cron | extract | fetch | update | alfred)
144                         COMMANDS="${COMMANDS} $1"
145                         ;;
146                 up)
147                         COMMANDS="${COMMANDS} update"
148                         ;;
149                 *)
150                         if [ $# -gt 1 ]; then usage; fi
151                         if echo ${COMMANDS} | grep -vq extract; then
152                                 usage
153                         fi
154                         EXTRACTPATH="$1"
155                         ;;
156                 esac
157                 shift
158         done
159
160         if [ -z "${COMMANDS}" ]; then
161                 usage
162         fi
163 }
164
165 # If CONFFILE was specified at the command-line, make
166 # sure that it exists and is readable.
167 sanity_conffile() {
168         if [ ! -z "${CONFFILE}" ] && [ ! -r "${CONFFILE}" ]; then
169                 echo -n "File does not exist "
170                 echo -n "or is not readable: "
171                 echo ${CONFFILE}
172                 exit 1
173         fi
174 }
175
176 # If a configuration file hasn't been specified, use
177 # the default value (/etc/portsnap.conf)
178 default_conffile() {
179         if [ -z "${CONFFILE}" ]; then
180                 CONFFILE="/etc/portsnap.conf"
181         fi
182 }
183
184 # Read {KEYPRINT, SERVERNAME, WORKDIR, PORTSDIR} from the configuration
185 # file if they haven't already been set.  If the configuration
186 # file doesn't exist, do nothing.
187 # Also read REFUSE (which cannot be set via the command line) if it is
188 # present in the configuration file.
189 parse_conffile() {
190         if [ -r "${CONFFILE}" ]; then
191                 for X in KEYPRINT WORKDIR PORTSDIR SERVERNAME; do
192                         eval _=\$${X}
193                         if [ -z "${_}" ]; then
194                                 eval ${X}=`grep "^${X}=" "${CONFFILE}" |
195                                     cut -f 2- -d '=' | tail -1`
196                         fi
197                 done
198
199                 if grep -qE "^REFUSE[[:space:]]" ${CONFFILE}; then
200                         REFUSE="^(`
201                                 grep -E "^REFUSE[[:space:]]" "${CONFFILE}" |
202                                     cut -c 7- | xargs echo | tr ' ' '|'
203                                 `)"
204                 fi
205
206                 if grep -qE "^INDEX[[:space:]]" ${CONFFILE}; then
207                         INDEXPAIRS="`
208                                 grep -E "^INDEX[[:space:]]" "${CONFFILE}" |
209                                     cut -c 7- | tr ' ' '|' | xargs echo`"
210                 fi
211         fi
212 }
213
214 # If parameters have not been set, use default values
215 default_params() {
216         _QUIETREDIR="/dev/null"
217         _QUIETFLAG="-q"
218         _STATSREDIR="/dev/stdout"
219         _WORKDIR="/var/db/portsnap"
220         _PORTSDIR="/usr/ports"
221         _NDEBUG="-n"
222         _LOCALDESC="/dev/null"
223         for X in QUIETREDIR QUIETFLAG STATSREDIR WORKDIR PORTSDIR       \
224             NDEBUG LOCALDESC; do
225                 eval _=\$${X}
226                 eval __=\$_${X}
227                 if [ -z "${_}" ]; then
228                         eval ${X}=${__}
229                 fi
230         done
231 }
232
233 # Perform sanity checks and set some final parameters
234 # in preparation for fetching files.  Also chdir into
235 # the working directory.
236 fetch_check_params() {
237         export HTTP_USER_AGENT="portsnap (${COMMAND}, `uname -r`)"
238
239         _SERVERNAME_z=\
240 "SERVERNAME must be given via command line or configuration file."
241         _KEYPRINT_z="Key must be given via -k option or configuration file."
242         _KEYPRINT_bad="Invalid key fingerprint: "
243         _WORKDIR_bad="Directory does not exist or is not writable: "
244
245         if [ -z "${SERVERNAME}" ]; then
246                 echo -n "`basename $0`: "
247                 echo "${_SERVERNAME_z}"
248                 exit 1
249         fi
250         if [ -z "${KEYPRINT}" ]; then
251                 echo -n "`basename $0`: "
252                 echo "${_KEYPRINT_z}"
253                 exit 1
254         fi
255         if ! echo "${KEYPRINT}" | grep -qE "^[0-9a-f]{64}$"; then
256                 echo -n "`basename $0`: "
257                 echo -n "${_KEYPRINT_bad}"
258                 echo ${KEYPRINT}
259                 exit 1
260         fi
261         if ! [ -d "${WORKDIR}" -a -w "${WORKDIR}" ]; then
262                 echo -n "`basename $0`: "
263                 echo -n "${_WORKDIR_bad}"
264                 echo ${WORKDIR}
265                 exit 1
266         fi
267         cd ${WORKDIR} || exit 1
268
269         BSPATCH=/usr/bin/bspatch
270         SHA256=/sbin/sha256
271         PHTTPGET=/usr/libexec/phttpget
272 }
273
274 # Perform sanity checks and set some final parameters
275 # in preparation for extracting or updating ${PORTSDIR}
276 # Complain if ${PORTSDIR} exists but is not writable,
277 # but don't complain if ${PORTSDIR} doesn't exist.
278 extract_check_params() {
279         _WORKDIR_bad="Directory does not exist: "
280         _PORTSDIR_bad="Directory is not writable: "
281
282         if ! [ -d "${WORKDIR}" ]; then
283                 echo -n "`basename $0`: "
284                 echo -n "${_WORKDIR_bad}"
285                 echo ${WORKDIR}
286                 exit 1
287         fi
288         if [ -d "${PORTSDIR}" ] && ! [ -w "${PORTSDIR}" ]; then
289                 echo -n "`basename $0`: "
290                 echo -n "${_PORTSDIR_bad}"
291                 echo ${PORTSDIR}
292                 exit 1
293         fi
294
295         if ! [ -d "${WORKDIR}/files" -a -r "${WORKDIR}/tag"     \
296             -a -r "${WORKDIR}/INDEX" -a -r "${WORKDIR}/tINDEX" ]; then
297                 echo "No snapshot available.  Try running"
298                 echo "# `basename $0` fetch"
299                 exit 1
300         fi
301
302         MKINDEX=/usr/libexec/make_index
303 }
304
305 # Perform sanity checks and set some final parameters
306 # in preparation for updating ${PORTSDIR}
307 update_check_params() {
308         extract_check_params
309
310         if ! [ -r ${PORTSDIR}/.portsnap.INDEX ]; then
311                 echo "${PORTSDIR} was not created by portsnap."
312                 echo -n "You must run '`basename $0` extract' before "
313                 echo "running '`basename $0` update'."
314                 exit 1
315         fi
316
317 }
318
319 #### Core functionality -- the actual work gets done here
320
321 # Use an SRV query to pick a server.  If the SRV query doesn't provide
322 # a useful answer, use the server name specified by the user.
323 # Put another way... look up _http._tcp.${SERVERNAME} and pick a server
324 # from that; or if no servers are returned, use ${SERVERNAME}.
325 # This allows a user to specify "portsnap.freebsd.org" (in which case
326 # portsnap will select one of the mirrors) or "portsnap5.tld.freebsd.org"
327 # (in which case portsnap will use that particular server, since there
328 # won't be an SRV entry for that name).
329 #
330 # We ignore the Port field, since we are always going to use port 80.
331
332 # Fetch the mirror list, but do not pick a mirror yet.  Returns 1 if
333 # no mirrors are available for any reason.
334 fetch_pick_server_init() {
335         : > serverlist_tried
336
337 # Check that host(1) exists (i.e., that the system wasn't built with the
338 # WITHOUT_BIND set) and don't try to find a mirror if it doesn't exist.
339         if ! which -s host; then
340                 : > serverlist_full
341                 return 1
342         fi
343
344         echo -n "Looking up ${SERVERNAME} mirrors... "
345
346 # Issue the SRV query and pull out the Priority, Weight, and Target fields.
347 # BIND 9 prints "$name has SRV record ..." while BIND 8 prints
348 # "$name server selection ..."; we allow either format.
349         MLIST="_http._tcp.${SERVERNAME}"
350         host -t srv "${MLIST}" |
351             sed -nE "s/${MLIST} (has SRV record|server selection) //p" |
352             cut -f 1,2,4 -d ' ' |
353             sed -e 's/\.$//' |
354             sort > serverlist_full
355
356 # If no records, give up -- we'll just use the server name we were given.
357         if [ `wc -l < serverlist_full` -eq 0 ]; then
358                 echo "none found."
359                 return 1
360         fi
361
362 # Report how many mirrors we found.
363         echo `wc -l < serverlist_full` "mirrors found."
364
365 # Generate a random seed for use in picking mirrors.  If HTTP_PROXY
366 # is set, this will be used to generate the seed; otherwise, the seed
367 # will be random.
368         if [ -n "${HTTP_PROXY}${http_proxy}" ]; then
369                 RANDVALUE=`sha256 -qs "${HTTP_PROXY}${http_proxy}" |
370                     tr -d 'a-f' |
371                     cut -c 1-9`
372         else
373                 RANDVALUE=`jot -r 1 0 999999999`
374         fi
375 }
376
377 # Pick a mirror.  Returns 1 if we have run out of mirrors to try.
378 fetch_pick_server() {
379 # Generate a list of not-yet-tried mirrors
380         sort serverlist_tried |
381             comm -23 serverlist_full - > serverlist
382
383 # Have we run out of mirrors?
384         if [ `wc -l < serverlist` -eq 0 ]; then
385                 echo "No mirrors remaining, giving up."
386                 return 1
387         fi
388
389 # Find the highest priority level (lowest numeric value).
390         SRV_PRIORITY=`cut -f 1 -d ' ' serverlist | sort -n | head -1`
391
392 # Add up the weights of the response lines at that priority level.
393         SRV_WSUM=0;
394         while read X; do
395                 case "$X" in
396                 ${SRV_PRIORITY}\ *)
397                         SRV_W=`echo $X | cut -f 2 -d ' '`
398                         SRV_WSUM=$(($SRV_WSUM + $SRV_W))
399                         ;;
400                 esac
401         done < serverlist
402
403 # If all the weights are 0, pretend that they are all 1 instead.
404         if [ ${SRV_WSUM} -eq 0 ]; then
405                 SRV_WSUM=`grep -E "^${SRV_PRIORITY} " serverlist | wc -l`
406                 SRV_W_ADD=1
407         else
408                 SRV_W_ADD=0
409         fi
410
411 # Pick a value between 0 and the sum of the weights - 1
412         SRV_RND=`expr ${RANDVALUE} % ${SRV_WSUM}`
413
414 # Read through the list of mirrors and set SERVERNAME.  Write the line
415 # corresponding to the mirror we selected into serverlist_tried so that
416 # we won't try it again.
417         while read X; do
418                 case "$X" in
419                 ${SRV_PRIORITY}\ *)
420                         SRV_W=`echo $X | cut -f 2 -d ' '`
421                         SRV_W=$(($SRV_W + $SRV_W_ADD))
422                         if [ $SRV_RND -lt $SRV_W ]; then
423                                 SERVERNAME=`echo $X | cut -f 3 -d ' '`
424                                 echo "$X" >> serverlist_tried
425                                 break
426                         else
427                                 SRV_RND=$(($SRV_RND - $SRV_W))
428                         fi
429                         ;;
430                 esac
431         done < serverlist
432 }
433
434 # Check that we have a public key with an appropriate hash, or
435 # fetch the key if it doesn't exist.  Returns 1 if the key has
436 # not yet been fetched.
437 fetch_key() {
438         if [ -r pub.ssl ] && [ `${SHA256} -q pub.ssl` = ${KEYPRINT} ]; then
439                 return 0
440         fi
441
442         echo -n "Fetching public key from ${SERVERNAME}... "
443         rm -f pub.ssl
444         fetch ${QUIETFLAG} http://${SERVERNAME}/pub.ssl \
445             2>${QUIETREDIR} || true
446         if ! [ -r pub.ssl ]; then
447                 echo "failed."
448                 return 1
449         fi
450         if ! [ `${SHA256} -q pub.ssl` = ${KEYPRINT} ]; then
451                 echo "key has incorrect hash."
452                 rm -f pub.ssl
453                 return 1
454         fi
455         echo "done."
456 }
457
458 # Fetch a snapshot tag
459 fetch_tag() {
460         rm -f snapshot.ssl tag.new
461
462         echo ${NDEBUG} "Fetching snapshot tag from ${SERVERNAME}... "
463         fetch ${QUIETFLAG} http://${SERVERNAME}/$1.ssl          \
464             2>${QUIETREDIR} || true
465         if ! [ -r $1.ssl ]; then
466                 echo "failed."
467                 return 1
468         fi
469
470         openssl rsautl -pubin -inkey pub.ssl -verify            \
471             < $1.ssl > tag.new 2>${QUIETREDIR} || true
472         rm $1.ssl
473
474         if ! [ `wc -l < tag.new` = 1 ] ||
475             ! grep -qE "^portsnap\|[0-9]{10}\|[0-9a-f]{64}" tag.new; then
476                 echo "invalid snapshot tag."
477                 return 1
478         fi
479
480         echo "done."
481
482         SNAPSHOTDATE=`cut -f 2 -d '|' < tag.new`
483         SNAPSHOTHASH=`cut -f 3 -d '|' < tag.new`
484 }
485
486 # Sanity-check the date on a snapshot tag
487 fetch_snapshot_tagsanity() {
488         if [ `date "+%s"` -gt `expr ${SNAPSHOTDATE} + 31536000` ]; then
489                 echo "Snapshot appears to be more than a year old!"
490                 echo "(Is the system clock correct?)"
491                 echo "Cowardly refusing to proceed any further."
492                 return 1
493         fi
494         if [ `date "+%s"` -lt `expr ${SNAPSHOTDATE} - 86400` ]; then
495                 echo -n "Snapshot appears to have been created more than "
496                 echo "one day into the future!"
497                 echo "(Is the system clock correct?)"
498                 echo "Cowardly refusing to proceed any further."
499                 return 1
500         fi
501 }
502
503 # Sanity-check the date on a snapshot update tag
504 fetch_update_tagsanity() {
505         fetch_snapshot_tagsanity || return 1
506
507         if [ ${OLDSNAPSHOTDATE} -gt ${SNAPSHOTDATE} ]; then
508                 echo -n "Latest snapshot on server is "
509                 echo "older than what we already have!"
510                 echo -n "Cowardly refusing to downgrade from "
511                 date -r ${OLDSNAPSHOTDATE}
512                 echo "to `date -r ${SNAPSHOTDATE}`."
513                 return 1
514         fi
515 }
516
517 # Compare old and new tags; return 1 if update is unnecessary
518 fetch_update_neededp() {
519         if [ ${OLDSNAPSHOTDATE} -eq ${SNAPSHOTDATE} ]; then
520                 echo -n "Latest snapshot on server matches "
521                 echo "what we already have."
522                 echo "No updates needed."
523                 rm tag.new
524                 return 1
525         fi
526         if [ ${OLDSNAPSHOTHASH} = ${SNAPSHOTHASH} ]; then
527                 echo -n "Ports tree hasn't changed since "
528                 echo "last snapshot."
529                 echo "No updates needed."
530                 rm tag.new
531                 return 1
532         fi
533
534         return 0
535 }
536
537 # Fetch snapshot metadata file
538 fetch_metadata() {
539         rm -f ${SNAPSHOTHASH} tINDEX.new
540
541         echo ${NDEBUG} "Fetching snapshot metadata... "
542         fetch ${QUIETFLAG} http://${SERVERNAME}/t/${SNAPSHOTHASH} \
543             2>${QUIETREDIR} || return
544         if [ "`${SHA256} -q ${SNAPSHOTHASH}`" != ${SNAPSHOTHASH} ]; then
545                 echo "snapshot metadata corrupt."
546                 return 1
547         fi
548         mv ${SNAPSHOTHASH} tINDEX.new
549         echo "done."
550 }
551
552 # Warn user about bogus metadata
553 fetch_metadata_freakout() {
554         echo
555         echo "Portsnap metadata is correctly signed, but contains"
556         echo "at least one line which appears bogus."
557         echo "Cowardly refusing to proceed any further."
558 }
559
560 # Sanity-check a snapshot metadata file
561 fetch_metadata_sanity() {
562         if grep -qvE "^[0-9A-Z.]+\|[0-9a-f]{64}$" tINDEX.new; then
563                 fetch_metadata_freakout
564                 return 1
565         fi
566         if [ `look INDEX tINDEX.new | wc -l` != 1 ]; then
567                 echo
568                 echo "Portsnap metadata appears bogus."
569                 echo "Cowardly refusing to proceed any further."
570                 return 1
571         fi
572 }
573
574 # Take a list of ${oldhash}|${newhash} and output a list of needed patches
575 fetch_make_patchlist() {
576         grep -vE "^([0-9a-f]{64})\|\1$" | 
577                 while read LINE; do
578                         X=`echo ${LINE} | cut -f 1 -d '|'`
579                         Y=`echo ${LINE} | cut -f 2 -d '|'`
580                         if [ -f "files/${Y}.gz" ]; then continue; fi
581                         if [ ! -f "files/${X}.gz" ]; then continue; fi
582                         echo "${LINE}"
583                 done
584 }
585
586 # Print user-friendly progress statistics
587 fetch_progress() {
588         LNC=0
589         while read x; do
590                 LNC=$(($LNC + 1))
591                 if [ $(($LNC % 10)) = 0 ]; then
592                         echo -n $LNC
593                 elif [ $(($LNC % 2)) = 0 ]; then
594                         echo -n .
595                 fi
596         done
597         echo -n " "
598 }
599
600 # Sanity-check an index file
601 fetch_index_sanity() {
602         if grep -qvE "^[-_+./@0-9A-Za-z]+\|[0-9a-f]{64}$" INDEX.new ||
603             fgrep -q "./" INDEX.new; then
604                 fetch_metadata_freakout
605                 return 1
606         fi
607 }
608
609 # Verify a list of files
610 fetch_snapshot_verify() {
611         while read F; do
612                 if [ "`gunzip -c snap/${F} | ${SHA256} -q`" != ${F} ]; then
613                         echo "snapshot corrupt."
614                         return 1
615                 fi
616         done
617         return 0
618 }
619
620 # Fetch a snapshot tarball, extract, and verify.
621 fetch_snapshot() {
622         while ! fetch_tag snapshot; do
623                 fetch_pick_server || return 1
624         done
625         fetch_snapshot_tagsanity || return 1
626         fetch_metadata || return 1
627         fetch_metadata_sanity || return 1
628
629         rm -rf snap/
630
631 # Don't ask fetch(1) to be quiet -- downloading a snapshot of ~ 35MB will
632 # probably take a while, so the progrees reports that fetch(1) generates
633 # will be useful for keeping the users' attention from drifting.
634         echo "Fetching snapshot generated at `date -r ${SNAPSHOTDATE}`:"
635         fetch -r http://${SERVERNAME}/s/${SNAPSHOTHASH}.tgz || return 1
636
637         echo -n "Extracting snapshot... "
638         tar -xz --numeric-owner -f ${SNAPSHOTHASH}.tgz snap/ || return 1
639         rm ${SNAPSHOTHASH}.tgz
640         echo "done."
641
642         echo -n "Verifying snapshot integrity... "
643 # Verify the metadata files
644         cut -f 2 -d '|' tINDEX.new | fetch_snapshot_verify || return 1
645 # Extract the index
646         rm -f INDEX.new
647         gunzip -c snap/`look INDEX tINDEX.new |
648             cut -f 2 -d '|'`.gz > INDEX.new
649         fetch_index_sanity || return 1
650 # Verify the snapshot contents
651         cut -f 2 -d '|' INDEX.new | fetch_snapshot_verify || return 1
652         echo "done."
653
654 # Move files into their proper locations
655         rm -f tag INDEX tINDEX
656         rm -rf files
657         mv tag.new tag
658         mv tINDEX.new tINDEX
659         mv INDEX.new INDEX
660         mv snap/ files/
661
662         return 0
663 }
664
665 # Update a compressed snapshot
666 fetch_update() {
667         rm -f patchlist diff OLD NEW filelist INDEX.new
668
669         OLDSNAPSHOTDATE=`cut -f 2 -d '|' < tag`
670         OLDSNAPSHOTHASH=`cut -f 3 -d '|' < tag`
671
672         while ! fetch_tag latest; do
673                 fetch_pick_server || return 1
674         done
675         fetch_update_tagsanity || return 1
676         fetch_update_neededp || return 0
677         fetch_metadata || return 1
678         fetch_metadata_sanity || return 1
679
680         echo -n "Updating from `date -r ${OLDSNAPSHOTDATE}` "
681         echo "to `date -r ${SNAPSHOTDATE}`."
682
683 # Generate a list of wanted metadata patches
684         join -t '|' -o 1.2,2.2 tINDEX tINDEX.new |
685             fetch_make_patchlist > patchlist
686
687 # Attempt to fetch metadata patches
688         echo -n "Fetching `wc -l < patchlist | tr -d ' '` "
689         echo ${NDEBUG} "metadata patches.${DDSTATS}"
690         tr '|' '-' < patchlist |
691             lam -s "tp/" - -s ".gz" |
692             xargs ${XARGST} ${PHTTPGET} ${SERVERNAME}   \
693             2>${STATSREDIR} | fetch_progress
694         echo "done."
695
696 # Attempt to apply metadata patches
697         echo -n "Applying metadata patches... "
698         while read LINE; do
699                 X=`echo ${LINE} | cut -f 1 -d '|'`
700                 Y=`echo ${LINE} | cut -f 2 -d '|'`
701                 if [ ! -f "${X}-${Y}.gz" ]; then continue; fi
702                 gunzip -c < ${X}-${Y}.gz > diff
703                 gunzip -c < files/${X}.gz > OLD
704                 cut -c 2- diff | join -t '|' -v 2 - OLD > ptmp
705                 grep '^\+' diff | cut -c 2- |
706                     sort -k 1,1 -t '|' -m - ptmp > NEW
707                 if [ `${SHA256} -q NEW` = ${Y} ]; then
708                         mv NEW files/${Y}
709                         gzip -n files/${Y}
710                 fi
711                 rm -f diff OLD NEW ${X}-${Y}.gz ptmp
712         done < patchlist 2>${QUIETREDIR}
713         echo "done."
714
715 # Update metadata without patches
716         join -t '|' -v 2 tINDEX tINDEX.new |
717             cut -f 2 -d '|' /dev/stdin patchlist |
718                 while read Y; do
719                         if [ ! -f "files/${Y}.gz" ]; then
720                                 echo ${Y};
721                         fi
722                 done > filelist
723         echo -n "Fetching `wc -l < filelist | tr -d ' '` "
724         echo ${NDEBUG} "metadata files... "
725         lam -s "f/" - -s ".gz" < filelist |
726             xargs ${XARGST} ${PHTTPGET} ${SERVERNAME}   \
727             2>${QUIETREDIR}
728
729         while read Y; do
730                 if [ `gunzip -c < ${Y}.gz | ${SHA256} -q` = ${Y} ]; then
731                         mv ${Y}.gz files/${Y}.gz
732                 else
733                         echo "metadata is corrupt."
734                         return 1
735                 fi
736         done < filelist
737         echo "done."
738
739 # Extract the index
740         gunzip -c files/`look INDEX tINDEX.new |
741             cut -f 2 -d '|'`.gz > INDEX.new
742         fetch_index_sanity || return 1
743
744 # If we have decided to refuse certain updates, construct a hybrid index which
745 # is equal to the old index for parts of the tree which we don't want to
746 # update, and equal to the new index for parts of the tree which gets updates.
747 # This means that we should always have a "complete snapshot" of the ports
748 # tree -- with the caveat that it isn't actually a snapshot.
749         if [ ! -z "${REFUSE}" ]; then
750                 echo "Refusing to download updates for ${REFUSE}"       \
751                     >${QUIETREDIR}
752
753                 grep -Ev "${REFUSE}" INDEX.new > INDEX.tmp
754                 grep -E "${REFUSE}" INDEX |
755                     sort -m -k 1,1 -t '|' - INDEX.tmp > INDEX.new
756                 rm -f INDEX.tmp
757         fi
758
759 # Generate a list of wanted ports patches
760         join -t '|' -o 1.2,2.2 INDEX INDEX.new |
761             fetch_make_patchlist > patchlist
762
763 # Attempt to fetch ports patches
764         echo -n "Fetching `wc -l < patchlist | tr -d ' '` "
765         echo ${NDEBUG} "patches.${DDSTATS}"
766         tr '|' '-' < patchlist | lam -s "bp/" - |
767             xargs ${XARGST} ${PHTTPGET} ${SERVERNAME}   \
768             2>${STATSREDIR} | fetch_progress
769         echo "done."
770
771 # Attempt to apply ports patches
772         echo -n "Applying patches... "
773         while read LINE; do
774                 X=`echo ${LINE} | cut -f 1 -d '|'`
775                 Y=`echo ${LINE} | cut -f 2 -d '|'`
776                 if [ ! -f "${X}-${Y}" ]; then continue; fi
777                 gunzip -c < files/${X}.gz > OLD
778                 ${BSPATCH} OLD NEW ${X}-${Y}
779                 if [ `${SHA256} -q NEW` = ${Y} ]; then
780                         mv NEW files/${Y}
781                         gzip -n files/${Y}
782                 fi
783                 rm -f diff OLD NEW ${X}-${Y}
784         done < patchlist 2>${QUIETREDIR}
785         echo "done."
786
787 # Update ports without patches
788         join -t '|' -v 2 INDEX INDEX.new |
789             cut -f 2 -d '|' /dev/stdin patchlist |
790                 while read Y; do
791                         if [ ! -f "files/${Y}.gz" ]; then
792                                 echo ${Y};
793                         fi
794                 done > filelist
795         echo -n "Fetching `wc -l < filelist | tr -d ' '` "
796         echo ${NDEBUG} "new ports or files... "
797         lam -s "f/" - -s ".gz" < filelist |
798             xargs ${XARGST} ${PHTTPGET} ${SERVERNAME}   \
799             2>${QUIETREDIR}
800
801         while read Y; do
802                 if [ `gunzip -c < ${Y}.gz | ${SHA256} -q` = ${Y} ]; then
803                         mv ${Y}.gz files/${Y}.gz
804                 else
805                         echo "snapshot is corrupt."
806                         return 1
807                 fi
808         done < filelist
809         echo "done."
810
811 # Remove files which are no longer needed
812         cut -f 2 -d '|' tINDEX INDEX | sort -u > oldfiles
813         cut -f 2 -d '|' tINDEX.new INDEX.new | sort -u | comm -13 - oldfiles |
814             lam -s "files/" - -s ".gz" | xargs rm -f
815         rm patchlist filelist oldfiles
816
817 # We're done!
818         mv INDEX.new INDEX
819         mv tINDEX.new tINDEX
820         mv tag.new tag
821
822         return 0
823 }
824
825 # Do the actual work involved in "fetch" / "cron".
826 fetch_run() {
827         fetch_pick_server_init && fetch_pick_server
828
829         while ! fetch_key; do
830                 fetch_pick_server || return 1
831         done
832
833         if ! [ -d files -a -r tag -a -r INDEX -a -r tINDEX ]; then
834                 fetch_snapshot || return 1
835         fi
836         fetch_update || return 1
837 }
838
839 # Build a ports INDEX file
840 extract_make_index() {
841         if ! look $1 ${WORKDIR}/tINDEX > /dev/null; then
842                 echo -n "$1 not provided by portsnap server; "
843                 echo "$2 not being generated."
844         else
845         gunzip -c "${WORKDIR}/files/`look $1 ${WORKDIR}/tINDEX |
846             cut -f 2 -d '|'`.gz" |
847             cat - ${LOCALDESC} |
848             ${MKINDEX} /dev/stdin > ${PORTSDIR}/$2
849         fi
850 }
851
852 # Create INDEX, INDEX-5, INDEX-6
853 extract_indices() {
854         echo -n "Building new INDEX files... "
855         for PAIR in ${INDEXPAIRS}; do
856                 INDEXFILE=`echo ${PAIR} | cut -f 1 -d '|'`
857                 DESCRIBEFILE=`echo ${PAIR} | cut -f 2 -d '|'`
858                 extract_make_index ${DESCRIBEFILE} ${INDEXFILE} || return 1
859         done
860         echo "done."
861 }
862
863 # Create .portsnap.INDEX; if we are REFUSEing to touch certain directories,
864 # merge the values from any exiting .portsnap.INDEX file.
865 extract_metadata() {
866         if [ -z "${REFUSE}" ]; then
867                 sort ${WORKDIR}/INDEX > ${PORTSDIR}/.portsnap.INDEX
868         elif [ -f ${PORTSDIR}/.portsnap.INDEX ]; then
869                 grep -E "${REFUSE}" ${PORTSDIR}/.portsnap.INDEX \
870                     > ${PORTSDIR}/.portsnap.INDEX.tmp
871                 grep -vE "${REFUSE}" ${WORKDIR}/INDEX | sort |
872                     sort -m - ${PORTSDIR}/.portsnap.INDEX.tmp   \
873                     > ${PORTSDIR}/.portsnap.INDEX
874                 rm -f ${PORTSDIR}/.portsnap.INDEX.tmp
875         else
876                 grep -vE "${REFUSE}" ${WORKDIR}/INDEX | sort \
877                     > ${PORTSDIR}/.portsnap.INDEX
878         fi
879 }
880
881 # Do the actual work involved in "extract"
882 extract_run() {
883         mkdir -p ${PORTSDIR} || return 1
884
885         if !
886                 if ! [ -z "${EXTRACTPATH}" ]; then
887                         grep "^${EXTRACTPATH}" ${WORKDIR}/INDEX
888                 elif ! [ -z "${REFUSE}" ]; then
889                         grep -vE "${REFUSE}" ${WORKDIR}/INDEX
890                 else
891                         cat ${WORKDIR}/INDEX
892                 fi | tr '|' ' ' | while read FILE HASH; do
893                 echo ${PORTSDIR}/${FILE}
894                 if ! [ -r "${WORKDIR}/files/${HASH}.gz" ]; then
895                         echo "files/${HASH}.gz not found -- snapshot corrupt."
896                         return 1
897                 fi
898                 case ${FILE} in
899                 */)
900                         rm -rf ${PORTSDIR}/${FILE%/}
901                         mkdir -p ${PORTSDIR}/${FILE}
902                         tar -xz --numeric-owner -f ${WORKDIR}/files/${HASH}.gz \
903                             -C ${PORTSDIR}/${FILE}
904                         ;;
905                 *)
906                         rm -f ${PORTSDIR}/${FILE}
907                         tar -xz --numeric-owner -f ${WORKDIR}/files/${HASH}.gz \
908                             -C ${PORTSDIR} ${FILE}
909                         ;;
910                 esac
911         done; then
912                 return 1
913         fi
914         if [ ! -z "${EXTRACTPATH}" ]; then
915                 return 0;
916         fi
917
918         extract_metadata
919         extract_indices
920 }
921
922 # Do the actual work involved in "update"
923 update_run() {
924         if ! [ -z "${INDEXONLY}" ]; then
925                 extract_indices >/dev/null || return 1
926                 return 0
927         fi
928
929         if sort ${WORKDIR}/INDEX |
930             cmp -s ${PORTSDIR}/.portsnap.INDEX -; then
931                 echo "Ports tree is already up to date."
932                 return 0
933         fi
934
935 # If we are REFUSEing to touch certain directories, don't remove files
936 # from those directories (even if they are out of date)
937         echo -n "Removing old files and directories... "
938         if ! [ -z "${REFUSE}" ]; then 
939                 sort ${WORKDIR}/INDEX |
940                     comm -23 ${PORTSDIR}/.portsnap.INDEX - | cut -f 1 -d '|' |
941                     grep -vE "${REFUSE}" |
942                     lam -s "${PORTSDIR}/" - |
943                     sed -e 's|/$||' | xargs rm -rf
944         else
945                 sort ${WORKDIR}/INDEX |
946                     comm -23 ${PORTSDIR}/.portsnap.INDEX - | cut -f 1 -d '|' |
947                     lam -s "${PORTSDIR}/" - |
948                     sed -e 's|/$||' | xargs rm -rf
949         fi
950         echo "done."
951
952 # Install new files
953         echo "Extracting new files:"
954         if !
955                 if ! [ -z "${REFUSE}" ]; then
956                         grep -vE "${REFUSE}" ${WORKDIR}/INDEX | sort
957                 else
958                         sort ${WORKDIR}/INDEX
959                 fi |
960             comm -13 ${PORTSDIR}/.portsnap.INDEX - |
961             while read LINE; do
962                 FILE=`echo ${LINE} | cut -f 1 -d '|'`
963                 HASH=`echo ${LINE} | cut -f 2 -d '|'`
964                 echo ${PORTSDIR}/${FILE}
965                 if ! [ -r "${WORKDIR}/files/${HASH}.gz" ]; then
966                         echo "files/${HASH}.gz not found -- snapshot corrupt."
967                         return 1
968                 fi
969                 case ${FILE} in
970                 */)
971                         mkdir -p ${PORTSDIR}/${FILE}
972                         tar -xz --numeric-owner -f ${WORKDIR}/files/${HASH}.gz \
973                             -C ${PORTSDIR}/${FILE}
974                         ;;
975                 *)
976                         tar -xz --numeric-owner -f ${WORKDIR}/files/${HASH}.gz \
977                             -C ${PORTSDIR} ${FILE}
978                         ;;
979                 esac
980         done; then
981                 return 1
982         fi
983
984         extract_metadata
985         extract_indices
986 }
987
988 #### Main functions -- call parameter-handling and core functions
989
990 # Using the command line, configuration file, and defaults,
991 # set all the parameters which are needed later.
992 get_params() {
993         init_params
994         parse_cmdline $@
995         sanity_conffile
996         default_conffile
997         parse_conffile
998         default_params
999 }
1000
1001 # Fetch command.  Make sure that we're being called
1002 # interactively, then run fetch_check_params and fetch_run
1003 cmd_fetch() {
1004         if [ ! -t 0 ]; then
1005                 echo -n "`basename $0` fetch should not "
1006                 echo "be run non-interactively."
1007                 echo "Run `basename $0` cron instead."
1008                 exit 1
1009         fi
1010         fetch_check_params
1011         fetch_run || exit 1
1012 }
1013
1014 # Cron command.  Make sure the parameters are sensible; wait
1015 # rand(3600) seconds; then fetch updates.  While fetching updates,
1016 # send output to a temporary file; only print that file if the
1017 # fetching failed.
1018 cmd_cron() {
1019         fetch_check_params
1020         sleep `jot -r 1 0 3600`
1021
1022         TMPFILE=`mktemp /tmp/portsnap.XXXXXX` || exit 1
1023         if ! fetch_run >> ${TMPFILE}; then
1024                 cat ${TMPFILE}
1025                 rm ${TMPFILE}
1026                 exit 1
1027         fi
1028
1029         rm ${TMPFILE}
1030 }
1031
1032 # Extract command.  Make sure the parameters are sensible,
1033 # then extract the ports tree (or part thereof).
1034 cmd_extract() {
1035         extract_check_params
1036         extract_run || exit 1
1037 }
1038
1039 # Update command.  Make sure the parameters are sensible,
1040 # then update the ports tree.
1041 cmd_update() {
1042         update_check_params
1043         update_run || exit 1
1044 }
1045
1046 # Alfred command.  Run 'fetch' or 'cron' depending on
1047 # whether stdin is a terminal; then run 'update' or
1048 # 'extract' depending on whether ${PORTSDIR} exists.
1049 cmd_alfred() {
1050         if [ -t 0 ]; then
1051                 cmd_fetch
1052         else
1053                 cmd_cron
1054         fi
1055         if [ -d ${PORTSDIR} ]; then
1056                 cmd_update
1057         else
1058                 cmd_extract
1059         fi
1060 }
1061
1062 #### Entry point
1063
1064 # Make sure we find utilities from the base system
1065 export PATH=/sbin:/bin:/usr/sbin:/usr/bin:${PATH}
1066
1067 # Set LC_ALL in order to avoid problems with character ranges like [A-Z].
1068 export LC_ALL=C
1069
1070 get_params $@
1071 for COMMAND in ${COMMANDS}; do
1072         cmd_${COMMAND}
1073 done