root / trunk / twisted / conch / ssh / filetransfer.py

Revision 27062, 32.5 kB (checked in by jml, 3 days ago)

Silence the deprecation warning in filetransfer.py that occurs in Python 2.6.

  • Author: jml
  • Reviewer: exarkun
  • Fixes #3897

SFTPError used to assign to 'message'. It's deprecated to assign to message
on Exceptions in Python 2.6. This patch changes SFTPError to use _message
internally, and makes the message attribute available as a property.

Line 
1 # -*- test-case-name: twisted.conch.test.test_filetransfer -*-
2 #
3 # Copyright (c) 2001-2008 Twisted Matrix Laboratories.
4 # See LICENSE for details.
5
6
7 import struct, errno
8
9 from twisted.internet import defer, protocol
10 from twisted.python import failure, log
11
12 from common import NS, getNS
13 from twisted.conch.interfaces import ISFTPServer, ISFTPFile
14
15 from zope import interface
16
17
18
19 class FileTransferBase(protocol.Protocol):
20
21     versions = (3, )
22
23     packetTypes = {}
24
25     def __init__(self):
26         self.buf = ''
27         self.otherVersion = None # this gets set
28
29     def sendPacket(self, kind, data):
30         self.transport.write(struct.pack('!LB', len(data)+1, kind) + data)
31
32     def dataReceived(self, data):
33         self.buf += data
34         while len(self.buf) > 5:
35             length, kind = struct.unpack('!LB', self.buf[:5])
36             if len(self.buf) < 4 + length:
37                 return
38             data, self.buf = self.buf[5:4+length], self.buf[4+length:]
39             packetType = self.packetTypes.get(kind, None)
40             if not packetType:
41                 log.msg('no packet type for', kind)
42                 continue
43             f = getattr(self, 'packet_%s' % packetType, None)
44             if not f:
45                 log.msg('not implemented: %s' % packetType)
46                 log.msg(repr(data[4:]))
47                 reqId, = struct.unpack('!L', data[:4])
48                 self._sendStatus(reqId, FX_OP_UNSUPPORTED,
49                                  "don't understand %s" % packetType)
50                 #XXX not implemented
51                 continue
52             try:
53                 f(data)
54             except:
55                 log.err()
56                 continue
57                 reqId ,= struct.unpack('!L', data[:4])
58                 self._ebStatus(failure.Failure(e), reqId)
59
60     def _parseAttributes(self, data):
61         flags ,= struct.unpack('!L', data[:4])
62         attrs = {}
63         data = data[4:]
64         if flags & FILEXFER_ATTR_SIZE == FILEXFER_ATTR_SIZE:
65             size ,= struct.unpack('!Q', data[:8])
66             attrs['size'] = size
67             data = data[8:]
68         if flags & FILEXFER_ATTR_OWNERGROUP == FILEXFER_ATTR_OWNERGROUP:
69             uid, gid = struct.unpack('!2L', data[:8])
70             attrs['uid'] = uid
71             attrs['gid'] = gid
72             data = data[8:]
73         if flags & FILEXFER_ATTR_PERMISSIONS == FILEXFER_ATTR_PERMISSIONS:
74             perms ,= struct.unpack('!L', data[:4])
75             attrs['permissions'] = perms
76             data = data[4:]
77         if flags & FILEXFER_ATTR_ACMODTIME == FILEXFER_ATTR_ACMODTIME:
78             atime, mtime = struct.unpack('!2L', data[:8])
79             attrs['atime'] = atime
80             attrs['mtime'] = mtime
81             data = data[8:]
82         if flags & FILEXFER_ATTR_EXTENDED == FILEXFER_ATTR_EXTENDED:
83             extended_count ,= struct.unpack('!L', data[:4])
84             data = data[4:]
85             for i in xrange(extended_count):
86                 extended_type, data = getNS(data)
87                 extended_data, data = getNS(data)
88                 attrs['ext_%s' % extended_type] = extended_data
89         return attrs, data
90
91     def _packAttributes(self, attrs):
92         flags = 0
93         data = ''
94         if 'size' in attrs:
95             data += struct.pack('!Q', attrs['size'])
96             flags |= FILEXFER_ATTR_SIZE
97         if 'uid' in attrs and 'gid' in attrs:
98             data += struct.pack('!2L', attrs['uid'], attrs['gid'])
99             flags |= FILEXFER_ATTR_OWNERGROUP
100         if 'permissions' in attrs:
101             data += struct.pack('!L', attrs['permissions'])
102             flags |= FILEXFER_ATTR_PERMISSIONS
103         if 'atime' in attrs and 'mtime' in attrs:
104             data += struct.pack('!2L', attrs['atime'], attrs['mtime'])
105             flags |= FILEXFER_ATTR_ACMODTIME
106         extended = []
107         for k in attrs:
108             if k.startswith('ext_'):
109                 ext_type = NS(k[4:])
110                 ext_data = NS(attrs[k])
111                 extended.append(ext_type+ext_data)
112         if extended:
113             data += struct.pack('!L', len(extended))
114             data += ''.join(extended)
115             flags |= FILEXFER_ATTR_EXTENDED
116         return struct.pack('!L', flags) + data
117
118 class FileTransferServer(FileTransferBase):
119
120     def __init__(self, data=None, avatar=None):
121         FileTransferBase.__init__(self)
122         self.client = ISFTPServer(avatar) # yay interfaces
123         self.openFiles = {}
124         self.openDirs = {}
125
126     def packet_INIT(self, data):
127         version ,= struct.unpack('!L', data[:4])
128         self.version = min(list(self.versions) + [version])
129         data = data[4:]
130         ext = {}
131         while data:
132             ext_name, data = getNS(data)
133             ext_data, data = getNS(data)
134             ext[ext_name] = ext_data
135         our_ext = self.client.gotVersion(version, ext)
136         our_ext_data = ""
137         for (k,v) in our_ext.items():
138             our_ext_data += NS(k) + NS(v)
139         self.sendPacket(FXP_VERSION, struct.pack('!L', self.version) + \
140                                      our_ext_data)
141
142     def packet_OPEN(self, data):
143         requestId = data[:4]
144         data = data[4:]
145         filename, data = getNS(data)
146         flags ,= struct.unpack('!L', data[:4])
147         data = data[4:]
148         attrs, data = self._parseAttributes(data)
149         assert data == '', 'still have data in OPEN: %s' % repr(data)
150         d = defer.maybeDeferred(self.client.openFile, filename, flags, attrs)
151         d.addCallback(self._cbOpenFile, requestId)
152         d.addErrback(self._ebStatus, requestId, "open failed")
153
154     def _cbOpenFile(self, fileObj, requestId):
155         fileId = str(hash(fileObj))
156         if fileId in self.openFiles:
157             raise KeyError, 'id already open'
158         self.openFiles[fileId] = fileObj
159         self.sendPacket(FXP_HANDLE, requestId + NS(fileId))
160
161     def packet_CLOSE(self, data):
162         requestId = data[:4]
163         data = data[4:]
164         handle, data = getNS(data)
165         assert data == '', 'still have data in CLOSE: %s' % repr(data)
166         if handle in self.openFiles:
167             fileObj = self.openFiles[handle]
168             d = defer.maybeDeferred(fileObj.close)
169             d.addCallback(self._cbClose, handle, requestId)
170             d.addErrback(self._ebStatus, requestId, "close failed")
171         elif handle in self.openDirs:
172             dirObj = self.openDirs[handle][0]
173             d = defer.maybeDeferred(dirObj.close)
174             d.addCallback(self._cbClose, handle, requestId, 1)
175             d.addErrback(self._ebStatus, requestId, "close failed")
176         else:
177             self._ebClose(failure.Failure(KeyError()), requestId)
178
179     def _cbClose(self, result, handle, requestId, isDir = 0):
180         if isDir:
181             del self.openDirs[handle]
182         else:
183             del self.openFiles[handle]
184         self._sendStatus(requestId, FX_OK, 'file closed')
185
186     def packet_READ(self, data):
187         requestId = data[:4]
188         data = data[4:]
189         handle, data = getNS(data)
190         (offset, length), data = struct.unpack('!QL', data[:12]), data[12:]
191         assert data == '', 'still have data in READ: %s' % repr(data)
192         if handle not in self.openFiles:
193             self._ebRead(failure.Failure(KeyError()), requestId)
194         else:
195             fileObj = self.openFiles[handle]
196             d = defer.maybeDeferred(fileObj.readChunk, offset, length)
197             d.addCallback(self._cbRead, requestId)
198             d.addErrback(self._ebStatus, requestId, "read failed")
199
200     def _cbRead(self, result, requestId):
201         if result == '': # python's read will return this for EOF
202             raise EOFError()
203         self.sendPacket(FXP_DATA, requestId + NS(result))
204
205     def packet_WRITE(self, data):
206         requestId = data[:4]
207         data = data[4:]
208         handle, data = getNS(data)
209         offset, = struct.unpack('!Q', data[:8])
210         data = data[8:]
211         writeData, data = getNS(data)
212         assert data == '', 'still have data in WRITE: %s' % repr(data)
213         if handle not in self.openFiles:
214             self._ebWrite(failure.Failure(KeyError()), requestId)
215         else:
216             fileObj = self.openFiles[handle]
217             d = defer.maybeDeferred(fileObj.writeChunk, offset, writeData)
218             d.addCallback(self._cbStatus, requestId, "write succeeded")
219             d.addErrback(self._ebStatus, requestId, "write failed")
220
221     def packet_REMOVE(self, data):
222         requestId = data[:4]
223         data = data[4:]
224         filename, data = getNS(data)
225         assert data == '', 'still have data in REMOVE: %s' % repr(data)
226         d = defer.maybeDeferred(self.client.removeFile, filename)
227         d.addCallback(self._cbStatus, requestId, "remove succeeded")
228         d.addErrback(self._ebStatus, requestId, "remove failed")
229
230     def packet_RENAME(self, data):
231         requestId = data[:4]
232         data = data[4:]
233         oldPath, data = getNS(data)
234         newPath, data = getNS(data)
235         assert data == '', 'still have data in RENAME: %s' % repr(data)
236         d = defer.maybeDeferred(self.client.renameFile, oldPath, newPath)
237         d.addCallback(self._cbStatus, requestId, "rename succeeded")
238         d.addErrback(self._ebStatus, requestId, "rename failed")
239
240     def packet_MKDIR(self, data):
241         requestId = data[:4]
242         data = data[4:]
243         path, data = getNS(data)
244         attrs, data = self._parseAttributes(data)
245         assert data == '', 'still have data in MKDIR: %s' % repr(data)
246         d = defer.maybeDeferred(self.client.makeDirectory, path, attrs)
247         d.addCallback(self._cbStatus, requestId, "mkdir succeeded")
248         d.addErrback(self._ebStatus, requestId, "mkdir failed")
249
250     def packet_RMDIR(self, data):
251         requestId = data[:4]
252         data = data[4:]
253         path, data = getNS(data)
254         assert data == '', 'still have data in RMDIR: %s' % repr(data)
255         d = defer.maybeDeferred(self.client.removeDirectory, path)
256         d.addCallback(self._cbStatus, requestId, "rmdir succeeded")
257         d.addErrback(self._ebStatus, requestId, "rmdir failed")
258
259     def packet_OPENDIR(self, data):
260         requestId = data[:4]
261         data = data[4:]
262         path, data = getNS(data)
263         assert data == '', 'still have data in OPENDIR: %s' % repr(data)
264         d = defer.maybeDeferred(self.client.openDirectory, path)
265         d.addCallback(self._cbOpenDirectory, requestId)
266         d.addErrback(self._ebStatus, requestId, "opendir failed")
267
268     def _cbOpenDirectory(self, dirObj, requestId):
269         handle = str(hash(dirObj))
270         if handle in self.openDirs:
271             raise KeyError, "already opened this directory"
272         self.openDirs[handle] = [dirObj, iter(dirObj)]
273         self.sendPacket(FXP_HANDLE, requestId + NS(handle))
274
275     def packet_READDIR(self, data):
276         requestId = data[:4]
277         data = data[4:]
278         handle, data = getNS(data)
279         assert data == '', 'still have data in READDIR: %s' % repr(data)
280         if handle not in self.openDirs:
281             self._ebStatus(failure.Failure(KeyError()), requestId)
282         else:
283             dirObj, dirIter = self.openDirs[handle]
284             d = defer.maybeDeferred(self._scanDirectory, dirIter, [])
285             d.addCallback(self._cbSendDirectory, requestId)
286             d.addErrback(self._ebStatus, requestId, "scan directory failed")
287
288     def _scanDirectory(self, dirIter, f):
289         while len(f) < 250:
290             try:
291                 info = dirIter.next()
292             except StopIteration:
293                 if not f:
294                     raise EOFError
295                 return f
296             if isinstance(info, defer.Deferred):
297                 info.addCallback(self._cbScanDirectory, dirIter, f)
298                 return
299             else:
300                 f.append(info)
301         return f
302
303     def _cbScanDirectory(self, result, dirIter, f):
304         f.append(result)
305         return self._scanDirectory(dirIter, f)
306
307     def _cbSendDirectory(self, result, requestId):
308         data = ''
309         for (filename, longname, attrs) in result:
310             data += NS(filename)
311             data += NS(longname)
312             data += self._packAttributes(attrs)
313         self.sendPacket(FXP_NAME, requestId +
314                         struct.pack('!L', len(result))+data)
315
316     def packet_STAT(self, data, followLinks = 1):
317         requestId = data[:4]
318         data = data[4:]
319         path, data = getNS(data)
320         assert data == '', 'still have data in STAT/LSTAT: %s' % repr(data)
321         d = defer.maybeDeferred(self.client.getAttrs, path, followLinks)
322         d.addCallback(self._cbStat, requestId)
323         d.addErrback(self._ebStatus, requestId, 'stat/lstat failed')
324
325     def packet_LSTAT(self, data):
326         self.packet_STAT(data, 0)
327
328     def packet_FSTAT(self, data):
329         requestId = data[:4]
330         data = data[4:]
331         handle, data = getNS(data)
332         assert data == '', 'still have data in FSTAT: %s' % repr(data)
333         if handle not in self.openFiles:
334             self._ebStatus(failure.Failure(KeyError('%s not in self.openFiles'
335                                         % handle)), requestId)
336         else:
337             fileObj = self.openFiles[handle]
338             d = defer.maybeDeferred(fileObj.getAttrs)
339             d.addCallback(self._cbStat, requestId)
340             d.addErrback(self._ebStatus, requestId, 'fstat failed')
341
342     def _cbStat(self, result, requestId):
343         data = requestId + self._packAttributes(result)
344         self.sendPacket(FXP_ATTRS, data)
345
346     def packet_SETSTAT(self, data):
347         requestId = data[:4]
348         data = data[4:]
349         path, data = getNS(data)
350         attrs, data = self._parseAttributes(data)
351         if data != '':
352             log.msg('WARN: still have data in SETSTAT: %s' % repr(data))
353         d = defer.maybeDeferred(self.client.setAttrs, path, attrs)
354         d.addCallback(self._cbStatus, requestId, 'setstat succeeded')
355         d.addErrback(self._ebStatus, requestId, 'setstat failed')
356
357     def packet_FSETSTAT(self, data):
358         requestId = data[:4]
359         data = data[4:]
360         handle, data = getNS(data)
361         attrs, data = self._parseAttributes(data)
362         assert data == '', 'still have data in FSETSTAT: %s' % repr(data)
363         if handle not in self.openFiles:
364             self._ebStatus(failure.Failure(KeyError()), requestId)
365         else:
366             fileObj = self.openFiles[handle]
367             d = defer.maybeDeferred(fileObj.setAttrs, attrs)
368             d.addCallback(self._cbStatus, requestId, 'fsetstat succeeded')
369             d.addErrback(self._ebStatus, requestId, 'fsetstat failed')
370
371     def packet_READLINK(self, data):
372         requestId = data[:4]
373         data = data[4:]
374         path, data = getNS(data)
375         assert data == '', 'still have data in READLINK: %s' % repr(data)
376         d = defer.maybeDeferred(self.client.readLink, path)
377         d.addCallback(self._cbReadLink, requestId)
378         d.addErrback(self._ebStatus, requestId, 'readlink failed')
379
380     def _cbReadLink(self, result, requestId):
381         self._cbSendDirectory([(result, '', {})], requestId)
382
383     def packet_SYMLINK(self, data):
384         requestId = data[:4]
385         data = data[4:]
386         linkPath, data = getNS(data)
387         targetPath, data = getNS(data)
388         d = defer.maybeDeferred(self.client.makeLink, linkPath, targetPath)
389         d.addCallback(self._cbStatus, requestId, 'symlink succeeded')
390         d.addErrback(self._ebStatus, requestId, 'symlink failed')
391
392     def packet_REALPATH(self, data):
393         requestId = data[:4]
394         data = data[4:]
395         path, data = getNS(data)
396         assert data == '', 'still have data in REALPATH: %s' % repr(data)
397         d = defer.maybeDeferred(self.client.realPath, path)
398         d.addCallback(self._cbReadLink, requestId) # same return format
399         d.addErrback(self._ebStatus, requestId, 'realpath failed')
400
401     def packet_EXTENDED(self, data):
402         requestId = data[:4]
403         data = data[4:]
404         extName, extData = getNS(data)
405         d = defer.maybeDeferred(self.client.extendedRequest, extName, extData)
406         d.addCallback(self._cbExtended, requestId)
407         d.addErrback(self._ebStatus, requestId, 'extended %s failed' % extName)
408
409     def _cbExtended(self, data, requestId):
410         self.sendPacket(FXP_EXTENDED_REPLY, requestId + data)
411
412     def _cbStatus(self, result, requestId, msg = "request succeeded"):
413         self._sendStatus(requestId, FX_OK, msg)
414
415     def _ebStatus(self, reason, requestId, msg = "request failed"):
416         code = FX_FAILURE
417         message = msg
418         if reason.type in (IOError, OSError):
419             if reason.value.errno == errno.ENOENT: # no such file
420                 code = FX_NO_SUCH_FILE
421                 message = reason.value.strerror
422             elif reason.value.errno == errno.EACCES: # permission denied
423                 code = FX_PERMISSION_DENIED
424                 message = reason.value.strerror
425             elif reason.value.errno == errno.EEXIST:
426                 code = FX_FILE_ALREADY_EXISTS
427             else:
428                 log.err(reason)
429         elif reason.type == EOFError: # EOF
430             code = FX_EOF
431             if reason.value.args:
432                 message = reason.value.args[0]
433         elif reason.type == NotImplementedError:
434             code = FX_OP_UNSUPPORTED
435             if reason.value.args:
436                 message = reason.value.args[0]
437         elif reason.type == SFTPError:
438             code = reason.value.code
439             message = reason.value.message
440         else:
441             log.err(reason)
442         self._sendStatus(requestId, code, message)
443
444     def _sendStatus(self, requestId, code, message, lang = ''):
445         """
446         Helper method to send a FXP_STATUS message.
447         """
448         data = requestId + struct.pack('!L', code)
449         data += NS(message)
450         data += NS(lang)
451         self.sendPacket(FXP_STATUS, data)
452
453
454     def connectionLost(self, reason):
455         """
456         Clean all opened files and directories.
457         """
458         for fileObj in self.openFiles.values():
459             fileObj.close()
460         self.openFiles = {}
461         for (dirObj, dirIter) in self.openDirs.values():
462             dirObj.close()
463         self.openDirs = {}
464
465
466
467 class FileTransferClient(FileTransferBase):
468
469     def __init__(self, extData = {}):
470         """
471         @param extData: a dict of extended_name : extended_data items
472         to be sent to the server.
473         """
474         FileTransferBase.__init__(self)
475         self.extData = {}
476         self.counter = 0
477         self.openRequests = {} # id -> Deferred
478         self.wasAFile = {} # Deferred -> 1 TERRIBLE HACK
479
480     def connectionMade(self):
481         data = struct.pack('!L', max(self.versions))
482         for k,v in self.extData.itervalues():
483             data += NS(k) + NS(v)
484         self.sendPacket(FXP_INIT, data)
485
486     def _sendRequest(self, msg, data):
487         data = struct.pack('!L', self.counter) + data
488         d = defer.Deferred()
489         self.openRequests[self.counter] = d
490         self.counter += 1
491         self.sendPacket(msg, data)
492         return d
493
494     def _parseRequest(self, data):
495         (id,) = struct.unpack('!L', data[:4])
496         d = self.openRequests[id]
497         del self.openRequests[id]
498         return d, data[4:]
499
500     def openFile(self, filename, flags, attrs):
501         """
502         Open a file.
503
504         This method returns a L{Deferred} that is called back with an object
505         that provides the L{ISFTPFile} interface.
506
507         @param filename: a string representing the file to open.
508
509         @param flags: a integer of the flags to open the file with, ORed together.
510         The flags and their values are listed at the bottom of this file.
511
512         @param attrs: a list of attributes to open the file with.  It is a
513         dictionary, consisting of 0 or more keys.  The possible keys are::
514
515             size: the size of the file in bytes
516             uid: the user ID of the file as an integer
517             gid: the group ID of the file as an integer
518             permissions: the permissions of the file with as an integer.
519             the bit representation of this field is defined by POSIX.
520             atime: the access time of the file as seconds since the epoch.
521             mtime: the modification time of the file as seconds since the epoch.
522             ext_*: extended attributes.  The server is not required to
523             understand this, but it may.
524
525         NOTE: there is no way to indicate text or binary files.  it is up
526         to the SFTP client to deal with this.
527         """
528         data = NS(filename) + struct.pack('!L', flags) + self._packAttributes(attrs)
529         d = self._sendRequest(FXP_OPEN, data)
530         self.wasAFile[d] = (1, filename) # HACK
531         return d
532
533     def removeFile(self, filename):
534         """
535         Remove the given file.
536
537         This method returns a Deferred that is called back when it succeeds.
538
539         @param filename: the name of the file as a string.
540         """
541         return self._sendRequest(FXP_REMOVE, NS(filename))
542
543     def renameFile(self, oldpath, newpath):
544         """
545         Rename the given file.
546
547         This method returns a Deferred that is called back when it succeeds.
548
549         @param oldpath: the current location of the file.
550         @param newpath: the new file name.
551         """
552         return self._sendRequest(FXP_RENAME, NS(oldpath)+NS(newpath))
553
554     def makeDirectory(self, path, attrs):
555         """
556         Make a directory.
557
558         This method returns a Deferred that is called back when it is
559         created.
560
561         @param path: the name of the directory to create as a string.
562
563         @param attrs: a dictionary of attributes to create the directory
564         with.  Its meaning is the same as the attrs in the openFile method.
565         """
566         return self._sendRequest(FXP_MKDIR, NS(path)+self._packAttributes(attrs))
567
568     def removeDirectory(self, path):
569         """
570         Remove a directory (non-recursively)
571
572         It is an error to remove a directory that has files or directories in
573         it.
574
575         This method returns a Deferred that is called back when it is removed.
576
577         @param path: the directory to remove.
578         """
579         return self._sendRequest(FXP_RMDIR, NS(path))
580
581     def openDirectory(self, path):
582         """
583         Open a directory for scanning.
584
585         This method returns a Deferred that is called back with an iterable
586         object that has a close() method.
587
588         The close() method is called when the client is finished reading
589         from the directory.  At this point, the iterable will no longer
590         be used.
591
592         The iterable returns triples of the form (filename, longname, attrs)
593         or a Deferred that returns the same.  The sequence must support
594         __getitem__, but otherwise may be any 'sequence-like' object.
595
596         filename is the name of the file relative to the directory.
597         logname is an expanded format of the filename.  The recommended format
598         is:
599         -rwxr-xr-x   1 mjos     staff      348911 Mar 25 14:29 t-filexfer
600         1234567890 123 12345678 12345678 12345678 123456789012
601
602         The first line is sample output, the second is the length of the field.
603         The fields are: permissions, link count, user owner, group owner,
604         size in bytes, modification time.
605
606         attrs is a dictionary in the format of the attrs argument to openFile.
607
608         @param path: the directory to open.
609         """
610         d = self._sendRequest(FXP_OPENDIR, NS(path))
611         self.wasAFile[d] = (0, path)
612         return d
613
614     def getAttrs(self, path, followLinks=0):
615         """
616         Return the attributes for the given path.
617
618         This method returns a dictionary in the same format as the attrs
619         argument to openFile or a Deferred that is called back with same.
620
621         @param path: the path to return attributes for as a string.
622         @param followLinks: a boolean.  if it is True, follow symbolic links
623         and return attributes for the real path at the base.  if it is False,
624         return attributes for the specified path.
625         """
626         if followLinks: m = FXP_STAT
627         else: m = FXP_LSTAT
628         return self._sendRequest(m, NS(path))
629
630     def setAttrs(self, path, attrs):
631         """
632         Set the attributes for the path.
633
634         This method returns when the attributes are set or a Deferred that is
635         called back when they are.
636
637         @param path: the path to set attributes for as a string.
638         @param attrs: a dictionary in the same format as the attrs argument to
639         openFile.
640         """
641         data = NS(path) + self._packAttributes(attrs)
642         return self._sendRequest(FXP_SETSTAT, data)
643
644     def readLink(self, path):
645         """
646         Find the root of a set of symbolic links.
647
648         This method returns the target of the link, or a Deferred that
649         returns the same.
650
651         @param path: the path of the symlink to read.
652         """
653         d = self._sendRequest(FXP_READLINK, NS(path))
654         return d.addCallback(self._cbRealPath)
655
656     def makeLink(self, linkPath, targetPath):
657         """
658         Create a symbolic link.
659
660         This method returns when the link is made, or a Deferred that
661         returns the same.
662
663         @param linkPath: the pathname of the symlink as a string
664         @param targetPath: the path of the target of the link as a string.
665         """
666         return self._sendRequest(FXP_SYMLINK, NS(linkPath)+NS(targetPath))
667
668     def realPath(self, path):
669         """
670         Convert any path to an absolute path.
671
672         This method returns the absolute path as a string, or a Deferred
673         that returns the same.
674
675         @param path: the path to convert as a string.
676         """
677         d = self._sendRequest(FXP_REALPATH, NS(path))
678         return d.addCallback(self._cbRealPath)
679
680     def _cbRealPath(self, result):
681         name, longname, attrs = result[0]
682         return name
683
684     def extendedRequest(self, request, data):
685         """
686         Make an extended request of the server.
687
688         The method returns a Deferred that is called back with
689         the result of the extended request.
690
691         @param request: the name of the extended request to make.
692         @param data: any other data that goes along with the request.
693         """
694         return self._sendRequest(FXP_EXTENDED, NS(request) + data)
695
696     def packet_VERSION(self, data):
697         version, = struct.unpack('!L', data[:4])
698         data = data[4:]
699         d = {}
700         while data:
701             k, data = getNS(data)
702             v, data = getNS(data)
703             d[k]=v
704         self.version = version
705         self.gotServerVersion(version, d)
706
707     def packet_STATUS(self, data):
708         d, data = self._parseRequest(data)
709         code, = struct.unpack('!L', data[:4])
710         data = data[4:]
711         msg, data = getNS(data)
712         lang = getNS(data)
713         if code == FX_OK:
714             d.callback((msg, lang))
715         elif code == FX_EOF:
716             d.errback(EOFError(msg))
717         elif code == FX_OP_UNSUPPORTED:
718             d.errback(NotImplementedError(msg))
719         else:
720             d.errback(SFTPError(code, msg, lang))
721
722     def packet_HANDLE(self, data):
723         d, data = self._parseRequest(data)
724         isFile, name = self.wasAFile.pop(d)
725         if isFile:
726             cb = ClientFile(self, getNS(data)[0])
727         else:
728             cb = ClientDirectory(self, getNS(data)[0])
729         cb.name = name
730         d.callback(cb)
731
732     def packet_DATA(self, data):
733         d, data = self._parseRequest(data)
734         d.callback(getNS(data)[0])
735
736     def packet_NAME(self, data):
737         d, data = self._parseRequest(data)
738         count, = struct.unpack('!L', data[:4])
739         data = data[4:]
740         files = []
741         for i in range(count):
742             filename, data = getNS(data)
743             longname, data = getNS(data)
744             attrs, data = self._parseAttributes(data)
745             files.append((filename, longname, attrs))
746         d.callback(files)
747
748     def packet_ATTRS(self, data):
749         d, data = self._parseRequest(data)
750         d.callback(self._parseAttributes(data)[0])
751
752     def packet_EXTENDED_REPLY(self, data):
753         d, data = self._parseRequest(data)
754         d.callback(data)
755
756     def gotServerVersion(self, serverVersion, extData):
757         """
758         Called when the client sends their version info.
759
760         @param otherVersion: an integer representing the version of the SFTP
761         protocol they are claiming.
762         @param extData: a dictionary of extended_name : extended_data items.
763         These items are sent by the client to indicate additional features.
764         """
765
766 class ClientFile:
767
768     interface.implements(ISFTPFile)
769
770     def __init__(self, parent, handle):
771         self.parent = parent
772         self.handle = NS(handle)
773
774     def close(self):
775         return self.parent._sendRequest(FXP_CLOSE, self.handle)
776
777     def readChunk(self, offset, length):
778         data = self.handle + struct.pack("!QL", offset, length)
779         return self.parent._sendRequest(FXP_READ, data)
780
781     def writeChunk(self, offset, chunk):
782         data = self.handle + struct.pack("!Q", offset) + NS(chunk)
783         return self.parent._sendRequest(FXP_WRITE, data)
784
785     def getAttrs(self):
786         return self.parent._sendRequest(FXP_FSTAT, self.handle)
787
788     def setAttrs(self, attrs):
789         data = self.handle + self.parent._packAttributes(attrs)
790         return self.parent._sendRequest(FXP_FSTAT, data)
791
792 class ClientDirectory:
793
794     def __init__(self, parent, handle):
795         self.parent = parent
796         self.handle = NS(handle)
797         self.filesCache = []
798
799     def read(self):
800         d = self.parent._sendRequest(FXP_READDIR, self.handle)
801         return d
802
803     def close(self):
804         return self.parent._sendRequest(FXP_CLOSE, self.handle)
805
806     def __iter__(self):
807         return self
808
809     def next(self):
810         if self.filesCache:
811             return self.filesCache.pop(0)
812         d = self.read()
813         d.addCallback(self._cbReadDir)
814         d.addErrback(self._ebReadDir)
815         return d
816
817     def _cbReadDir(self, names):
818         self.filesCache = names[1:]
819         return names[0]
820
821     def _ebReadDir(self, reason):
822         reason.trap(EOFError)
823         def _():
824             raise StopIteration
825         self.next = _
826         return reason
827
828
829 class SFTPError(Exception):
830
831     def __init__(self, errorCode, errorMessage, lang = ''):
832         Exception.__init__(self)
833         self.code = errorCode
834         self._message = errorMessage
835         self.lang = lang
836
837
838     def message(self):
839         """
840         A string received over the network that explains the error to a human.
841         """
842         # Python 2.6 deprecates assigning to the 'message' attribute of an
843         # exception. We define this read-only property here in order to
844         # prevent the warning about deprecation while maintaining backwards
845         # compatibility with object clients that rely on the 'message'
846         # attribute being set correctly. See bug #3897.
847         return self._message
848     message = property(message)
849
850
851     def __str__(self):
852         return 'SFTPError %s: %s' % (self.code, self.message)
853
854 FXP_INIT            =   1
855 FXP_VERSION         =   2
856 FXP_OPEN            =   3
857 FXP_CLOSE           =   4
858 FXP_READ            =   5
859 FXP_WRITE           =   6
860 FXP_LSTAT           =   7
861 FXP_FSTAT           =   8
862 FXP_SETSTAT         =   9
863 FXP_FSETSTAT        =  10
864 FXP_OPENDIR         =  11
865 FXP_READDIR         =  12
866 FXP_REMOVE          =  13
867 FXP_MKDIR           =  14
868 FXP_RMDIR           =  15
869 FXP_REALPATH        =  16
870 FXP_STAT            =  17
871 FXP_RENAME          =  18
872 FXP_READLINK        =  19
873 FXP_SYMLINK         =  20
874 FXP_STATUS          = 101
875 FXP_HANDLE          = 102
876 FXP_DATA            = 103
877 FXP_NAME            = 104
878 FXP_ATTRS           = 105
879 FXP_EXTENDED        = 200
880 FXP_EXTENDED_REPLY  = 201
881
882 FILEXFER_ATTR_SIZE        = 0x00000001
883 FILEXFER_ATTR_UIDGID      = 0x00000002
884 FILEXFER_ATTR_OWNERGROUP  = FILEXFER_ATTR_UIDGID
885 FILEXFER_ATTR_PERMISSIONS = 0x00000004
886 FILEXFER_ATTR_ACMODTIME   = 0x00000008
887 FILEXFER_ATTR_EXTENDED    = 0x80000000L
888
889 FILEXFER_TYPE_REGULAR        = 1
890 FILEXFER_TYPE_DIRECTORY      = 2
891 FILEXFER_TYPE_SYMLINK        = 3
892 FILEXFER_TYPE_SPECIAL        = 4
893 FILEXFER_TYPE_UNKNOWN        = 5
894
895 FXF_READ          = 0x00000001
896 FXF_WRITE         = 0x00000002
897 FXF_APPEND        = 0x00000004
898 FXF_CREAT         = 0x00000008
899 FXF_TRUNC         = 0x00000010
900 FXF_EXCL          = 0x00000020
901 FXF_TEXT          = 0x00000040
902
903 FX_OK                          = 0
904 FX_EOF                         = 1
905 FX_NO_SUCH_FILE                = 2
906 FX_PERMISSION_DENIED           = 3
907 FX_FAILURE                     = 4
908 FX_BAD_MESSAGE                 = 5
909 FX_NO_CONNECTION               = 6
910 FX_CONNECTION_LOST             = 7
911 FX_OP_UNSUPPORTED              = 8
912 FX_FILE_ALREADY_EXISTS         = 11
913 # http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/ defines more
914 # useful error codes, but so far OpenSSH doesn't implement them.  We use them
915 # internally for clarity, but for now define them all as FX_FAILURE to be
916 # compatible with existing software.
917 FX_NOT_A_DIRECTORY             = FX_FAILURE
918 FX_FILE_IS_A_DIRECTORY         = FX_FAILURE
919
920
921 # initialize FileTransferBase.packetTypes:
922 g = globals()
923 for name in g.keys():
924     if name.startswith('FXP_'):
925         value = g[name]
926         FileTransferBase.packetTypes[value] = name[4:]
927 del g, name, value
Note: See TracBrowser for help on using the browser.