1
# -*- test-case-name: twisted.conch.test.test_cftp -*-
2
# Copyright (c) 2001-2009 Twisted Matrix Laboratories.
3
# See LICENSE for details.
6
Implementation module for the I{cftp} command.
9
import os, sys, getpass, struct, tty, fcntl, stat
10
import fnmatch, pwd, time, glob
12
from twisted.conch.client import connect, default, options
13
from twisted.conch.ssh import connection, common
14
from twisted.conch.ssh import channel, filetransfer
15
from twisted.protocols import basic
16
from twisted.internet import reactor, stdio, defer, utils
17
from twisted.python import log, usage, failure
19
class ClientOptions(options.ConchOptions):
21
synopsis = """Usage: cftp [options] [user@]host
22
cftp [options] [user@]host[:dir[/]]
23
cftp [options] [user@]host[:file [localfile]]
25
longdesc = ("cftp is a client for logging into a remote machine and "
26
"executing commands to send and receive file information")
29
['buffersize', 'B', 32768, 'Size of the buffer to use for sending/receiving.'],
30
['batchfile', 'b', None, 'File to read commands from, or \'-\' for stdin.'],
31
['requests', 'R', 5, 'Number of requests to make before waiting for a reply.'],
32
['subsystem', 's', 'sftp', 'Subsystem/server program to connect to.']]
33
zsh_altArgDescr = {"buffersize":"Size of send/receive buffer (default: 32768)"}
34
#zsh_multiUse = ["foo", "bar"]
35
#zsh_mutuallyExclusive = [("foo", "bar"), ("bar", "baz")]
36
#zsh_actions = {"foo":'_files -g "*.foo"', "bar":"(one two three)"}
37
#zsh_actionDescr = {"logfile":"log file name", "random":"random seed"}
38
zsh_extras = ['2::localfile:{if [[ $words[1] == *:* ]]; then; _files; fi}']
40
def parseArgs(self, host, localPath=None):
41
self['remotePath'] = ''
43
host, self['remotePath'] = host.split(':', 1)
44
self['remotePath'].rstrip('/')
46
self['localPath'] = localPath
50
# prof = hotshot.Profile('cftp.prof')
53
if '-l' in args: # cvs is an idiot
55
args = args[i:i+2]+args
57
options = ClientOptions()
59
options.parseOptions(args)
60
except usage.UsageError, u:
65
log.startLogging(sys.stderr)
80
log.err(failure.Failure())
83
def doConnect(options):
84
# log.deferr = handleError # HACK
85
if '@' in options['host']:
86
options['user'], options['host'] = options['host'].split('@',1)
87
host = options['host']
88
if not options['user']:
89
options['user'] = getpass.getuser()
90
if not options['port']:
93
options['port'] = int(options['port'])
94
host = options['host']
95
port = options['port']
96
conn = SSHConnection()
97
conn.options = options
98
vhk = default.verifyHostKey
99
uao = default.SSHUserAuthClient(options['user'], options, conn)
100
connect.connect(host, port, options, vhk, uao).addErrback(_ebExit)
104
if hasattr(f.value, 'value'):
109
#exitStatus = "conch: exiting with error %s" % f
114
def _ignore(*args): pass
118
def __init__(self, f):
121
f.seek(0, 2) # seek to the end
124
def __getattr__(self, attr):
125
return getattr(self.f, attr)
127
class StdioClient(basic.LineReceiver):
134
def __init__(self, client, f = None):
136
self.currentDirectory = ''
138
self.useProgressBar = (not f and 1) or 0
140
def connectionMade(self):
141
self.client.realPath('').addCallback(self._cbSetCurDir)
143
def _cbSetCurDir(self, path):
144
self.currentDirectory = path
147
def lineReceived(self, line):
148
if self.client.transport.localClosed:
150
log.msg('got line %s' % repr(line))
155
if self.file and line.startswith('-'):
156
self.ignoreErrors = 1
159
self.ignoreErrors = 0
160
d = self._dispatchCommand(line)
162
d.addCallback(self._cbCommand)
163
d.addErrback(self._ebCommand)
166
def _dispatchCommand(self, line):
168
command, rest = line.split(' ', 1)
171
command, rest = line, ''
172
if command.startswith('!'): # command
174
rest = (command[1:] + ' ' + rest).strip()
176
command = command.upper()
177
log.msg('looking up cmd %s' % command)
178
f = getattr(self, 'cmd_%s' % command, None)
180
return defer.maybeDeferred(f, rest)
182
self._ebCommand(failure.Failure(NotImplementedError(
183
"No command called `%s'" % command)))
186
def _printFailure(self, f):
188
e = f.trap(NotImplementedError, filetransfer.SFTPError, OSError, IOError)
189
if e == NotImplementedError:
190
self.transport.write(self.cmd_HELP(''))
191
elif e == filetransfer.SFTPError:
192
self.transport.write("remote error %i: %s\n" %
193
(f.value.code, f.value.message))
194
elif e in (OSError, IOError):
195
self.transport.write("local error %i: %s\n" %
196
(f.value.errno, f.value.strerror))
199
if self.client.transport.localClosed:
201
self.transport.write(self.ps)
202
self.ignoreErrors = 0
204
l = self.file.readline()
206
self.client.transport.loseConnection()
208
self.transport.write(l)
209
self.lineReceived(l.strip())
211
def _cbCommand(self, result):
212
if result is not None:
213
self.transport.write(result)
214
if not result.endswith('\n'):
215
self.transport.write('\n')
218
def _ebCommand(self, f):
219
self._printFailure(f)
220
if self.file and not self.ignoreErrors:
221
self.client.transport.loseConnection()
224
def cmd_CD(self, path):
225
path, rest = self._getFilename(path)
226
if not path.endswith('/'):
228
newPath = path and os.path.join(self.currentDirectory, path) or ''
229
d = self.client.openDirectory(newPath)
230
d.addCallback(self._cbCd)
231
d.addErrback(self._ebCommand)
234
def _cbCd(self, directory):
236
d = self.client.realPath(directory.name)
237
d.addCallback(self._cbCurDir)
240
def _cbCurDir(self, path):
241
self.currentDirectory = path
243
def cmd_CHGRP(self, rest):
244
grp, rest = rest.split(None, 1)
245
path, rest = self._getFilename(rest)
247
d = self.client.getAttrs(path)
248
d.addCallback(self._cbSetUsrGrp, path, grp=grp)
251
def cmd_CHMOD(self, rest):
252
mod, rest = rest.split(None, 1)
253
path, rest = self._getFilename(rest)
255
d = self.client.setAttrs(path, {'permissions':mod})
256
d.addCallback(_ignore)
259
def cmd_CHOWN(self, rest):
260
usr, rest = rest.split(None, 1)
261
path, rest = self._getFilename(rest)
263
d = self.client.getAttrs(path)
264
d.addCallback(self._cbSetUsrGrp, path, usr=usr)
267
def _cbSetUsrGrp(self, attrs, path, usr=None, grp=None):
269
new['uid'] = (usr is not None) and usr or attrs['uid']
270
new['gid'] = (grp is not None) and grp or attrs['gid']
271
d = self.client.setAttrs(path, new)
272
d.addCallback(_ignore)
275
def cmd_GET(self, rest):
276
remote, rest = self._getFilename(rest)
277
if '*' in remote or '?' in remote: # wildcard
279
local, rest = self._getFilename(rest)
280
if not os.path.isdir(local):
281
return "Wildcard get with non-directory target."
284
d = self._remoteGlob(remote)
285
d.addCallback(self._cbGetMultiple, local)
288
local, rest = self._getFilename(rest)
290
local = os.path.split(remote)[1]
291
log.msg((remote, local))
292
lf = file(local, 'w', 0)
293
path = os.path.join(self.currentDirectory, remote)
294
d = self.client.openFile(path, filetransfer.FXF_READ, {})
295
d.addCallback(self._cbGetOpenFile, lf)
296
d.addErrback(self._ebCloseLf, lf)
299
def _cbGetMultiple(self, files, local):
300
#if self._useProgressBar: # one at a time
301
# XXX this can be optimized for times w/o progress bar
302
return self._cbGetMultipleNext(None, files, local)
304
def _cbGetMultipleNext(self, res, files, local):
305
if isinstance(res, failure.Failure):
306
self._printFailure(res)
308
self.transport.write(res)
309
if not res.endswith('\n'):
310
self.transport.write('\n')
314
lf = file(os.path.join(local, os.path.split(f)[1]), 'w', 0)
315
path = os.path.join(self.currentDirectory, f)
316
d = self.client.openFile(path, filetransfer.FXF_READ, {})
317
d.addCallback(self._cbGetOpenFile, lf)
318
d.addErrback(self._ebCloseLf, lf)
319
d.addBoth(self._cbGetMultipleNext, files, local)
322
def _ebCloseLf(self, f, lf):
326
def _cbGetOpenFile(self, rf, lf):
327
return rf.getAttrs().addCallback(self._cbGetFileSize, rf, lf)
329
def _cbGetFileSize(self, attrs, rf, lf):
330
if not stat.S_ISREG(attrs['permissions']):
333
return "Can't get non-regular file: %s" % rf.name
334
rf.size = attrs['size']
335
bufferSize = self.client.transport.conn.options['buffersize']
336
numRequests = self.client.transport.conn.options['requests']
340
startTime = time.time()
341
for i in range(numRequests):
342
d = self._cbGetRead('', rf, lf, chunks, 0, bufferSize, startTime)
344
dl = defer.DeferredList(dList, fireOnOneErrback=1)
345
dl.addCallback(self._cbGetDone, rf, lf)
348
def _getNextChunk(self, chunks):
352
return # nothing more to get
354
i = chunks.index(chunk)
355
chunks.insert(i, (end, chunk[0]))
356
return (end, chunk[0] - end)
358
bufSize = int(self.client.transport.conn.options['buffersize'])
359
chunks.append((end, end + bufSize))
360
return (end, bufSize)
362
def _cbGetRead(self, data, rf, lf, chunks, start, size, startTime):
363
if data and isinstance(data, failure.Failure):
364
log.msg('get read err: %s' % data)
366
reason.trap(EOFError)
367
i = chunks.index((start, start + size))
369
chunks.insert(i, (start, 'eof'))
371
log.msg('get read data: %i' % len(data))
374
if len(data) != size:
375
log.msg('got less than we asked for: %i < %i' %
377
i = chunks.index((start, start + size))
379
chunks.insert(i, (start, start + len(data)))
380
rf.total += len(data)
381
if self.useProgressBar:
382
self._printProgessBar(rf, startTime)
383
chunk = self._getNextChunk(chunks)
387
start, length = chunk
388
log.msg('asking for %i -> %i' % (start, start+length))
389
d = rf.readChunk(start, length)
390
d.addBoth(self._cbGetRead, rf, lf, chunks, start, length, startTime)
393
def _cbGetDone(self, ignored, rf, lf):
397
if self.useProgressBar:
398
self.transport.write('\n')
399
return "Transferred %s to %s" % (rf.name, lf.name)
401
def cmd_PUT(self, rest):
402
local, rest = self._getFilename(rest)
403
if '*' in local or '?' in local: # wildcard
405
remote, rest = self._getFilename(rest)
406
path = os.path.join(self.currentDirectory, remote)
407
d = self.client.getAttrs(path)
408
d.addCallback(self._cbPutTargetAttrs, remote, local)
412
files = glob.glob(local)
413
return self._cbPutMultipleNext(None, files, remote)
415
remote, rest = self._getFilename(rest)
417
remote = os.path.split(local)[1]
418
lf = file(local, 'r')
419
path = os.path.join(self.currentDirectory, remote)
420
flags = filetransfer.FXF_WRITE|filetransfer.FXF_CREAT|filetransfer.FXF_TRUNC
421
d = self.client.openFile(path, flags, {})
422
d.addCallback(self._cbPutOpenFile, lf)
423
d.addErrback(self._ebCloseLf, lf)
426
def _cbPutTargetAttrs(self, attrs, path, local):
427
if not stat.S_ISDIR(attrs['permissions']):
428
return "Wildcard put with non-directory target."
429
return self._cbPutMultipleNext(None, files, path)
431
def _cbPutMultipleNext(self, res, files, path):
432
if isinstance(res, failure.Failure):
433
self._printFailure(res)
435
self.transport.write(res)
436
if not res.endswith('\n'):
437
self.transport.write('\n')
439
while files and not f:
444
self._printFailure(failure.Failure())
448
name = os.path.split(f)[1]
449
remote = os.path.join(self.currentDirectory, path, name)
450
log.msg((name, remote, path))
451
flags = filetransfer.FXF_WRITE|filetransfer.FXF_CREAT|filetransfer.FXF_TRUNC
452
d = self.client.openFile(remote, flags, {})
453
d.addCallback(self._cbPutOpenFile, lf)
454
d.addErrback(self._ebCloseLf, lf)
455
d.addBoth(self._cbPutMultipleNext, files, path)
458
def _cbPutOpenFile(self, rf, lf):
459
numRequests = self.client.transport.conn.options['requests']
460
if self.useProgressBar:
464
startTime = time.time()
465
for i in range(numRequests):
466
d = self._cbPutWrite(None, rf, lf, chunks, startTime)
469
dl = defer.DeferredList(dList, fireOnOneErrback=1)
470
dl.addCallback(self._cbPutDone, rf, lf)
473
def _cbPutWrite(self, ignored, rf, lf, chunks, startTime):
474
chunk = self._getNextChunk(chunks)
478
if self.useProgressBar:
479
lf.total += len(data)
480
self._printProgessBar(lf, startTime)
482
d = rf.writeChunk(start, data)
483
d.addCallback(self._cbPutWrite, rf, lf, chunks, startTime)
488
def _cbPutDone(self, ignored, rf, lf):
491
if self.useProgressBar:
492
self.transport.write('\n')
493
return 'Transferred %s to %s' % (lf.name, rf.name)
495
def cmd_LCD(self, path):
498
def cmd_LN(self, rest):
499
linkpath, rest = self._getFilename(rest)
500
targetpath, rest = self._getFilename(rest)
501
linkpath, targetpath = map(
502
lambda x: os.path.join(self.currentDirectory, x),
503
(linkpath, targetpath))
504
return self.client.makeLink(linkpath, targetpath).addCallback(_ignore)
506
def cmd_LS(self, rest):
508
# ls current directory
509
# ls name_of_file that file
510
# ls name_of_directory that directory
511
# ls some_glob_string current directory, globbed for that string
514
while rest and rest[0] and rest[0][0] == '-':
515
opts = rest.pop(0)[1:]
518
options.append('verbose')
520
options.append('all')
521
rest = ' '.join(rest)
522
path, rest = self._getFilename(rest)
524
fullPath = self.currentDirectory + '/'
526
fullPath = os.path.join(self.currentDirectory, path)
527
d = self._remoteGlob(fullPath)
528
d.addCallback(self._cbDisplayFiles, options)
531
def _cbDisplayFiles(self, files, options):
533
if 'all' not in options:
534
files = [f for f in files if not f[0].startswith('.')]
535
if 'verbose' in options:
536
lines = [f[1] for f in files]
538
lines = [f[0] for f in files]
542
return '\n'.join(lines)
544
def cmd_MKDIR(self, path):
545
path, rest = self._getFilename(path)
546
path = os.path.join(self.currentDirectory, path)
547
return self.client.makeDirectory(path, {}).addCallback(_ignore)
549
def cmd_RMDIR(self, path):
550
path, rest = self._getFilename(path)
551
path = os.path.join(self.currentDirectory, path)
552
return self.client.removeDirectory(path).addCallback(_ignore)
554
def cmd_LMKDIR(self, path):
555
os.system("mkdir %s" % path)
557
def cmd_RM(self, path):
558
path, rest = self._getFilename(path)
559
path = os.path.join(self.currentDirectory, path)
560
return self.client.removeFile(path).addCallback(_ignore)
562
def cmd_LLS(self, rest):
563
os.system("ls %s" % rest)
565
def cmd_RENAME(self, rest):
566
oldpath, rest = self._getFilename(rest)
567
newpath, rest = self._getFilename(rest)
568
oldpath, newpath = map (
569
lambda x: os.path.join(self.currentDirectory, x),
571
return self.client.renameFile(oldpath, newpath).addCallback(_ignore)
573
def cmd_EXIT(self, ignored):
574
self.client.transport.loseConnection()
578
def cmd_VERSION(self, ignored):
579
return "SFTP version %i" % self.client.version
581
def cmd_HELP(self, ignored):
582
return """Available commands:
583
cd path Change remote directory to 'path'.
584
chgrp gid path Change gid of 'path' to 'gid'.
585
chmod mode path Change mode of 'path' to 'mode'.
586
chown uid path Change uid of 'path' to 'uid'.
587
exit Disconnect from the server.
588
get remote-path [local-path] Get remote file.
589
help Get a list of available commands.
590
lcd path Change local directory to 'path'.
591
lls [ls-options] [path] Display local directory listing.
592
lmkdir path Create local directory.
593
ln linkpath targetpath Symlink remote file.
594
lpwd Print the local working directory.
595
ls [-l] [path] Display remote directory listing.
596
mkdir path Create remote directory.
597
progress Toggle progress bar.
598
put local-path [remote-path] Put local file.
599
pwd Print the remote working directory.
600
quit Disconnect from the server.
601
rename oldpath newpath Rename remote file.
602
rmdir path Remove remote directory.
603
rm path Remove remote file.
604
version Print the SFTP version.
605
? Synonym for 'help'.
608
def cmd_PWD(self, ignored):
609
return self.currentDirectory
611
def cmd_LPWD(self, ignored):
614
def cmd_PROGRESS(self, ignored):
615
self.useProgressBar = not self.useProgressBar
616
return "%ssing progess bar." % (self.useProgressBar and "U" or "Not u")
618
def cmd_EXEC(self, rest):
620
Run C{rest} using the user's shell (or /bin/sh if they do not have
623
shell = self._pwd.getpwnam(getpass.getuser())[6]
628
return utils.getProcessOutput(shell, cmds, errortoo=1)
632
# 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:
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)
647
d = self.client.openDirectory(head)
648
d.addCallback(self._cbOpenList, tail)
651
def _cbOpenList(self, directory, glob):
654
d.addBoth(self._cbReadFile, files, directory, glob)
657
def _ebNotADirectory(self, reason, path, glob):
658
d = self.client.openDirectory(path)
659
d.addCallback(self._cbOpenList, glob)
662
def _cbReadFile(self, files, l, directory, glob):
663
if not isinstance(files, failure.Failure):
665
l.extend([f for f in files if fnmatch.fnmatch(f[0], glob)])
669
d.addBoth(self._cbReadFile, l, directory, glob)
673
reason.trap(EOFError)
677
def _abbrevSize(self, size):
678
# from http://mail.python.org/pipermail/python-list/1999-December/018395.html
688
for factor, suffix in _abbrevs:
691
return '%.1f' % (size/factor) + suffix
693
def _abbrevTime(self, t):
694
if t > 3600: # 1 hour
695
hours = int(t / 3600)
699
return "%i:%02i:%02i" % (hours, mins, t)
703
return "%02i:%02i" % (mins, t)
705
def _printProgessBar(self, f, startTime):
706
diff = time.time() - startTime
709
winSize = struct.unpack('4H',
710
fcntl.ioctl(0, tty.TIOCGWINSZ, '12345679'))
715
timeLeft = (f.size - total) / speed
719
back = '%3i%% %s %sps %s ' % ((total/f.size)*100, self._abbrevSize(total),
720
self._abbrevSize(total/diff), self._abbrevTime(timeLeft))
721
spaces = (winSize[1] - (len(front) + len(back) + 1)) * ' '
722
self.transport.write('\r%s%s%s' % (front, spaces, back))
724
def _getFilename(self, line):
732
for i in range(1,len(line)):
735
return ''.join(ret), ''.join(line[i+1:]).lstrip()
736
elif c == '\\': # quoted character
738
if line[i] not in '\'"\\':
739
raise IndexError, "bad quote: \\%s" % line[i]
744
raise IndexError, "unterminated quote"
745
ret = line.split(None, 1)
751
StdioClient.__dict__['cmd_?'] = StdioClient.cmd_HELP
753
class SSHConnection(connection.SSHConnection):
754
def serviceStarted(self):
755
self.openChannel(SSHSession())
757
class SSHSession(channel.SSHChannel):
761
def channelOpen(self, foo):
762
log.msg('session %s open' % self.id)
763
if self.conn.options['subsystem'].startswith('/'):
766
request = 'subsystem'
767
d = self.conn.sendRequest(self, request, \
768
common.NS(self.conn.options['subsystem']), wantReply=1)
769
d.addCallback(self._cbSubsystem)
770
d.addErrback(_ebExit)
772
def _cbSubsystem(self, result):
773
self.client = filetransfer.FileTransferClient()
774
self.client.makeConnection(self)
775
self.dataReceived = self.client.dataReceived
777
if self.conn.options['batchfile']:
778
fn = self.conn.options['batchfile']
781
self.stdio = stdio.StandardIO(StdioClient(self.client, f))
783
def extReceived(self, t, data):
784
if t==connection.EXTENDED_DATA_STDERR:
785
log.msg('got %s stderr data' % len(data))
786
sys.stderr.write(data)
789
def eofReceived(self):
791
self.stdio.closeStdin()
793
def closeReceived(self):
794
log.msg('remote side closed %s' % self)
795
self.conn.sendClose(self)
803
def stopWriting(self):
804
self.stdio.pauseProducing()
806
def startWriting(self):
807
self.stdio.resumeProducing()
809
if __name__ == '__main__':