Initial import to Tizen
[profile/ivi/python-twisted.git] / twisted / conch / ssh / filetransfer.py
1 # -*- test-case-name: twisted.conch.test.test_filetransfer -*-
2 #
3 # Copyright (c) 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         if len(data) >= 4:
712             msg, data = getNS(data)
713             if len(data) >= 4:
714                 lang, data = getNS(data)
715             else:
716                 lang = ''
717         else:
718             msg = ''
719             lang = ''
720         if code == FX_OK:
721             d.callback((msg, lang))
722         elif code == FX_EOF:
723             d.errback(EOFError(msg))
724         elif code == FX_OP_UNSUPPORTED:
725             d.errback(NotImplementedError(msg))
726         else:
727             d.errback(SFTPError(code, msg, lang))
728
729     def packet_HANDLE(self, data):
730         d, data = self._parseRequest(data)
731         isFile, name = self.wasAFile.pop(d)
732         if isFile:
733             cb = ClientFile(self, getNS(data)[0])
734         else:
735             cb = ClientDirectory(self, getNS(data)[0])
736         cb.name = name
737         d.callback(cb)
738
739     def packet_DATA(self, data):
740         d, data = self._parseRequest(data)
741         d.callback(getNS(data)[0])
742
743     def packet_NAME(self, data):
744         d, data = self._parseRequest(data)
745         count, = struct.unpack('!L', data[:4])
746         data = data[4:]
747         files = []
748         for i in range(count):
749             filename, data = getNS(data)
750             longname, data = getNS(data)
751             attrs, data = self._parseAttributes(data)
752             files.append((filename, longname, attrs))
753         d.callback(files)
754
755     def packet_ATTRS(self, data):
756         d, data = self._parseRequest(data)
757         d.callback(self._parseAttributes(data)[0])
758
759     def packet_EXTENDED_REPLY(self, data):
760         d, data = self._parseRequest(data)
761         d.callback(data)
762
763     def gotServerVersion(self, serverVersion, extData):
764         """
765         Called when the client sends their version info.
766
767         @param otherVersion: an integer representing the version of the SFTP
768         protocol they are claiming.
769         @param extData: a dictionary of extended_name : extended_data items.
770         These items are sent by the client to indicate additional features.
771         """
772
773 class ClientFile:
774
775     interface.implements(ISFTPFile)
776
777     def __init__(self, parent, handle):
778         self.parent = parent
779         self.handle = NS(handle)
780
781     def close(self):
782         return self.parent._sendRequest(FXP_CLOSE, self.handle)
783
784     def readChunk(self, offset, length):
785         data = self.handle + struct.pack("!QL", offset, length)
786         return self.parent._sendRequest(FXP_READ, data)
787
788     def writeChunk(self, offset, chunk):
789         data = self.handle + struct.pack("!Q", offset) + NS(chunk)
790         return self.parent._sendRequest(FXP_WRITE, data)
791
792     def getAttrs(self):
793         return self.parent._sendRequest(FXP_FSTAT, self.handle)
794
795     def setAttrs(self, attrs):
796         data = self.handle + self.parent._packAttributes(attrs)
797         return self.parent._sendRequest(FXP_FSTAT, data)
798
799 class ClientDirectory:
800
801     def __init__(self, parent, handle):
802         self.parent = parent
803         self.handle = NS(handle)
804         self.filesCache = []
805
806     def read(self):
807         d = self.parent._sendRequest(FXP_READDIR, self.handle)
808         return d
809
810     def close(self):
811         return self.parent._sendRequest(FXP_CLOSE, self.handle)
812
813     def __iter__(self):
814         return self
815
816     def next(self):
817         if self.filesCache:
818             return self.filesCache.pop(0)
819         d = self.read()
820         d.addCallback(self._cbReadDir)
821         d.addErrback(self._ebReadDir)
822         return d
823
824     def _cbReadDir(self, names):
825         self.filesCache = names[1:]
826         return names[0]
827
828     def _ebReadDir(self, reason):
829         reason.trap(EOFError)
830         def _():
831             raise StopIteration
832         self.next = _
833         return reason
834
835
836 class SFTPError(Exception):
837
838     def __init__(self, errorCode, errorMessage, lang = ''):
839         Exception.__init__(self)
840         self.code = errorCode
841         self._message = errorMessage
842         self.lang = lang
843
844
845     def message(self):
846         """
847         A string received over the network that explains the error to a human.
848         """
849         # Python 2.6 deprecates assigning to the 'message' attribute of an
850         # exception. We define this read-only property here in order to
851         # prevent the warning about deprecation while maintaining backwards
852         # compatibility with object clients that rely on the 'message'
853         # attribute being set correctly. See bug #3897.
854         return self._message
855     message = property(message)
856
857
858     def __str__(self):
859         return 'SFTPError %s: %s' % (self.code, self.message)
860
861 FXP_INIT            =   1
862 FXP_VERSION         =   2
863 FXP_OPEN            =   3
864 FXP_CLOSE           =   4
865 FXP_READ            =   5
866 FXP_WRITE           =   6
867 FXP_LSTAT           =   7
868 FXP_FSTAT           =   8
869 FXP_SETSTAT         =   9
870 FXP_FSETSTAT        =  10
871 FXP_OPENDIR         =  11
872 FXP_READDIR         =  12
873 FXP_REMOVE          =  13
874 FXP_MKDIR           =  14
875 FXP_RMDIR           =  15
876 FXP_REALPATH        =  16
877 FXP_STAT            =  17
878 FXP_RENAME          =  18
879 FXP_READLINK        =  19
880 FXP_SYMLINK         =  20
881 FXP_STATUS          = 101
882 FXP_HANDLE          = 102
883 FXP_DATA            = 103
884 FXP_NAME            = 104
885 FXP_ATTRS           = 105
886 FXP_EXTENDED        = 200
887 FXP_EXTENDED_REPLY  = 201
888
889 FILEXFER_ATTR_SIZE        = 0x00000001
890 FILEXFER_ATTR_UIDGID      = 0x00000002
891 FILEXFER_ATTR_OWNERGROUP  = FILEXFER_ATTR_UIDGID
892 FILEXFER_ATTR_PERMISSIONS = 0x00000004
893 FILEXFER_ATTR_ACMODTIME   = 0x00000008
894 FILEXFER_ATTR_EXTENDED    = 0x80000000L
895
896 FILEXFER_TYPE_REGULAR        = 1
897 FILEXFER_TYPE_DIRECTORY      = 2
898 FILEXFER_TYPE_SYMLINK        = 3
899 FILEXFER_TYPE_SPECIAL        = 4
900 FILEXFER_TYPE_UNKNOWN        = 5
901
902 FXF_READ          = 0x00000001
903 FXF_WRITE         = 0x00000002
904 FXF_APPEND        = 0x00000004
905 FXF_CREAT         = 0x00000008
906 FXF_TRUNC         = 0x00000010
907 FXF_EXCL          = 0x00000020
908 FXF_TEXT          = 0x00000040
909
910 FX_OK                          = 0
911 FX_EOF                         = 1
912 FX_NO_SUCH_FILE                = 2
913 FX_PERMISSION_DENIED           = 3
914 FX_FAILURE                     = 4
915 FX_BAD_MESSAGE                 = 5
916 FX_NO_CONNECTION               = 6
917 FX_CONNECTION_LOST             = 7
918 FX_OP_UNSUPPORTED              = 8
919 FX_FILE_ALREADY_EXISTS         = 11
920 # http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/ defines more
921 # useful error codes, but so far OpenSSH doesn't implement them.  We use them
922 # internally for clarity, but for now define them all as FX_FAILURE to be
923 # compatible with existing software.
924 FX_NOT_A_DIRECTORY             = FX_FAILURE
925 FX_FILE_IS_A_DIRECTORY         = FX_FAILURE
926
927
928 # initialize FileTransferBase.packetTypes:
929 g = globals()
930 for name in g.keys():
931     if name.startswith('FXP_'):
932         value = g[name]
933         FileTransferBase.packetTypes[value] = name[4:]
934 del g, name, value