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