| 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 |