1 2# This is the rsync file transfer interface. 3# 4# Rsync does not support a command line mode, i.e. each 5# invocation completes a given operation and then 6# terminates. There are only three commands possible: 7# 1) File(s) transfer. This can be either read or write. 8# 2) Directory transfer, i.e. get a file list 9# 3) Delete 10# 11# The above directly inform the "get" and "put" comands 12# to move files and the "list" command to get dirs and the 13# "delete" command. 14 15# Indirectly, we manage the working directory (cd and pwd). 16# Internally we carry the "pwd" with out the VFS header 17# (i.e. we keep the pwd "path" component in variable pwd) 18# 19# Also with rsync, there is no real notion of alive, so the 20# alive functions are disabled. 21 22# package require -exact Expect 0.0.0 23package require Expect 24package require fileutil 25package provide VFSrsync 1.0 26 27namespace eval VFSrsync { 28 variable commands 29 variable debug 0 30 variable os $::tcl_platform(platform) 31 variable abortflag 32 variable tmpdir 33 34 array set commands [list\ 35 open ::VFSrsync::RSYNCopen\ 36 live ::VFSrsync::RSYNClive\ 37 cd ::VFSrsync::RSYNCcd\ 38 mkdir ::VFSrsync::RSYNCmkdir\ 39 delete ::VFSrsync::RSYNCdelete\ 40 rmdir ::VFSrsync::RSYNCdelete\ 41 rmdirEmpty 1\ 42 isdir ::VFSrsync::RSYNCisDir\ 43 pwd ::VFSrsync::RSYNCpwd\ 44 list ::VFSrsync::RSYNClist\ 45 get ::VFSrsync::RSYNCget\ 46 put ::VFSrsync::RSYNCput\ 47 RcopyOk 3\ 48 close ::VFSrsync::RSYNCclose\ 49 debug ::VFSrsync::RSYNC_debug] 50 51 # Support routines ********************************* 52 proc eId {n} {return "(rsync $n): "} 53 # ******************* all with names "RSYNC_..." 54 proc RSYNClog_user {} { 55 variable debug 56 variable tmpdir 57 upvar VFStok VFStok 58 VFSglobal logging 59 if {$logging || $debug} { 60 exp_log_user $debug 61 if {$debug} { 62 catch {exp_log_file} 63 exp_log_file -a $tmpdir/rsync.log 64 } 65 } 66 } 67 proc dproc {args} {} 68 69 # On logging. We set logging true at open time and if there is 70 # an error. We turn it off on sucess of any given command. 71 72 proc RSYNClog {mess} { 73 upvar VFStok VFStok 74 VFSglobal errorExpected logging log 75 set mess [regsub {\r} $mess {}] 76 if {$logging && !$errorExpected } { 77 eval [list {*}$log $mess] 78 } 79 } 80 81 proc RSYNCfixParms {wh} { 82 upvar VFStok VFStok 83 upvar $wh fx 84 VFSglobal mode module 85 frputs mode module fx 86 if {$mode == "daemon" && [string match /${module}* $fx]} { 87 set fx [string range $fx [string length $module]+1 end] 88 frputs fx 89 if {$fx == {}} {set fx /} 90 } 91 frputs fx 92 return 93 } 94 # The open request. We have host, user, password & port 95 # host and VFStok are usually the same and MUST not be "" 96 # possible connection modes are: 97 98 proc RSYNCopen {URL args} { 99 global config 100 variable debug 101 variable tmpdir 102 IsVFS $URL 103 frputs args 104 105 106 # We are likely to change the URL so lets sort that out so we 107 # can keep stuff where we can find it... 108 109 frputs args "[llength $args] " 110 array set parms {*}$args 111 set Xuser $parms(user) 112 # over ride user if found in address 113 # The VFSadd (set by IsVFS) may contain a user name 114 regexp {(.*)@(.*)} $VFSadd ma Xuser VFSadd 115 116 set Xhost [regsub {:[0-9]*} $VFSadd {}] 117 # host is address sans user and port (so far) 118 # set timeout $rq_timeout 119 set searchOps [list -index 0 -all -inline] 120 set Xmodule [regsub {^/$|^/([^/]+).*} $VFSpath {\1}] 121 set daemonKey $Xhost/$Xmodule 122 # Lean on the rsync table for extra options... 123 # mostly we want the mode info. 124 # host is VFSadd cleared of user & port 125 set info [lsearch {*}$searchOps $config(rsync,table) "*$Xhost*"] 126 if {$info == {}} { 127 set info [list [lindex $config(rsync,table) 0]] 128 } 129 frputs info 130 if {[llength $info] > 1} { 131 # try to pick the most useful one... 132 # if we are dealing with modules (i.e. mode == deamon) we 133 # need to match the host + the first element of the path. 134 # defaults to last looked at... 135 136 foreach inf $info { 137 lassign $inf key rPath Mode tops 138 if {($Mode == "daemon" && [string match *$daemonKey $key]) ||\ 139 [string match *sftp* $key] ||\ 140 [string match *scp* $Mode]} { 141 incr found 142 break 143 } 144 } 145 if {![info exist found]} { 146 lassign {} key rPath Mode 147 } 148 } else { 149 lassign [lindex $info 0] key rPath Mode tops 150 } 151 frputs key rPath Mode ops tops 152 # An alternate program can be indicated by Mode "use=alt" 153 # we don't use this module (it should be part of the host...) 154 lassign [split $Mode =] Xmode modulex 155 set Xprog [expr {$Xmode == {use} ? "$Xmodule" : "rsync -s"}] 156 # Default port depends on mode... 157 set Xport [expr {$Xmode == "daemon" ? 873 : 22}] 158 regexp {(.*):(.*)} $VFSadd ma VFSadd Xport 159 # allow override of that if port in URL 160 # 161 # Here is the URL as we will use it: 162 set URL [URL norm rsync://$Xuser@${VFSadd}:$Xport/$VFSpath] 163 IsVFS $URL 164 VFSglobal echo errorExpected user host log logging mode module password port prog\ 165 passwordx spawn_id timeout ops filePrefix pwd 166 167 foreach p {port user host mode module prog} { 168 set $p [set X$p] 169 } 170 set logging 1 171 set debug 0 172 set errorExpected 0 173 set passwordx {} 174 set timeout {} 175 set abortflag ::[namespace current]::abtflag 176 set timefmt "%d%m%y " 177 set tmpdir "/tmp" 178 set log ::[namespace current]::dproc 179 # ops is a global option to use on all requests.. 180 if {$::MSW} { 181 # check the table opts (tops) to see if we have plink 182 # if so, pass it the creds. 183 } 184 set ops {--no-motd} 185 186 set poss {timeout abortflag timefmt log debug tmpdir} 187 foreach opt $poss { 188 if {[info exists parms($opt)]} { 189 set $opt $parms($opt) 190 } 191 } 192 set echo [expr {1 && $debug ? "" : "-noecho"}] 193 # pelim done, lets see if we can talk... 194 # we inclued the module address as the first path comp 195 set filePrefix rsync://$VFSadd 196 frputs VFStok host user password port rq_timeout 197 if {$mode == "daemon"} { 198 set filePrefix $filePrefix/$module 199 set pwd [regsub {(^/[^/]+)} $VFSpath {}] 200 if {$pwd == {}} { 201 set pwd / 202 } 203 } else { 204 set pwd $VFSpath 205 } 206 frputs filePrefix 207 # Here we try a list request to prove we can logon... 208 ::VFSvars::VFS_WriteCache $filePrefix [RSYNClist $URL all] 209 return $URL 210 } 211 212 213 proc RSYNCsingle {VFStok what args} { 214 variable debug 215 VFSglobal echo logging mode module user password passwordx prog\ 216 spawn_id timeout ops filePrefix pwd 217 218 RSYNClog_user 219 frputs #4 #3 #2 #1 ::glob(right,pwd) 220 #lassign $password Pword none passphrase 221 # set send [string trim [slList {*}$what {*}$args]] 222 # exp_send -- $send\r 223 # We have two protocols to deal with, shell & daemon 224 if {$mode != "daemon"} { 225 foreach fil $args { 226 if {[regsub -all {rsync://|:[0-9]+} $fil {} nfil] == 0} { 227 # simple file, pass it 228 lappend nargs $fil 229 } else { 230 # rsync remote file, need a : in front of the first / 231 lappend nargs [regsub {/} $nfil {:/}] 232 } 233 } 234 } else { 235 # For daemon transfers, we have modules 236 # The module is part of the filePrefix and should already be 237 # in the args as such... 238 # lappend nargs [regsub {(.*://.*)(/)} $args \\1/$module/] 239 set nargs $args 240 } 241 frputs what args nargs 242 set cmd [frECF [list spawn {*}$echo {*}$prog {*}$ops {*}$what]\ 243 $nargs\ 244 [list -f 1]] 245 while {1} { 246 lassign $passwordx password none passphrase 247 set acmd $cmd 248 if {$::MSW} { 249 # we need the password here... 250 set acmd [subst -nocommands -nobackslashes $cmd] 251 } 252 frputs "spawn " acmd 253 set r [catch {{*}$acmd} out] 254 if {$r != 0} { 255 return -code error "[eId 2]Really bad error: $out" 256 } 257 set stuff {} 258 # The first pattern takes care of the input line limit where back 259 # spaces are inserted in the echo... 260 expect_after timeout \ 261 {set expect_out(buffer) "Connection timed out $VFStok";\ 262 set re 1}\ 263 eof {set expect_out(buffer) "Connection closed $VFStok" ;\ 264 frputs "1.1 " 265 set re 0} 266 267 expect -re "(.*assword:.*)" \ 268 {RSYNClog $expect_out(buffer) 269 # Log "sending password" 270 if {$password == {}} { 271 set passwordx [::pwLocker::getPw $filePrefix \ 272 full 1 \ 273 abort 1\ 274 prompt "(2)[regsub {\r} $expect_out(1,string) {}]"] 275 lassign $passwordx password none passphrase 276 } 277 frputs "open password " Pword 278 RSYNClog "sending pasword\n" 279 frputs "2 " 280 if {$::MSW} { 281 set re 5 282 } else { 283 exp_send $Pword\r 284 exp_continue 285 } 286 } \ 287 -re "(.*.?assphrase for key .*: )" { 288 RSYNClog $expect_out(buffer) 289 if {$passphrase == {}} { 290 set passwordx [::pwLocker::getPw $filePrefix \ 291 full 1 \ 292 abort 1\ 293 prompt "(3)[regsub {\r} $expect_out(1,string) {}]"] 294 lassign $passwordx password none passphrase 295 } 296 RSYNClog "sending passphrase\n" 297 frputs "3 " 298 exp_send "$passphrase\r" 299 exp_continue} \ 300 -re "(.* host key is not .*y/n. |.* authenticity of host .*\(yes/no\)\? )"\ 301 { 302 # clean the out string for the smart_dialog call 303 set st [regsub -all {\r} $expect_out(1,string) {}] 304 RSYNClog 1$expect_out(buffer) 305 frputs "4 " 306 incr ignorto 307 if { [smart_dialog .apop . [_ "Accept new host?"] \ 308 [list {} "$st" [_ "\nClick your answer."]] \ 309 1 2 [list [_ "No"] [_ "Yes"]]] == 1} { 310 set an [expr {[string match "*(yes/*" $expect_out(1,string)] ? \ 311 "yes" : "y" } ] 312 unset ignorto 313 frputs "back from y/n " an 314 RSYNClog "sending $an\n" 315 frputs "5 " 316 exp_send "$an\r" 317 exp_continue 318 } else { 319 RSYNClog "sending 'no'\n" 320 frputs "6 " 321 exp_send "no\r" 322 Log "Aborting Login" 323 set re 10 324 } 325 } \ 326 -re {(^[^\n]{0,10}yes\r\n)} { 327 RSYNClog 6$expect_out(buffer) 328 frputs "6 " 329 exp_continue} \ 330 -re "(.*Warning: .*)\r?\n" { 331 RSYNClog $expect_out(buffer) 332 frputs "7 " 333 exp_continue}\ 334 -re "(.*Connecting .*)" { 335 RSYNClog $expect_out(buffer) 336 RSYNClog "sending password2\n" 337 frputs "8" 338 exp_send $password\r 339 exp_continue} \ 340 -re "(.*Permanently added .*)\r*\n" { 341 RSYNClog $expect_out(buffer) 342 frputs "9 " 343 exp_continue}\ 344 -re "(.*assword.*|.*unable.*|.*assphrase.*|.*onnection closed.*)" { 345 if {[string match {* *} $expect_out(1,string)]} { 346 append stuff $expect_out(buffer) 347 exp_continue 348 } 349 set ms "(10)$expect_out(buffer)\ 350 \n\nPlease try again." 351 set passwordx [::pwLocker::getPw $filePrefix \ 352 full 1 \ 353 startwith $password\ 354 abort 1\ 355 prompt [regsub {\r} $ms {}]] 356 lassign $passwordx password none passphrase 357 358 RSYNClog "exit open: Failed with: $expect_out(1,string)\n" 359 frputs "10 " 360 set re 5}\ 361 -re "(.*)\r*\n" { 362 #frputs expect_out(1,string) expect_out(0,string) 363 append stuff $expect_out(buffer) 364 #exp_log_user $debug 365 exp_continue}\ 366 -re "(.*)\r" { 367 #frputs expect_out(1,string) expect_out(0,string) 368 append stuff $expect_out(buffer) 369 #exp_log_user $debug 370 exp_continue} 371 372 if {$re != 5} {break} 373 } 374 # We get here on: 375 # close (re = 0) 376 # timeout (re = 1) 377 # password failure (re = 5) 378 # reject new host (re = 10) 379 # 380 # After a close/timeout we need to do a wait to get the 381 # status of the completion...m 382 frputs "10.1 " 383 catch "exp_close -i $spawn_id" 384 frputs "10.2 " 385 #ForceUpdate $inst 386 set rtn [exp_wait -i $spawn_id] 387 lassign $rtn r1 r2 r3 r4 388 if {$r3 == 0 && $r4 == 0} { 389 frputs "11 " 390 set logging 0 391 frputs "12 " 392 return [regsub -all \r $stuff {}] 393 } 394 set logging 1 395 switch $re { 396 5 - 397 10 {set err $expect_out(buffer)} 398 default { 399 # some sort of error... 400 set err "rsync [expr {$r3 == 0 ? {threw system error} :\ 401 {returned completion code}}]: $r4\n" 402 } 403 } 404 frputs re err 405 RSYNClog $err 406 return -code error [eId 3]$err 407 } 408 409 # To verify if a directory exists, we try to cd to it. This is 410 # rather painful (mostly due to the reconnedtion overhead) Sooo 411 # we try to verify only when it makes sense. 412 proc RSYNCcd {VFStok new_wd} { 413 VFSglobal filePrefix pwd 414 415 RSYNCfixParms new_wd 416 # We depend on this to throw an error if "new_wd" is not a dir 417 frputs #6 #5 #4 #3 #2 #1 new_wd 418 # Don't verify if we are going to subset of where we are (including 419 # not moving at all). 420 if {![string match $new_wd* $pwd]} { 421 set tmp [RSYNCsingle $VFStok {--list-only --include=. --exclude=* }\ 422 [URL norm $filePrefix/$new_wd]/] 423 } 424 frputs tmp new_wd 425 set pwd $new_wd 426 return 1 427 } 428 429 proc RSYNCisDir {VFStok dir} { 430 # what to do here??? 431 VFSglobal errorExpected logging 432 set errorExpected 1 433 set r [catch {RSYNCcd $VFStok $dir} really] 434 set logging 0 435 set errorExpected 0 436 if {$r == 0 || [string match -nocase *permission* $really]} { 437 return [RSYNCpwd $VFStok] 438 } 439 return 0 440 } 441 442 # proc RSYNCrename {VFStok old new} { 443 # # current name old change to new 444 # return -code error "VFS rsync does not support rename" 445 # } 446 447 # There is no way to do a delete with out excluding all the 448 # files that are not to be deleted. For now, at least, this 449 # is just too much. 450 # Ok, lets try overlaying the file with a zero length file 451 # and then bring it back with a delete-source option 452 # The --force option to rsync allows us to delete dirs 453 # by forcing a file where a directory was so this works 454 # for files and dirs. 455 456 proc RSYNCdelete {VFStok filename} { 457 VFSglobal ops filePrefix 458 459 RSYNCfixParms fileName 460 461 set tmp [makeTmp]/[file tail $filename] 462 file delete -force $tmp 463 ::fileutil::touch $tmp 464 RSYNCsingle $VFStok [list {*}$ops --force] $tmp\ 465 [URL norm $filePrefix/[file dir $filename]] 466 file delete -force $tmp 467 468 set op [list -rvv --remove-source-files] 469 RSYNCsingle $VFStok $op [URL norm $filePrefix/$filename] $tmp 470 file delete -force $tmp 471 return 472 } 473 474 475 proc RSYNCmkdir {VFStok dir} { 476 VFSglobal filePrefix 477 # No create command, so we make one and copy it over 478 set tmp [makeTmp]/[file tail $dir] 479 set r [Try {file mkdir $tmp} "" 1] 480 if {!$r} { 481 RSYNCput $VFStok $tmp $dir 482 file delete $tmp 483 } 484 return 485 } 486 487 proc RSYNClive { VFStok } { 488 # we just require a simple transation to prove life.. 489 # Not really feasable for rsync 490 return 1 491 } 492 493 proc RSYNCpwd {VFStok} { 494 VFSglobal filePrefix pwd 495 # just when you thought this was simple... 496 frputs pwd filePrefix 497 if {$pwd != "/"} { 498 set ppwd [RSYNCsingle $VFStok {--list-only -d -l --no-h} \ 499 [URL norm $filePrefix/$pwd]] 500 if {[set st [string first { -> } $ppwd]] != -1} { 501 # It is a link... Clean it up 502 regsub -all {\n} [string range $ppwd $st+3 end] {} ppwd 503 set pwd [URL norm [file dir $pwd]/[string trim $ppwd]] 504 } 505 } 506 return [URL norm $filePrefix/$pwd] 507 } 508 509 proc RSYNClist {URL all args} { 510 VFSglobal filePrefix pwd 511 # use -l (long list) -f (no sort) -a depends on all 512 # about 12.5k entries at 80 chars each (hay, just poke around on sourceforge) 513 # set op [expr {$all ? "-l -a" : "-l"}] 514 #set dir "${pwd}[expr {$pwd == {/} ? {} : {/}}]" 515 set rtn [split [RSYNCsingle $VFStok {--list-only -d -l --no-h} \ 516 [URL norm $filePrefix/$pwd/]] \n] 517 if {[llength $rtn] < 3 && [string match -nocase *permission* $rtn]} { 518 set errorInfo {} 519 return -code error "[eId 4]Permission denied" 520 } 521 frputs rtn' 522 return $rtn 523 } 524 525 526 proc RSYNCget { VFStok remoteFileName localFileName args} { 527 RSYNCfixParms remoteFileName 528 VFSglobal ops filePrefix pwd 529 frputs localFileName remoteFileName 530 # if {[file tail $remoteFileName] != [file tail $localFileName]} { 531 # return -code error "CopyAs not supported by VFS rsync." 532 # } 533 return [RSYNCsingle $VFStok "$ops -LkK"\ 534 [URL norm $filePrefix/$remoteFileName] $localFileName] 535 } 536 537 538 proc RSYNCput { VFStok localFileName remoteFileName args} { 539 RSYNCfixParms remoteFileName 540 VFSglobal timeout ops filePrefix pwd 541 frputs localFileName remoteFileName 542 # if {[file tail $remoteFileName] != [file tail $localFileName]} { 543 # return -code error "CopyAs not supported by VFS rsync." 544 # } 545 set localname [regsub -all " " $localFileName {\\ }] 546 # 547 return [RSYNCsingle $VFStok "$ops -LkKa " $localFileName\ 548 [URL norm $filePrefix/$remoteFileName]] 549 } 550 551 proc RSYNC_debug {onoff} { 552 variable debug 553 set debug $onoff 554 } 555} 556