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