portsnap.sh revision 149023
1275970Scy#!/bin/sh
2275970Scy
3275970Scy#-
4275970Scy# Copyright 2004-2005 Colin Percival
5285612Sdelphij# All rights reserved
6275970Scy#
7275970Scy# Redistribution and use in source and binary forms, with or without
8275970Scy# modification, are permitted providing that the following conditions 
9275970Scy# are met:
10275970Scy# 1. Redistributions of source code must retain the above copyright
11275970Scy#    notice, this list of conditions and the following disclaimer.
12275970Scy# 2. Redistributions in binary form must reproduce the above copyright
13275970Scy#    notice, this list of conditions and the following disclaimer in the
14275970Scy#    documentation and/or other materials provided with the distribution.
15275970Scy#
16275970Scy# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
17275970Scy# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18275970Scy# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19275970Scy# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
20275970Scy# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21275970Scy# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
22275970Scy# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
23275970Scy# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
24275970Scy# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
25275970Scy# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26275970Scy# POSSIBILITY OF SUCH DAMAGE.
27275970Scy
28275970Scy# $FreeBSD: head/usr.sbin/portsnap/portsnap/portsnap.sh 149023 2005-08-13 11:55:29Z cperciva $
29275970Scy
30275970Scy#### Usage function -- called from command-line handling code.
31275970Scy
32275970Scy# Usage instructions.  Options not listed:
33275970Scy# --debug	-- don't filter output from utilities
34285612Sdelphij# --no-stats	-- don't show progress statistics while fetching files
35275970Scyusage() {
36275970Scy	cat <<EOF
37275970Scyusage: `basename $0` [options] command [path]
38275970Scy
39275970ScyOptions:
40275970Scy  -d workdir   -- Store working files in workdir
41275970Scy                  (default: /var/db/portsnap/)
42275970Scy  -f conffile  -- Read configuration options from conffile
43275970Scy                  (default: /etc/portsnap.conf)
44275970Scy  -I           -- Update INDEX only. (update command only)
45275970Scy  -k KEY       -- Trust an RSA key with SHA256 hash of KEY
46275970Scy  -p portsdir  -- Location of uncompressed ports tree
47275970Scy                  (default: /usr/ports/)
48275970Scy  -s server    -- Server from which to fetch updates.
49275970Scy                  (default: portsnap.FreeBSD.org)
50275970Scy  path         -- Extract only parts of the tree starting with the given
51275970Scy                  string.  (extract command only)
52275970ScyCommands:
53275970Scy  fetch        -- Fetch a compressed snapshot of the ports tree,
54275970Scy                  or update an existing snapshot.
55275970Scy  cron         -- Sleep rand(3600) seconds, and then fetch updates.
56275970Scy  extract      -- Extract snapshot of ports tree, replacing existing
57275970Scy                  files and directories.
58275970Scy  update       -- Update ports tree to match current snapshot, replacing
59275970Scy                  files and directories which have changed.
60275970ScyEOF
61275970Scy	exit 0
62275970Scy}
63275970Scy
64275970Scy#### Parameter handling functions.
65275970Scy
66275970Scy# Initialize parameters to null, just in case they're
67275970Scy# set in the environment.
68275970Scyinit_params() {
69275970Scy	KEYPRINT=""
70275970Scy	EXTRACTPATH=""
71275970Scy	WORKDIR=""
72275970Scy	PORTSDIR=""
73275970Scy	CONFFILE=""
74275970Scy	COMMAND=""
75275970Scy	QUIETREDIR=""
76275970Scy	QUIETFLAG=""
77275970Scy	STATSREDIR=""
78275970Scy	XARGST=""
79275970Scy	NDEBUG=""
80275970Scy	DDSTATS=""
81275970Scy	INDEXONLY=""
82275970Scy	SERVERNAME=""
83275970Scy}
84275970Scy
85275970Scy# Parse the command line
86275970Scyparse_cmdline() {
87275970Scy	while [ $# -gt 0 ]; do
88275970Scy		case "$1" in
89275970Scy		-d)
90275970Scy			if [ $# -eq 1 ]; then usage; fi
91275970Scy			if [ ! -z "${WORKDIR}" ]; then usage; fi
92275970Scy			shift; WORKDIR="$1"
93275970Scy			;;
94275970Scy		--debug)
95275970Scy			QUIETREDIR="/dev/stderr"
96275970Scy			STATSREDIR="/dev/stderr"
97275970Scy			QUIETFLAG=" "
98275970Scy			NDEBUG=" "
99275970Scy			XARGST="-t"
100275970Scy			DDSTATS=".."
101275970Scy			;;
102275970Scy		-f)
103275970Scy			if [ $# -eq 1 ]; then usage; fi
104275970Scy			if [ ! -z "${CONFFILE}" ]; then usage; fi
105275970Scy			shift; CONFFILE="$1"
106275970Scy			;;
107275970Scy		-h | --help | help)
108275970Scy			usage
109275970Scy			;;
110275970Scy		-I)
111275970Scy			INDEXONLY="YES"
112275970Scy			;;
113275970Scy		-k)
114275970Scy			if [ $# -eq 1 ]; then usage; fi
115275970Scy			if [ ! -z "${KEYPRINT}" ]; then usage; fi
116275970Scy			shift; KEYPRINT="$1"
117275970Scy			;;
118275970Scy		--no-stats)
119275970Scy			if [ -z "${STATSREDIR}" ]; then
120275970Scy				STATSREDIR="/dev/null"
121275970Scy				DDSTATS=".. "
122275970Scy			fi
123275970Scy			;;
124275970Scy		-p)
125275970Scy			if [ $# -eq 1 ]; then usage; fi
126275970Scy			if [ ! -z "${PORTSDIR}" ]; then usage; fi
127275970Scy			shift; PORTSDIR="$1"
128275970Scy			;;
129275970Scy		-s)
130275970Scy			if [ $# -eq 1 ]; then usage; fi
131275970Scy			if [ ! -z "${SERVERNAME}" ]; then usage; fi
132275970Scy			shift; SERVERNAME="$1"
133275970Scy			;;
134275970Scy		cron | extract | fetch | update)
135275970Scy			if [ ! -z "${COMMAND}" ]; then usage; fi
136275970Scy			COMMAND="$1"
137275970Scy			;;
138275970Scy		*)
139275970Scy			if [ $# -gt 1 ]; then usage; fi
140275970Scy			if [ "${COMMAND}" = "extract" ]; then usage; fi
141275970Scy			EXTRACTPATH="$1"
142275970Scy			;;
143275970Scy		esac
144275970Scy		shift
145275970Scy	done
146275970Scy
147275970Scy	if [ -z "${COMMAND}" ]; then
148275970Scy		usage
149	fi
150}
151
152# If CONFFILE was specified at the command-line, make
153# sure that it exists and is readable.
154sanity_conffile() {
155	if [ ! -z "${CONFFILE}" ] && [ ! -r "${CONFFILE}" ]; then
156		echo -n "File does not exist "
157		echo -n "or is not readable: "
158		echo ${CONFFILE}
159		exit 1
160	fi
161}
162
163# If a configuration file hasn't been specified, use
164# the default value (/etc/portsnap.conf)
165default_conffile() {
166	if [ -z "${CONFFILE}" ]; then
167		CONFFILE="/etc/portsnap.conf"
168	fi
169}
170
171# Read {KEYPRINT, SERVERNAME, WORKDIR, PORTSDIR} from the configuration
172# file if they haven't already been set.  If the configuration
173# file doesn't exist, do nothing.
174parse_conffile() {
175	if [ -r "${CONFFILE}" ]; then
176		for X in KEYPRINT WORKDIR PORTSDIR SERVERNAME; do
177			eval _=\$${X}
178			if [ -z "${_}" ]; then
179				eval ${X}=`grep "^${X}=" "${CONFFILE}" |
180				    cut -f 2- -d '=' | tail -1`
181			fi
182		done
183	fi
184}
185
186# If parameters have not been set, use default values
187default_params() {
188	_QUIETREDIR="/dev/null"
189	_QUIETFLAG="-q"
190	_STATSREDIR="/dev/stdout"
191	_WORKDIR="/var/db/portsnap"
192	_PORTSDIR="/usr/ports"
193	_NDEBUG="-n"
194	for X in QUIETREDIR QUIETFLAG STATSREDIR WORKDIR PORTSDIR NDEBUG; do
195		eval _=\$${X}
196		eval __=\$_${X}
197		if [ -z "${_}" ]; then
198			eval ${X}=${__}
199		fi
200	done
201}
202
203# Perform sanity checks and set some final parameters
204# in preparation for fetching files.  Also chdir into
205# the working directory.
206fetch_check_params() {
207	export HTTP_USER_AGENT="portsnap (${COMMAND}, `uname -r`)"
208
209	_SERVERNAME_z=\
210"SERVERNAME must be given via command line or configuration file."
211	_KEYPRINT_z="Key must be given via -k option or configuration file."
212	_KEYPRINT_bad="Invalid key fingerprint: "
213	_WORKDIR_bad="Directory does not exist or is not writable: "
214
215	if [ -z "${SERVERNAME}" ]; then
216		echo -n "`basename $0`: "
217		echo "${_SERVERNAME_z}"
218		exit 1
219	fi
220	if [ -z "${KEYPRINT}" ]; then
221		echo -n "`basename $0`: "
222		echo "${_KEYPRINT_z}"
223		exit 1
224	fi
225	if ! echo "${KEYPRINT}" | grep -qE "^[0-9a-f]{64}$"; then
226		echo -n "`basename $0`: "
227		echo -n "${_KEYPRINT_bad}"
228		echo ${KEYPRINT}
229		exit 1
230	fi
231	if ! [ -d "${WORKDIR}" -a -w "${WORKDIR}" ]; then
232		echo -n "`basename $0`: "
233		echo -n "${_WORKDIR_bad}"
234		echo ${WORKDIR}
235		exit 1
236	fi
237	cd ${WORKDIR} || exit 1
238
239	BSPATCH=/usr/bin/bspatch
240	SHA256=/sbin/sha256
241	PHTTPGET=/usr/libexec/phttpget
242}
243
244# Perform sanity checks and set some final parameters
245# in preparation for extracting or updating ${PORTSDIR}
246extract_check_params() {
247	_WORKDIR_bad="Directory does not exist: "
248	_PORTSDIR_bad="Directory does not exist or is not writable: "
249
250	if ! [ -d "${WORKDIR}" ]; then
251		echo -n "`basename $0`: "
252		echo -n "${_WORKDIR_bad}"
253		echo ${WORKDIR}
254		exit 1
255	fi
256	if ! [ -d "${PORTSDIR}" -a -w "${PORTSDIR}" ]; then
257		echo -n "`basename $0`: "
258		echo -n "${_PORTSDIR_bad}"
259		echo ${PORTSDIR}
260		exit 1
261	fi
262
263	if ! [ -d "${WORKDIR}/files" -a -r "${WORKDIR}/tag"	\
264	    -a -r "${WORKDIR}/INDEX" -a -r "${WORKDIR}/tINDEX" ]; then
265		echo "No snapshot available.  Try running"
266		echo "# `basename $0` fetch"
267		exit 1
268	fi
269
270	MKINDEX=/usr/libexec/make_index
271}
272
273# Perform sanity checks and set some final parameters
274# in preparation for updating ${PORTSDIR}
275update_check_params() {
276	extract_check_params
277
278	if ! [ -r ${PORTSDIR}/.portsnap.INDEX ]; then
279		echo "${PORTSDIR} was not created by portsnap."
280		echo -n "You must run '`basename $0` extract' before "
281		echo "running '`basename $0` update'."
282		exit 1
283	fi
284
285}
286
287#### Core functionality -- the actual work gets done here
288
289# Use an SRV query to pick a server.  If the SRV query doesn't provide
290# a useful answer, use the server name specified by the user.
291# Put another way... look up _http._tcp.${SERVERNAME} and pick a server
292# from that; or if no servers are returned, use ${SERVERNAME}.
293# This allows a user to specify "portsnap.freebsd.org" (in which case
294# portsnap will select one of the mirrors) or "portsnap5.tld.freebsd.org"
295# (in which case portsnap will use that particular server, since there
296# won't be an SRV entry for that name).
297#
298# We don't implement the recommendations from RFC 2782 completely, since
299# we are only looking to pick a single server -- the recommendations are
300# targetted at applications which obtain a list of servers and then try
301# each in turn, but we are instead just going to pick one server and let
302# the user re-run portsnap if a broken server was selected.
303#
304# We also ignore the Port field, since we are always going to use port 80.
305fetch_pick_server() {
306	echo -n "Looking up ${SERVERNAME} mirrors..."
307
308# Issue the SRV query and pull out the Priority, Weight, and Target fields.
309	host -t srv "_http._tcp.${SERVERNAME}" |
310	    grep -E "^_http._tcp.${SERVERNAME} has SRV record" |
311	    cut -f 5,6,8 -d ' ' > serverlist
312
313# If no records, give up -- we'll just use the server name we were given.
314	if [ `wc -l < serverlist` -eq 0 ]; then
315		echo " none found."
316		return
317	fi
318
319# Find the highest priority level (lowest numeric value).
320	SRV_PRIORITY=`cut -f 1 -d ' ' serverlist | sort -n | head -1`
321
322# Add up the weights of the response lines at that priority level.
323	SRV_WSUM=0;
324	while read X; do
325		case "$X" in
326		${SRV_PRIORITY}\ *)
327			SRV_W=`echo $X | cut -f 2 -d ' '`
328			SRV_WSUM=$(($SRV_WSUM + $SRV_W))
329			;;
330		esac
331	done < serverlist
332
333# If all the weights are 0, pretend that they are all 1 instead.
334	if [ ${SRV_WSUM} -eq 0 ]; then
335		SRV_WSUM=`grep -E "^${SRV_PRIORITY} " serverlist | wc -l`
336		SRV_W_ADD=1
337	else
338		SRV_W_ADD=0
339	fi
340
341# Pick a random value between 1 and the sum of the weights
342	SRV_RND=`jot -r 1 1 ${SRV_WSUM}`
343
344# Read through the list of mirrors and set SERVERNAME
345	while read X; do
346		case "$X" in
347		${SRV_PRIORITY}\ *)
348			SRV_W=`echo $X | cut -f 2 -d ' '`
349			SRV_W=$(($SRV_W + $SRV_W_ADD))
350			if [ $SRV_RND -le $SRV_W ]; then
351				SERVERNAME=`echo $X | cut -f 3 -d ' '`
352				break
353			else
354				SRV_RND=$(($SRV_RND - $SRV_W))
355			fi
356			;;
357		esac
358	done < serverlist
359
360	echo " using ${SERVERNAME}"
361}
362
363# Check that we have a public key with an appropriate hash, or
364# fetch the key if it doesn't exist.
365fetch_key() {
366	if [ -r pub.ssl ] && [ `${SHA256} -q pub.ssl` = ${KEYPRINT} ]; then
367		return
368	fi
369
370	echo -n "Fetching public key... "
371	rm -f pub.ssl
372	fetch ${QUIETFLAG} http://${SERVERNAME}/pub.ssl \
373	    2>${QUIETREDIR} || true
374	if ! [ -r pub.ssl ]; then
375		echo "failed."
376		return 1
377	fi
378	if ! [ `${SHA256} -q pub.ssl` = ${KEYPRINT} ]; then
379		echo "key has incorrect hash."
380		rm -f pub.ssl
381		return 1
382	fi
383	echo "done."
384}
385
386# Fetch a snapshot tag
387fetch_tag() {
388	rm -f snapshot.ssl tag.new
389
390	echo ${NDEBUG} "Fetching snapshot tag... "
391	fetch ${QUIETFLAG} http://${SERVERNAME}/$1.ssl
392	    2>${QUIETREDIR} || true
393	if ! [ -r $1.ssl ]; then
394		echo "failed."
395		return 1
396	fi
397
398	openssl rsautl -pubin -inkey pub.ssl -verify		\
399	    < $1.ssl > tag.new 2>${QUIETREDIR} || true
400	rm $1.ssl
401
402	if ! [ `wc -l < tag.new` = 1 ] ||
403	    ! grep -qE "^portsnap\|[0-9]{10}\|[0-9a-f]{64}" tag.new; then
404		echo "invalid snapshot tag."
405		return 1
406	fi
407
408	echo "done."
409
410	SNAPSHOTDATE=`cut -f 2 -d '|' < tag.new`
411	SNAPSHOTHASH=`cut -f 3 -d '|' < tag.new`
412}
413
414# Sanity-check the date on a snapshot tag
415fetch_snapshot_tagsanity() {
416	if [ `date "+%s"` -gt `expr ${SNAPSHOTDATE} + 31536000` ]; then
417		echo "Snapshot appears to be more than a year old!"
418		echo "(Is the system clock correct?)"
419		echo "Cowarly refusing to proceed any further."
420		return 1
421	fi
422	if [ `date "+%s"` -lt `expr ${SNAPSHOTDATE} - 86400` ]; then
423		echo -n "Snapshot appears to have been created more than "
424		echo "one day into the future!"
425		echo "(Is the system clock correct?)"
426		echo "Cowardly refusing to proceed any further."
427		return 1
428	fi
429}
430
431# Sanity-check the date on a snapshot update tag
432fetch_update_tagsanity() {
433	fetch_snapshot_tagsanity || return 1
434
435	if [ ${OLDSNAPSHOTDATE} -gt ${SNAPSHOTDATE} ]; then
436		echo -n "Latest snapshot on server is "
437		echo "older than what we already have!"
438		echo -n "Cowardly refusing to downgrade from "
439		date -r ${OLDSNAPSHOTDATE}
440		echo "to `date -r ${SNAPSHOTDATE}`."
441		return 1
442	fi
443}
444
445# Compare old and new tags; return 1 if update is unnecessary
446fetch_update_neededp() {
447	if [ ${OLDSNAPSHOTDATE} -eq ${SNAPSHOTDATE} ]; then
448		echo -n "Latest snapshot on server matches "
449		echo "what we already have."
450		echo "No updates needed."
451		rm tag.new
452		return 1
453	fi
454	if [ ${OLDSNAPSHOTHASH} = ${SNAPSHOTHASH} ]; then
455		echo -n "Ports tree hasn't changed since "
456		echo "last snapshot."
457		echo "No updates needed."
458		rm tag.new
459		return 1
460	fi
461
462	return 0
463}
464
465# Fetch snapshot metadata file
466fetch_metadata() {
467	rm -f ${SNAPSHOTHASH} tINDEX.new
468
469	echo ${NDEBUG} "Fetching snapshot metadata... "
470	fetch ${QUIETFLAG} http://${SERVERNAME}/t/${SNAPSHOTHASH}
471	    2>${QUIETREDIR} || return
472	if [ `${SHA256} -q ${SNAPSHOTHASH}` != ${SNAPSHOTHASH} ]; then
473		echo "snapshot metadata corrupt."
474		return 1
475	fi
476	mv ${SNAPSHOTHASH} tINDEX.new
477	echo "done."
478}
479
480# Warn user about bogus metadata
481fetch_metadata_freakout() {
482	echo
483	echo "Portsnap metadata is correctly signed, but contains"
484	echo "at least one line which appears bogus."
485	echo "Cowardly refusing to proceed any further."
486}
487
488# Sanity-check a snapshot metadata file
489fetch_metadata_sanity() {
490	if grep -qvE "^[0-9A-Z.]+\|[0-9a-f]{64}$" tINDEX.new; then
491		fetch_metadata_freakout
492		return 1
493	fi
494	if [ `look INDEX tINDEX.new | wc -l` != 1 ]; then
495		echo
496		echo "Portsnap metadata appears bogus."
497		echo "Cowardly refusing to proceed any further."
498		return 1
499	fi
500}
501
502# Take a list of ${oldhash}|${newhash} and output a list of needed patches
503fetch_make_patchlist() {
504	grep -vE "^([0-9a-f]{64})\|\1$" | 
505		while read LINE; do
506			X=`echo ${LINE} | cut -f 1 -d '|'`
507			Y=`echo ${LINE} | cut -f 2 -d '|'`
508			if [ -f "files/${Y}.gz" ]; then continue; fi
509			if [ ! -f "files/${X}.gz" ]; then continue; fi
510			echo "${LINE}"
511		done
512}
513
514# Print user-friendly progress statistics
515fetch_progress() {
516	LNC=0
517	while read x; do
518		LNC=$(($LNC + 1))
519		if [ $(($LNC % 10)) = 0 ]; then
520			echo -n $LNC
521		elif [ $(($LNC % 2)) = 0 ]; then
522			echo -n .
523		fi
524	done
525	echo -n " "
526}
527
528# Sanity-check an index file
529fetch_index_sanity() {
530	if grep -qvE "^[-_+./@0-9A-Za-z]+\|[0-9a-f]{64}$" INDEX.new ||
531	    fgrep -q "./" INDEX.new; then
532		fetch_metadata_freakout
533		return 1
534	fi
535}
536
537# Verify a list of files
538fetch_snapshot_verify() {
539	while read F; do
540		if [ `gunzip -c snap/${F} | ${SHA256} -q` != ${F} ]; then
541			echo "snapshot corrupt."
542			return 1
543		fi
544	done
545	return 0
546}
547
548# Fetch a snapshot tarball, extract, and verify.
549fetch_snapshot() {
550	fetch_tag snapshot || return 1
551	fetch_snapshot_tagsanity || return 1
552	fetch_metadata || return 1
553	fetch_metadata_sanity || return 1
554
555	rm -f ${SNAPSHOTHASH}.tgz
556	rm -rf snap/
557
558# Don't ask fetch(1) to be quiet -- downloading a snapshot of ~ 35MB will
559# probably take a while, so the progrees reports that fetch(1) generates
560# will be useful for keeping the users' attention from drifting.
561	echo "Fetching snapshot generated at `date -r ${SNAPSHOTDATE}`:"
562	fetch http://${SERVERNAME}/s/${SNAPSHOTHASH}.tgz || return 1
563
564	echo -n "Extracting snapshot... "
565	tar -xzf ${SNAPSHOTHASH}.tgz snap/ || return 1
566	rm ${SNAPSHOTHASH}.tgz
567	echo "done."
568
569	echo -n "Verifying snapshot integrity... "
570# Verify the metadata files
571	cut -f 2 -d '|' tINDEX.new | fetch_snapshot_verify || return 1
572# Extract the index
573	rm -f INDEX.new
574	gunzip -c snap/`look INDEX tINDEX.new |
575	    cut -f 2 -d '|'`.gz > INDEX.new
576	fetch_index_sanity || return 1
577# Verify the snapshot contents
578	cut -f 2 -d '|' INDEX.new | fetch_snapshot_verify || return 1
579	echo "done."
580
581# Move files into their proper locations
582	rm -f tag INDEX tINDEX
583	rm -rf files
584	mv tag.new tag
585	mv tINDEX.new tINDEX
586	mv INDEX.new INDEX
587	mv snap/ files/
588
589	return 0
590}
591
592# Update a compressed snapshot
593fetch_update() {
594	rm -f patchlist diff OLD NEW filelist INDEX.new
595
596	OLDSNAPSHOTDATE=`cut -f 2 -d '|' < tag`
597	OLDSNAPSHOTHASH=`cut -f 3 -d '|' < tag`
598
599	fetch_tag latest || return 1
600	fetch_update_tagsanity || return 1
601	fetch_update_neededp || return 0
602	fetch_metadata || return 1
603	fetch_metadata_sanity || return 1
604
605	echo -n "Updating from `date -r ${OLDSNAPSHOTDATE}` "
606	echo "to `date -r ${SNAPSHOTDATE}`."
607
608# Generate a list of wanted metadata patches
609	join -t '|' -o 1.2,2.2 tINDEX tINDEX.new |
610	    fetch_make_patchlist > patchlist
611
612# Attempt to fetch metadata patches
613	echo -n "Fetching `wc -l < patchlist | tr -d ' '` "
614	echo ${NDEBUG} "metadata patches.${DDSTATS}"
615	tr '|' '-' < patchlist |
616	    lam -s "tp/" - -s ".gz" |
617	    xargs ${XARGST} ${PHTTPGET} ${SERVERNAME}	\
618	    2>${STATSREDIR} | fetch_progress
619	echo "done."
620
621# Attempt to apply metadata patches
622	echo -n "Applying metadata patches... "
623	while read LINE; do
624		X=`echo ${LINE} | cut -f 1 -d '|'`
625		Y=`echo ${LINE} | cut -f 2 -d '|'`
626		if [ ! -f "${X}-${Y}.gz" ]; then continue; fi
627		gunzip -c < ${X}-${Y}.gz > diff
628		gunzip -c < files/${X}.gz > OLD
629		cut -c 2- diff | join -t '|' -v 2 - OLD > ptmp
630		grep '^\+' diff | cut -c 2- |
631		    sort -k 1,1 -t '|' -m - ptmp > NEW
632		if [ `${SHA256} -q NEW` = ${Y} ]; then
633			mv NEW files/${Y}
634			gzip -n files/${Y}
635		fi
636		rm -f diff OLD NEW ${X}-${Y}.gz ptmp
637	done < patchlist 2>${QUIETREDIR}
638	echo "done."
639
640# Update metadata without patches
641	join -t '|' -v 2 tINDEX tINDEX.new |
642	    cut -f 2 -d '|' /dev/stdin patchlist |
643		while read Y; do
644			if [ ! -f "files/${Y}.gz" ]; then
645				echo ${Y};
646			fi
647		done > filelist
648	echo -n "Fetching `wc -l < filelist | tr -d ' '` "
649	echo ${NDEBUG} "metadata files... "
650	lam -s "f/" - -s ".gz" < filelist |
651	    xargs ${XARGST} ${PHTTPGET} ${SERVERNAME}	\
652	    2>${QUIETREDIR}
653
654	while read Y; do
655		if [ `gunzip -c < ${Y}.gz | ${SHA256} -q` = ${Y} ]; then
656			mv ${Y}.gz files/${Y}.gz
657		else
658			echo "metadata is corrupt."
659			return 1
660		fi
661	done < filelist
662	echo "done."
663
664# Extract the index
665	gunzip -c files/`look INDEX tINDEX.new |
666	    cut -f 2 -d '|'`.gz > INDEX.new
667	fetch_index_sanity || return 1
668
669# Generate a list of wanted ports patches
670	join -t '|' -o 1.2,2.2 INDEX INDEX.new |
671	    fetch_make_patchlist > patchlist
672
673# Attempt to fetch ports patches
674	echo -n "Fetching `wc -l < patchlist | tr -d ' '` "
675	echo ${NDEBUG} "patches.${DDSTATS}"
676	tr '|' '-' < patchlist | lam -s "bp/" - |
677	    xargs ${XARGST} ${PHTTPGET} ${SERVERNAME}	\
678	    2>${STATSREDIR} | fetch_progress
679	echo "done."
680
681# Attempt to apply ports patches
682	echo -n "Applying patches... "
683	while read LINE; do
684		X=`echo ${LINE} | cut -f 1 -d '|'`
685		Y=`echo ${LINE} | cut -f 2 -d '|'`
686		if [ ! -f "${X}-${Y}" ]; then continue; fi
687		gunzip -c < files/${X}.gz > OLD
688		${BSPATCH} OLD NEW ${X}-${Y}
689		if [ `${SHA256} -q NEW` = ${Y} ]; then
690			mv NEW files/${Y}
691			gzip -n files/${Y}
692		fi
693		rm -f diff OLD NEW ${X}-${Y}
694	done < patchlist 2>${QUIETREDIR}
695	echo "done."
696
697# Update ports without patches
698	join -t '|' -v 2 INDEX INDEX.new |
699	    cut -f 2 -d '|' /dev/stdin patchlist |
700		while read Y; do
701			if [ ! -f "files/${Y}.gz" ]; then
702				echo ${Y};
703			fi
704		done > filelist
705	echo -n "Fetching `wc -l < filelist | tr -d ' '` "
706	echo ${NDEBUG} "new ports or files... "
707	lam -s "f/" - -s ".gz" < filelist |
708	    xargs ${XARGST} ${PHTTPGET} ${SERVERNAME}	\
709	    2>${QUIETREDIR}
710
711	while read Y; do
712		if [ `gunzip -c < ${Y}.gz | ${SHA256} -q` = ${Y} ]; then
713			mv ${Y}.gz files/${Y}.gz
714		else
715			echo "snapshot is corrupt."
716			return 1
717		fi
718	done < filelist
719	echo "done."
720
721# Remove files which are no longer needed
722	cut -f 2 -d '|' tINDEX INDEX | sort > oldfiles
723	cut -f 2 -d '|' tINDEX.new INDEX.new | sort | comm -13 - oldfiles |
724	    lam -s "files/" - -s ".gz" | xargs rm -f
725	rm patchlist filelist oldfiles
726
727# We're done!
728	mv INDEX.new INDEX
729	mv tINDEX.new tINDEX
730	mv tag.new tag
731
732	return 0
733}
734
735# Do the actual work involved in "fetch" / "cron".
736fetch_run() {
737	fetch_pick_server
738
739	fetch_key || return 1
740
741	if ! [ -d files -a -r tag -a -r INDEX -a -r tINDEX ]; then
742		fetch_snapshot || return 1
743	fi
744	fetch_update || return 1
745}
746
747# Build a ports INDEX file
748extract_make_index() {
749	gunzip -c "${WORKDIR}/files/`look $1 ${WORKDIR}/tINDEX |
750	    cut -f 2 -d '|'`.gz" | ${MKINDEX} /dev/stdin > ${PORTSDIR}/$2
751}
752
753# Create INDEX, INDEX-5, INDEX-6
754extract_indices() {
755	echo -n "Building new INDEX files... "
756	extract_make_index DESCRIBE.4 INDEX || return 1
757	extract_make_index DESCRIBE.5 INDEX-5 || return 1
758	extract_make_index DESCRIBE.6 INDEX-6 || return 1
759	echo "done."
760}
761
762# Create .portsnap.INDEX
763extract_metadata() {
764	sort ${WORKDIR}/INDEX > ${PORTSDIR}/.portsnap.INDEX
765}
766
767# Do the actual work involved in "extract"
768extract_run() {
769	grep "^${EXTRACTPATH}" ${WORKDIR}/INDEX | while read LINE; do
770		FILE=`echo ${LINE} | cut -f 1 -d '|'`
771		HASH=`echo ${LINE} | cut -f 2 -d '|'`
772		echo ${PORTSDIR}/${FILE}
773		if ! [ -r "${WORKDIR}/files/${HASH}.gz" ]; then
774			echo "files/${HASH}.gz not found -- snapshot corrupt."
775			return 1
776		fi
777		case ${FILE} in
778		*/)
779			rm -rf ${PORTSDIR}/${FILE}
780			mkdir -p ${PORTSDIR}/${FILE}
781			tar -xzf ${WORKDIR}/files/${HASH}.gz	\
782			    -C ${PORTSDIR}/${FILE}
783			;;
784		*)
785			rm -f ${PORTSDIR}/${FILE}
786			tar -xzf ${WORKDIR}/files/${HASH}.gz	\
787			    -C ${PORTSDIR} ${FILE}
788			;;
789		esac
790	done
791	if [ ! -z "${EXTRACTPATH}" ]; then
792		return 0;
793	fi
794
795	extract_metadata
796	extract_indices
797}
798
799# Do the actual work involved in "update"
800update_run() {
801	if ! [ -z "${INDEXONLY}" ]; then
802		extract_indices >/dev/null || return 1
803		return 0
804	fi
805
806	if sort ${WORKDIR}/INDEX |
807	    cmp ${PORTSDIR}/.portsnap.INDEX - >/dev/null; then
808		echo "Ports tree is already up to date."
809		return 0
810	fi
811
812	echo -n "Removing old files and directories... "
813	sort ${WORKDIR}/INDEX | comm -23 ${PORTSDIR}/.portsnap.INDEX - |
814	    cut -f 1 -d '|' | lam -s "${PORTSDIR}/" - | xargs rm -rf
815	echo "done."
816
817# Install new files
818	echo "Extracting new files:"
819	sort ${WORKDIR}/INDEX | comm -13 ${PORTSDIR}/.portsnap.INDEX - |
820	    while read LINE; do
821		FILE=`echo ${LINE} | cut -f 1 -d '|'`
822		HASH=`echo ${LINE} | cut -f 2 -d '|'`
823		echo ${PORTSDIR}/${FILE}
824		if ! [ -r "${WORKDIR}/files/${HASH}.gz" ]; then
825			echo "files/${HASH}.gz not found -- snapshot corrupt."
826			return 1
827		fi
828		case ${FILE} in
829		*/)
830			mkdir -p ${PORTSDIR}/${FILE}
831			tar -xzf ${WORKDIR}/files/${HASH}.gz	\
832			    -C ${PORTSDIR}/${FILE}
833			;;
834		*)
835			tar -xzf ${WORKDIR}/files/${HASH}.gz	\
836			    -C ${PORTSDIR} ${FILE}
837			;;
838		esac
839	done
840
841	extract_metadata
842	extract_indices
843}
844
845#### Main functions -- call parameter-handling and core functions
846
847# Using the command line, configuration file, and defaults,
848# set all the parameters which are needed later.
849get_params() {
850	init_params
851	parse_cmdline $@
852	sanity_conffile
853	default_conffile
854	parse_conffile
855	default_params
856}
857
858# Fetch command.  Make sure that we're being called
859# interactively, then run fetch_check_params and fetch_run
860cmd_fetch() {
861	if [ ! -t 0 ]; then
862		echo -n "`basename $0` fetch should not "
863		echo "be run non-interactively."
864		echo "Run `basename $0` cron instead."
865		exit 1
866	fi
867	fetch_check_params
868	fetch_run || exit 1
869}
870
871# Cron command.  Make sure the parameters are sensible; wait
872# rand(3600) seconds; then fetch updates.  While fetching updates,
873# send output to a temporary file; only print that file if the
874# fetching failed.
875cmd_cron() {
876	fetch_check_params
877	sleep `jot -r 1 0 3600`
878
879	TMPFILE=`mktemp /tmp/portsnap.XXXXXX` || exit 1
880	if ! fetch_run >> ${TMPFILE}; then
881		cat ${TMPFILE}
882		rm ${TMPFILE}
883		exit 1
884	fi
885
886	rm ${TMPFILE}
887}
888
889# Extract command.  Make sure the parameters are sensible,
890# then extract the ports tree (or part thereof).
891cmd_extract() {
892	extract_check_params
893	extract_run || exit 1
894}
895
896# Update command.  Make sure the parameters are sensible,
897# then update the ports tree.
898cmd_update() {
899	update_check_params
900	update_run || exit 1
901}
902
903#### Entry point
904
905# Make sure we find utilities from the base system
906export PATH=/sbin:/bin:/usr/sbin:/usr/bin:${PATH}
907
908get_params $@
909cmd_${COMMAND}
910