1########################################################################
2#                                                                      #
3#               This software is part of the ast package               #
4#          Copyright (c) 1994-2011 AT&T Intellectual Property          #
5#                      and is licensed under the                       #
6#                 Eclipse Public License, Version 1.0                  #
7#                    by AT&T Intellectual Property                     #
8#                                                                      #
9#                A copy of the License is available at                 #
10#          http://www.eclipse.org/org/documents/epl-v10.html           #
11#         (with md5 checksum b35adb5213ca9657e911e9befb180842)         #
12#                                                                      #
13#              Information and Software Systems Research               #
14#                            AT&T Research                             #
15#                           Florham Park NJ                            #
16#                                                                      #
17#                 Glenn Fowler <gsf@research.att.com>                  #
18#                                                                      #
19########################################################################
20: replicate directory hierarchies
21
22COMMAND=ditto
23case `(getopts '[-][123:xyz]' opt --xyz; echo 0$opt) 2>/dev/null` in
240123)	ARGV0="-a $COMMAND"
25	USAGE=$'
26[-?
27@(#)$Id: ditto (AT&T Labs Research) 2010-11-22 $
28]
29'$USAGE_LICENSE$'
30[+NAME?ditto - replicate directory hierarchies]
31[+DESCRIPTION?\bditto\b replicates the \asource\a directory hierarchy
32	to the \adestination\a directory hierarchy. Both \asource\a and
33	\adestination\a may be of the form
34	[\auser\a@]][\ahost\a:]][\adirectory\a]]. At least one of
35	\ahost\a: or \adirectory\a must be specified. The current user is used
36	if \auser@\a is omitted, the local host is used if \ahost\a: is
37	omitted, and the user home directory is used if \adirectory\a is
38	omitted.]
39[+?Remote hosts and files are accessed via \bssh\b(1) or \brsh\b(1). \bksh\b(1),
40	\bpax\b(1), and \btw\b(1) must be installed on the local and remote hosts.]
41[+?For each source file \bditto\b does one of these actions:]{
42	[+chmod|chown?change the mode and/or ownership of the destination
43		file to match the source]
44	[+copy?copy the source file to the destination]
45	[+delete?delete the destination file]
46	[+skip?the destination file is not changed]
47}
48[+?The source and destination hierarchies are generated by \btw\b(1) with
49	the \b--logical\b option. An \b--expr\b option may
50	be specified to prune the search. The \btw\b searches are relative to
51	the \asource\a and \adestination\a directories.]
52[c:checksum?Copy if the \btw\b(1) 32x4 checksum mismatches.]
53[d:delete?Delete \adestination\a files that are not in the \asource\a.]
54[e:expr?\btw\b(1) select expression.]:[tw-expression]
55[m!:mode?Preserve file mode.]
56[n:show?Show the operations but do not exectute.]
57[o:owner?Preserve file user and group ownership.]
58[p:physical?Generate source and destination hierarchies by \btw\b(1) with
59	the \b--physical\b option.]
60[r:remote?The remote access protocol; either \bssh\b or
61	\brsh\b.]:[protocol:=ssh]
62[u:update?Copy only if the \asource\a file is newer than the
63	\adestination\a file.]
64[v:verbose?Trace the operations as they are executed.]
65[D:debug?Enable the debug trace.]
66
67source destination
68
69[+SEE ALSO?\brdist\b(1), \brsync\b(1), \brsh\b(1), \bssh\b(1), \btw\b(1)]
70'
71	;;
72*)	ARGV0=""
73	USAGE="de:[tw-expression]mnouvD source destination"
74	;;
75esac
76
77usage()
78{
79	OPTIND=0
80	getopts $ARGV0 "$USAGE" OPT '-?'
81	exit 2
82}
83
84parse() # id user@host:dir
85{
86	typeset id dir user host
87	id=$1
88	dir=$2
89	(( debug || ! exec )) && print -r $id $dir
90	if [[ $dir == *@* ]]
91	then
92		user=${dir%%@*}
93		dir=${dir#${user}@}
94	else
95		user=
96	fi
97	if [[ $dir == *:* ]]
98	then
99		host=${dir%%:*}
100		dir=${dir#${host}:}
101	else
102		host=
103	fi
104	if [[ $user ]]
105	then
106		user="-l $user"
107		if [[ ! $host ]]
108		then
109			host=$(hostname)
110		fi
111	fi
112	eval ${id}_user='$user'
113	eval ${id}_host='$host'
114	eval ${id}_dir='$dir'
115}
116
117# initialize
118
119typeset -A chown chmod
120typeset tw cp rm link
121integer ntw=0 ncp=0 nrm=0 nlink=0 n
122
123typeset src_user src_host src_path src_type src_uid src_gid src_perm src_sum
124typeset dst_user dst_host dst_path dst_type dst_uid dst_gid dst_perm dst_sum
125integer src_size src_mtime src_eof
126integer dst_size dst_mtime dst_eof
127
128integer debug=0 delete=0 exec=1 mode=1 owner=0 update=0 verbose=0 logical
129
130typeset remote=ssh trace
131typeset checksum='"-"' pax="pax"
132typeset paxreadflags="" paxwriteflags="--write --format=tgz --nosummary"
133
134tw[ntw++]=tw
135(( logical=ntw ))
136tw[ntw++]=--logical
137tw[ntw++]=--chop
138tw[ntw++]=--ignore-errors
139tw[ntw++]=--expr=sort:name
140
141# grab the options
142
143while	getopts $ARGV0 "$USAGE" OPT
144do	case $OPT in
145	c)	checksum=checksum ;;
146	d)	delete=1 ;;
147	e)	tw[ntw++]=--expr=\"$OPTARG\" ;;
148	m)	mode=0 ;;
149	n)	exec=0 verbose=1 ;;
150	o)	owner=1 ;;
151	p)	tw[logical]=--physical ;;
152	r)	remote=$OPTARG ;;
153	u)	update=1 ;;
154	v)	verbose=1 ;;
155	D)	debug=1 ;;
156	*)	usage ;;
157	esac
158done
159shift $OPTIND-1
160if (( $# != 2 ))
161then	usage
162fi
163tw[ntw++]=--expr=\''action:printf("%d\t%d\t%s\t%s\t%s\t%-.1s\t%o\t%s\t%s\n", size, mtime, '$checksum', uid, gid, mode, perm, path, symlink);'\'
164if (( exec ))
165then
166	paxreadflags="$paxreadflags --read"
167fi
168if (( verbose ))
169then
170	paxreadflags="$paxreadflags --verbose"
171fi
172
173# start the source and destination path list generators
174
175parse src "$1"
176parse dst "$2"
177
178# the |& command may exit before the exec &p
179# the print sync + read delays the |& until the exec &p finishes
180
181if [[ $src_host ]]
182then	($remote $src_user $src_host "{ test ! -f .profile || . ./.profile ;} && cd $src_dir && read && ${tw[*]}") 2>&1 |&
183else	(cd $src_dir && read && eval "${tw[@]}") 2>&1 |&
184fi
185exec 5<&p 7>&p
186print -u7 sync
187exec 7>&-
188
189if [[ $dst_host ]]
190then	($remote $dst_user $dst_host "{ test ! -f .profile || . ./.profile ;} && cd $dst_dir && read && ${tw[*]}") 2>&1 |&
191else	(cd $dst_dir && read && eval "${tw[@]}") 2>&1 |&
192fi
193exec 6<&p 7>&p
194print -u7 sync
195exec 7>&-
196
197# scan through the sorted path lists
198
199if (( exec ))
200then
201	src_skip=*
202	dst_skip=*
203else
204	src_skip=
205	dst_skip=
206fi
207src_path='' src_eof=0
208dst_path='' dst_eof=0
209ifs=${IFS-$' \t\n'}
210IFS=$'\t'
211while	:
212do
213	# get the next source path
214
215	if [[ ! $src_path ]] &&	(( ! src_eof ))
216	then
217		if read -r -u5 text src_mtime src_sum src_uid src_gid src_type src_perm src_path src_link
218		then
219			if [[ $text != +([[:digit:]]) ]]
220			then
221				print -u2 $COMMAND: source: "'$text'"
222				src_path=
223				continue
224			fi
225			src_size=$text
226		elif (( dst_eof ))
227		then
228			break
229		elif (( src_size==0 ))
230		then
231			exit 1
232		else
233			src_path=
234			src_eof=1
235		fi
236	fi
237
238	# get the next destination path
239
240	if [[ ! $dst_path ]] && (( ! dst_eof ))
241	then
242		if read -r -u6 text dst_mtime dst_sum dst_uid dst_gid dst_type dst_perm dst_path dst_link
243		then
244			if [[ $text != +([[:digit:]]) ]]
245			then
246				print -u2 $COMMAND: destination: $text
247				dst_path=
248				continue
249			fi
250			dst_size=$text
251		elif (( src_eof ))
252		then
253			break
254		elif (( dst_size==0 ))
255		then
256			exit 1
257		else
258			dst_path=
259			dst_eof=1
260		fi
261	fi
262
263	# determine the { cp rm chmod chown } ops
264
265	if (( debug ))
266	then
267		[[ $src_path ]] && print -r -u2 -f $': src %8s %10s %s %s %s %s %3s %s\n' $src_size $src_mtime $src_sum $src_uid $src_gid $src_type $src_perm "$src_path"
268		[[ $dst_path ]] && print -r -u2 -f $': dst %8s %10s %s %s %s %s %3s %s\n' $dst_size $dst_mtime $dst_sum $dst_uid $dst_gid $dst_type $dst_perm "$dst_path"
269	fi
270	if [[ $src_path == $dst_path ]]
271	then
272		if [[ $src_type != $dst_type ]]
273		then
274			rm[nrm++]=$dst_path
275			if [[ $dst_path != $dst_skip ]]
276			then
277				if [[ $dst_type == d ]]
278				then
279					dst_skip="$dst_path/*"
280					print -r rm -r "'$dst_path'"
281				else
282					dst_skip=
283					print -r rm "'$dst_path'"
284				fi
285			fi
286		fi
287		if [[ $src_type == l ]]
288		then	if [[ $src_link != $dst_link ]]
289			then
290				cp[ncp++]=$src_path
291				if [[ $src_path != $src_skip ]]
292				then
293					src_skip=
294					print -r cp "'$src_path'"
295				fi
296			fi
297		elif [[ $src_type != d ]] && { (( update && src_mtime > dst_mtime )) || (( ! update )) && { (( src_size != dst_size )) || [[ $src_sum != $dst_sum ]] ;} ;}
298		then
299			if [[ $src_path != . ]]
300			then
301				cp[ncp++]=$src_path
302				if [[ $src_path != $src_skip ]]
303				then
304					src_skip=
305					print -r cp "'$src_path'"
306				fi
307			fi
308		else
309			if (( owner )) && [[ $src_uid != $dst_uid || $src_gid != $dst_gid ]]
310			then
311				chown[$src_uid.$src_gid]="${chown[$src_uid.$src_gid]}	'$src_path'"
312				if [[ $src_path != $src_skip ]]
313				then
314					src_skip=
315					print -r chown $src_uid.$src_gid "'$src_path'"
316				fi
317				if (( (src_perm & 07000) || mode && src_perm != dst_perm ))
318				then
319					chmod[$src_perm]="${chmod[$src_perm]}	'$src_path'"
320					if [[ $src_path != $src_skip ]]
321					then
322						src_skip=
323						print -r chmod $src_perm "'$src_path'"
324					fi
325				fi
326			elif (( mode && src_perm != dst_perm ))
327			then
328				chmod[$src_perm]="${chmod[$src_perm]}	'$src_path'"
329				if [[ $src_path != $src_skip ]]
330				then
331					src_skip=
332					print -r chmod $src_perm "'$src_path'"
333				fi
334			fi
335		fi
336		src_path=
337		dst_path=
338	elif [[ ! $dst_path || $src_path && $src_path < $dst_path ]]
339	then
340		if [[ $src_path != . ]]
341		then
342			cp[ncp++]=$src_path
343			if [[ $src_path != $src_skip ]]
344			then
345				if [[ $src_type == d ]]
346				then
347					src_skip="$src_path/*"
348					print -r cp -r "'$src_path'"
349				else
350					src_skip=
351					print -r cp "'$src_path'"
352				fi
353			fi
354		fi
355		src_path=
356	elif [[ $dst_path ]]
357	then
358		if (( delete ))
359		then
360			rm[nrm++]=$dst_path
361			if [[ $dst_path != $dst_skip ]]
362			then
363				if [[ $dst_type == d ]]
364				then
365					dst_skip="$dst_path/*"
366					print -r rm -r "'$dst_path'"
367				else
368					dst_skip=
369					print -r rm "'$dst_path'"
370				fi
371			fi
372		fi
373		dst_path=
374	fi
375done
376IFS=$ifs
377
378(( exec )) || exit 0
379
380# generate, transfer and execute the { rm chown chmod } script
381
382if (( ${#rm[@]} || ${#chmod[@]} || ${#chown[@]} ))
383then
384	{
385		if (( verbose ))
386		then
387			print -r -- set -x
388		fi
389		print -nr -- cd "'$dst_dir'"
390		n=0
391		for i in ${rm[@]}
392		do
393			if (( --n <= 0 ))
394			then
395				n=32
396				print
397				print -nr -- rm -rf
398			fi
399			print -nr -- " '$i'"
400		done
401		for i in ${!chown[@]}
402		do
403			n=0
404			for j in ${chown[$i]}
405			do
406				if (( --n <= 0 ))
407				then
408					n=32
409					print
410					print -nr -- chown $i
411				fi
412				print -nr -- " $j"
413			done
414		done
415		for i in ${!chmod[@]}
416		do
417			n=0
418			for j in ${chmod[$i]}
419			do
420				if (( --n <= 0 ))
421				then
422					n=32
423					print
424					print -nr -- chmod $i
425				fi
426				print -nr -- " $j"
427			done
428		done
429		print
430	} | {
431		if (( ! exec ))
432		then
433			cat
434		elif [[ $dst_host ]]
435		then
436			$remote $dst_user $dst_host sh
437		else
438			$SHELL
439		fi
440	}
441fi
442
443# generate, transfer and read back the { cp } tarball
444
445if (( ${#cp[@]} ))
446then
447	{
448		cd $src_dir &&
449		print -r -f $'%s\n' "${cp[@]}" |
450		$pax $paxwriteflags
451	} | {
452		if [[ $dst_host ]]
453		then
454			$remote $dst_user $dst_host "{ test ! -f .profile || . ./.profile ;} && { test -d \"$dst_dir\" || mkdir -p \"$dst_dir\" ;} && cd \"$dst_dir\" && gunzip | $pax $paxreadflags"
455		else
456			( { test -d "$dst_dir" || mkdir -p "$dst_dir" ;} && cd "$dst_dir" && gunzip | $pax $paxreadflags )
457		fi
458	}
459	wait
460fi
461