1# -*- test-case-name: twisted.conch.test.test_cftp -*- 2# Copyright (c) Twisted Matrix Laboratories. 3# See LICENSE for details. 4 5""" 6Implementation module for the I{cftp} command. 7""" 8import fcntl 9import fnmatch 10import getpass 11import glob 12import os 13import pwd 14import stat 15import struct 16import sys 17import tty 18from typing import List, Optional, Union 19 20from twisted.conch.client import connect, default, options 21from twisted.conch.ssh import channel, common, connection, filetransfer 22from twisted.internet import defer, reactor, stdio, utils 23from twisted.protocols import basic 24from twisted.python import failure, log, usage 25from twisted.python.filepath import FilePath 26 27 28class ClientOptions(options.ConchOptions): 29 30 synopsis = """Usage: cftp [options] [user@]host 31 cftp [options] [user@]host[:dir[/]] 32 cftp [options] [user@]host[:file [localfile]] 33""" 34 longdesc = ( 35 "cftp is a client for logging into a remote machine and " 36 "executing commands to send and receive file information" 37 ) 38 39 optParameters: List[List[Optional[Union[str, int]]]] = [ 40 ["buffersize", "B", 32768, "Size of the buffer to use for sending/receiving."], 41 ["batchfile", "b", None, "File to read commands from, or '-' for stdin."], 42 ["requests", "R", 5, "Number of requests to make before waiting for a reply."], 43 ["subsystem", "s", "sftp", "Subsystem/server program to connect to."], 44 ] 45 46 compData = usage.Completions( 47 descriptions={"buffersize": "Size of send/receive buffer (default: 32768)"}, 48 extraActions=[ 49 usage.CompleteUserAtHost(), 50 usage.CompleteFiles(descr="local file"), 51 ], 52 ) 53 54 def parseArgs(self, host, localPath=None): 55 self["remotePath"] = "" 56 if ":" in host: 57 host, self["remotePath"] = host.split(":", 1) 58 self["remotePath"].rstrip("/") 59 self["host"] = host 60 self["localPath"] = localPath 61 62 63def run(): 64 args = sys.argv[1:] 65 if "-l" in args: # cvs is an idiot 66 i = args.index("-l") 67 args = args[i : i + 2] + args 68 del args[i + 2 : i + 4] 69 options = ClientOptions() 70 try: 71 options.parseOptions(args) 72 except usage.UsageError as u: 73 print("ERROR: %s" % u) 74 sys.exit(1) 75 if options["log"]: 76 realout = sys.stdout 77 log.startLogging(sys.stderr) 78 sys.stdout = realout 79 else: 80 log.discardLogs() 81 doConnect(options) 82 reactor.run() 83 84 85def handleError(): 86 global exitStatus 87 exitStatus = 2 88 try: 89 reactor.stop() 90 except BaseException: 91 pass 92 log.err(failure.Failure()) 93 raise 94 95 96def doConnect(options): 97 if "@" in options["host"]: 98 options["user"], options["host"] = options["host"].split("@", 1) 99 host = options["host"] 100 if not options["user"]: 101 options["user"] = getpass.getuser() 102 if not options["port"]: 103 options["port"] = 22 104 else: 105 options["port"] = int(options["port"]) 106 host = options["host"] 107 port = options["port"] 108 conn = SSHConnection() 109 conn.options = options 110 vhk = default.verifyHostKey 111 uao = default.SSHUserAuthClient(options["user"], options, conn) 112 connect.connect(host, port, options, vhk, uao).addErrback(_ebExit) 113 114 115def _ebExit(f): 116 if hasattr(f.value, "value"): 117 s = f.value.value 118 else: 119 s = str(f) 120 print(s) 121 try: 122 reactor.stop() 123 except BaseException: 124 pass 125 126 127def _ignore(*args): 128 pass 129 130 131class FileWrapper: 132 def __init__(self, f): 133 self.f = f 134 self.total = 0.0 135 f.seek(0, 2) # seek to the end 136 self.size = f.tell() 137 138 def __getattr__(self, attr): 139 return getattr(self.f, attr) 140 141 142class StdioClient(basic.LineReceiver): 143 144 _pwd = pwd 145 146 ps = "cftp> " 147 delimiter = b"\n" 148 149 reactor = reactor 150 151 def __init__(self, client, f=None): 152 self.client = client 153 self.currentDirectory = "" 154 self.file = f 155 self.useProgressBar = (not f and 1) or 0 156 157 def connectionMade(self): 158 self.client.realPath("").addCallback(self._cbSetCurDir) 159 160 def _cbSetCurDir(self, path): 161 self.currentDirectory = path 162 self._newLine() 163 164 def _writeToTransport(self, msg): 165 if isinstance(msg, str): 166 msg = msg.encode("utf-8") 167 return self.transport.write(msg) 168 169 def lineReceived(self, line): 170 if self.client.transport.localClosed: 171 return 172 if isinstance(line, bytes): 173 line = line.decode("utf-8") 174 log.msg("got line %s" % line) 175 line = line.lstrip() 176 if not line: 177 self._newLine() 178 return 179 if self.file and line.startswith("-"): 180 self.ignoreErrors = 1 181 line = line[1:] 182 else: 183 self.ignoreErrors = 0 184 d = self._dispatchCommand(line) 185 if d is not None: 186 d.addCallback(self._cbCommand) 187 d.addErrback(self._ebCommand) 188 189 def _dispatchCommand(self, line): 190 if " " in line: 191 command, rest = line.split(" ", 1) 192 rest = rest.lstrip() 193 else: 194 command, rest = line, "" 195 if command.startswith("!"): # command 196 f = self.cmd_EXEC 197 rest = (command[1:] + " " + rest).strip() 198 else: 199 command = command.upper() 200 log.msg("looking up cmd %s" % command) 201 f = getattr(self, "cmd_%s" % command, None) 202 if f is not None: 203 return defer.maybeDeferred(f, rest) 204 else: 205 errMsg = "No command called `%s'" % (command) 206 self._ebCommand(failure.Failure(NotImplementedError(errMsg))) 207 self._newLine() 208 209 def _printFailure(self, f): 210 log.msg(f) 211 e = f.trap(NotImplementedError, filetransfer.SFTPError, OSError, IOError) 212 if e == NotImplementedError: 213 self._writeToTransport(self.cmd_HELP("")) 214 elif e == filetransfer.SFTPError: 215 errMsg = "remote error %i: %s\n" % (f.value.code, f.value.message) 216 self._writeToTransport(errMsg) 217 elif e in (OSError, IOError): 218 errMsg = "local error %i: %s\n" % (f.value.errno, f.value.strerror) 219 self._writeToTransport(errMsg) 220 221 def _newLine(self): 222 if self.client.transport.localClosed: 223 return 224 self._writeToTransport(self.ps) 225 self.ignoreErrors = 0 226 if self.file: 227 l = self.file.readline() 228 if not l: 229 self.client.transport.loseConnection() 230 else: 231 self._writeToTransport(l) 232 self.lineReceived(l.strip()) 233 234 def _cbCommand(self, result): 235 if result is not None: 236 if isinstance(result, str): 237 result = result.encode("utf-8") 238 self._writeToTransport(result) 239 if not result.endswith(b"\n"): 240 self._writeToTransport(b"\n") 241 self._newLine() 242 243 def _ebCommand(self, f): 244 self._printFailure(f) 245 if self.file and not self.ignoreErrors: 246 self.client.transport.loseConnection() 247 self._newLine() 248 249 def cmd_CD(self, path): 250 path, rest = self._getFilename(path) 251 if not path.endswith("/"): 252 path += "/" 253 newPath = path and os.path.join(self.currentDirectory, path) or "" 254 d = self.client.openDirectory(newPath) 255 d.addCallback(self._cbCd) 256 d.addErrback(self._ebCommand) 257 return d 258 259 def _cbCd(self, directory): 260 directory.close() 261 d = self.client.realPath(directory.name) 262 d.addCallback(self._cbCurDir) 263 return d 264 265 def _cbCurDir(self, path): 266 self.currentDirectory = path 267 268 def cmd_CHGRP(self, rest): 269 grp, rest = rest.split(None, 1) 270 path, rest = self._getFilename(rest) 271 grp = int(grp) 272 d = self.client.getAttrs(path) 273 d.addCallback(self._cbSetUsrGrp, path, grp=grp) 274 return d 275 276 def cmd_CHMOD(self, rest): 277 mod, rest = rest.split(None, 1) 278 path, rest = self._getFilename(rest) 279 mod = int(mod, 8) 280 d = self.client.setAttrs(path, {"permissions": mod}) 281 d.addCallback(_ignore) 282 return d 283 284 def cmd_CHOWN(self, rest): 285 usr, rest = rest.split(None, 1) 286 path, rest = self._getFilename(rest) 287 usr = int(usr) 288 d = self.client.getAttrs(path) 289 d.addCallback(self._cbSetUsrGrp, path, usr=usr) 290 return d 291 292 def _cbSetUsrGrp(self, attrs, path, usr=None, grp=None): 293 new = {} 294 new["uid"] = (usr is not None) and usr or attrs["uid"] 295 new["gid"] = (grp is not None) and grp or attrs["gid"] 296 d = self.client.setAttrs(path, new) 297 d.addCallback(_ignore) 298 return d 299 300 def cmd_GET(self, rest): 301 remote, rest = self._getFilename(rest) 302 if "*" in remote or "?" in remote: # wildcard 303 if rest: 304 local, rest = self._getFilename(rest) 305 if not os.path.isdir(local): 306 return "Wildcard get with non-directory target." 307 else: 308 local = b"" 309 d = self._remoteGlob(remote) 310 d.addCallback(self._cbGetMultiple, local) 311 return d 312 if rest: 313 local, rest = self._getFilename(rest) 314 else: 315 local = os.path.split(remote)[1] 316 log.msg((remote, local)) 317 lf = open(local, "wb", 0) 318 path = FilePath(self.currentDirectory).child(remote) 319 d = self.client.openFile(path.path, filetransfer.FXF_READ, {}) 320 d.addCallback(self._cbGetOpenFile, lf) 321 d.addErrback(self._ebCloseLf, lf) 322 return d 323 324 def _cbGetMultiple(self, files, local): 325 # XXX this can be optimized for times w/o progress bar 326 return self._cbGetMultipleNext(None, files, local) 327 328 def _cbGetMultipleNext(self, res, files, local): 329 if isinstance(res, failure.Failure): 330 self._printFailure(res) 331 elif res: 332 self._writeToTransport(res) 333 if not res.endswith("\n"): 334 self._writeToTransport("\n") 335 if not files: 336 return 337 f = files.pop(0)[0] 338 lf = open(os.path.join(local, os.path.split(f)[1]), "wb", 0) 339 path = FilePath(self.currentDirectory).child(f) 340 d = self.client.openFile(path.path, filetransfer.FXF_READ, {}) 341 d.addCallback(self._cbGetOpenFile, lf) 342 d.addErrback(self._ebCloseLf, lf) 343 d.addBoth(self._cbGetMultipleNext, files, local) 344 return d 345 346 def _ebCloseLf(self, f, lf): 347 lf.close() 348 return f 349 350 def _cbGetOpenFile(self, rf, lf): 351 return rf.getAttrs().addCallback(self._cbGetFileSize, rf, lf) 352 353 def _cbGetFileSize(self, attrs, rf, lf): 354 if not stat.S_ISREG(attrs["permissions"]): 355 rf.close() 356 lf.close() 357 return "Can't get non-regular file: %s" % rf.name 358 rf.size = attrs["size"] 359 bufferSize = self.client.transport.conn.options["buffersize"] 360 numRequests = self.client.transport.conn.options["requests"] 361 rf.total = 0.0 362 dList = [] 363 chunks = [] 364 startTime = self.reactor.seconds() 365 for i in range(numRequests): 366 d = self._cbGetRead("", rf, lf, chunks, 0, bufferSize, startTime) 367 dList.append(d) 368 dl = defer.DeferredList(dList, fireOnOneErrback=1) 369 dl.addCallback(self._cbGetDone, rf, lf) 370 return dl 371 372 def _getNextChunk(self, chunks): 373 end = 0 374 for chunk in chunks: 375 if end == "eof": 376 return # nothing more to get 377 if end != chunk[0]: 378 i = chunks.index(chunk) 379 chunks.insert(i, (end, chunk[0])) 380 return (end, chunk[0] - end) 381 end = chunk[1] 382 bufSize = int(self.client.transport.conn.options["buffersize"]) 383 chunks.append((end, end + bufSize)) 384 return (end, bufSize) 385 386 def _cbGetRead(self, data, rf, lf, chunks, start, size, startTime): 387 if data and isinstance(data, failure.Failure): 388 log.msg("get read err: %s" % data) 389 reason = data 390 reason.trap(EOFError) 391 i = chunks.index((start, start + size)) 392 del chunks[i] 393 chunks.insert(i, (start, "eof")) 394 elif data: 395 log.msg("get read data: %i" % len(data)) 396 lf.seek(start) 397 lf.write(data) 398 if len(data) != size: 399 log.msg("got less than we asked for: %i < %i" % (len(data), size)) 400 i = chunks.index((start, start + size)) 401 del chunks[i] 402 chunks.insert(i, (start, start + len(data))) 403 rf.total += len(data) 404 if self.useProgressBar: 405 self._printProgressBar(rf, startTime) 406 chunk = self._getNextChunk(chunks) 407 if not chunk: 408 return 409 else: 410 start, length = chunk 411 log.msg("asking for %i -> %i" % (start, start + length)) 412 d = rf.readChunk(start, length) 413 d.addBoth(self._cbGetRead, rf, lf, chunks, start, length, startTime) 414 return d 415 416 def _cbGetDone(self, ignored, rf, lf): 417 log.msg("get done") 418 rf.close() 419 lf.close() 420 if self.useProgressBar: 421 self._writeToTransport("\n") 422 return f"Transferred {rf.name} to {lf.name}" 423 424 def cmd_PUT(self, rest): 425 """ 426 Do an upload request for a single local file or a globing expression. 427 428 @param rest: Requested command line for the PUT command. 429 @type rest: L{str} 430 431 @return: A deferred which fires with L{None} when transfer is done. 432 @rtype: L{defer.Deferred} 433 """ 434 local, rest = self._getFilename(rest) 435 436 # FIXME: https://twistedmatrix.com/trac/ticket/7241 437 # Use a better check for globbing expression. 438 if "*" in local or "?" in local: 439 if rest: 440 remote, rest = self._getFilename(rest) 441 remote = os.path.join(self.currentDirectory, remote) 442 else: 443 remote = "" 444 445 files = glob.glob(local) 446 return self._putMultipleFiles(files, remote) 447 448 else: 449 if rest: 450 remote, rest = self._getFilename(rest) 451 else: 452 remote = os.path.split(local)[1] 453 return self._putSingleFile(local, remote) 454 455 def _putSingleFile(self, local, remote): 456 """ 457 Perform an upload for a single file. 458 459 @param local: Path to local file. 460 @type local: L{str}. 461 462 @param remote: Remote path for the request relative to current working 463 directory. 464 @type remote: L{str} 465 466 @return: A deferred which fires when transfer is done. 467 """ 468 return self._cbPutMultipleNext(None, [local], remote, single=True) 469 470 def _putMultipleFiles(self, files, remote): 471 """ 472 Perform an upload for a list of local files. 473 474 @param files: List of local files. 475 @type files: C{list} of L{str}. 476 477 @param remote: Remote path for the request relative to current working 478 directory. 479 @type remote: L{str} 480 481 @return: A deferred which fires when transfer is done. 482 """ 483 return self._cbPutMultipleNext(None, files, remote) 484 485 def _cbPutMultipleNext(self, previousResult, files, remotePath, single=False): 486 """ 487 Perform an upload for the next file in the list of local files. 488 489 @param previousResult: Result form previous file form the list. 490 @type previousResult: L{str} 491 492 @param files: List of local files. 493 @type files: C{list} of L{str} 494 495 @param remotePath: Remote path for the request relative to current 496 working directory. 497 @type remotePath: L{str} 498 499 @param single: A flag which signals if this is a transfer for a single 500 file in which case we use the exact remote path 501 @type single: L{bool} 502 503 @return: A deferred which fires when transfer is done. 504 """ 505 if isinstance(previousResult, failure.Failure): 506 self._printFailure(previousResult) 507 elif previousResult: 508 if isinstance(previousResult, str): 509 previousResult = previousResult.encode("utf-8") 510 self._writeToTransport(previousResult) 511 if not previousResult.endswith(b"\n"): 512 self._writeToTransport(b"\n") 513 514 currentFile = None 515 while files and not currentFile: 516 try: 517 currentFile = files.pop(0) 518 localStream = open(currentFile, "rb") 519 except BaseException: 520 self._printFailure(failure.Failure()) 521 currentFile = None 522 523 # No more files to transfer. 524 if not currentFile: 525 return None 526 527 if single: 528 remote = remotePath 529 else: 530 name = os.path.split(currentFile)[1] 531 remote = os.path.join(remotePath, name) 532 log.msg((name, remote, remotePath)) 533 534 d = self._putRemoteFile(localStream, remote) 535 d.addBoth(self._cbPutMultipleNext, files, remotePath) 536 return d 537 538 def _putRemoteFile(self, localStream, remotePath): 539 """ 540 Do an upload request. 541 542 @param localStream: Local stream from where data is read. 543 @type localStream: File like object. 544 545 @param remotePath: Remote path for the request relative to current working directory. 546 @type remotePath: L{str} 547 548 @return: A deferred which fires when transfer is done. 549 """ 550 remote = os.path.join(self.currentDirectory, remotePath) 551 flags = filetransfer.FXF_WRITE | filetransfer.FXF_CREAT | filetransfer.FXF_TRUNC 552 d = self.client.openFile(remote, flags, {}) 553 d.addCallback(self._cbPutOpenFile, localStream) 554 d.addErrback(self._ebCloseLf, localStream) 555 return d 556 557 def _cbPutOpenFile(self, rf, lf): 558 numRequests = self.client.transport.conn.options["requests"] 559 if self.useProgressBar: 560 lf = FileWrapper(lf) 561 dList = [] 562 chunks = [] 563 startTime = self.reactor.seconds() 564 for i in range(numRequests): 565 d = self._cbPutWrite(None, rf, lf, chunks, startTime) 566 if d: 567 dList.append(d) 568 dl = defer.DeferredList(dList, fireOnOneErrback=1) 569 dl.addCallback(self._cbPutDone, rf, lf) 570 return dl 571 572 def _cbPutWrite(self, ignored, rf, lf, chunks, startTime): 573 chunk = self._getNextChunk(chunks) 574 start, size = chunk 575 lf.seek(start) 576 data = lf.read(size) 577 if self.useProgressBar: 578 lf.total += len(data) 579 self._printProgressBar(lf, startTime) 580 if data: 581 d = rf.writeChunk(start, data) 582 d.addCallback(self._cbPutWrite, rf, lf, chunks, startTime) 583 return d 584 else: 585 return 586 587 def _cbPutDone(self, ignored, rf, lf): 588 lf.close() 589 rf.close() 590 if self.useProgressBar: 591 self._writeToTransport("\n") 592 return f"Transferred {lf.name} to {rf.name}" 593 594 def cmd_LCD(self, path): 595 os.chdir(path) 596 597 def cmd_LN(self, rest): 598 linkpath, rest = self._getFilename(rest) 599 targetpath, rest = self._getFilename(rest) 600 linkpath, targetpath = map( 601 lambda x: os.path.join(self.currentDirectory, x), (linkpath, targetpath) 602 ) 603 return self.client.makeLink(linkpath, targetpath).addCallback(_ignore) 604 605 def cmd_LS(self, rest): 606 # possible lines: 607 # ls current directory 608 # ls name_of_file that file 609 # ls name_of_directory that directory 610 # ls some_glob_string current directory, globbed for that string 611 options = [] 612 rest = rest.split() 613 while rest and rest[0] and rest[0][0] == "-": 614 opts = rest.pop(0)[1:] 615 for o in opts: 616 if o == "l": 617 options.append("verbose") 618 elif o == "a": 619 options.append("all") 620 rest = " ".join(rest) 621 path, rest = self._getFilename(rest) 622 if not path: 623 fullPath = self.currentDirectory + "/" 624 else: 625 fullPath = os.path.join(self.currentDirectory, path) 626 d = self._remoteGlob(fullPath) 627 d.addCallback(self._cbDisplayFiles, options) 628 return d 629 630 def _cbDisplayFiles(self, files, options): 631 files.sort() 632 if "all" not in options: 633 files = [f for f in files if not f[0].startswith(b".")] 634 if "verbose" in options: 635 lines = [f[1] for f in files] 636 else: 637 lines = [f[0] for f in files] 638 if not lines: 639 return None 640 else: 641 return b"\n".join(lines) 642 643 def cmd_MKDIR(self, path): 644 path, rest = self._getFilename(path) 645 path = os.path.join(self.currentDirectory, path) 646 return self.client.makeDirectory(path, {}).addCallback(_ignore) 647 648 def cmd_RMDIR(self, path): 649 path, rest = self._getFilename(path) 650 path = os.path.join(self.currentDirectory, path) 651 return self.client.removeDirectory(path).addCallback(_ignore) 652 653 def cmd_LMKDIR(self, path): 654 os.system("mkdir %s" % path) 655 656 def cmd_RM(self, path): 657 path, rest = self._getFilename(path) 658 path = os.path.join(self.currentDirectory, path) 659 return self.client.removeFile(path).addCallback(_ignore) 660 661 def cmd_LLS(self, rest): 662 os.system("ls %s" % rest) 663 664 def cmd_RENAME(self, rest): 665 oldpath, rest = self._getFilename(rest) 666 newpath, rest = self._getFilename(rest) 667 oldpath, newpath = map( 668 lambda x: os.path.join(self.currentDirectory, x), (oldpath, newpath) 669 ) 670 return self.client.renameFile(oldpath, newpath).addCallback(_ignore) 671 672 def cmd_EXIT(self, ignored): 673 self.client.transport.loseConnection() 674 675 cmd_QUIT = cmd_EXIT 676 677 def cmd_VERSION(self, ignored): 678 version = "SFTP version %i" % self.client.version 679 if isinstance(version, str): 680 version = version.encode("utf-8") 681 return version 682 683 def cmd_HELP(self, ignored): 684 return """Available commands: 685cd path Change remote directory to 'path'. 686chgrp gid path Change gid of 'path' to 'gid'. 687chmod mode path Change mode of 'path' to 'mode'. 688chown uid path Change uid of 'path' to 'uid'. 689exit Disconnect from the server. 690get remote-path [local-path] Get remote file. 691help Get a list of available commands. 692lcd path Change local directory to 'path'. 693lls [ls-options] [path] Display local directory listing. 694lmkdir path Create local directory. 695ln linkpath targetpath Symlink remote file. 696lpwd Print the local working directory. 697ls [-l] [path] Display remote directory listing. 698mkdir path Create remote directory. 699progress Toggle progress bar. 700put local-path [remote-path] Put local file. 701pwd Print the remote working directory. 702quit Disconnect from the server. 703rename oldpath newpath Rename remote file. 704rmdir path Remove remote directory. 705rm path Remove remote file. 706version Print the SFTP version. 707? Synonym for 'help'. 708""" 709 710 def cmd_PWD(self, ignored): 711 return self.currentDirectory 712 713 def cmd_LPWD(self, ignored): 714 return os.getcwd() 715 716 def cmd_PROGRESS(self, ignored): 717 self.useProgressBar = not self.useProgressBar 718 return "%ssing progess bar." % (self.useProgressBar and "U" or "Not u") 719 720 def cmd_EXEC(self, rest): 721 """ 722 Run C{rest} using the user's shell (or /bin/sh if they do not have 723 one). 724 """ 725 shell = self._pwd.getpwnam(getpass.getuser())[6] 726 if not shell: 727 shell = "/bin/sh" 728 if rest: 729 cmds = ["-c", rest] 730 return utils.getProcessOutput(shell, cmds, errortoo=1) 731 else: 732 os.system(shell) 733 734 # accessory functions 735 736 def _remoteGlob(self, fullPath): 737 log.msg("looking up %s" % fullPath) 738 head, tail = os.path.split(fullPath) 739 if "*" in tail or "?" in tail: 740 glob = 1 741 else: 742 glob = 0 743 if tail and not glob: # could be file or directory 744 # try directory first 745 d = self.client.openDirectory(fullPath) 746 d.addCallback(self._cbOpenList, "") 747 d.addErrback(self._ebNotADirectory, head, tail) 748 else: 749 d = self.client.openDirectory(head) 750 d.addCallback(self._cbOpenList, tail) 751 return d 752 753 def _cbOpenList(self, directory, glob): 754 files = [] 755 d = directory.read() 756 d.addBoth(self._cbReadFile, files, directory, glob) 757 return d 758 759 def _ebNotADirectory(self, reason, path, glob): 760 d = self.client.openDirectory(path) 761 d.addCallback(self._cbOpenList, glob) 762 return d 763 764 def _cbReadFile(self, files, matchedFiles, directory, glob): 765 if not isinstance(files, failure.Failure): 766 if glob: 767 glob = glob.encode("utf-8") 768 matchedFiles.extend([f for f in files if fnmatch.fnmatch(f[0], glob)]) 769 else: 770 matchedFiles.extend(files) 771 d = directory.read() 772 d.addBoth(self._cbReadFile, matchedFiles, directory, glob) 773 return d 774 else: 775 reason = files 776 reason.trap(EOFError) 777 directory.close() 778 return matchedFiles 779 780 def _abbrevSize(self, size): 781 # from http://mail.python.org/pipermail/python-list/1999-December/018395.html 782 _abbrevs = [ 783 (1 << 50, "PB"), 784 (1 << 40, "TB"), 785 (1 << 30, "GB"), 786 (1 << 20, "MB"), 787 (1 << 10, "kB"), 788 (1, "B"), 789 ] 790 791 for factor, suffix in _abbrevs: 792 if size > factor: 793 break 794 return "%.1f" % (size / factor) + suffix 795 796 def _abbrevTime(self, t): 797 if t > 3600: # 1 hour 798 hours = int(t / 3600) 799 t -= 3600 * hours 800 mins = int(t / 60) 801 t -= 60 * mins 802 return "%i:%02i:%02i" % (hours, mins, t) 803 else: 804 mins = int(t / 60) 805 t -= 60 * mins 806 return "%02i:%02i" % (mins, t) 807 808 def _printProgressBar(self, f, startTime): 809 """ 810 Update a console progress bar on this L{StdioClient}'s transport, based 811 on the difference between the start time of the operation and the 812 current time according to the reactor, and appropriate to the size of 813 the console window. 814 815 @param f: a wrapper around the file which is being written or read 816 @type f: L{FileWrapper} 817 818 @param startTime: The time at which the operation being tracked began. 819 @type startTime: L{float} 820 """ 821 diff = self.reactor.seconds() - startTime 822 total = f.total 823 try: 824 winSize = struct.unpack("4H", fcntl.ioctl(0, tty.TIOCGWINSZ, "12345679")) 825 except OSError: 826 winSize = [None, 80] 827 if diff == 0.0: 828 speed = 0.0 829 else: 830 speed = total / diff 831 if speed: 832 timeLeft = (f.size - total) / speed 833 else: 834 timeLeft = 0 835 front = f.name 836 if f.size: 837 percentage = (total / f.size) * 100 838 else: 839 percentage = 100 840 back = "%3i%% %s %sps %s " % ( 841 percentage, 842 self._abbrevSize(total), 843 self._abbrevSize(speed), 844 self._abbrevTime(timeLeft), 845 ) 846 spaces = (winSize[1] - (len(front) + len(back) + 1)) * " " 847 command = f"\r{front}{spaces}{back}" 848 self._writeToTransport(command) 849 850 def _getFilename(self, line): 851 """ 852 Parse line received as command line input and return first filename 853 together with the remaining line. 854 855 @param line: Arguments received from command line input. 856 @type line: L{str} 857 858 @return: Tupple with filename and rest. Return empty values when no path was not found. 859 @rtype: C{tupple} 860 """ 861 line = line.strip() 862 if not line: 863 return "", "" 864 if line[0] in "'\"": 865 ret = [] 866 line = list(line) 867 try: 868 for i in range(1, len(line)): 869 c = line[i] 870 if c == line[0]: 871 return "".join(ret), "".join(line[i + 1 :]).lstrip() 872 elif c == "\\": # quoted character 873 del line[i] 874 if line[i] not in "'\"\\": 875 raise IndexError(f"bad quote: \\{line[i]}") 876 ret.append(line[i]) 877 else: 878 ret.append(line[i]) 879 except IndexError: 880 raise IndexError("unterminated quote") 881 ret = line.split(None, 1) 882 if len(ret) == 1: 883 return ret[0], "" 884 else: 885 return ret[0], ret[1] 886 887 888setattr(StdioClient, "cmd_?", StdioClient.cmd_HELP) 889 890 891class SSHConnection(connection.SSHConnection): 892 def serviceStarted(self): 893 self.openChannel(SSHSession()) 894 895 896class SSHSession(channel.SSHChannel): 897 898 name = b"session" 899 900 def channelOpen(self, foo): 901 log.msg("session %s open" % self.id) 902 if self.conn.options["subsystem"].startswith("/"): 903 request = "exec" 904 else: 905 request = "subsystem" 906 d = self.conn.sendRequest( 907 self, request, common.NS(self.conn.options["subsystem"]), wantReply=1 908 ) 909 d.addCallback(self._cbSubsystem) 910 d.addErrback(_ebExit) 911 912 def _cbSubsystem(self, result): 913 self.client = filetransfer.FileTransferClient() 914 self.client.makeConnection(self) 915 self.dataReceived = self.client.dataReceived 916 f = None 917 if self.conn.options["batchfile"]: 918 fn = self.conn.options["batchfile"] 919 if fn != "-": 920 f = open(fn) 921 self.stdio = stdio.StandardIO(StdioClient(self.client, f)) 922 923 def extReceived(self, t, data): 924 if t == connection.EXTENDED_DATA_STDERR: 925 log.msg("got %s stderr data" % len(data)) 926 sys.stderr.write(data) 927 sys.stderr.flush() 928 929 def eofReceived(self): 930 log.msg("got eof") 931 self.stdio.loseWriteConnection() 932 933 def closeReceived(self): 934 log.msg("remote side closed %s" % self) 935 self.conn.sendClose(self) 936 937 def closed(self): 938 try: 939 reactor.stop() 940 except BaseException: 941 pass 942 943 def stopWriting(self): 944 self.stdio.pauseProducing() 945 946 def startWriting(self): 947 self.stdio.resumeProducing() 948 949 950if __name__ == "__main__": 951 run() 952