3 from zope.interface import implements
4 from twisted.application import service, strports
5 from twisted.internet import defer
6 from twisted.internet.interfaces import IConsumer
7 from twisted.cred import portal
8 from twisted.protocols import ftp
10 from allmydata.interfaces import IDirectoryNode, ExistingChildError, \
12 from allmydata.immutable.download import ConsumerAdapter
13 from allmydata.immutable.upload import FileHandle
16 implements(ftp.IReadFile)
17 def __init__(self, node):
19 def send(self, consumer):
20 ad = ConsumerAdapter(consumer)
21 d = self.node.download(ad)
22 return d # when consumed
27 def registerProducer(self, producer, streaming):
29 raise NotImplementedError("Non-streaming producer not supported.")
30 # we write the data to a temporary file, since Tahoe can't do
31 # streaming upload yet.
32 self.f = tempfile.TemporaryFile()
35 def unregisterProducer(self):
36 # the upload actually happens in WriteFile.close()
39 def write(self, data):
43 implements(ftp.IWriteFile)
45 def __init__(self, parent, childname, convergence):
47 self.childname = childname
48 self.convergence = convergence
52 return defer.succeed(self.c)
55 u = FileHandle(self.c.f, self.convergence)
56 d = self.parent.add_file(self.childname, u)
60 class NoParentError(Exception):
64 implements(ftp.IFTPShell)
65 def __init__(self, client, rootnode, username, convergence):
68 self.username = username
69 self.convergence = convergence
71 def makeDirectory(self, path):
72 d = self._get_root(path)
73 d.addCallback(lambda (root,path):
74 self._get_or_create_directories(root, path))
77 def _get_or_create_directories(self, node, path):
78 if not IDirectoryNode.providedBy(node):
79 # unfortunately it is too late to provide the name of the
80 # blocking directory in the error message.
81 raise ftp.FileExistsError("cannot create directory because there "
82 "is a file in the way")
84 return defer.succeed(node)
87 f.trap(NoSuchChildError)
88 return node.create_empty_directory(path[0])
89 d.addErrback(_maybe_create)
90 d.addCallback(self._get_or_create_directories, path[1:])
93 def _get_parent(self, path):
94 # fire with (parentnode, childname)
95 path = [unicode(p) for p in path]
99 d = self._get_root(path)
100 def _got_root((root, path)):
103 return root.get_child_at_path(path[:-1])
104 d.addCallback(_got_root)
105 def _got_parent(parent):
106 return (parent, childname)
107 d.addCallback(_got_parent)
110 def _remove_thing(self, path, must_be_directory=False, must_be_file=False):
111 d = defer.maybeDeferred(self._get_parent, path)
112 def _convert_error(f):
113 f.trap(NoParentError)
114 raise ftp.PermissionDeniedError("cannot delete root directory")
115 d.addErrback(_convert_error)
116 def _got_parent( (parent, childname) ):
117 d = parent.get(childname)
118 def _got_child(child):
119 if must_be_directory and not IDirectoryNode.providedBy(child):
120 raise ftp.IsNotADirectoryError("rmdir called on a file")
121 if must_be_file and IDirectoryNode.providedBy(child):
122 raise ftp.IsADirectoryError("rmfile called on a directory")
123 return parent.delete(childname)
124 d.addCallback(_got_child)
125 d.addErrback(self._convert_error)
127 d.addCallback(_got_parent)
130 def removeDirectory(self, path):
131 return self._remove_thing(path, must_be_directory=True)
133 def removeFile(self, path):
134 return self._remove_thing(path, must_be_file=True)
136 def rename(self, fromPath, toPath):
137 # the target directory must already exist
138 d = self._get_parent(fromPath)
139 def _got_from_parent( (fromparent, childname) ):
140 d = self._get_parent(toPath)
141 d.addCallback(lambda (toparent, tochildname):
142 fromparent.move_child_to(childname,
143 toparent, tochildname,
146 d.addCallback(_got_from_parent)
147 d.addErrback(self._convert_error)
150 def access(self, path):
151 # we allow access to everything that exists. We are required to raise
152 # an error for paths that don't exist: FTP clients (at least ncftp)
153 # uses this to decide whether to mkdir or not.
154 d = self._get_node_and_metadata_for_path(path)
155 d.addErrback(self._convert_error)
156 d.addCallback(lambda res: None)
159 def _convert_error(self, f):
160 if f.check(NoSuchChildError):
161 childname = f.value.args[0].encode("utf-8")
162 msg = "'%s' doesn't exist" % childname
163 raise ftp.FileNotFoundError(msg)
164 if f.check(ExistingChildError):
165 msg = f.value.args[0].encode("utf-8")
166 raise ftp.FileExistsError(msg)
169 def _get_root(self, path):
170 # return (root, remaining_path)
171 path = [unicode(p) for p in path]
172 if path and path[0] == "uri":
173 d = defer.maybeDeferred(self.client.create_node_from_uri,
175 d.addCallback(lambda root: (root, path[2:]))
177 d = defer.succeed((self.root,path))
180 def _get_node_and_metadata_for_path(self, path):
181 d = self._get_root(path)
182 def _got_root((root,path)):
184 return root.get_child_and_metadata_at_path(path)
187 d.addCallback(_got_root)
190 def _populate_row(self, keys, (childnode, metadata)):
192 isdir = bool(IDirectoryNode.providedBy(childnode))
198 value = childnode.get_size()
199 elif key == "directory":
201 elif key == "permissions":
203 elif key == "hardlinks":
205 elif key == "modified":
206 value = metadata.get("mtime", 0)
208 value = self.username
210 value = self.username
216 def stat(self, path, keys=()):
217 # for files only, I think
218 d = self._get_node_and_metadata_for_path(path)
219 def _render((node,metadata)):
220 assert not IDirectoryNode.providedBy(node)
221 return self._populate_row(keys, (node,metadata))
222 d.addCallback(_render)
223 d.addErrback(self._convert_error)
226 def list(self, path, keys=()):
227 # the interface claims that path is a list of unicodes, but in
229 d = self._get_node_and_metadata_for_path(path)
230 def _list((node, metadata)):
231 if IDirectoryNode.providedBy(node):
233 return { path[-1]: (node, metadata) } # need last-edge metadata
235 def _render(children):
237 for (name, childnode) in children.iteritems():
238 # the interface claims that the result should have a unicode
239 # object as the name, but it fails unless you give it a
241 results.append( (name.encode("utf-8"),
242 self._populate_row(keys, childnode) ) )
244 d.addCallback(_render)
245 d.addErrback(self._convert_error)
248 def openForReading(self, path):
249 d = self._get_node_and_metadata_for_path(path)
250 d.addCallback(lambda (node,metadata): ReadFile(node))
251 d.addErrback(self._convert_error)
254 def openForWriting(self, path):
255 path = [unicode(p) for p in path]
257 raise ftp.PermissionDeniedError("cannot STOR to root directory")
259 d = self._get_root(path)
260 def _got_root((root, path)):
262 raise ftp.PermissionDeniedError("cannot STOR to root directory")
263 return root.get_child_at_path(path[:-1])
264 d.addCallback(_got_root)
265 def _got_parent(parent):
266 return WriteFile(parent, childname, self.convergence)
267 d.addCallback(_got_parent)
270 from auth import AccountURLChecker, AccountFileChecker
274 implements(portal.IRealm)
275 def __init__(self, client):
278 def requestAvatar(self, avatarID, mind, interface):
279 assert interface == ftp.IFTPShell
280 rootnode = self.client.create_node_from_uri(avatarID.rootcap)
281 convergence = self.client.convergence
282 s = Handler(self.client, rootnode, avatarID.username, convergence)
284 return (interface, s, None)
287 class FTPServer(service.MultiService):
288 def __init__(self, client, accountfile, accounturl, ftp_portstr):
289 service.MultiService.__init__(self)
291 # make sure we're using a patched Twisted that uses IWriteFile.close:
292 # see docs/frontends/ftp.txt for details.
293 assert "close" in ftp.IWriteFile.names(), "your twisted is lacking"
295 r = Dispatcher(client)
299 c = AccountFileChecker(self, accountfile)
302 c = AccountURLChecker(self, accounturl)
304 if not accountfile and not accounturl:
305 # we could leave this anonymous, with just the /uri/CAP form
306 raise RuntimeError("must provide some translation")
308 f = ftp.FTPFactory(p)
309 s = strports.service(ftp_portstr, f)
310 s.setServiceParent(self)