--- old-tahoe/src/allmydata/frontends/sftpd.py 2010-02-08 05:27:41.176049400 +0000 +++ new-tahoe/src/allmydata/frontends/sftpd.py 2010-02-08 05:27:41.303049400 +0000 @@ -91,8 +91,20 @@ def close(self): pass -class FakeStat: - pass +class _FakeStat: + def __init__(self, attrs): + # This is only used by twisted.conch.ls.lsLine, which doesn't require + # st_uid, st_gid or st_size to be numeric. + self.st_uid = "tahoe" + self.st_gid = "tahoe" + self.st_mtime = attrs.get("mtime", 0) + self.st_mode = attrs["permissions"] + # TODO: check that clients are okay with this being a "?". + # (They should be because the longname is intended for human + # consumption.) + self.st_size = attrs.get("size", "?") + # We don't know how many links there really are to this object. + self.st_nlink = 1 class BadRemoveRequest(Exception): pass @@ -160,15 +172,25 @@ return d if flags & FXF_WRITE: + # FIXME: this is too restrictive. If the file does not already + # exist, then FXF_CREAT and FXF_TRUNC should not be needed. + # This might cause us to fail to interoperate with clients other + # than /usr/bin/sftp. if not (flags & FXF_CREAT) or not (flags & FXF_TRUNC): raise NotImplementedError if not path: - raise PermissionError("cannot STOR to root directory") + raise PermissionError("cannot create file in root directory") + + # We are also incorrectly ignoring FXF_EXCL when the file does + # already exist (we should fail in that case, although it's + # impossible to implement that completely reliably if there + # are uncoordinated writes). + childname = path[-1] d = self._get_root(path) def _got_root((root, path)): if not path: - raise PermissionError("cannot STOR to root directory") + raise PermissionError("cannot create file in root directory") return root.get_child_at_path(path[:-1]) d.addCallback(_got_root) def _got_parent(parent): @@ -201,7 +223,8 @@ def makeDirectory(self, path, attrs): print "MAKEDIRECTORY", path, attrs - # TODO: extract attrs["mtime"], use it to set the parent metadata. + # TODO: extract attrs["mtime"] and attrs["createtime"], use them + # to set the parent metadata. # Maybe also copy attrs["ext_*"] . path = self._convert_sftp_path(path) d = self._get_root(path) @@ -212,7 +235,7 @@ def _get_or_create_directories(self, node, path): if not IDirectoryNode.providedBy(node): # unfortunately it is too late to provide the name of the - # blocking directory in the error message. + # blocking file in the error message. raise ExistingChildError("cannot create directory because there " "is a file in the way") # close enough if not path: @@ -259,30 +282,31 @@ def _render(children): results = [] for filename, (node, metadata) in children.iteritems(): - s = FakeStat() - if IDirectoryNode.providedBy(node): - s.st_mode = 040700 - s.st_size = 0 - else: - s.st_mode = 0100600 - s.st_size = node.get_size() - s.st_nlink = 1 - s.st_uid = 0 - s.st_gid = 0 - s.st_mtime = int(metadata.get("mtime", 0)) - longname = ls.lsLine(filename.encode("utf-8"), s) + # The file size may be cached or absent. attrs = self._populate_attrs(node, metadata) - results.append( (filename.encode("utf-8"), longname, attrs) ) + filename_utf8 = filename.encode("utf-8") + longname = ls.lsLine(filename_utf8, _FakeStat(attrs)) + results.append( (filename_utf8, longname, attrs) ) return StoppableList(results) d.addCallback(_render) return d def getAttrs(self, path, followLinks): print "GETATTRS", path, followLinks - # from ftp.stat d = self._get_node_and_metadata_for_path(self._convert_sftp_path(path)) - def _render((node,metadata)): - return self._populate_attrs(node, metadata) + def _render((node, metadata)): + # When asked about a specific file, report its current size. + # TODO: the modification time for a mutable file should be + # reported as the update time of the best version. But that + # information isn't currently stored in mutable shares, I think. + d2 = node.get_current_size() + def _got_size(size): + attrs = self._populate_attrs(node, metadata) + if size is not None: + attrs["size"] = size + return attrs + d2.addCallback(_got_size) + return d2 d.addCallback(_render) d.addErrback(self._convert_error) def _done(res): @@ -327,19 +351,53 @@ def _populate_attrs(self, childnode, metadata): attrs = {} - attrs["uid"] = 1000 - attrs["gid"] = 1000 - attrs["atime"] = 0 - attrs["mtime"] = int(metadata.get("mtime", 0)) - isdir = bool(IDirectoryNode.providedBy(childnode)) - if isdir: - attrs["size"] = 1 - # the permissions must have the extra bits (040000 or 0100000), - # otherwise the client will not call openDirectory - attrs["permissions"] = 040700 # S_IFDIR + + # see webapi.txt for what these times mean + if "tahoe" in metadata and "linkmotime" in metadata["tahoe"]: + attrs["mtime"] = int(metadata["tahoe"]["linkmotime"]) + elif "mtime" in metadata: + attrs["mtime"] = int(metadata["mtime"]) + + if "tahoe" in metadata and "linkcrtime" in metadata["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 have no size, and SFTP doesn't require us to make + # one up. For files and unknown nodes, omit the size if we don't + # immediately know it. + + if childnode.is_unknown(): + perms = 0 + elif IDirectoryNode.providedBy(childnode): + perms = 040777 # S_IFDIR else: - attrs["size"] = childnode.get_size() - attrs["permissions"] = 0100600 # S_IFREG + size = childnode.get_size() + if size is not None: + assert isinstance(size, (int, long)), repr(size) + attrs["size"] = size + perms = 0100666 # S_IFREG + + if not childnode.is_unknown() and childnode.is_readonly(): + perms &= 0140555 + + # We could set the SSH_FILEXFER_ATTR_FLAGS here: + # ENCRYPTED would always be true ("The file is stored on disk + # using file-system level transparent encryption.") + # SYSTEM, HIDDEN, ARCHIVE and SYNC would always be false. + # READONLY and IMMUTABLE would be set according to + # is_readonly() and is_immutable(). + # However, twisted.conch.ssh.filetransfer only implements + # SFTP version 3, which doesn't include these flags. + + attrs["permissions"] = perms return attrs def _convert_error(self, f):