OLD | NEW |
| (Empty) |
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 else: | |
426 log.err(reason) | |
427 elif reason.type == EOFError: # EOF | |
428 code = FX_EOF | |
429 if reason.value.args: | |
430 message = reason.value.args[0] | |
431 elif reason.type == NotImplementedError: | |
432 code = FX_OP_UNSUPPORTED | |
433 if reason.value.args: | |
434 message = reason.value.args[0] | |
435 elif reason.type == SFTPError: | |
436 code = reason.value.code | |
437 message = reason.value.message | |
438 else: | |
439 log.err(reason) | |
440 self._sendStatus(requestId, code, message) | |
441 | |
442 def _sendStatus(self, requestId, code, message, lang = ''): | |
443 """ | |
444 Helper method to send a FXP_STATUS message. | |
445 """ | |
446 data = requestId + struct.pack('!L', code) | |
447 data += NS(message) | |
448 data += NS(lang) | |
449 self.sendPacket(FXP_STATUS, data) | |
450 | |
451 class FileTransferClient(FileTransferBase): | |
452 | |
453 def __init__(self, extData = {}): | |
454 """ | |
455 @param extData: a dict of extended_name : extended_data items | |
456 to be sent to the server. | |
457 """ | |
458 FileTransferBase.__init__(self) | |
459 self.extData = {} | |
460 self.counter = 0 | |
461 self.openRequests = {} # id -> Deferred | |
462 self.wasAFile = {} # Deferred -> 1 TERRIBLE HACK | |
463 | |
464 def connectionMade(self): | |
465 data = struct.pack('!L', max(self.versions)) | |
466 for k,v in self.extData.itervalues(): | |
467 data += NS(k) + NS(v) | |
468 self.sendPacket(FXP_INIT, data) | |
469 | |
470 def _sendRequest(self, msg, data): | |
471 data = struct.pack('!L', self.counter) + data | |
472 d = defer.Deferred() | |
473 self.openRequests[self.counter] = d | |
474 self.counter += 1 | |
475 self.sendPacket(msg, data) | |
476 return d | |
477 | |
478 def _parseRequest(self, data): | |
479 (id,) = struct.unpack('!L', data[:4]) | |
480 d = self.openRequests[id] | |
481 del self.openRequests[id] | |
482 return d, data[4:] | |
483 | |
484 def openFile(self, filename, flags, attrs): | |
485 """ | |
486 Open a file. | |
487 | |
488 This method returns a L{Deferred} that is called back with an object | |
489 that provides the L{ISFTPFile} interface. | |
490 | |
491 @param filename: a string representing the file to open. | |
492 | |
493 @param flags: a integer of the flags to open the file with, ORed togethe
r. | |
494 The flags and their values are listed at the bottom of this file. | |
495 | |
496 @param attrs: a list of attributes to open the file with. It is a | |
497 dictionary, consisting of 0 or more keys. The possible keys are:: | |
498 | |
499 size: the size of the file in bytes | |
500 uid: the user ID of the file as an integer | |
501 gid: the group ID of the file as an integer | |
502 permissions: the permissions of the file with as an integer. | |
503 the bit representation of this field is defined by POSIX. | |
504 atime: the access time of the file as seconds since the epoch. | |
505 mtime: the modification time of the file as seconds since the epoch. | |
506 ext_*: extended attributes. The server is not required to | |
507 understand this, but it may. | |
508 | |
509 NOTE: there is no way to indicate text or binary files. it is up | |
510 to the SFTP client to deal with this. | |
511 """ | |
512 data = NS(filename) + struct.pack('!L', flags) + self._packAttributes(at
trs) | |
513 d = self._sendRequest(FXP_OPEN, data) | |
514 self.wasAFile[d] = (1, filename) # HACK | |
515 return d | |
516 | |
517 def removeFile(self, filename): | |
518 """ | |
519 Remove the given file. | |
520 | |
521 This method returns a Deferred that is called back when it succeeds. | |
522 | |
523 @param filename: the name of the file as a string. | |
524 """ | |
525 return self._sendRequest(FXP_REMOVE, NS(filename)) | |
526 | |
527 def renameFile(self, oldpath, newpath): | |
528 """ | |
529 Rename the given file. | |
530 | |
531 This method returns a Deferred that is called back when it succeeds. | |
532 | |
533 @param oldpath: the current location of the file. | |
534 @param newpath: the new file name. | |
535 """ | |
536 return self._sendRequest(FXP_RENAME, NS(oldpath)+NS(newpath)) | |
537 | |
538 def makeDirectory(self, path, attrs): | |
539 """ | |
540 Make a directory. | |
541 | |
542 This method returns a Deferred that is called back when it is | |
543 created. | |
544 | |
545 @param path: the name of the directory to create as a string. | |
546 | |
547 @param attrs: a dictionary of attributes to create the directory | |
548 with. Its meaning is the same as the attrs in the openFile method. | |
549 """ | |
550 return self._sendRequest(FXP_MKDIR, NS(path)+self._packAttributes(attrs)
) | |
551 | |
552 def removeDirectory(self, path): | |
553 """ | |
554 Remove a directory (non-recursively) | |
555 | |
556 It is an error to remove a directory that has files or directories in | |
557 it. | |
558 | |
559 This method returns a Deferred that is called back when it is removed. | |
560 | |
561 @param path: the directory to remove. | |
562 """ | |
563 return self._sendRequest(FXP_RMDIR, NS(path)) | |
564 | |
565 def openDirectory(self, path): | |
566 """ | |
567 Open a directory for scanning. | |
568 | |
569 This method returns a Deferred that is called back with an iterable | |
570 object that has a close() method. | |
571 | |
572 The close() method is called when the client is finished reading | |
573 from the directory. At this point, the iterable will no longer | |
574 be used. | |
575 | |
576 The iterable returns triples of the form (filename, longname, attrs) | |
577 or a Deferred that returns the same. The sequence must support | |
578 __getitem__, but otherwise may be any 'sequence-like' object. | |
579 | |
580 filename is the name of the file relative to the directory. | |
581 logname is an expanded format of the filename. The recommended format | |
582 is: | |
583 -rwxr-xr-x 1 mjos staff 348911 Mar 25 14:29 t-filexfer | |
584 1234567890 123 12345678 12345678 12345678 123456789012 | |
585 | |
586 The first line is sample output, the second is the length of the field. | |
587 The fields are: permissions, link count, user owner, group owner, | |
588 size in bytes, modification time. | |
589 | |
590 attrs is a dictionary in the format of the attrs argument to openFile. | |
591 | |
592 @param path: the directory to open. | |
593 """ | |
594 d = self._sendRequest(FXP_OPENDIR, NS(path)) | |
595 self.wasAFile[d] = (0, path) | |
596 return d | |
597 | |
598 def getAttrs(self, path, followLinks=0): | |
599 """ | |
600 Return the attributes for the given path. | |
601 | |
602 This method returns a dictionary in the same format as the attrs | |
603 argument to openFile or a Deferred that is called back with same. | |
604 | |
605 @param path: the path to return attributes for as a string. | |
606 @param followLinks: a boolean. if it is True, follow symbolic links | |
607 and return attributes for the real path at the base. if it is False, | |
608 return attributes for the specified path. | |
609 """ | |
610 if followLinks: m = FXP_STAT | |
611 else: m = FXP_LSTAT | |
612 return self._sendRequest(m, NS(path)) | |
613 | |
614 def setAttrs(self, path, attrs): | |
615 """ | |
616 Set the attributes for the path. | |
617 | |
618 This method returns when the attributes are set or a Deferred that is | |
619 called back when they are. | |
620 | |
621 @param path: the path to set attributes for as a string. | |
622 @param attrs: a dictionary in the same format as the attrs argument to | |
623 openFile. | |
624 """ | |
625 data = NS(path) + self._packAttributes(attrs) | |
626 return self._sendRequest(FXP_SETSTAT, data) | |
627 | |
628 def readLink(self, path): | |
629 """ | |
630 Find the root of a set of symbolic links. | |
631 | |
632 This method returns the target of the link, or a Deferred that | |
633 returns the same. | |
634 | |
635 @param path: the path of the symlink to read. | |
636 """ | |
637 d = self._sendRequest(FXP_READLINK, NS(path)) | |
638 return d.addCallback(self._cbRealPath) | |
639 | |
640 def makeLink(self, linkPath, targetPath): | |
641 """ | |
642 Create a symbolic link. | |
643 | |
644 This method returns when the link is made, or a Deferred that | |
645 returns the same. | |
646 | |
647 @param linkPath: the pathname of the symlink as a string | |
648 @param targetPath: the path of the target of the link as a string. | |
649 """ | |
650 return self._sendRequest(FXP_SYMLINK, NS(linkPath)+NS(targetPath)) | |
651 | |
652 def realPath(self, path): | |
653 """ | |
654 Convert any path to an absolute path. | |
655 | |
656 This method returns the absolute path as a string, or a Deferred | |
657 that returns the same. | |
658 | |
659 @param path: the path to convert as a string. | |
660 """ | |
661 d = self._sendRequest(FXP_REALPATH, NS(path)) | |
662 return d.addCallback(self._cbRealPath) | |
663 | |
664 def _cbRealPath(self, result): | |
665 name, longname, attrs = result[0] | |
666 return name | |
667 | |
668 def extendedRequest(self, request, data): | |
669 """ | |
670 Make an extended request of the server. | |
671 | |
672 The method returns a Deferred that is called back with | |
673 the result of the extended request. | |
674 | |
675 @param request: the name of the extended request to make. | |
676 @param data: any other data that goes along with the request. | |
677 """ | |
678 return self._sendRequest(FXP_EXTENDED, NS(request) + data) | |
679 | |
680 def packet_VERSION(self, data): | |
681 version, = struct.unpack('!L', data[:4]) | |
682 data = data[4:] | |
683 d = {} | |
684 while data: | |
685 k, data = getNS(data) | |
686 v, data = getNS(data) | |
687 d[k]=v | |
688 self.version = version | |
689 self.gotServerVersion(version, d) | |
690 | |
691 def packet_STATUS(self, data): | |
692 d, data = self._parseRequest(data) | |
693 code, = struct.unpack('!L', data[:4]) | |
694 data = data[4:] | |
695 msg, data = getNS(data) | |
696 lang = getNS(data) | |
697 if code == FX_OK: | |
698 d.callback((msg, lang)) | |
699 elif code == FX_EOF: | |
700 d.errback(EOFError(msg)) | |
701 elif code == FX_OP_UNSUPPORTED: | |
702 d.errback(NotImplementedError(msg)) | |
703 else: | |
704 d.errback(SFTPError(code, msg, lang)) | |
705 | |
706 def packet_HANDLE(self, data): | |
707 d, data = self._parseRequest(data) | |
708 isFile, name = self.wasAFile.pop(d) | |
709 if isFile: | |
710 cb = ClientFile(self, getNS(data)[0]) | |
711 else: | |
712 cb = ClientDirectory(self, getNS(data)[0]) | |
713 cb.name = name | |
714 d.callback(cb) | |
715 | |
716 def packet_DATA(self, data): | |
717 d, data = self._parseRequest(data) | |
718 d.callback(getNS(data)[0]) | |
719 | |
720 def packet_NAME(self, data): | |
721 d, data = self._parseRequest(data) | |
722 count, = struct.unpack('!L', data[:4]) | |
723 data = data[4:] | |
724 files = [] | |
725 for i in range(count): | |
726 filename, data = getNS(data) | |
727 longname, data = getNS(data) | |
728 attrs, data = self._parseAttributes(data) | |
729 files.append((filename, longname, attrs)) | |
730 d.callback(files) | |
731 | |
732 def packet_ATTRS(self, data): | |
733 d, data = self._parseRequest(data) | |
734 d.callback(self._parseAttributes(data)[0]) | |
735 | |
736 def packet_EXTENDED_REPLY(self, data): | |
737 d, data = self._parseRequest(data) | |
738 d.callback(data) | |
739 | |
740 def gotServerVersion(self, serverVersion, extData): | |
741 """ | |
742 Called when the client sends their version info. | |
743 | |
744 @param otherVersion: an integer representing the version of the SFTP | |
745 protocol they are claiming. | |
746 @param extData: a dictionary of extended_name : extended_data items. | |
747 These items are sent by the client to indicate additional features. | |
748 """ | |
749 | |
750 class ClientFile: | |
751 | |
752 interface.implements(ISFTPFile) | |
753 | |
754 def __init__(self, parent, handle): | |
755 self.parent = parent | |
756 self.handle = NS(handle) | |
757 | |
758 def close(self): | |
759 return self.parent._sendRequest(FXP_CLOSE, self.handle) | |
760 | |
761 def readChunk(self, offset, length): | |
762 data = self.handle + struct.pack("!QL", offset, length) | |
763 return self.parent._sendRequest(FXP_READ, data) | |
764 | |
765 def writeChunk(self, offset, chunk): | |
766 data = self.handle + struct.pack("!Q", offset) + NS(chunk) | |
767 return self.parent._sendRequest(FXP_WRITE, data) | |
768 | |
769 def getAttrs(self): | |
770 return self.parent._sendRequest(FXP_FSTAT, self.handle) | |
771 | |
772 def setAttrs(self, attrs): | |
773 data = self.handle + self.parent._packAttributes(attrs) | |
774 return self.parent._sendRequest(FXP_FSTAT, data) | |
775 | |
776 class ClientDirectory: | |
777 | |
778 def __init__(self, parent, handle): | |
779 self.parent = parent | |
780 self.handle = NS(handle) | |
781 self.filesCache = [] | |
782 | |
783 def read(self): | |
784 d = self.parent._sendRequest(FXP_READDIR, self.handle) | |
785 return d | |
786 | |
787 def close(self): | |
788 return self.parent._sendRequest(FXP_CLOSE, self.handle) | |
789 | |
790 def __iter__(self): | |
791 return self | |
792 | |
793 def next(self): | |
794 if self.filesCache: | |
795 return self.filesCache.pop(0) | |
796 d = self.read() | |
797 d.addCallback(self._cbReadDir) | |
798 d.addErrback(self._ebReadDir) | |
799 return d | |
800 | |
801 def _cbReadDir(self, names): | |
802 self.filesCache = names[1:] | |
803 return names[0] | |
804 | |
805 def _ebReadDir(self, reason): | |
806 reason.trap(EOFError) | |
807 def _(): | |
808 raise StopIteration | |
809 self.next = _ | |
810 return reason | |
811 | |
812 | |
813 class SFTPError(Exception): | |
814 | |
815 def __init__(self, errorCode, errorMessage, lang = ''): | |
816 Exception.__init__(self) | |
817 self.code = errorCode | |
818 self.message = errorMessage | |
819 self.lang = lang | |
820 | |
821 def __str__(self): | |
822 return 'SFTPError %s: %s' % (self.code, self.message) | |
823 | |
824 FXP_INIT = 1 | |
825 FXP_VERSION = 2 | |
826 FXP_OPEN = 3 | |
827 FXP_CLOSE = 4 | |
828 FXP_READ = 5 | |
829 FXP_WRITE = 6 | |
830 FXP_LSTAT = 7 | |
831 FXP_FSTAT = 8 | |
832 FXP_SETSTAT = 9 | |
833 FXP_FSETSTAT = 10 | |
834 FXP_OPENDIR = 11 | |
835 FXP_READDIR = 12 | |
836 FXP_REMOVE = 13 | |
837 FXP_MKDIR = 14 | |
838 FXP_RMDIR = 15 | |
839 FXP_REALPATH = 16 | |
840 FXP_STAT = 17 | |
841 FXP_RENAME = 18 | |
842 FXP_READLINK = 19 | |
843 FXP_SYMLINK = 20 | |
844 FXP_STATUS = 101 | |
845 FXP_HANDLE = 102 | |
846 FXP_DATA = 103 | |
847 FXP_NAME = 104 | |
848 FXP_ATTRS = 105 | |
849 FXP_EXTENDED = 200 | |
850 FXP_EXTENDED_REPLY = 201 | |
851 | |
852 FILEXFER_ATTR_SIZE = 0x00000001 | |
853 FILEXFER_ATTR_OWNERGROUP = 0x00000002 | |
854 FILEXFER_ATTR_PERMISSIONS = 0x00000004 | |
855 FILEXFER_ATTR_ACMODTIME = 0x00000009 | |
856 FILEXFER_ATTR_EXTENDED = 0x80000000L | |
857 | |
858 FILEXFER_TYPE_REGULAR = 1 | |
859 FILEXFER_TYPE_DIRECTORY = 2 | |
860 FILEXFER_TYPE_SYMLINK = 3 | |
861 FILEXFER_TYPE_SPECIAL = 4 | |
862 FILEXFER_TYPE_UNKNOWN = 5 | |
863 | |
864 FXF_READ = 0x00000001 | |
865 FXF_WRITE = 0x00000002 | |
866 FXF_APPEND = 0x00000004 | |
867 FXF_CREAT = 0x00000008 | |
868 FXF_TRUNC = 0x00000010 | |
869 FXF_EXCL = 0x00000020 | |
870 FXF_TEXT = 0x00000040 | |
871 | |
872 FX_OK = 0 | |
873 FX_EOF = 1 | |
874 FX_NO_SUCH_FILE = 2 | |
875 FX_PERMISSION_DENIED = 3 | |
876 FX_FAILURE = 4 | |
877 FX_BAD_MESSAGE = 5 | |
878 FX_NO_CONNECTION = 6 | |
879 FX_CONNECTION_LOST = 7 | |
880 FX_OP_UNSUPPORTED = 8 | |
881 # http://www.ietf.org/internet-drafts/draft-ietf-secsh-filexfer-12.txt defines | |
882 # more useful error codes, but so far OpenSSH doesn't implement them. We use | |
883 # them internally for clarity, but for now define them all as FX_FAILURE to be | |
884 # compatible with existing software. | |
885 FX_FILE_ALREADY_EXISTS = FX_FAILURE | |
886 FX_NOT_A_DIRECTORY = FX_FAILURE | |
887 FX_FILE_IS_A_DIRECTORY = FX_FAILURE | |
888 | |
889 | |
890 # initialize FileTransferBase.packetTypes: | |
891 g = globals() | |
892 for name in g.keys(): | |
893 if name.startswith('FXP_'): | |
894 value = g[name] | |
895 FileTransferBase.packetTypes[value] = name[4:] | |
896 del g, name, value | |
OLD | NEW |