SFTP: fixes related to reporting of permissions (needed for sshfs).

This commit is contained in:
david-sarah 2010-05-17 22:45:21 -07:00
parent 8d118e28f1
commit 819eaa74c1
2 changed files with 272 additions and 192 deletions

View File

@ -17,6 +17,7 @@ from twisted.conch.avatar import ConchUser
from twisted.conch.openssh_compat import primes from twisted.conch.openssh_compat import primes
from twisted.cred import portal from twisted.cred import portal
from twisted.internet.error import ProcessDone, ProcessTerminated from twisted.internet.error import ProcessDone, ProcessTerminated
from twisted.internet.interfaces import ITransport
from twisted.internet import defer from twisted.internet import defer
from twisted.internet.interfaces import IFinishableConsumer from twisted.internet.interfaces import IFinishableConsumer
@ -96,7 +97,7 @@ def _raise_error(err):
if err is None: if err is None:
return None return None
if noisy: logmsg("RAISE %r" % (err,), level=NOISY) if noisy: logmsg("RAISE %r" % (err,), level=NOISY)
#traceback.print_exc(err) if noisy and not use_foolscap_logging: traceback.print_exc(err)
# The message argument to SFTPError must not reveal information that # The message argument to SFTPError must not reveal information that
# might compromise anonymity. # might compromise anonymity.
@ -129,6 +130,7 @@ def _raise_error(err):
# We assume that the error message is not anonymity-sensitive. # We assume that the error message is not anonymity-sensitive.
raise SFTPError(FX_FAILURE, str(err.value)) raise SFTPError(FX_FAILURE, str(err.value))
def _repr_flags(flags): def _repr_flags(flags):
return "|".join([f for f in return "|".join([f for f in
[(flags & FXF_READ) and "FXF_READ" or None, [(flags & FXF_READ) and "FXF_READ" or None,
@ -140,6 +142,7 @@ def _repr_flags(flags):
] ]
if f]) if f])
def _lsLine(name, attrs): def _lsLine(name, attrs):
st_uid = "tahoe" st_uid = "tahoe"
st_gid = "tahoe" st_gid = "tahoe"
@ -159,26 +162,26 @@ def _lsLine(name, attrs):
mode = st_mode mode = st_mode
perms = array.array('c', '-'*10) perms = array.array('c', '-'*10)
ft = stat.S_IFMT(mode) ft = stat.S_IFMT(mode)
if stat.S_ISDIR(ft): perms[0] = 'd' if stat.S_ISDIR(ft): perms[0] = 'd'
elif stat.S_ISCHR(ft): perms[0] = 'c' elif stat.S_ISCHR(ft): perms[0] = 'c'
elif stat.S_ISBLK(ft): perms[0] = 'b' elif stat.S_ISBLK(ft): perms[0] = 'b'
elif stat.S_ISREG(ft): perms[0] = '-' elif stat.S_ISREG(ft): perms[0] = '-'
elif stat.S_ISFIFO(ft): perms[0] = 'f' elif stat.S_ISFIFO(ft): perms[0] = 'f'
elif stat.S_ISLNK(ft): perms[0] = 'l' elif stat.S_ISLNK(ft): perms[0] = 'l'
elif stat.S_ISSOCK(ft): perms[0] = 's' elif stat.S_ISSOCK(ft): perms[0] = 's'
else: perms[0] = '?' else: perms[0] = '?'
# user # user
if mode&stat.S_IRUSR:perms[1] = 'r' if mode&stat.S_IRUSR: perms[1] = 'r'
if mode&stat.S_IWUSR:perms[2] = 'w' if mode&stat.S_IWUSR: perms[2] = 'w'
if mode&stat.S_IXUSR:perms[3] = 'x' if mode&stat.S_IXUSR: perms[3] = 'x'
# group # group
if mode&stat.S_IRGRP:perms[4] = 'r' if mode&stat.S_IRGRP: perms[4] = 'r'
if mode&stat.S_IWGRP:perms[5] = 'w' if mode&stat.S_IWGRP: perms[5] = 'w'
if mode&stat.S_IXGRP:perms[6] = 'x' if mode&stat.S_IXGRP: perms[6] = 'x'
# other # other
if mode&stat.S_IROTH:perms[7] = 'r' if mode&stat.S_IROTH: perms[7] = 'r'
if mode&stat.S_IWOTH:perms[8] = 'w' if mode&stat.S_IWOTH: perms[8] = 'w'
if mode&stat.S_IXOTH:perms[9] = 'x' if mode&stat.S_IXOTH: perms[9] = 'x'
# suid/sgid never set # suid/sgid never set
l = perms.tostring() l = perms.tostring()
@ -198,64 +201,74 @@ def _lsLine(name, attrs):
l += name l += name
return l return l
def _populate_attrs(childnode, metadata, writeable, size=None):
def _is_readonly(parent_readonly, child):
"""Whether child should be treated as having read-only permissions when listed
in parent."""
if child.is_unknown():
return True
elif child.is_mutable():
return child.is_readonly()
else:
return parent_readonly
def _populate_attrs(childnode, metadata, size=None):
attrs = {} attrs = {}
# see webapi.txt for what these times mean # The permissions must have the S_IFDIR (040000) or S_IFREG (0100000)
if metadata: # bits, otherwise the client may refuse to open a directory.
if "linkmotime" in metadata.get("tahoe", {}): # Also, sshfs run as a non-root user requires files and directories
attrs["mtime"] = int(metadata["tahoe"]["linkmotime"]) # to be world-readable/writeable.
elif "mtime" in metadata: #
attrs["mtime"] = int(metadata["mtime"])
if "linkcrtime" in metadata.get("tahoe", {}):
attrs["createtime"] = int(metadata["tahoe"]["linkcrtime"])
if "ctime" in metadata:
attrs["ctime"] = int(metadata["ctime"])
# We would prefer to omit atime, but SFTP version 3 can only
# accept mtime if atime is also set.
attrs["atime"] = attrs["mtime"]
# The permissions must have the extra bits (040000 or 0100000),
# otherwise the client will not call openDirectory.
# Directories and unknown nodes have no size, and SFTP doesn't # Directories and unknown nodes have no size, and SFTP doesn't
# require us to make one up. # require us to make one up.
#
# childnode might be None, meaning that the file doesn't exist yet, # childnode might be None, meaning that the file doesn't exist yet,
# but we're going to write it later. # but we're going to write it later.
if childnode and childnode.is_unknown(): if childnode and childnode.is_unknown():
perms = 0 perms = 0
elif childnode and IDirectoryNode.providedBy(childnode): elif childnode and IDirectoryNode.providedBy(childnode):
perms = S_IFDIR | 0770 perms = S_IFDIR | 0777
else: else:
# For files, omit the size if we don't immediately know it. # For files, omit the size if we don't immediately know it.
if childnode and size is None: if childnode and size is None:
size = childnode.get_size() size = childnode.get_size()
if size is not None: if size is not None:
assert isinstance(size, (int, long)), repr(size) assert isinstance(size, (int, long)) and not isinstance(size, bool), repr(size)
attrs["size"] = size attrs['size'] = size
perms = S_IFREG | 0660 perms = S_IFREG | 0666
if not writeable: if metadata:
perms &= S_IFDIR | S_IFREG | 0555 # clear 'w' bits assert 'readonly' in metadata, metadata
if metadata['readonly']:
perms &= S_IFDIR | S_IFREG | 0555 # clear 'w' bits
attrs["permissions"] = perms # see webapi.txt for what these times mean
if 'linkmotime' in metadata.get('tahoe', {}):
attrs['mtime'] = int(metadata['tahoe']['linkmotime'])
elif 'mtime' in metadata:
# We would prefer to omit atime, but SFTP version 3 can only
# accept mtime if atime is also set.
attrs['mtime'] = int(metadata['mtime'])
attrs['atime'] = attrs['mtime']
# We could set the SSH_FILEXFER_ATTR_FLAGS here: if 'linkcrtime' in metadata.get('tahoe', {}):
# ENCRYPTED would always be true ("The file is stored on disk attrs['createtime'] = int(metadata['tahoe']['linkcrtime'])
# using file-system level transparent encryption.")
# SYSTEM, HIDDEN, ARCHIVE and SYNC would always be false. if 'ctime' in metadata:
# READONLY and IMMUTABLE would be set according to attrs['ctime'] = int(metadata['ctime'])
# childnode.is_readonly() and childnode.is_immutable()
# for known nodes. attrs['permissions'] = perms
# However, twisted.conch.ssh.filetransfer only implements
# SFTP version 3, which doesn't include these flags. # twisted.conch.ssh.filetransfer only implements SFTP version 3,
# which doesn't include SSH_FILEXFER_ATTR_FLAGS.
return attrs return attrs
class EncryptedTemporaryFile(PrefixingLogMixin): class EncryptedTemporaryFile(PrefixingLogMixin):
# not implemented: next, readline, readlines, xreadlines, writelines # not implemented: next, readline, readlines, xreadlines, writelines
@ -533,6 +546,7 @@ def _make_sftp_file(check_abort, flags, convergence, parent=None, childname=None
if noisy: logmsg("_make_sftp_file(%r, %r, <convergence censored>, parent=%r, childname=%r, filenode=%r, metadata=%r" % if noisy: logmsg("_make_sftp_file(%r, %r, <convergence censored>, parent=%r, childname=%r, filenode=%r, metadata=%r" %
(check_abort, flags, parent, childname, filenode, metadata), level=NOISY) (check_abort, flags, parent, childname, filenode, metadata), level=NOISY)
assert metadata is None or 'readonly' in metadata, metadata
if not (flags & (FXF_WRITE | FXF_CREAT)) and (flags & FXF_READ) and filenode and \ if not (flags & (FXF_WRITE | FXF_CREAT)) and (flags & FXF_READ) and filenode and \
not filenode.is_mutable() and filenode.get_size() <= SIZE_THRESHOLD: not filenode.is_mutable() and filenode.get_size() <= SIZE_THRESHOLD:
return ShortReadOnlySFTPFile(filenode, metadata) return ShortReadOnlySFTPFile(filenode, metadata)
@ -604,7 +618,7 @@ class ShortReadOnlySFTPFile(PrefixingLogMixin):
def _closed(): raise SFTPError(FX_BAD_MESSAGE, "cannot get attributes for a closed file handle") def _closed(): raise SFTPError(FX_BAD_MESSAGE, "cannot get attributes for a closed file handle")
return defer.execute(_closed) return defer.execute(_closed)
return defer.succeed(_populate_attrs(self.filenode, self.metadata, False)) return defer.succeed(_populate_attrs(self.filenode, self.metadata))
def setAttrs(self, attrs): def setAttrs(self, attrs):
self.log(".setAttrs(%r)" % (attrs,), level=OPERATIONAL) self.log(".setAttrs(%r)" % (attrs,), level=OPERATIONAL)
@ -660,7 +674,7 @@ class GeneralSFTPFile(PrefixingLogMixin):
self.async.addCallback(_downloaded) self.async.addCallback(_downloaded)
else: else:
download_size = filenode.get_size() download_size = filenode.get_size()
assert download_size is not None assert download_size is not None, "download_size is None"
self.consumer = OverwriteableFileConsumer(self.check_abort, download_size, tempfile_maker) self.consumer = OverwriteableFileConsumer(self.check_abort, download_size, tempfile_maker)
def _read(ign): def _read(ign):
if noisy: self.log("_read immutable", level=NOISY) if noisy: self.log("_read immutable", level=NOISY)
@ -741,10 +755,10 @@ class GeneralSFTPFile(PrefixingLogMixin):
d2.addCallback(lambda ign: self.consumer.get_current_size()) d2.addCallback(lambda ign: self.consumer.get_current_size())
d2.addCallback(lambda size: self.consumer.read(0, size)) d2.addCallback(lambda size: self.consumer.read(0, size))
d2.addCallback(lambda new_contents: self.filenode.overwrite(new_contents)) d2.addCallback(lambda new_contents: self.filenode.overwrite(new_contents))
#elif (self.flags & FXF_EXCL) and self.consumer.get_current_size() == 0: elif (self.flags & FXF_EXCL) and self.consumer.get_current_size() == 0:
# # The file will already have been written by the open call, so we can # The file will already have been written by the open call, so we can
# # optimize out the extra directory write (useful for zero-length lockfiles). # optimize out the extra directory write (useful for zero-length lockfiles).
# pass pass
else: else:
def _add_file(ign): def _add_file(ign):
self.log("_add_file childname=%r" % (self.childname,), level=OPERATIONAL) self.log("_add_file childname=%r" % (self.childname,), level=OPERATIONAL)
@ -769,14 +783,12 @@ class GeneralSFTPFile(PrefixingLogMixin):
# Optimization for read-only handles, when we already know the metadata. # Optimization for read-only handles, when we already know the metadata.
if not(self.flags & (FXF_WRITE | FXF_CREAT)) and self.metadata and self.filenode and not self.filenode.is_mutable(): if not(self.flags & (FXF_WRITE | FXF_CREAT)) and self.metadata and self.filenode and not self.filenode.is_mutable():
return defer.succeed(_populate_attrs(self.filenode, self.metadata, False)) return defer.succeed(_populate_attrs(self.filenode, self.metadata))
d = defer.Deferred() d = defer.Deferred()
def _get(ign): def _get(ign):
# FIXME: pass correct value for writeable
# self.filenode might be None, but that's ok. # self.filenode might be None, but that's ok.
attrs = _populate_attrs(self.filenode, self.metadata, False, attrs = _populate_attrs(self.filenode, self.metadata, size=self.consumer.get_current_size())
size=self.consumer.get_current_size())
eventually_callback(d)(attrs) eventually_callback(d)(attrs)
return None return None
self.async.addCallbacks(_get, eventually_errback(d)) self.async.addCallbacks(_get, eventually_errback(d))
@ -902,7 +914,6 @@ class SFTPUserHandler(ConchUser, PrefixingLogMixin):
# Note that the permission checks below are for more precise error reporting on # Note that the permission checks below are for more precise error reporting on
# the open call; later operations would fail even if we did not make these checks. # the open call; later operations would fail even if we did not make these checks.
stash = {'parent': None}
d = self._get_root(path) d = self._get_root(path)
def _got_root((root, path)): def _got_root((root, path)):
if root.is_unknown(): if root.is_unknown():
@ -919,7 +930,7 @@ class SFTPUserHandler(ConchUser, PrefixingLogMixin):
raise SFTPError(FX_PERMISSION_DENIED, raise SFTPError(FX_PERMISSION_DENIED,
"cannot write to a non-writeable filecap without a parent directory") "cannot write to a non-writeable filecap without a parent directory")
if flags & FXF_EXCL: if flags & FXF_EXCL:
raise SFTPError(FX_PERMISSION_DENIED, raise SFTPError(FX_FAILURE,
"cannot create a file exclusively when it already exists") "cannot create a file exclusively when it already exists")
return _make_sftp_file(self.check_abort, flags, self._convergence, filenode=root) return _make_sftp_file(self.check_abort, flags, self._convergence, filenode=root)
@ -931,8 +942,13 @@ class SFTPUserHandler(ConchUser, PrefixingLogMixin):
d2 = root.get_child_at_path(path[:-1]) d2 = root.get_child_at_path(path[:-1])
def _got_parent(parent): def _got_parent(parent):
if noisy: self.log("_got_parent(%r)" % (parent,), level=NOISY) if noisy: self.log("_got_parent(%r)" % (parent,), level=NOISY)
stash['parent'] = parent if parent.is_unknown():
raise SFTPError(FX_PERMISSION_DENIED,
"cannot open an unknown cap (or child of an unknown directory). "
"Upgrading the gateway to a later Tahoe-LAFS version may help")
parent_readonly = parent.is_readonly()
d3 = defer.succeed(None)
if flags & FXF_EXCL: if flags & FXF_EXCL:
# FXF_EXCL means that the link to the file (not the file itself) must # FXF_EXCL means that the link to the file (not the file itself) must
# be created atomically wrt updates by this storage client. # be created atomically wrt updates by this storage client.
@ -941,60 +957,64 @@ class SFTPUserHandler(ConchUser, PrefixingLogMixin):
# case). We make the link initially point to a zero-length LIT file, # case). We make the link initially point to a zero-length LIT file,
# which is consistent with what might happen on a POSIX filesystem. # which is consistent with what might happen on a POSIX filesystem.
if parent.is_readonly(): if parent_readonly:
raise SFTPError(FX_PERMISSION_DENIED, raise SFTPError(FX_FAILURE,
"cannot create a file exclusively when the parent directory is read-only") "cannot create a file exclusively when the parent directory is read-only")
# 'overwrite=False' ensures failure if the link already exists. # 'overwrite=False' ensures failure if the link already exists.
# FIXME: should use a single call to set_uri and return (child, metadata) (#1035) # FIXME: should use a single call to set_uri and return (child, metadata) (#1035)
zero_length_lit = "URI:LIT:" zero_length_lit = "URI:LIT:"
d3 = parent.set_uri(childname, None, zero_length_lit, overwrite=False) if noisy: self.log("%r.set_uri(%r, None, readcap=%r, overwrite=False)" %
(parent, zero_length_lit, childname), level=NOISY)
d3.addCallback(lambda ign: parent.set_uri(childname, None, readcap=zero_length_lit, overwrite=False))
def _seturi_done(child): def _seturi_done(child):
stash['child'] = child if noisy: self.log("%r.get_metadata_for(%r)" % (parent, childname), level=NOISY)
return parent.get_metadata_for(childname) d4 = parent.get_metadata_for(childname)
d4.addCallback(lambda metadata: (child, metadata))
return d4
d3.addCallback(_seturi_done) d3.addCallback(_seturi_done)
d3.addCallback(lambda metadata: (stash['child'], metadata))
return d3
else: else:
if noisy: self.log("get_child_and_metadata(%r)" % (childname,), level=NOISY) if noisy: self.log("%r.get_child_and_metadata(%r)" % (parent, childname), level=NOISY)
return parent.get_child_and_metadata(childname) d3.addCallback(lambda ign: parent.get_child_and_metadata(childname))
def _got_child( (filenode, metadata) ):
if noisy: self.log("_got_child( (%r, %r) )" % (filenode, metadata), level=NOISY)
if filenode.is_unknown():
raise SFTPError(FX_PERMISSION_DENIED,
"cannot open an unknown cap. Upgrading the gateway "
"to a later Tahoe-LAFS version may help")
if not IFileNode.providedBy(filenode):
raise SFTPError(FX_PERMISSION_DENIED,
"cannot open a directory as if it were a file")
if (flags & FXF_WRITE) and filenode.is_mutable() and filenode.is_readonly():
raise SFTPError(FX_PERMISSION_DENIED,
"cannot open a read-only mutable file for writing")
if (flags & FXF_WRITE) and parent_readonly:
raise SFTPError(FX_PERMISSION_DENIED,
"cannot open a file for writing when the parent directory is read-only")
metadata['readonly'] = _is_readonly(parent_readonly, filenode)
return _make_sftp_file(self.check_abort, flags, self._convergence, parent=parent,
childname=childname, filenode=filenode, metadata=metadata)
def _no_child(f):
if noisy: self.log("_no_child(%r)" % (f,), level=NOISY)
f.trap(NoSuchChildError)
if not (flags & FXF_CREAT):
raise SFTPError(FX_NO_SUCH_FILE,
"the file does not exist, and was not opened with the creation (CREAT) flag")
if parent_readonly:
raise SFTPError(FX_PERMISSION_DENIED,
"cannot create a file when the parent directory is read-only")
return _make_sftp_file(self.check_abort, flags, self._convergence, parent=parent,
childname=childname)
d3.addCallbacks(_got_child, _no_child)
return d3
d2.addCallback(_got_parent) d2.addCallback(_got_parent)
def _got_child( (filenode, metadata) ):
if noisy: self.log("_got_child( (%r, %r) )" % (filenode, metadata), level=NOISY)
parent = stash['parent']
if filenode.is_unknown():
raise SFTPError(FX_PERMISSION_DENIED,
"cannot open an unknown cap. Upgrading the gateway "
"to a later Tahoe-LAFS version may help")
if not IFileNode.providedBy(filenode):
raise SFTPError(FX_PERMISSION_DENIED,
"cannot open a directory as if it were a file")
if (flags & FXF_WRITE) and filenode.is_mutable() and filenode.is_readonly():
raise SFTPError(FX_PERMISSION_DENIED,
"cannot open a read-only mutable file for writing")
if (flags & FXF_WRITE) and parent.is_readonly():
raise SFTPError(FX_PERMISSION_DENIED,
"cannot open a file for writing when the parent directory is read-only")
return _make_sftp_file(self.check_abort, flags, self._convergence, parent=parent,
childname=childname, filenode=filenode, metadata=metadata)
def _no_child(f):
if noisy: self.log("_no_child(%r)" % (f,), level=NOISY)
f.trap(NoSuchChildError)
parent = stash['parent']
if parent is None:
return f
if not (flags & FXF_CREAT):
raise SFTPError(FX_NO_SUCH_FILE,
"the file does not exist, and was not opened with the creation (CREAT) flag")
if parent.is_readonly():
raise SFTPError(FX_PERMISSION_DENIED,
"cannot create a file when the parent directory is read-only")
return _make_sftp_file(self.check_abort, flags, self._convergence, parent=parent,
childname=childname)
d2.addCallbacks(_got_child, _no_child)
return d2 return d2
d.addCallback(_got_root) d.addCallback(_got_root)
d.addErrback(_raise_error) d.addErrback(_raise_error)
@ -1013,14 +1033,19 @@ class SFTPUserHandler(ConchUser, PrefixingLogMixin):
toPath = self._path_from_string(newpathstring) toPath = self._path_from_string(newpathstring)
# the target directory must already exist # the target directory must already exist
d = deferredutil.gatherResults([self._get_parent(fromPath), d = deferredutil.gatherResults([self._get_parent_or_node(fromPath),
self._get_parent(toPath)]) self._get_parent_or_node(toPath)])
def _got( (fromPair, toPair) ): def _got( (fromPair, toPair) ):
if noisy: self.log("_got( (%r, %r) ) in .renameFile(%r, %r)" % if noisy: self.log("_got( (%r, %r) ) in .renameFile(%r, %r)" %
(fromPair, toPair, oldpathstring, newpathstring), level=NOISY) (fromPair, toPair, oldpathstring, newpathstring), level=NOISY)
(fromParent, fromChildname) = fromPair (fromParent, fromChildname) = fromPair
(toParent, toChildname) = toPair (toParent, toChildname) = toPair
if fromChildname is None:
raise SFTPError(FX_NO_SUCH_FILE, "cannot rename a source object specified by URI")
if toChildname is None:
raise SFTPError(FX_NO_SUCH_FILE, "cannot rename to a destination specified by URI")
# <http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-6.5> # <http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-6.5>
# "It is an error if there already exists a file with the name specified # "It is an error if there already exists a file with the name specified
# by newpath." # by newpath."
@ -1046,9 +1071,8 @@ class SFTPUserHandler(ConchUser, PrefixingLogMixin):
def _get_or_create_directories(self, node, path, metadata): def _get_or_create_directories(self, node, path, metadata):
if not IDirectoryNode.providedBy(node): if not IDirectoryNode.providedBy(node):
# unfortunately it is too late to provide the name of the # TODO: provide the name of the blocking file in the error message.
# blocking file in the error message. raise SFTPError(FX_FAILURE,
raise SFTPError(FX_PERMISSION_DENIED,
"cannot create directory because there " "cannot create directory because there "
"is a file in the way") # close enough "is a file in the way") # close enough
if not path: if not path:
@ -1069,8 +1093,15 @@ class SFTPUserHandler(ConchUser, PrefixingLogMixin):
return self._remove_object(path, must_be_directory=True) return self._remove_object(path, must_be_directory=True)
def _remove_object(self, path, must_be_directory=False, must_be_file=False): def _remove_object(self, path, must_be_directory=False, must_be_file=False):
d = defer.maybeDeferred(self._get_parent, path) d = defer.maybeDeferred(self._get_parent_or_node, path)
def _got_parent( (parent, childname) ): def _got_parent( (parent, childname) ):
# FIXME (minor): there is a race condition between the 'get' and 'delete',
# so it is possible that the must_be_directory or must_be_file restrictions
# might not be enforced correctly if the type has just changed.
if childname is None:
raise SFTPError(FX_NO_SUCH_FILE, "cannot delete an object specified by URI")
d2 = parent.get(childname) d2 = parent.get(childname)
def _got_child(child): def _got_child(child):
# Unknown children can be removed by either removeFile or removeDirectory. # Unknown children can be removed by either removeFile or removeDirectory.
@ -1098,15 +1129,15 @@ class SFTPUserHandler(ConchUser, PrefixingLogMixin):
if not IDirectoryNode.providedBy(dirnode): if not IDirectoryNode.providedBy(dirnode):
raise SFTPError(FX_PERMISSION_DENIED, raise SFTPError(FX_PERMISSION_DENIED,
"cannot list a file as if it were a directory") "cannot list a file as if it were a directory")
d2 = dirnode.list() d2 = dirnode.list()
def _render(children): def _render(children):
parent_writeable = not dirnode.is_readonly() parent_readonly = dirnode.is_readonly()
results = [] results = []
for filename, (node, metadata) in children.iteritems(): for filename, (child, metadata) in children.iteritems():
# The file size may be cached or absent. # The file size may be cached or absent.
writeable = parent_writeable and (node.is_unknown() or metadata['readonly'] = _is_readonly(parent_readonly, child)
not (node.is_mutable() and node.is_readonly())) attrs = _populate_attrs(child, metadata)
attrs = _populate_attrs(node, metadata, writeable)
filename_utf8 = filename.encode('utf-8') filename_utf8 = filename.encode('utf-8')
longname = _lsLine(filename_utf8, attrs) longname = _lsLine(filename_utf8, attrs)
results.append( (filename_utf8, longname, attrs) ) results.append( (filename_utf8, longname, attrs) )
@ -1126,12 +1157,9 @@ class SFTPUserHandler(ConchUser, PrefixingLogMixin):
# TODO: the modification time for a mutable file should be # TODO: the modification time for a mutable file should be
# reported as the update time of the best version. But that # reported as the update time of the best version. But that
# information isn't currently stored in mutable shares, I think. # information isn't currently stored in mutable shares, I think.
d2 = node.get_current_size() d2 = node.get_current_size()
def _got_size(size): d2.addCallback(lambda size: _populate_attrs(node, metadata, size=size))
# FIXME: pass correct value for writeable
attrs = _populate_attrs(node, metadata, False, size=size)
return attrs
d2.addCallback(_got_size)
return d2 return d2
d.addCallback(_render) d.addCallback(_render)
d.addErrback(_raise_error) d.addErrback(_raise_error)
@ -1212,19 +1240,8 @@ class SFTPUserHandler(ConchUser, PrefixingLogMixin):
if noisy: self.log(" PATH %r" % (path,), level=NOISY) if noisy: self.log(" PATH %r" % (path,), level=NOISY)
return path return path
def _get_node_and_metadata_for_path(self, path):
d = self._get_root(path)
def _got_root( (root, path) ):
if noisy: self.log("_got_root( (%r, %r) )" % (root, path), level=NOISY)
if path:
return root.get_child_and_metadata_at_path(path)
else:
return (root, {})
d.addCallback(_got_root)
return d
def _get_root(self, path): def _get_root(self, path):
# return (root, remaining_path) # return Deferred (root, remaining_path)
if path and path[0] == u"uri": if path and path[0] == u"uri":
d = defer.maybeDeferred(self._client.create_node_from_uri, path[1].encode('utf-8')) d = defer.maybeDeferred(self._client.create_node_from_uri, path[1].encode('utf-8'))
d.addCallback(lambda root: (root, path[2:])) d.addCallback(lambda root: (root, path[2:]))
@ -1232,23 +1249,38 @@ class SFTPUserHandler(ConchUser, PrefixingLogMixin):
d = defer.succeed((self._root, path)) d = defer.succeed((self._root, path))
return d return d
def _get_parent(self, path): def _get_parent_or_node(self, path):
# fire with (parentnode, childname) # return Deferred (parent, childname) or (node, None)
if not path:
def _nosuch(): raise SFTPError(FX_NO_SUCH_FILE, "path does not exist")
return defer.execute(_nosuch)
childname = path[-1]
assert isinstance(childname, unicode), repr(childname)
d = self._get_root(path) d = self._get_root(path)
def _got_root( (root, path) ): def _got_root( (root, remaining_path) ):
if not path: if not remaining_path:
raise SFTPError(FX_NO_SUCH_FILE, "path does not exist") return (root, None)
return root.get_child_at_path(path[:-1]) else:
d2 = root.get_child_at_path(remaining_path[:-1])
d2.addCallback(lambda parent: (parent, remaining_path[-1]))
return d2
d.addCallback(_got_root) d.addCallback(_got_root)
def _got_parent(parent): return d
return (parent, childname)
d.addCallback(_got_parent) def _get_node_and_metadata_for_path(self, path):
# return Deferred (node, metadata)
# where metadata always has a 'readonly' key
d = self._get_parent_or_node(path)
def _got_parent_or_node( (parent_or_node, childname) ):
if noisy: self.log("_got_parent_or_node( (%r, %r) )" % (parent_or_node, childname), level=NOISY)
if childname is None:
node = parent_or_node
return (node, {'readonly': node.is_unknown() or node.is_readonly()})
else:
parent = parent_or_node
d2 = parent.get_child_and_metadata_at_path([childname])
def _got( (child, metadata) ):
assert IDirectoryNode.providedBy(parent), parent
metadata['readonly'] = _is_readonly(parent.is_readonly(), child)
return (child, metadata)
d2.addCallback(_got)
return d2
d.addCallback(_got_parent_or_node)
return d return d
def _attrs_to_metadata(self, attrs): def _attrs_to_metadata(self, attrs):
@ -1312,7 +1344,7 @@ class Dispatcher:
self._client = client self._client = client
def requestAvatar(self, avatarID, mind, interface): def requestAvatar(self, avatarID, mind, interface):
assert interface == IConchUser assert interface == IConchUser, interface
rootnode = self._client.create_node_from_uri(avatarID.rootcap) rootnode = self._client.create_node_from_uri(avatarID.rootcap)
handler = SFTPUserHandler(self._client, rootnode, avatarID.username) handler = SFTPUserHandler(self._client, rootnode, avatarID.username)
return (interface, handler, handler.logout) return (interface, handler, handler.logout)

View File

@ -46,7 +46,7 @@ def trace_calls(frame, event, arg):
sys.settrace(trace_calls) sys.settrace(trace_calls)
""" """
timeout = 30 timeout = 60
from allmydata.interfaces import IDirectoryNode, ExistingChildError, NoSuchChildError from allmydata.interfaces import IDirectoryNode, ExistingChildError, NoSuchChildError
from allmydata.mutable.common import NotWriteableError from allmydata.mutable.common import NotWriteableError
@ -71,8 +71,8 @@ class Handler(GridTestMixin, ShouldFailMixin, unittest.TestCase):
if isinstance(res, Failure): if isinstance(res, Failure):
res.trap(sftp.SFTPError) res.trap(sftp.SFTPError)
self.failUnlessReallyEqual(res.value.code, expected_code, self.failUnlessReallyEqual(res.value.code, expected_code,
"%s was supposed to raise SFTPError(%d), not SFTPError(%d): %s" % "%s was supposed to raise SFTPError(%d), not SFTPError(%d): %s" %
(which, expected_code, res.value.code, res)) (which, expected_code, res.value.code, res))
else: else:
print '@' + '@'.join(s) print '@' + '@'.join(s)
self.fail("%s was supposed to raise SFTPError(%d), not get '%s'" % self.fail("%s was supposed to raise SFTPError(%d), not get '%s'" %
@ -276,11 +276,13 @@ class Handler(GridTestMixin, ShouldFailMixin, unittest.TestCase):
(name, text, attrs) = a (name, text, attrs) = a
(expected_name, expected_text_re, expected_attrs) = b (expected_name, expected_text_re, expected_attrs) = b
self.failUnlessReallyEqual(name, expected_name) self.failUnlessReallyEqual(name, expected_name)
self.failUnless(re.match(expected_text_re, text), "%r does not match %r" % (text, expected_text_re)) self.failUnless(re.match(expected_text_re, text),
"%r does not match %r in\n%r" % (text, expected_text_re, actual_list))
# it is ok for there to be extra actual attributes # it is ok for there to be extra actual attributes
# TODO: check times # TODO: check times
for e in expected_attrs: for e in expected_attrs:
self.failUnlessReallyEqual(attrs[e], expected_attrs[e]) self.failUnlessReallyEqual(attrs[e], expected_attrs[e],
"%r:%r is not %r in\n%r" % (e, attrs[e], expected_attrs[e], attrs))
def test_openDirectory_and_attrs(self): def test_openDirectory_and_attrs(self):
d = self._set_up("openDirectory_and_attrs") d = self._set_up("openDirectory_and_attrs")
@ -301,14 +303,14 @@ class Handler(GridTestMixin, ShouldFailMixin, unittest.TestCase):
gross = u"gro\u00DF".encode("utf-8") gross = u"gro\u00DF".encode("utf-8")
expected_root = [ expected_root = [
('empty_lit_dir', r'drwxrwx--- .* \? .* empty_lit_dir$', {'permissions': S_IFDIR | 0770}), ('empty_lit_dir', r'drwxrwxrwx .* \? .* empty_lit_dir$', {'permissions': S_IFDIR | 0777}),
(gross, r'-rw-rw---- .* 1010 .* '+gross+'$', {'permissions': S_IFREG | 0660, 'size': 1010}), (gross, r'-rw-rw-rw- .* 1010 .* '+gross+'$', {'permissions': S_IFREG | 0666, 'size': 1010}),
('loop', r'drwxrwx--- .* \? .* loop$', {'permissions': S_IFDIR | 0770}), ('loop', r'drwxrwxrwx .* \? .* loop$', {'permissions': S_IFDIR | 0777}),
('mutable', r'-rw-rw---- .* \? .* mutable$', {'permissions': S_IFREG | 0660}), ('mutable', r'-rw-rw-rw- .* \? .* mutable$', {'permissions': S_IFREG | 0666}),
('readonly', r'-r--r----- .* \? .* readonly$', {'permissions': S_IFREG | 0440}), ('readonly', r'-r--r--r-- .* \? .* readonly$', {'permissions': S_IFREG | 0444}),
('small', r'-rw-rw---- .* 10 .* small$', {'permissions': S_IFREG | 0660, 'size': 10}), ('small', r'-rw-rw-rw- .* 10 .* small$', {'permissions': S_IFREG | 0666, 'size': 10}),
('small2', r'-rw-rw---- .* 26 .* small2$', {'permissions': S_IFREG | 0660, 'size': 26}), ('small2', r'-rw-rw-rw- .* 26 .* small2$', {'permissions': S_IFREG | 0666, 'size': 26}),
('tiny_lit_dir', r'drwxrwx--- .* \? .* tiny_lit_dir$', {'permissions': S_IFDIR | 0770}), ('tiny_lit_dir', r'drwxrwxrwx .* \? .* tiny_lit_dir$', {'permissions': S_IFDIR | 0777}),
('unknown', r'\?--------- .* \? .* unknown$', {'permissions': 0}), ('unknown', r'\?--------- .* \? .* unknown$', {'permissions': 0}),
] ]
@ -325,7 +327,7 @@ class Handler(GridTestMixin, ShouldFailMixin, unittest.TestCase):
d.addCallback(lambda res: self._compareDirLists(res, [])) d.addCallback(lambda res: self._compareDirLists(res, []))
expected_tiny_lit = [ expected_tiny_lit = [
('short', r'-r--r----- .* 8 Jan 01 1970 short$', {'permissions': S_IFREG | 0440, 'size': 8}), ('short', r'-r--r--r-- .* 8 Jan 01 1970 short$', {'permissions': S_IFREG | 0444, 'size': 8}),
] ]
d.addCallback(lambda ign: self.handler.openDirectory("tiny_lit_dir")) d.addCallback(lambda ign: self.handler.openDirectory("tiny_lit_dir"))
@ -333,8 +335,8 @@ class Handler(GridTestMixin, ShouldFailMixin, unittest.TestCase):
d.addCallback(lambda ign: self.handler.getAttrs("small", True)) d.addCallback(lambda ign: self.handler.getAttrs("small", True))
def _check_attrs(attrs): def _check_attrs(attrs):
self.failUnlessReallyEqual(attrs['permissions'], S_IFREG | 0440) #FIXME self.failUnlessReallyEqual(attrs['permissions'], S_IFREG | 0666)
self.failUnlessReallyEqual(attrs['size'], 10) self.failUnlessReallyEqual(attrs['size'], 10, repr(attrs))
d.addCallback(_check_attrs) d.addCallback(_check_attrs)
d.addCallback(lambda ign: d.addCallback(lambda ign:
@ -391,6 +393,9 @@ class Handler(GridTestMixin, ShouldFailMixin, unittest.TestCase):
d2.addCallback(lambda ign: rf.readChunk(2, 6)) d2.addCallback(lambda ign: rf.readChunk(2, 6))
d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "234567")) d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "234567"))
d2.addCallback(lambda ign: rf.readChunk(1, 0))
d2.addCallback(lambda data: self.failUnlessReallyEqual(data, ""))
d2.addCallback(lambda ign: rf.readChunk(8, 4)) # read that starts before EOF is OK d2.addCallback(lambda ign: rf.readChunk(8, 4)) # read that starts before EOF is OK
d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "89")) d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "89"))
@ -406,8 +411,8 @@ class Handler(GridTestMixin, ShouldFailMixin, unittest.TestCase):
d2.addCallback(lambda ign: rf.getAttrs()) d2.addCallback(lambda ign: rf.getAttrs())
def _check_attrs(attrs): def _check_attrs(attrs):
self.failUnlessReallyEqual(attrs['permissions'], S_IFREG | 0440) #FIXME self.failUnlessReallyEqual(attrs['permissions'], S_IFREG | 0666)
self.failUnlessReallyEqual(attrs['size'], 10) self.failUnlessReallyEqual(attrs['size'], 10, repr(attrs))
d2.addCallback(_check_attrs) d2.addCallback(_check_attrs)
d2.addCallback(lambda ign: d2.addCallback(lambda ign:
@ -440,6 +445,9 @@ class Handler(GridTestMixin, ShouldFailMixin, unittest.TestCase):
d2.addCallback(lambda ign: rf.readChunk(2, 6)) d2.addCallback(lambda ign: rf.readChunk(2, 6))
d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "234567")) d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "234567"))
d2.addCallback(lambda ign: rf.readChunk(1, 0))
d2.addCallback(lambda data: self.failUnlessReallyEqual(data, ""))
d2.addCallback(lambda ign: rf.readChunk(1008, 4)) # read that starts before EOF is OK d2.addCallback(lambda ign: rf.readChunk(1008, 4)) # read that starts before EOF is OK
d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "89")) d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "89"))
@ -455,7 +463,7 @@ class Handler(GridTestMixin, ShouldFailMixin, unittest.TestCase):
d2.addCallback(lambda ign: rf.getAttrs()) d2.addCallback(lambda ign: rf.getAttrs())
def _check_attrs(attrs): def _check_attrs(attrs):
self.failUnlessReallyEqual(attrs['permissions'], S_IFREG | 0440) #FIXME self.failUnlessReallyEqual(attrs['permissions'], S_IFREG | 0666)
self.failUnlessReallyEqual(attrs['size'], 1010) self.failUnlessReallyEqual(attrs['size'], 1010)
d2.addCallback(_check_attrs) d2.addCallback(_check_attrs)
@ -521,54 +529,90 @@ class Handler(GridTestMixin, ShouldFailMixin, unittest.TestCase):
d = self._set_up("openFile_write") d = self._set_up("openFile_write")
d.addCallback(lambda ign: self._set_up_tree()) d.addCallback(lambda ign: self._set_up_tree())
# '' is an invalid filename
d.addCallback(lambda ign: d.addCallback(lambda ign:
self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "openFile '' WRITE|CREAT|TRUNC", self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "openFile '' WRITE|CREAT|TRUNC",
self.handler.openFile, "", sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_TRUNC, {})) self.handler.openFile, "", sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_TRUNC, {}))
# TRUNC is not valid without CREAT
d.addCallback(lambda ign: d.addCallback(lambda ign:
self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "openFile newfile WRITE|TRUNC", self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "openFile newfile WRITE|TRUNC",
self.handler.openFile, "newfile", sftp.FXF_WRITE | sftp.FXF_TRUNC, {})) self.handler.openFile, "newfile", sftp.FXF_WRITE | sftp.FXF_TRUNC, {}))
# EXCL is not valid without CREAT
d.addCallback(lambda ign: d.addCallback(lambda ign:
self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "openFile small WRITE|EXCL", self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "openFile small WRITE|EXCL",
self.handler.openFile, "small", sftp.FXF_WRITE | sftp.FXF_EXCL, {})) self.handler.openFile, "small", sftp.FXF_WRITE | sftp.FXF_EXCL, {}))
# cannot write to an existing directory
d.addCallback(lambda ign: d.addCallback(lambda ign:
self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir WRITE", self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir WRITE",
self.handler.openFile, "tiny_lit_dir", sftp.FXF_WRITE, {})) self.handler.openFile, "tiny_lit_dir", sftp.FXF_WRITE, {}))
# cannot write to an existing unknown
d.addCallback(lambda ign: d.addCallback(lambda ign:
self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile unknown WRITE", self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile unknown WRITE",
self.handler.openFile, "unknown", sftp.FXF_WRITE, {})) self.handler.openFile, "unknown", sftp.FXF_WRITE, {}))
# cannot write to a new file in an immutable directory
d.addCallback(lambda ign: d.addCallback(lambda ign:
self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir/newfile WRITE|CREAT|TRUNC", self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir/newfile WRITE|CREAT|TRUNC",
self.handler.openFile, "tiny_lit_dir/newfile", self.handler.openFile, "tiny_lit_dir/newfile",
sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_TRUNC, {})) sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_TRUNC, {}))
# cannot write to an existing immutable file in an immutable directory (with or without CREAT and EXCL)
d.addCallback(lambda ign: d.addCallback(lambda ign:
self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir/short WRITE", self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir/short WRITE",
self.handler.openFile, "tiny_lit_dir/short", sftp.FXF_WRITE, {})) self.handler.openFile, "tiny_lit_dir/short", sftp.FXF_WRITE, {}))
d.addCallback(lambda ign: d.addCallback(lambda ign:
self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir/short WRITE|CREAT|EXCL", self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir/short WRITE|CREAT",
self.handler.openFile, "tiny_lit_dir/short", self.handler.openFile, "tiny_lit_dir/short",
sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_EXCL, {})) sftp.FXF_WRITE | sftp.FXF_CREAT, {}))
# cannot write to a mutable file via a readonly cap (by path or uri)
d.addCallback(lambda ign: d.addCallback(lambda ign:
self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile readonly WRITE", self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile readonly WRITE",
self.handler.openFile, "readonly", sftp.FXF_WRITE, {})) self.handler.openFile, "readonly", sftp.FXF_WRITE, {}))
d.addCallback(lambda ign: d.addCallback(lambda ign:
self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile small WRITE|CREAT|EXCL", self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile readonly uri WRITE",
self.handler.openFile, "uri/"+self.readonly_uri, sftp.FXF_WRITE, {}))
# cannot create a file with the EXCL flag if it already exists
d.addCallback(lambda ign:
self.shouldFailWithSFTPError(sftp.FX_FAILURE, "openFile small WRITE|CREAT|EXCL",
self.handler.openFile, "small", self.handler.openFile, "small",
sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_EXCL, {})) sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_EXCL, {}))
d.addCallback(lambda ign: d.addCallback(lambda ign:
self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile readonly uri WRITE", self.shouldFailWithSFTPError(sftp.FX_FAILURE, "openFile mutable WRITE|CREAT|EXCL",
self.handler.openFile, "uri/"+self.readonly_uri, sftp.FXF_WRITE, {})) self.handler.openFile, "mutable",
sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_EXCL, {}))
d.addCallback(lambda ign:
self.shouldFailWithSFTPError(sftp.FX_FAILURE, "openFile mutable uri WRITE|CREAT|EXCL",
self.handler.openFile, "uri/"+self.mutable_uri,
sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_EXCL, {}))
d.addCallback(lambda ign:
self.shouldFailWithSFTPError(sftp.FX_FAILURE, "openFile tiny_lit_dir/short WRITE|CREAT|EXCL",
self.handler.openFile, "tiny_lit_dir/short",
sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_EXCL, {}))
# cannot write to an immutable file if we don't have its parent (with or without CREAT, TRUNC, or EXCL)
d.addCallback(lambda ign: d.addCallback(lambda ign:
self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile small uri WRITE", self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile small uri WRITE",
self.handler.openFile, "uri/"+self.small_uri, sftp.FXF_WRITE, {})) self.handler.openFile, "uri/"+self.small_uri, sftp.FXF_WRITE, {}))
d.addCallback(lambda ign:
self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile small uri WRITE|CREAT",
self.handler.openFile, "uri/"+self.small_uri,
sftp.FXF_WRITE | sftp.FXF_CREAT, {}))
d.addCallback(lambda ign: d.addCallback(lambda ign:
self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile small uri WRITE|CREAT|TRUNC", self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile small uri WRITE|CREAT|TRUNC",
self.handler.openFile, "uri/"+self.small_uri, self.handler.openFile, "uri/"+self.small_uri,
sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_TRUNC, {})) sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_TRUNC, {}))
d.addCallback(lambda ign: d.addCallback(lambda ign:
self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile mutable uri WRITE|CREAT|EXCL", self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile small uri WRITE|CREAT|EXCL",
self.handler.openFile, "uri/"+self.mutable_uri, self.handler.openFile, "uri/"+self.small_uri,
sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_EXCL, {})) sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_EXCL, {}))
# test creating a new file with truncation
d.addCallback(lambda ign: d.addCallback(lambda ign:
self.handler.openFile("newfile", sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_TRUNC, {})) self.handler.openFile("newfile", sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_TRUNC, {}))
def _write(wf): def _write(wf):
@ -580,7 +624,7 @@ class Handler(GridTestMixin, ShouldFailMixin, unittest.TestCase):
d2.addCallback(lambda ign: wf.getAttrs()) d2.addCallback(lambda ign: wf.getAttrs())
def _check_attrs(attrs): def _check_attrs(attrs):
self.failUnlessReallyEqual(attrs['permissions'], S_IFREG | 0440) #FIXME self.failUnlessReallyEqual(attrs['permissions'], S_IFREG | 0666)
self.failUnlessReallyEqual(attrs['size'], 16) self.failUnlessReallyEqual(attrs['size'], 16)
d2.addCallback(_check_attrs) d2.addCallback(_check_attrs)
@ -614,7 +658,7 @@ class Handler(GridTestMixin, ShouldFailMixin, unittest.TestCase):
d.addCallback(lambda node: download_to_data(node)) d.addCallback(lambda node: download_to_data(node))
d.addCallback(lambda data: self.failUnlessReallyEqual(data, "012345670123\x00a")) d.addCallback(lambda data: self.failUnlessReallyEqual(data, "012345670123\x00a"))
# test APPEND flag, and also replacing an existing file ("newfile") # test APPEND flag, and also replacing an existing file ("newfile" created by the previous test)
d.addCallback(lambda ign: d.addCallback(lambda ign:
self.handler.openFile("newfile", sftp.FXF_WRITE | sftp.FXF_CREAT | self.handler.openFile("newfile", sftp.FXF_WRITE | sftp.FXF_CREAT |
sftp.FXF_TRUNC | sftp.FXF_APPEND, {})) sftp.FXF_TRUNC | sftp.FXF_APPEND, {}))
@ -655,12 +699,16 @@ class Handler(GridTestMixin, ShouldFailMixin, unittest.TestCase):
d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "")) d2.addCallback(lambda data: self.failUnlessReallyEqual(data, ""))
# FIXME: no API to get the best version number exists (fix as part of #993) # FIXME: no API to get the best version number exists (fix as part of #993)
#stash = {} """
#d2.addCallback(lambda ign: self.root.get_best_version_number()) d2.addCallback(lambda ign: self.root.get_best_version_number())
#d2.addCallback(lambda version: stash['version'] = version) def _check_version(version):
d3 = wf.close()
d3.addCallback(lambda ign: self.root.get_best_version_number())
d3.addCallback(lambda new_version: self.failUnlessReallyEqual(new_version, version))
return d3
d2.addCallback(_check_version)
"""
d2.addCallback(lambda ign: wf.close()) d2.addCallback(lambda ign: wf.close())
#d2.addCallback(lambda ign: self.root.get_best_version_number())
#d2.addCallback(lambda new_version: self.failUnlessReallyEqual(new_version, stash['version'])
return d2 return d2
d.addCallback(_write_excl_zerolength) d.addCallback(_write_excl_zerolength)
d.addCallback(lambda ign: self.root.get(u"zerolength")) d.addCallback(lambda ign: self.root.get(u"zerolength"))
@ -847,13 +895,13 @@ class Handler(GridTestMixin, ShouldFailMixin, unittest.TestCase):
# renaming a file onto an existing file, directory or unknown should fail # renaming a file onto an existing file, directory or unknown should fail
d.addCallback(lambda ign: d.addCallback(lambda ign:
self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "renameFile small small2", self.shouldFailWithSFTPError(sftp.FX_FAILURE, "renameFile small small2",
self.handler.renameFile, "small", "small2")) self.handler.renameFile, "small", "small2"))
d.addCallback(lambda ign: d.addCallback(lambda ign:
self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "renameFile small tiny_lit_dir", self.shouldFailWithSFTPError(sftp.FX_FAILURE, "renameFile small tiny_lit_dir",
self.handler.renameFile, "small", "tiny_lit_dir")) self.handler.renameFile, "small", "tiny_lit_dir"))
d.addCallback(lambda ign: d.addCallback(lambda ign:
self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "renameFile small unknown", self.shouldFailWithSFTPError(sftp.FX_FAILURE, "renameFile small unknown",
self.handler.renameFile, "small", "unknown")) self.handler.renameFile, "small", "unknown"))
# renaming a file to a correct path should succeed # renaming a file to a correct path should succeed
@ -919,7 +967,7 @@ class Handler(GridTestMixin, ShouldFailMixin, unittest.TestCase):
# should fail because there is an existing file "small" # should fail because there is an existing file "small"
d.addCallback(lambda ign: d.addCallback(lambda ign:
self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "makeDirectory small", self.shouldFailWithSFTPError(sftp.FX_FAILURE, "makeDirectory small",
self.handler.makeDirectory, "small", {})) self.handler.makeDirectory, "small", {}))
return d return d