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