3 from stat import S_IFREG, S_IFDIR
5 from twisted.trial import unittest
6 from twisted.internet import defer
7 from twisted.python.failure import Failure
13 from Crypto import Util
20 from twisted.conch.ssh import filetransfer as sftp
21 from allmydata.frontends import sftpd
24 #from twisted.internet.base import DelayedCall
25 #DelayedCall.debug = True
31 def trace_exceptions(frame, event, arg):
32 if event != 'exception':
35 func_name = co.co_name
36 line_no = frame.f_lineno
37 filename = co.co_filename
38 exc_type, exc_value, exc_traceback = arg
39 print 'Tracing exception: %r %r on line %r of %r in %r' % \
40 (exc_type.__name__, exc_value, line_no, func_name, filename)
42 def trace_calls(frame, event, arg):
45 return trace_exceptions
47 sys.settrace(trace_calls)
52 from allmydata.interfaces import IDirectoryNode, ExistingChildError, NoSuchChildError
53 from allmydata.mutable.common import NotWriteableError
55 from allmydata.util.consumer import download_to_data
56 from allmydata.immutable import upload
57 from allmydata.test.no_network import GridTestMixin
58 from allmydata.test.common import ShouldFailMixin
60 class Handler(GridTestMixin, ShouldFailMixin, unittest.TestCase):
61 """This is a no-network unit test of the SFTPHandler class."""
64 skip = "SFTP support requires pycrypto, which is not installed"
66 def shouldFailWithSFTPError(self, expected_code, which, callable, *args, **kwargs):
67 assert isinstance(expected_code, int), repr(expected_code)
68 assert isinstance(which, str), repr(which)
69 s = traceback.format_stack()
70 d = defer.maybeDeferred(callable, *args, **kwargs)
72 if isinstance(res, Failure):
73 res.trap(sftp.SFTPError)
74 self.failUnlessReallyEqual(res.value.code, expected_code,
75 "%s was supposed to raise SFTPError(%d), not SFTPError(%d): %s" %
76 (which, expected_code, res.value.code, res))
78 print '@' + '@'.join(s)
79 self.fail("%s was supposed to raise SFTPError(%d), not get '%s'" %
80 (which, expected_code, res))
84 def failUnlessReallyEqual(self, a, b, msg=None):
85 self.failUnlessEqual(a, b, msg=msg)
86 self.failUnlessEqual(type(a), type(b), msg=msg)
88 def _set_up(self, basedir, num_clients=1, num_servers=10):
89 self.basedir = "sftp/" + basedir
90 self.set_up_grid(num_clients=num_clients, num_servers=num_servers)
94 self.client = self.g.clients[0]
95 self.username = "alice"
96 self.convergence = "convergence"
98 d = self.client.create_dirnode()
99 def _created_root(node):
101 self.root_uri = node.get_uri()
102 self.user = sftpd.SFTPUser(check_abort, self.client, self.root, self.username, self.convergence)
103 self.handler = sftpd.SFTPHandler(self.user)
104 d.addCallback(_created_root)
107 def _set_up_tree(self):
108 d = self.client.create_mutable_file("mutable file contents")
109 d.addCallback(lambda node: self.root.set_node(u"mutable", node))
110 def _created_mutable(n):
112 self.mutable_uri = n.get_uri()
113 d.addCallback(_created_mutable)
115 d.addCallback(lambda ign:
116 self.root._create_and_validate_node(None, self.mutable.get_readonly_uri(), name=u"readonly"))
117 d.addCallback(lambda node: self.root.set_node(u"readonly", node))
118 def _created_readonly(n):
120 self.readonly_uri = n.get_uri()
121 d.addCallback(_created_readonly)
123 gross = upload.Data("0123456789" * 101, None)
124 d.addCallback(lambda ign: self.root.add_file(u"gro\u00DF", gross))
125 def _created_gross(n):
127 self.gross_uri = n.get_uri()
128 d.addCallback(_created_gross)
130 small = upload.Data("0123456789", None)
131 d.addCallback(lambda ign: self.root.add_file(u"small", small))
132 def _created_small(n):
134 self.small_uri = n.get_uri()
135 d.addCallback(_created_small)
137 small2 = upload.Data("Small enough for a LIT too", None)
138 d.addCallback(lambda ign: self.root.add_file(u"small2", small2))
139 def _created_small2(n):
141 self.small2_uri = n.get_uri()
142 d.addCallback(_created_small2)
144 empty_litdir_uri = "URI:DIR2-LIT:"
146 # contains one child which is itself also LIT:
147 tiny_litdir_uri = "URI:DIR2-LIT:gqytunj2onug64tufqzdcosvkjetutcjkq5gw4tvm5vwszdgnz5hgyzufqydulbshj5x2lbm"
149 unknown_uri = "x-tahoe-crazy://I_am_from_the_future."
151 d.addCallback(lambda ign: self.root._create_and_validate_node(None, empty_litdir_uri, name=u"empty_lit_dir"))
152 def _created_empty_lit_dir(n):
153 self.empty_lit_dir = n
154 self.empty_lit_dir_uri = n.get_uri()
155 self.root.set_node(u"empty_lit_dir", n)
156 d.addCallback(_created_empty_lit_dir)
158 d.addCallback(lambda ign: self.root._create_and_validate_node(None, tiny_litdir_uri, name=u"tiny_lit_dir"))
159 def _created_tiny_lit_dir(n):
160 self.tiny_lit_dir = n
161 self.tiny_lit_dir_uri = n.get_uri()
162 self.root.set_node(u"tiny_lit_dir", n)
163 d.addCallback(_created_tiny_lit_dir)
165 d.addCallback(lambda ign: self.root._create_and_validate_node(None, unknown_uri, name=u"unknown"))
166 def _created_unknown(n):
168 self.unknown_uri = n.get_uri()
169 self.root.set_node(u"unknown", n)
170 d.addCallback(_created_unknown)
172 d.addCallback(lambda ign: self.root.set_node(u"loop", self.root))
175 def test_basic(self):
176 d = self._set_up("basic")
178 # Test operations that have no side-effects, and don't need the tree.
180 version = self.handler.gotVersion(3, {})
181 self.failUnless(isinstance(version, dict))
183 self.failUnlessReallyEqual(self.handler._path_from_string(""), [])
184 self.failUnlessReallyEqual(self.handler._path_from_string("/"), [])
185 self.failUnlessReallyEqual(self.handler._path_from_string("."), [])
186 self.failUnlessReallyEqual(self.handler._path_from_string("//"), [])
187 self.failUnlessReallyEqual(self.handler._path_from_string("/."), [])
188 self.failUnlessReallyEqual(self.handler._path_from_string("/./"), [])
189 self.failUnlessReallyEqual(self.handler._path_from_string("foo"), [u"foo"])
190 self.failUnlessReallyEqual(self.handler._path_from_string("/foo"), [u"foo"])
191 self.failUnlessReallyEqual(self.handler._path_from_string("foo/"), [u"foo"])
192 self.failUnlessReallyEqual(self.handler._path_from_string("/foo/"), [u"foo"])
193 self.failUnlessReallyEqual(self.handler._path_from_string("foo/bar"), [u"foo", u"bar"])
194 self.failUnlessReallyEqual(self.handler._path_from_string("/foo/bar"), [u"foo", u"bar"])
195 self.failUnlessReallyEqual(self.handler._path_from_string("foo/bar//"), [u"foo", u"bar"])
196 self.failUnlessReallyEqual(self.handler._path_from_string("/foo/bar//"), [u"foo", u"bar"])
197 self.failUnlessReallyEqual(self.handler._path_from_string("foo/../bar"), [u"bar"])
198 self.failUnlessReallyEqual(self.handler._path_from_string("/foo/../bar"), [u"bar"])
199 self.failUnlessReallyEqual(self.handler._path_from_string("../bar"), [u"bar"])
200 self.failUnlessReallyEqual(self.handler._path_from_string("/../bar"), [u"bar"])
202 self.failUnlessReallyEqual(self.handler.realPath(""), "/")
203 self.failUnlessReallyEqual(self.handler.realPath("/"), "/")
204 self.failUnlessReallyEqual(self.handler.realPath("."), "/")
205 self.failUnlessReallyEqual(self.handler.realPath("//"), "/")
206 self.failUnlessReallyEqual(self.handler.realPath("/."), "/")
207 self.failUnlessReallyEqual(self.handler.realPath("/./"), "/")
208 self.failUnlessReallyEqual(self.handler.realPath("foo"), "/foo")
209 self.failUnlessReallyEqual(self.handler.realPath("/foo"), "/foo")
210 self.failUnlessReallyEqual(self.handler.realPath("foo/"), "/foo")
211 self.failUnlessReallyEqual(self.handler.realPath("/foo/"), "/foo")
212 self.failUnlessReallyEqual(self.handler.realPath("foo/bar"), "/foo/bar")
213 self.failUnlessReallyEqual(self.handler.realPath("/foo/bar"), "/foo/bar")
214 self.failUnlessReallyEqual(self.handler.realPath("foo/bar//"), "/foo/bar")
215 self.failUnlessReallyEqual(self.handler.realPath("/foo/bar//"), "/foo/bar")
216 self.failUnlessReallyEqual(self.handler.realPath("foo/../bar"), "/bar")
217 self.failUnlessReallyEqual(self.handler.realPath("/foo/../bar"), "/bar")
218 self.failUnlessReallyEqual(self.handler.realPath("../bar"), "/bar")
219 self.failUnlessReallyEqual(self.handler.realPath("/../bar"), "/bar")
220 d.addCallback(_check)
222 d.addCallback(lambda ign:
223 self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "_path_from_string invalid UTF-8",
224 self.handler._path_from_string, "\xFF"))
225 d.addCallback(lambda ign:
226 self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "realPath invalid UTF-8",
227 self.handler.realPath, "\xFF"))
231 def test_raise_error(self):
232 self.failUnlessReallyEqual(sftpd._raise_error(None), None)
234 d = defer.succeed(None)
235 d.addCallback(lambda ign:
236 self.shouldFailWithSFTPError(sftp.FX_FAILURE, "_raise_error SFTPError",
237 sftpd._raise_error, Failure(sftp.SFTPError(sftp.FX_FAILURE, "foo"))))
238 d.addCallback(lambda ign:
239 self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "_raise_error NoSuchChildError",
240 sftpd._raise_error, Failure(NoSuchChildError("foo"))))
241 d.addCallback(lambda ign:
242 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "_raise_error ExistingChildError",
243 sftpd._raise_error, Failure(ExistingChildError("foo"))))
244 d.addCallback(lambda ign:
245 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "_raise_error NotWriteableError",
246 sftpd._raise_error, Failure(NotWriteableError("foo"))))
247 d.addCallback(lambda ign:
248 self.shouldFailWithSFTPError(sftp.FX_OP_UNSUPPORTED, "_raise_error NotImplementedError",
249 sftpd._raise_error, Failure(NotImplementedError("foo"))))
250 d.addCallback(lambda ign:
251 self.shouldFailWithSFTPError(sftp.FX_EOF, "_raise_error EOFError",
252 sftpd._raise_error, Failure(EOFError("foo"))))
253 d.addCallback(lambda ign:
254 self.shouldFailWithSFTPError(sftp.FX_EOF, "_raise_error defer.FirstError",
255 sftpd._raise_error, Failure(defer.FirstError(
256 Failure(sftp.SFTPError(sftp.FX_EOF, "foo")), 0))))
257 d.addCallback(lambda ign:
258 self.shouldFailWithSFTPError(sftp.FX_FAILURE, "_raise_error AssertionError",
259 sftpd._raise_error, Failure(AssertionError("foo"))))
263 def test_not_implemented(self):
264 d = self._set_up("not_implemented")
266 d.addCallback(lambda ign:
267 self.shouldFailWithSFTPError(sftp.FX_OP_UNSUPPORTED, "readLink link",
268 self.handler.readLink, "link"))
269 d.addCallback(lambda ign:
270 self.shouldFailWithSFTPError(sftp.FX_OP_UNSUPPORTED, "makeLink link file",
271 self.handler.makeLink, "link", "file"))
272 d.addCallback(lambda ign:
273 self.shouldFailWithSFTPError(sftp.FX_OP_UNSUPPORTED, "extendedRequest foo bar",
274 self.handler.extendedRequest, "foo", "bar"))
278 def _compareDirLists(self, actual, expected):
279 actual_list = sorted(actual)
280 expected_list = sorted(expected)
281 self.failUnlessReallyEqual(len(actual_list), len(expected_list),
282 "%r is wrong length, expecting %r" % (actual_list, expected_list))
283 for (a, b) in zip(actual_list, expected_list):
284 (name, text, attrs) = a
285 (expected_name, expected_text_re, expected_attrs) = b
286 self.failUnlessReallyEqual(name, expected_name)
287 self.failUnless(re.match(expected_text_re, text), "%r does not match %r" % (text, expected_text_re))
288 # it is ok for there to be extra actual attributes
290 for e in expected_attrs:
291 self.failUnlessReallyEqual(attrs[e], expected_attrs[e])
293 def test_openDirectory_and_attrs(self):
294 d = self._set_up("openDirectory_and_attrs")
295 d.addCallback(lambda ign: self._set_up_tree())
297 d.addCallback(lambda ign:
298 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openDirectory small",
299 self.handler.openDirectory, "small"))
300 d.addCallback(lambda ign:
301 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openDirectory unknown",
302 self.handler.openDirectory, "unknown"))
303 d.addCallback(lambda ign:
304 self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "openDirectory nodir",
305 self.handler.openDirectory, "nodir"))
306 d.addCallback(lambda ign:
307 self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "openDirectory nodir/nodir",
308 self.handler.openDirectory, "nodir/nodir"))
310 gross = u"gro\u00DF".encode("utf-8")
312 ('empty_lit_dir', r'drwxrwx--- .* \? .* empty_lit_dir$', {'permissions': S_IFDIR | 0770}),
313 (gross, r'-rw-rw---- .* 1010 .* '+gross+'$', {'permissions': S_IFREG | 0660, 'size': 1010}),
314 ('loop', r'drwxrwx--- .* \? .* loop$', {'permissions': S_IFDIR | 0770}),
315 ('mutable', r'-rw-rw---- .* \? .* mutable$', {'permissions': S_IFREG | 0660}),
316 ('readonly', r'-r--r----- .* \? .* readonly$', {'permissions': S_IFREG | 0440}),
317 ('small', r'-rw-rw---- .* 10 .* small$', {'permissions': S_IFREG | 0660, 'size': 10}),
318 ('small2', r'-rw-rw---- .* 26 .* small2$', {'permissions': S_IFREG | 0660, 'size': 26}),
319 ('tiny_lit_dir', r'drwxrwx--- .* \? .* tiny_lit_dir$', {'permissions': S_IFDIR | 0770}),
320 ('unknown', r'\?--------- .* \? .* unknown$', {'permissions': 0}),
323 d.addCallback(lambda ign: self.handler.openDirectory(""))
324 d.addCallback(lambda res: self._compareDirLists(res, expected_root))
326 d.addCallback(lambda ign: self.handler.openDirectory("loop"))
327 d.addCallback(lambda res: self._compareDirLists(res, expected_root))
329 d.addCallback(lambda ign: self.handler.openDirectory("loop/loop"))
330 d.addCallback(lambda res: self._compareDirLists(res, expected_root))
332 d.addCallback(lambda ign: self.handler.openDirectory("empty_lit_dir"))
333 d.addCallback(lambda res: self._compareDirLists(res, []))
335 expected_tiny_lit = [
336 ('short', r'-r--r----- .* 8 Jan 01 1970 short$', {'permissions': S_IFREG | 0440, 'size': 8}),
339 d.addCallback(lambda ign: self.handler.openDirectory("tiny_lit_dir"))
340 d.addCallback(lambda res: self._compareDirLists(res, expected_tiny_lit))
342 d.addCallback(lambda ign: self.handler.getAttrs("small", True))
343 def _check_attrs(attrs):
344 self.failUnlessReallyEqual(attrs['permissions'], S_IFREG | 0440) #FIXME
345 self.failUnlessReallyEqual(attrs['size'], 10)
346 d.addCallback(_check_attrs)
348 d.addCallback(lambda ign:
349 self.failUnlessReallyEqual(self.handler.setAttrs("small", {}), None))
351 d.addCallback(lambda ign:
352 self.shouldFailWithSFTPError(sftp.FX_OP_UNSUPPORTED, "setAttrs size",
353 self.handler.setAttrs, "small", {'size': 0}))
357 def test_openFile_read(self):
358 d = self._set_up("openFile_read")
359 d.addCallback(lambda ign: self._set_up_tree())
361 d.addCallback(lambda ign:
362 self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "openFile small 0",
363 self.handler.openFile, "small", 0, {}))
365 # attempting to open a non-existent file should fail
366 d.addCallback(lambda ign:
367 self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "openFile nofile READ",
368 self.handler.openFile, "nofile", sftp.FXF_READ, {}))
369 d.addCallback(lambda ign:
370 self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "openFile nodir/file READ",
371 self.handler.openFile, "nodir/file", sftp.FXF_READ, {}))
373 d.addCallback(lambda ign:
374 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile unknown READ denied",
375 self.handler.openFile, "unknown", sftp.FXF_READ, {}))
376 d.addCallback(lambda ign:
377 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir READ denied",
378 self.handler.openFile, "tiny_lit_dir", sftp.FXF_READ, {}))
379 d.addCallback(lambda ign:
380 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile unknown uri READ denied",
381 self.handler.openFile, "uri/"+self.unknown_uri, sftp.FXF_READ, {}))
382 d.addCallback(lambda ign:
383 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir uri READ denied",
384 self.handler.openFile, "uri/"+self.tiny_lit_dir_uri, sftp.FXF_READ, {}))
385 # FIXME: should be FX_NO_SUCH_FILE?
386 d.addCallback(lambda ign:
387 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile noexist uri READ denied",
388 self.handler.openFile, "uri/URI:noexist", sftp.FXF_READ, {}))
389 d.addCallback(lambda ign:
390 self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "openFile invalid UTF-8 uri READ denied",
391 self.handler.openFile, "uri/URI:\xFF", sftp.FXF_READ, {}))
393 # reading an existing file should succeed
394 d.addCallback(lambda ign: self.handler.openFile("small", sftp.FXF_READ, {}))
396 d2 = rf.readChunk(0, 10)
397 d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "0123456789"))
399 d2.addCallback(lambda ign: rf.readChunk(2, 6))
400 d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "234567"))
402 d2.addCallback(lambda ign: rf.readChunk(8, 4)) # read that starts before EOF is OK
403 d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "89"))
405 d2.addCallback(lambda ign:
406 self.shouldFailWithSFTPError(sftp.FX_EOF, "readChunk starting at EOF (0-byte)",
407 rf.readChunk, 10, 0))
408 d2.addCallback(lambda ign:
409 self.shouldFailWithSFTPError(sftp.FX_EOF, "readChunk starting at EOF",
410 rf.readChunk, 10, 1))
411 d2.addCallback(lambda ign:
412 self.shouldFailWithSFTPError(sftp.FX_EOF, "readChunk starting after EOF",
413 rf.readChunk, 11, 1))
415 d2.addCallback(lambda ign: rf.getAttrs())
416 def _check_attrs(attrs):
417 self.failUnlessReallyEqual(attrs['permissions'], S_IFREG | 0440) #FIXME
418 self.failUnlessReallyEqual(attrs['size'], 10)
419 d2.addCallback(_check_attrs)
421 d2.addCallback(lambda ign:
422 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "writeChunk on read-only handle denied",
423 rf.writeChunk, 0, "a"))
424 d2.addCallback(lambda ign:
425 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "setAttrs on read-only handle denied",
428 d2.addCallback(lambda ign: rf.close())
430 d2.addCallback(lambda ign:
431 self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "readChunk on closed file",
433 d2.addCallback(lambda ign:
434 self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "getAttrs on closed file",
437 d2.addCallback(lambda ign: rf.close()) # should be no-op
439 d.addCallback(_read_small)
441 # repeat for a large file
442 gross = u"gro\u00DF".encode("utf-8")
443 d.addCallback(lambda ign: self.handler.openFile(gross, sftp.FXF_READ, {}))
445 d2 = rf.readChunk(0, 10)
446 d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "0123456789"))
448 d2.addCallback(lambda ign: rf.readChunk(2, 6))
449 d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "234567"))
451 d2.addCallback(lambda ign: rf.readChunk(1008, 4)) # read that starts before EOF is OK
452 d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "89"))
454 d2.addCallback(lambda ign:
455 self.shouldFailWithSFTPError(sftp.FX_EOF, "readChunk starting at EOF (0-byte)",
456 rf.readChunk, 1010, 0))
457 d2.addCallback(lambda ign:
458 self.shouldFailWithSFTPError(sftp.FX_EOF, "readChunk starting at EOF",
459 rf.readChunk, 1010, 1))
460 d2.addCallback(lambda ign:
461 self.shouldFailWithSFTPError(sftp.FX_EOF, "readChunk starting after EOF",
462 rf.readChunk, 1011, 1))
464 d2.addCallback(lambda ign: rf.getAttrs())
465 def _check_attrs(attrs):
466 self.failUnlessReallyEqual(attrs['permissions'], S_IFREG | 0440) #FIXME
467 self.failUnlessReallyEqual(attrs['size'], 1010)
468 d2.addCallback(_check_attrs)
470 d2.addCallback(lambda ign:
471 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "writeChunk on read-only handle denied",
472 rf.writeChunk, 0, "a"))
473 d2.addCallback(lambda ign:
474 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "setAttrs on read-only handle denied",
477 d2.addCallback(lambda ign: rf.close())
479 d2.addCallback(lambda ign:
480 self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "readChunk on closed file",
482 d2.addCallback(lambda ign:
483 self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "getAttrs on closed file",
486 d2.addCallback(lambda ign: rf.close()) # should be no-op
488 d.addCallback(_read_gross)
490 # reading an existing small file via uri/ should succeed
491 d.addCallback(lambda ign: self.handler.openFile("uri/"+self.small_uri, sftp.FXF_READ, {}))
492 def _read_small_uri(rf):
493 d2 = rf.readChunk(0, 10)
494 d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "0123456789"))
495 d2.addCallback(lambda ign: rf.close())
497 d.addCallback(_read_small_uri)
499 # repeat for a large file
500 d.addCallback(lambda ign: self.handler.openFile("uri/"+self.gross_uri, sftp.FXF_READ, {}))
501 def _read_gross_uri(rf):
502 d2 = rf.readChunk(0, 10)
503 d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "0123456789"))
504 d2.addCallback(lambda ign: rf.close())
506 d.addCallback(_read_gross_uri)
508 # repeat for a mutable file
509 d.addCallback(lambda ign: self.handler.openFile("uri/"+self.mutable_uri, sftp.FXF_READ, {}))
510 def _read_mutable_uri(rf):
511 d2 = rf.readChunk(0, 100)
512 d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "mutable file contents"))
513 d2.addCallback(lambda ign: rf.close())
515 d.addCallback(_read_mutable_uri)
517 # repeat for a file within a directory referenced by URI
518 d.addCallback(lambda ign: self.handler.openFile("uri/"+self.tiny_lit_dir_uri+"/short", sftp.FXF_READ, {}))
520 d2 = rf.readChunk(0, 100)
521 d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "The end."))
522 d2.addCallback(lambda ign: rf.close())
524 d.addCallback(_read_short)
528 def test_openFile_write(self):
529 d = self._set_up("openFile_write")
530 d.addCallback(lambda ign: self._set_up_tree())
532 d.addCallback(lambda ign:
533 self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "openFile '' WRITE|CREAT|TRUNC",
534 self.handler.openFile, "", sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_TRUNC, {}))
535 d.addCallback(lambda ign:
536 self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "openFile newfile WRITE|TRUNC",
537 self.handler.openFile, "newfile", sftp.FXF_WRITE | sftp.FXF_TRUNC, {}))
538 d.addCallback(lambda ign:
539 self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "openFile small WRITE|EXCL",
540 self.handler.openFile, "small", sftp.FXF_WRITE | sftp.FXF_EXCL, {}))
541 d.addCallback(lambda ign:
542 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir WRITE",
543 self.handler.openFile, "tiny_lit_dir", sftp.FXF_WRITE, {}))
544 d.addCallback(lambda ign:
545 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile unknown WRITE",
546 self.handler.openFile, "unknown", sftp.FXF_WRITE, {}))
547 d.addCallback(lambda ign:
548 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir/newfile WRITE|CREAT|TRUNC",
549 self.handler.openFile, "tiny_lit_dir/newfile",
550 sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_TRUNC, {}))
551 d.addCallback(lambda ign:
552 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir/short WRITE",
553 self.handler.openFile, "tiny_lit_dir/short", sftp.FXF_WRITE, {}))
554 d.addCallback(lambda ign:
555 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir/short WRITE|CREAT|EXCL",
556 self.handler.openFile, "tiny_lit_dir/short",
557 sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_EXCL, {}))
558 d.addCallback(lambda ign:
559 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile readonly WRITE",
560 self.handler.openFile, "readonly", sftp.FXF_WRITE, {}))
561 d.addCallback(lambda ign:
562 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile small WRITE|CREAT|EXCL",
563 self.handler.openFile, "small",
564 sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_EXCL, {}))
565 d.addCallback(lambda ign:
566 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile readonly uri WRITE",
567 self.handler.openFile, "uri/"+self.readonly_uri, sftp.FXF_WRITE, {}))
568 d.addCallback(lambda ign:
569 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile small uri WRITE",
570 self.handler.openFile, "uri/"+self.small_uri, sftp.FXF_WRITE, {}))
571 d.addCallback(lambda ign:
572 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile small uri WRITE|CREAT|TRUNC",
573 self.handler.openFile, "uri/"+self.small_uri,
574 sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_TRUNC, {}))
575 d.addCallback(lambda ign:
576 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile mutable uri WRITE|CREAT|EXCL",
577 self.handler.openFile, "uri/"+self.mutable_uri,
578 sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_EXCL, {}))
580 d.addCallback(lambda ign:
581 self.handler.openFile("newfile", sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_TRUNC, {}))
583 d2 = wf.writeChunk(0, "0123456789")
584 d2.addCallback(lambda res: self.failUnlessReallyEqual(res, None))
586 d2.addCallback(lambda ign: wf.writeChunk(8, "0123"))
587 d2.addCallback(lambda ign: wf.writeChunk(13, "abc"))
589 d2.addCallback(lambda ign: wf.getAttrs())
590 def _check_attrs(attrs):
591 self.failUnlessReallyEqual(attrs['permissions'], S_IFREG | 0440) #FIXME
592 self.failUnlessReallyEqual(attrs['size'], 16)
593 d2.addCallback(_check_attrs)
595 d2.addCallback(lambda ign: wf.setAttrs({}))
597 d2.addCallback(lambda ign:
598 self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "setAttrs with negative size",
599 wf.setAttrs, {'size': -1}))
601 d2.addCallback(lambda ign: wf.setAttrs({'size': 14}))
602 d2.addCallback(lambda ign: wf.getAttrs())
603 d2.addCallback(lambda attrs: self.failUnlessReallyEqual(attrs['size'], 14))
605 d2.addCallback(lambda ign:
606 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "readChunk on write-only handle denied",
609 d2.addCallback(lambda ign: wf.close())
611 d2.addCallback(lambda ign:
612 self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "writeChunk on closed file",
613 wf.writeChunk, 0, "a"))
614 d2.addCallback(lambda ign:
615 self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "setAttrs on closed file",
616 wf.setAttrs, {'size': 0}))
618 d2.addCallback(lambda ign: wf.close()) # should be no-op
620 d.addCallback(_write)
621 d.addCallback(lambda ign: self.root.get(u"newfile"))
622 d.addCallback(lambda node: download_to_data(node))
623 d.addCallback(lambda data: self.failUnlessReallyEqual(data, "012345670123\x00a"))
625 # test APPEND flag, and also replacing an existing file ("newfile")
626 d.addCallback(lambda ign:
627 self.handler.openFile("newfile", sftp.FXF_WRITE | sftp.FXF_CREAT |
628 sftp.FXF_TRUNC | sftp.FXF_APPEND, {}))
629 def _write_append(wf):
630 d2 = wf.writeChunk(0, "0123456789")
631 d2.addCallback(lambda ign: wf.writeChunk(8, "0123"))
632 d2.addCallback(lambda ign: wf.close())
634 d.addCallback(_write_append)
635 d.addCallback(lambda ign: self.root.get(u"newfile"))
636 d.addCallback(lambda node: download_to_data(node))
637 d.addCallback(lambda data: self.failUnlessReallyEqual(data, "01234567890123"))
640 d.addCallback(lambda ign:
641 self.handler.openFile("excl", sftp.FXF_WRITE | sftp.FXF_CREAT |
642 sftp.FXF_TRUNC | sftp.FXF_EXCL, {}))
644 d2 = self.root.get(u"excl")
645 d2.addCallback(lambda node: download_to_data(node))
646 d2.addCallback(lambda data: self.failUnlessReallyEqual(data, ""))
648 d2.addCallback(lambda ign: wf.writeChunk(0, "0123456789"))
649 d2.addCallback(lambda ign: wf.close())
651 d.addCallback(_write_excl)
652 d.addCallback(lambda ign: self.root.get(u"excl"))
653 d.addCallback(lambda node: download_to_data(node))
654 d.addCallback(lambda data: self.failUnlessReallyEqual(data, "0123456789"))
657 # test that writing a zero-length file with EXCL only updates the directory once
658 d.addCallback(lambda ign:
659 self.handler.openFile("zerolength", sftp.FXF_WRITE | sftp.FXF_CREAT |
661 def _write_excl_zerolength(wf):
662 d2 = self.root.get(u"zerolength")
663 d2.addCallback(lambda node: download_to_data(node))
664 d2.addCallback(lambda data: self.failUnlessReallyEqual(data, ""))
666 # FIXME: no API to get the best version number exists (fix as part of #993)
668 #d2.addCallback(lambda ign: self.root.get_best_version_number())
669 #d2.addCallback(lambda version: stash['version'] = version)
670 d2.addCallback(lambda ign: wf.close())
671 #d2.addCallback(lambda ign: self.root.get_best_version_number())
672 #d2.addCallback(lambda new_version: self.failUnlessReallyEqual(new_version, stash['version'])
674 d.addCallback(_write_excl_zerolength)
675 d.addCallback(lambda ign: self.root.get(u"zerolength"))
676 d.addCallback(lambda node: download_to_data(node))
677 d.addCallback(lambda data: self.failUnlessReallyEqual(data, ""))
680 # test WRITE | CREAT without TRUNC
681 d.addCallback(lambda ign:
682 self.handler.openFile("newfile2", sftp.FXF_WRITE | sftp.FXF_CREAT, {}))
683 def _write_notrunc(wf):
684 d2 = wf.writeChunk(0, "0123456789")
685 d2.addCallback(lambda ign: wf.close())
687 d.addCallback(_write_notrunc)
688 d.addCallback(lambda ign: self.root.get(u"newfile2"))
689 d.addCallback(lambda node: download_to_data(node))
690 d.addCallback(lambda data: self.failUnlessReallyEqual(data, "0123456789"))
692 # test writing to a mutable file
693 d.addCallback(lambda ign:
694 self.handler.openFile("mutable", sftp.FXF_WRITE, {}))
695 def _write_mutable(wf):
696 d2 = wf.writeChunk(8, "new!")
697 d2.addCallback(lambda ign: wf.close())
699 d.addCallback(_write_mutable)
700 d.addCallback(lambda ign: self.root.get(u"mutable"))
701 def _check_same_file(node):
702 self.failUnless(node.is_mutable())
703 self.failUnlessReallyEqual(node.get_uri(), self.mutable_uri)
704 return node.download_best_version()
705 d.addCallback(_check_same_file)
706 d.addCallback(lambda data: self.failUnlessReallyEqual(data, "mutable new! contents"))
709 # test READ | WRITE without CREAT or TRUNC
710 d.addCallback(lambda ign:
711 self.handler.openFile("small", sftp.FXF_READ | sftp.FXF_WRITE, {}))
712 def _read_write(rwf):
713 d2 = rwf.writeChunk(8, "0123")
714 d2.addCallback(lambda ign: rwf.readChunk(0, 100))
715 d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "012345670123"))
716 d2.addCallback(lambda ign: rwf.close())
718 d.addCallback(_read_write)
719 d.addCallback(lambda ign: self.root.get(u"small"))
720 d.addCallback(lambda node: download_to_data(node))
721 d.addCallback(lambda data: self.failUnlessReallyEqual(data, "012345670123"))
725 def test_removeFile(self):
726 d = self._set_up("removeFile")
727 d.addCallback(lambda ign: self._set_up_tree())
729 d.addCallback(lambda ign:
730 self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removeFile nofile",
731 self.handler.removeFile, "nofile"))
732 d.addCallback(lambda ign:
733 self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removeFile nofile",
734 self.handler.removeFile, "nofile"))
735 d.addCallback(lambda ign:
736 self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removeFile nodir/file",
737 self.handler.removeFile, "nodir/file"))
738 d.addCallback(lambda ign:
739 self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removefile ''",
740 self.handler.removeFile, ""))
742 # removing a directory should fail
743 d.addCallback(lambda ign:
744 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "removeFile tiny_lit_dir",
745 self.handler.removeFile, "tiny_lit_dir"))
747 # removing a file should succeed
748 d.addCallback(lambda ign: self.root.get(u"gro\u00DF"))
749 d.addCallback(lambda ign: self.handler.removeFile(u"gro\u00DF".encode('utf-8')))
750 d.addCallback(lambda ign:
751 self.shouldFail(NoSuchChildError, "removeFile gross", "gro\\xdf",
752 self.root.get, u"gro\u00DF"))
754 # removing an unknown should succeed
755 d.addCallback(lambda ign: self.root.get(u"unknown"))
756 d.addCallback(lambda ign: self.handler.removeFile("unknown"))
757 d.addCallback(lambda ign:
758 self.shouldFail(NoSuchChildError, "removeFile unknown", "unknown",
759 self.root.get, u"unknown"))
761 # removing a link to an open file should not prevent it from being read
762 d.addCallback(lambda ign: self.handler.openFile("small", sftp.FXF_READ, {}))
763 def _remove_and_read_small(rf):
764 d2= self.handler.removeFile("small")
765 d2.addCallback(lambda ign:
766 self.shouldFail(NoSuchChildError, "removeFile small", "small",
767 self.root.get, u"small"))
768 d2.addCallback(lambda ign: rf.readChunk(0, 10))
769 d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "0123456789"))
770 d2.addCallback(lambda ign: rf.close())
772 d.addCallback(_remove_and_read_small)
776 def test_removeDirectory(self):
777 d = self._set_up("removeDirectory")
778 d.addCallback(lambda ign: self._set_up_tree())
780 d.addCallback(lambda ign:
781 self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removeDirectory nodir",
782 self.handler.removeDirectory, "nodir"))
783 d.addCallback(lambda ign:
784 self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removeDirectory nodir/nodir",
785 self.handler.removeDirectory, "nodir/nodir"))
786 d.addCallback(lambda ign:
787 self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removeDirectory ''",
788 self.handler.removeDirectory, ""))
790 # removing a file should fail
791 d.addCallback(lambda ign:
792 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "removeDirectory gross",
793 self.handler.removeDirectory, u"gro\u00DF".encode('utf-8')))
795 # removing a directory should succeed
796 d.addCallback(lambda ign: self.root.get(u"tiny_lit_dir"))
797 d.addCallback(lambda ign: self.handler.removeDirectory("tiny_lit_dir"))
798 d.addCallback(lambda ign:
799 self.shouldFail(NoSuchChildError, "removeDirectory tiny_lit_dir", "tiny_lit_dir",
800 self.root.get, u"tiny_lit_dir"))
802 # removing an unknown should succeed
803 d.addCallback(lambda ign: self.root.get(u"unknown"))
804 d.addCallback(lambda ign: self.handler.removeDirectory("unknown"))
805 d.addCallback(lambda err:
806 self.shouldFail(NoSuchChildError, "removeDirectory unknown", "unknown",
807 self.root.get, u"unknown"))
811 def test_renameFile(self):
812 d = self._set_up("renameFile")
813 d.addCallback(lambda ign: self._set_up_tree())
815 # renaming a non-existent file should fail
816 d.addCallback(lambda ign:
817 self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile nofile newfile",
818 self.handler.renameFile, "nofile", "newfile"))
819 d.addCallback(lambda ign:
820 self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile '' newfile",
821 self.handler.renameFile, "", "newfile"))
823 # renaming a file to a non-existent path should fail
824 d.addCallback(lambda ign:
825 self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile small nodir/small",
826 self.handler.renameFile, "small", "nodir/small"))
828 # renaming a file to an invalid UTF-8 name should fail
829 d.addCallback(lambda ign:
830 self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile small invalid",
831 self.handler.renameFile, "small", "\xFF"))
833 # renaming a file to or from an URI should fail
834 d.addCallback(lambda ign:
835 self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile small from uri",
836 self.handler.renameFile, "uri/"+self.small_uri, "new"))
837 d.addCallback(lambda ign:
838 self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile small to uri",
839 self.handler.renameFile, "small", "uri/fake_uri"))
841 # renaming a file onto an existing file, directory or unknown should fail
842 d.addCallback(lambda ign:
843 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "renameFile small small2",
844 self.handler.renameFile, "small", "small2"))
845 d.addCallback(lambda ign:
846 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "renameFile small tiny_lit_dir",
847 self.handler.renameFile, "small", "tiny_lit_dir"))
848 d.addCallback(lambda ign:
849 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "renameFile small unknown",
850 self.handler.renameFile, "small", "unknown"))
852 # renaming a file to a correct path should succeed
853 d.addCallback(lambda ign: self.handler.renameFile("small", "new_small"))
854 d.addCallback(lambda ign: self.root.get(u"new_small"))
855 d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.small_uri))
857 # renaming a file into a subdirectory should succeed (also tests Unicode names)
858 d.addCallback(lambda ign: self.handler.renameFile(u"gro\u00DF".encode('utf-8'),
859 u"loop/neue_gro\u00DF".encode('utf-8')))
860 d.addCallback(lambda ign: self.root.get(u"neue_gro\u00DF"))
861 d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.gross_uri))
863 # renaming a directory to a correct path should succeed
864 d.addCallback(lambda ign: self.handler.renameFile("tiny_lit_dir", "new_tiny_lit_dir"))
865 d.addCallback(lambda ign: self.root.get(u"new_tiny_lit_dir"))
866 d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.tiny_lit_dir_uri))
868 # renaming an unknown to a correct path should succeed
869 d.addCallback(lambda ign: self.handler.renameFile("unknown", "new_unknown"))
870 d.addCallback(lambda ign: self.root.get(u"new_unknown"))
871 d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.unknown_uri))
875 def test_makeDirectory(self):
876 d = self._set_up("makeDirectory")
877 d.addCallback(lambda ign: self._set_up_tree())
879 # making a directory at a correct path should succeed
880 d.addCallback(lambda ign: self.handler.makeDirectory("newdir", {'ext_foo': 'bar', 'ctime': 42}))
882 d.addCallback(lambda ign: self.root.get_child_and_metadata(u"newdir"))
883 def _got( (child, metadata) ):
884 self.failUnless(IDirectoryNode.providedBy(child))
885 self.failUnless(child.is_mutable())
887 #self.failUnless('ctime' in metadata, metadata)
888 #self.failUnlessReallyEqual(metadata['ctime'], 42)
889 #self.failUnless('ext_foo' in metadata, metadata)
890 #self.failUnlessReallyEqual(metadata['ext_foo'], 'bar')
891 # TODO: child should be empty
894 # making intermediate directories should also succeed
895 d.addCallback(lambda ign: self.handler.makeDirectory("newparent/newchild", {}))
897 d.addCallback(lambda ign: self.root.get(u"newparent"))
898 def _got_newparent(newparent):
899 self.failUnless(IDirectoryNode.providedBy(newparent))
900 self.failUnless(newparent.is_mutable())
901 return newparent.get(u"newchild")
902 d.addCallback(_got_newparent)
904 def _got_newchild(newchild):
905 self.failUnless(IDirectoryNode.providedBy(newchild))
906 self.failUnless(newchild.is_mutable())
907 d.addCallback(_got_newchild)
909 d.addCallback(lambda ign:
910 self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "makeDirectory invalid UTF-8",
911 self.handler.makeDirectory, "\xFF", {}))
913 # should fail because there is an existing file "small"
914 d.addCallback(lambda ign:
915 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "makeDirectory small",
916 self.handler.makeDirectory, "small", {}))