2 from types import NoneType
4 from zope.interface import implements
5 from twisted.application import service, strports
6 from twisted.internet import defer
7 from twisted.internet.interfaces import IConsumer
8 from twisted.cred import portal
9 from twisted.python import filepath
10 from twisted.protocols import ftp
12 from allmydata.interfaces import IDirectoryNode, ExistingChildError, \
14 from allmydata.immutable.upload import FileHandle
15 from allmydata.util.fileutil import EncryptedTemporaryFile
16 from allmydata.util.assertutil import precondition
19 implements(ftp.IReadFile)
20 def __init__(self, node):
22 def send(self, consumer):
23 d = self.node.read(consumer)
24 return d # when consumed
29 def registerProducer(self, producer, streaming):
31 raise NotImplementedError("Non-streaming producer not supported.")
32 # we write the data to a temporary file, since Tahoe can't do
33 # streaming upload yet.
34 self.f = EncryptedTemporaryFile()
37 def unregisterProducer(self):
38 # the upload actually happens in WriteFile.close()
41 def write(self, data):
45 implements(ftp.IWriteFile)
47 def __init__(self, parent, childname, convergence):
49 self.childname = childname
50 self.convergence = convergence
54 return defer.succeed(self.c)
57 u = FileHandle(self.c.f, self.convergence)
58 d = self.parent.add_file(self.childname, u)
62 class NoParentError(Exception):
65 if hasattr(filepath, "Permissions"):
66 # filepath.Permissions was added in Twisted-11.1.0, but we're compatible
67 # back to 11.0.0 (on windows). Fortunately we don't really need to
68 # provide anything more than an int until Twisted-15.0.0 .
69 class IntishPermissions(filepath.Permissions):
70 def __init__(self, statModeInt):
71 self.statModeInt = statModeInt
72 filepath.Permissions.__init__(self, statModeInt)
73 def __and__(self, other):
74 return self.statModeInt & other
76 IntishPermissions = lambda statModeInt: statModeInt
79 implements(ftp.IFTPShell)
80 def __init__(self, client, rootnode, username, convergence):
83 self.username = username
84 self.convergence = convergence
86 def makeDirectory(self, path):
87 d = self._get_root(path)
88 d.addCallback(lambda (root,path):
89 self._get_or_create_directories(root, path))
92 def _get_or_create_directories(self, node, path):
93 if not IDirectoryNode.providedBy(node):
94 # unfortunately it is too late to provide the name of the
95 # blocking directory in the error message.
96 raise ftp.FileExistsError("cannot create directory because there "
97 "is a file in the way")
99 return defer.succeed(node)
100 d = node.get(path[0])
101 def _maybe_create(f):
102 f.trap(NoSuchChildError)
103 return node.create_subdirectory(path[0])
104 d.addErrback(_maybe_create)
105 d.addCallback(self._get_or_create_directories, path[1:])
108 def _get_parent(self, path):
109 # fire with (parentnode, childname)
110 path = [unicode(p) for p in path]
114 d = self._get_root(path)
115 def _got_root((root, path)):
118 return root.get_child_at_path(path[:-1])
119 d.addCallback(_got_root)
120 def _got_parent(parent):
121 return (parent, childname)
122 d.addCallback(_got_parent)
125 def _remove_thing(self, path, must_be_directory=False, must_be_file=False):
126 d = defer.maybeDeferred(self._get_parent, path)
127 def _convert_error(f):
128 f.trap(NoParentError)
129 raise ftp.PermissionDeniedError("cannot delete root directory")
130 d.addErrback(_convert_error)
131 def _got_parent( (parent, childname) ):
132 d = parent.get(childname)
133 def _got_child(child):
134 if must_be_directory and not IDirectoryNode.providedBy(child):
135 raise ftp.IsNotADirectoryError("rmdir called on a file")
136 if must_be_file and IDirectoryNode.providedBy(child):
137 raise ftp.IsADirectoryError("rmfile called on a directory")
138 return parent.delete(childname)
139 d.addCallback(_got_child)
140 d.addErrback(self._convert_error)
142 d.addCallback(_got_parent)
145 def removeDirectory(self, path):
146 return self._remove_thing(path, must_be_directory=True)
148 def removeFile(self, path):
149 return self._remove_thing(path, must_be_file=True)
151 def rename(self, fromPath, toPath):
152 # the target directory must already exist
153 d = self._get_parent(fromPath)
154 def _got_from_parent( (fromparent, childname) ):
155 d = self._get_parent(toPath)
156 d.addCallback(lambda (toparent, tochildname):
157 fromparent.move_child_to(childname,
158 toparent, tochildname,
161 d.addCallback(_got_from_parent)
162 d.addErrback(self._convert_error)
165 def access(self, path):
166 # we allow access to everything that exists. We are required to raise
167 # an error for paths that don't exist: FTP clients (at least ncftp)
168 # uses this to decide whether to mkdir or not.
169 d = self._get_node_and_metadata_for_path(path)
170 d.addErrback(self._convert_error)
171 d.addCallback(lambda res: None)
174 def _convert_error(self, f):
175 if f.check(NoSuchChildError):
176 childname = f.value.args[0].encode("utf-8")
177 msg = "'%s' doesn't exist" % childname
178 raise ftp.FileNotFoundError(msg)
179 if f.check(ExistingChildError):
180 msg = f.value.args[0].encode("utf-8")
181 raise ftp.FileExistsError(msg)
184 def _get_root(self, path):
185 # return (root, remaining_path)
186 path = [unicode(p) for p in path]
187 if path and path[0] == "uri":
188 d = defer.maybeDeferred(self.client.create_node_from_uri,
190 d.addCallback(lambda root: (root, path[2:]))
192 d = defer.succeed((self.root,path))
195 def _get_node_and_metadata_for_path(self, path):
196 d = self._get_root(path)
197 def _got_root((root,path)):
199 return root.get_child_and_metadata_at_path(path)
202 d.addCallback(_got_root)
205 def _populate_row(self, keys, (childnode, metadata)):
207 isdir = bool(IDirectoryNode.providedBy(childnode))
213 value = childnode.get_size() or 0
214 elif key == "directory":
216 elif key == "permissions":
217 # Twisted-14.0.2 expected an int, and used it in a rendering
218 # function that did (mode & NUMBER). Twisted-15.0.0 expects a
219 # twisted.python.filepath.Permissions , and calls its
220 # .shorthand() method. Try to provide both.
221 value = IntishPermissions(0600)
222 elif key == "hardlinks":
224 elif key == "modified":
225 # follow sftpd convention (i.e. linkmotime in preference to mtime)
226 if "linkmotime" in metadata.get("tahoe", {}):
227 value = metadata["tahoe"]["linkmotime"]
229 value = metadata.get("mtime", 0)
231 value = self.username
233 value = self.username
239 def stat(self, path, keys=()):
240 # for files only, I think
241 d = self._get_node_and_metadata_for_path(path)
242 def _render((node,metadata)):
243 assert not IDirectoryNode.providedBy(node)
244 return self._populate_row(keys, (node,metadata))
245 d.addCallback(_render)
246 d.addErrback(self._convert_error)
249 def list(self, path, keys=()):
250 # the interface claims that path is a list of unicodes, but in
252 d = self._get_node_and_metadata_for_path(path)
253 def _list((node, metadata)):
254 if IDirectoryNode.providedBy(node):
256 return { path[-1]: (node, metadata) } # need last-edge metadata
258 def _render(children):
260 for (name, childnode) in children.iteritems():
261 # the interface claims that the result should have a unicode
262 # object as the name, but it fails unless you give it a
264 results.append( (name.encode("utf-8"),
265 self._populate_row(keys, childnode) ) )
267 d.addCallback(_render)
268 d.addErrback(self._convert_error)
271 def openForReading(self, path):
272 d = self._get_node_and_metadata_for_path(path)
273 d.addCallback(lambda (node,metadata): ReadFile(node))
274 d.addErrback(self._convert_error)
277 def openForWriting(self, path):
278 path = [unicode(p) for p in path]
280 raise ftp.PermissionDeniedError("cannot STOR to root directory")
282 d = self._get_root(path)
283 def _got_root((root, path)):
285 raise ftp.PermissionDeniedError("cannot STOR to root directory")
286 return root.get_child_at_path(path[:-1])
287 d.addCallback(_got_root)
288 def _got_parent(parent):
289 return WriteFile(parent, childname, self.convergence)
290 d.addCallback(_got_parent)
293 from allmydata.frontends.auth import AccountURLChecker, AccountFileChecker, NeedRootcapLookupScheme
297 implements(portal.IRealm)
298 def __init__(self, client):
301 def requestAvatar(self, avatarID, mind, interface):
302 assert interface == ftp.IFTPShell
303 rootnode = self.client.create_node_from_uri(avatarID.rootcap)
304 convergence = self.client.convergence
305 s = Handler(self.client, rootnode, avatarID.username, convergence)
307 return (interface, s, None)
310 class FTPServer(service.MultiService):
311 def __init__(self, client, accountfile, accounturl, ftp_portstr):
312 precondition(isinstance(accountfile, (unicode, NoneType)), accountfile)
313 service.MultiService.__init__(self)
315 r = Dispatcher(client)
319 c = AccountFileChecker(self, accountfile)
322 c = AccountURLChecker(self, accounturl)
324 if not accountfile and not accounturl:
325 # we could leave this anonymous, with just the /uri/CAP form
326 raise NeedRootcapLookupScheme("must provide some translation")
328 f = ftp.FTPFactory(p)
329 s = strports.service(ftp_portstr, f)
330 s.setServiceParent(self)