1#!/bin/sh
2#
3# httpdist.tcl -- Software packing lists, archives, and distribution
4#
5# Copyright (c) 1999 Jean-Claude Wippler and Equi4 Software.
6# Inspired by http://www.mibsoftware.com/httpsync/
7# \
8exec tclkitsh "$0" ${1+"$@"}
9
10set PINFO Package.info  ;# not in packing list, can be edited before sending
11set PLIST Packing.list  ;# do not touch the contents of this file in any way
12set PSEND Packurl.send  ;# list of settings used before to send packages out
13set PTEMP Httpdist.tmp  ;# temporary file in $env(TEMP) - used during upload
14set PVERS 101           ;# file version, not changed to allow using original
15
16package require md5
17
18proc calcDigest {file} {
19    set fd [open $file]
20    fconfigure $fd -trans binary
21    set sum [md5::md5 [read $fd]]
22    close $fd
23    return $sum
24}
25
26proc clockDisplay {{s ""}} {
27    if {$s == ""} { set s [clock seconds] }
28    clock format $s -format {%a, %m %b %Y %H:%M:%S} -gmt 1
29}
30
31proc packedItem {file} {
32    set s [file size $file]
33    set d [clockDisplay [file mtime $file]]
34    if {[catch {regexp {(...)$} [file attr $file -perm]} m]} {
35        set m 644
36    }
37    return "$file $s $d GMT $m [calcDigest $file]"
38}
39
40proc walker {{omit {}} {dirs .}} {
41    array set skips {. 1 .. 1}
42    foreach x $omit {
43        set skips($x) 1
44    }
45
46    set result {}
47
48    while {[llength $dirs] > 0} {
49        set d [lindex $dirs 0]
50        set dirs [lrange $dirs 1 end]
51        foreach f [glob -nocomplain [file join $d *] [file join $d .*]] {
52            set t [file tail $f]
53            if {![info exists skips($f)] && ![info exists skips($t)]} {
54                if {[file isdirectory $f]} {
55                    lappend dirs $f
56                } elseif {[file isfile $f]} {
57                    lappend result $f
58                }
59            }
60        }
61    }
62
63    return $result
64}
65
66proc localPackingList {a {omit {}}} {
67    upvar $a result
68
69    foreach f [walker $omit] {
70        set result($f) [packedItem $f]
71    }
72}
73
74proc splitList {a data {omit {}}} {
75    upvar $a result
76
77    foreach line [split $data \n] {
78        if {[regexp {^# Ignore: (.*)$} $line - omit]} continue
79        if {[string match #* $line]} continue
80
81        if {[regexp {^(\./[^ ]+) } $line - name]} {
82            set result($name) $line
83        }
84    }
85
86    return $omit
87}
88
89proc compareLists {a1 a2} {
90    upvar $a1 from $a2 to
91
92    set matches {}
93    set additions {}
94    set changes {}
95
96    array set unseen [lsort [array get to]]
97
98    foreach f [array names from] {
99        if {[info exists to($f)]} {         # in both
100            set fsz [lindex $from($f) 1]
101            set tsz [lindex $to($f) 1]
102            set fmd [lindex $from($f) 9]
103            set tmd [lindex $to($f) 9]
104
105            if {$fsz != $tsz} {             #   different size
106                lappend changes $f
107            } elseif {$fmd == $tmd || $fmd == "" || $tmd == ""} {
108                # compare digest if available at both ends
109                lappend matches $f
110            } else {                        #   different
111                lappend changes $f
112            }
113            unset unseen($f)
114        } else {                            # in from, not in to
115            lappend additions $f
116        }
117    }
118
119    set deletions [lsort [array names unseen]]
120
121    return [list $matches $additions $changes $deletions]
122}
123
124proc httpFetch {url {fd ""}} {
125    if {$fd != ""} {
126        set token [http::geturl $url -channel $fd]
127    } else {
128        set token [http::geturl $url]
129    }
130
131    upvar #0 $token state
132
133    if {$state(status) != "ok"} { error $state(error) }
134    if {[lindex $state(http) 1] != 200} { error $state(http) }
135
136    return $state(body)
137}
138
139proc makeDirsForFTP {f} {
140    set d {}
141    foreach s [file split [file dirname $f]] {
142        set d [file join $d $s]
143        FTP::MkDir $d
144    }
145}
146
147proc stowFileAway {fd f md5 odir} {
148    global failures
149
150    if {$odir == "" || [string length $md5] != 32} {
151        return 0
152    }
153
154    regsub {^(..)} $md5 {\1/} newf
155    set newf [file join $odir $newf]
156
157    if {![FTP::Rename $f $newf]} {
158        makeDirsForFTP $newf
159        if {![FTP::Rename $f $newf]} {
160            puts -nonewline { (NOT SAVED) }
161            incr failures
162            return 0
163        }
164    }
165
166    puts -nonewline { (saved) }
167    puts $fd "  $md5 -"
168    return 1
169}
170
171proc sendFiles {site dir user pw odir} {
172    global temp ignores failures
173    set failures 0
174
175    set fd [open $::PLIST]
176    set myPack [read $fd]
177    close $fd
178
179    set hisPack ""
180    set packSum ""
181    if {[FTP::Get ./$::PLIST $temp/$::PTEMP]} {
182        set fd [open $temp/$::PTEMP]
183        set hisPack [read $fd]
184        close $fd
185        set packSum [calcDigest $temp/$::PTEMP]
186    }
187
188    set infoSum ""
189    if {[FTP::Get ./$::PINFO $temp/$::PTEMP]} {
190        set infoSum [calcDigest $temp/$::PTEMP]
191    }
192
193    file delete $temp/$::PTEMP
194
195    array set here {}
196    set ignores [splitList here $myPack $ignores]
197
198    array set there {}
199    set ignores [splitList there $hisPack $ignores]
200
201    set diffs [compareLists here there]
202    foreach {matches additions changes deletions} $diffs break
203
204    set nm [llength $matches]
205    set na [llength $additions]
206    set nc [llength $changes]
207    set nd [llength $deletions]
208    set stats "$na additions, $nc changes, $nd deletions"
209
210    puts stderr " $nm matches, $stats"
211
212    set log [open $::PINFO a]
213    puts $log "\nHTTPDIST - [clockDisplay] - $stats\n"
214
215    if {$na + $nc + $nd > 0} {
216        puts -nonewline "* ./$::PLIST "
217        puts $log "* ./$::PLIST"
218        stowFileAway $log ./$::PLIST $packSum $odir
219        puts ""
220        if {$packSum != ""} {
221            puts $log "  [calcDigest $::PLIST] +"
222        }
223
224        foreach {v t} {additions a changes r deletions d} {
225            foreach x [set $v] {
226                set mods($x) $t
227            }
228        }
229
230        foreach f [lsort [array names mods]] {
231            puts -nonewline "$mods($f) $f "
232            flush stdout
233            puts $log "$mods($f) $f"
234
235            switch $mods($f) {
236                r {
237                    stowFileAway $log $f [lindex $there($f) 9] $odir
238                }
239                d {
240                    if {[stowFileAway $log $f [lindex $there($f) 9] $odir]} {
241                        set mods($f) x ;# it was moved away, prevent deletion
242                    }
243                }
244            }
245
246            switch $mods($f) {
247                a -
248                r {
249                    if {![FTP::Put $f]} {
250                        makeDirsForFTP $f
251                        if {![FTP::Put $f]} {
252                            puts -nonewline { (PUT?) }
253                            incr failures
254                        }
255                    }
256                }
257                d {
258                    if {![FTP::Delete $f]} {
259                        puts -nonewline { (DEL?) }
260                        incr failures
261                    }
262                    #!! should clean up empty dirs ...
263                }
264            }
265            puts ""
266        }
267
268        FTP::Put ./$::PLIST
269    } elseif {$packSum != ""} {
270        puts $log "* ./$::PLIST"
271        puts $log "  [calcDigest $::PLIST] +"
272    }
273
274    puts -nonewline "* ./$::PINFO "
275    puts $log "* ./$::PINFO"
276    stowFileAway $log ./$::PINFO $infoSum $odir
277    close $log
278
279    FTP::Put ./$::PINFO
280}
281
282proc usage {} {
283    puts stderr "  Usage: httpdist ?-proxy host? ?-dir path? command ?arg?
284
285    @?url?      Fetch packing list and update in and below current dir.
286                Looks for url in '$::PINFO' file if arg is just '@'.
287                Prefixes with 'http://purl.org/' if arg is not an URL.
288                WARNING: can alter (and delete) any files inside curr dir!
289
290    pack ?...?  Scan current directory and create a '$::PLIST' file.
291                Only file '$::PINFO' may be edited after this step.
292                Any remaining args are used as filenames to ignore.
293
294    send ftp://user?:pw?@site/dir ?archive?
295                Send out changed files as specified in the packing list.
296                Optional: archive old files out of the way to remote dir.
297                Send log is added to '$::PINFO' before sending it last.
298                Tip: use 'send <site>' to resend with its last settings.
299"
300if 0 {# not yet
301    test x      Compare packing list against the current set of files.
302                Values for x:   files   reports only files not listed
303                                sums    only files listed and different
304                                match   only files which are the same
305                                all     all differences (default)
306}
307    exit 1
308}
309
310    # strip off command line options
311array set opts {-dir . -proxy ""}
312
313set skip 0
314foreach {a b} $argv {
315    if {![info exists opts($a)]} break
316    set opts($a) $b
317    incr skip 2
318}
319set argv [lrange $argv $skip end]
320
321if {[llength $argv] < 1} usage
322
323    # change into the distribution directory
324    # this is very useful in combination with VFS automounting
325catch {
326    package require vfs
327    vfs::auto $opts(-dir)
328}
329cd $opts(-dir)
330
331set ignores "CVS .cvsignore core"
332
333if {[catch {set env(TEMP)} temp] && [catch {set env(TMP)} temp]} {
334    set temp .
335}
336
337switch -glob -- [lindex $argv 0] {
338    @*
339    {
340            # don't update an *outgoing* distribution area without asking
341        if {[file exists $::PSEND]} {
342            puts -nonewline stderr "Found a '$::PSEND' file,\
343                            do you really want to overwrite files here? "
344            if {![string match {[yY]*} [gets stdin]]} {
345                exit 1
346            }
347        }
348
349        regsub {^@} $argv {} argv
350
351            # when no url is specified, try to find one in $::PINFO
352        if {$argv == ""} {
353            if {![file exists $::PINFO]} {
354                puts stderr "There is no '$::PINFO' file here."
355                exit 1
356            }
357            set fd [open $::PINFO]
358            while {[gets $fd line] >= 0} {
359                if {[regexp {[Hh]ttpdist: ([^ ]+)} $line - argv]} break
360            }
361            close $fd
362
363            if {$argv == ""} {
364                puts stderr "No package distribution URL found in '$::PINFO'."
365                exit 1
366            }
367
368            puts stderr "Fetching updates from $argv ..."
369        }
370
371            # expand possible shorthand using a Persistent URL
372        regsub {^([^/:][^:]*[^/:])$} $argv {http://purl.org/&/} argv
373
374        if {![regexp -nocase {^(http://.+/)(.*)} $argv - url file]} usage
375
376        if {$file == ""} {
377            set file $::PLIST
378        }
379
380		if {[catch {package require nhttp}]} {
381        	package require http
382        }
383
384            # fetch/http "-proxy" setting is: <host>?:<port>?
385        set o [split "$opts(-proxy):80" :]
386        if {[llength $o] >= 2} {
387            http::config -proxyhost [lindex $o 0] -proxyport [lindex $o 1]
388        }
389
390        if {[catch {httpFetch $url$file} hisPack]} {
391            puts stderr "Cannot open packing list: $hisPack"
392            exit 1
393        }
394        puts stderr ""
395
396			# treat ./$::PINFO separately
397        set fd [open $::PINFO w+]
398        puts $fd [httpFetch $url$::PINFO]
399        puts $fd "Httpdist: $argv - [clockDisplay]"
400
401            # show the first three lines of the package info file
402        seek $fd 0
403        foreach x {1 2 3} {
404            set s [gets $fd]
405            if {$s == ""} break
406            puts stderr "  [string range $s 0 77]"
407        }
408        close $fd
409
410        array set there {}
411        set ignores [splitList there $hisPack $ignores]
412
413        array set here {}
414        lappend ignores ./$::PINFO ./$::PLIST ./$::PSEND
415        localPackingList here $ignores ;# uses ignore list from remote site
416
417        set diffs [compareLists there here]
418        foreach {matches additions changes deletions} $diffs break
419
420        set nm [llength $matches]
421        set na [llength $additions]
422        set nc [llength $changes]
423        set nd [llength $deletions]
424
425        if {$na + $nc + $nd > 0} {
426            puts stderr "\n$nm matches, $na additions, $nc changes, $nd deletions"
427            puts -nonewline stderr "Apply these changes to [pwd] ? "
428
429            if {[string match {[yY]*} [gets stdin]]} {
430                puts stderr ""
431
432		        foreach {v t} {additions a changes r deletions d} {
433		            foreach x [set $v] {
434		                set mods($x) $t
435		            }
436		        }
437
438		        foreach f [lsort [array names mods]] {
439		            puts -nonewline "$mods($f) $f "
440		            flush stdout
441
442		            switch $mods($f) {
443		                a -
444		                r {
445		                    set t $url$f
446		                    regsub -all {/\./} $t {/} t
447
448		                    file mkdir [file dirname $f]
449		                    set fd [open $f w]
450
451		                    httpFetch $t $fd
452
453		                	set size [tell $fd]
454		                    close $fd
455
456		                	set want [lindex $there($f) 1]
457		                    if {$size != $want} {
458		                        puts -nonewline " (SIZE IS $size INSTEAD OF $want) "
459		                    }
460
461		                }
462		                d {
463		                    file delete $f
464		                }
465		            }
466		            puts ""
467		        }
468
469	            set fd [open $::PLIST w]
470	            puts -nonewline $fd $hisPack
471	            close $fd
472	        }
473        }
474    }
475
476    pack
477    {
478        if {[llength $argv] > 1} { set ignores [lrange $argv 1 end] }
479
480        set fd [open $::PLIST w]
481        puts $fd "#-#httpsync $::PVERS Packing List for httpdist (with MD5)"
482        puts $fd "# Ignore: [list $ignores]"
483        puts $fd "# For details, see: http://www.equi4.com/httpdist/"
484
485        set count 0
486        set size 0
487
488        lappend ignores ./$::PINFO ./$::PLIST ./$::PSEND
489        foreach f [lsort [walker $ignores]] {
490            set item [packedItem $f]
491            puts $fd $item
492            incr count
493            incr size [expr {([lindex $item 1]+1023)/1024}]
494        }
495        close $fd
496
497        puts " File '$::PLIST' created ($count files, total $size Kb)"
498    }
499
500    send
501    {
502        if {![file isfile $::PLIST]} {
503            puts stderr "There is no '$::PLIST', you must create it first."
504            exit 1
505        }
506
507        foreach {x url odir} $argv break
508        if {[llength $argv] > 3 || $url == ""} usage
509
510        set re {^ftp://([^:/@]+):?([^/@]*)?@([^/]+)/(.*)}
511        if {![regexp $re $url - user pw site dir]} {
512                # look for a site abbreviation
513            set site ""
514            if {![catch {open $::PSEND r} fd]} {
515                while {[gets $fd line] >= 0} {
516                    foreach {site user pw dir odir} $line break
517                    if {$url == $site} break
518                }
519                close $fd
520            }
521            if {$url != $site} usage
522        } else { # save settings for later
523            set fd [open $::PSEND a]
524            puts $fd [list $site $user $pw $dir $odir]
525            close $fd
526        }
527
528		if {[catch {package require FTP}]} {
529			package require ftp_lib
530        }
531
532        set FTP::VERBOSE 0
533        set FTP::DEBUG 0
534
535        if {$pw == ""} {
536            puts -nonewline stderr "Password: "
537            set pw [gets stdin]
538        }
539
540            # send/ftp "-proxy" setting is: (active|passive):<port>
541        set o [split $opts(-proxy) :]
542        if {[llength $o] < 2} {
543            set o {active 21}
544        }
545
546        if {![FTP::Open $site $user $pw -mode [lindex $o 0] -port [lindex $o 1]]
547                || ![FTP::Cd $dir]} {
548            exit 1
549        }
550
551        proc FTP::DisplayMsg {args} {} ;# turn off all FTP error output
552
553        sendFiles $site $dir $user $pw $odir
554
555        FTP::Close
556
557        if {$failures > 0} {
558            puts stderr "\nThere were $failures errors."
559            exit $failures
560        }
561    }
562
563    test
564    {
565        if {![file isfile $::PLIST]} {
566            puts stderr "There is no '$::PLIST' file here to verify."
567            exit 1
568        }
569
570        puts stderr "Sorry, not yet implemented..."
571        exit 1
572    }
573
574    default
575        usage
576}
577
578exit
579