cftp.py 31 KB

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