2 from zope.interface import implements
3 from twisted.application import service, strports
4 from twisted.internet import defer
5 from twisted.internet.interfaces import IConsumer
6 from twisted.cred import portal
7 from twisted.protocols import ftp
9 from allmydata.interfaces import IDirectoryNode, ExistingChildError, \
11 from allmydata.immutable.upload import FileHandle
12 from allmydata.util.fileutil import EncryptedTemporaryFile
15 implements(ftp.IReadFile)
16 def __init__(self, node):
18 def send(self, consumer):
19 d = self.node.read(consumer)
20 return d # when consumed
25 def registerProducer(self, producer, streaming):
27 raise NotImplementedError("Non-streaming producer not supported.")
28 # we write the data to a temporary file, since Tahoe can't do
29 # streaming upload yet.
30 self.f = EncryptedTemporaryFile()
33 def unregisterProducer(self):
34 # the upload actually happens in WriteFile.close()
37 def write(self, data):
41 implements(ftp.IWriteFile)
43 def __init__(self, parent, childname, convergence):
45 self.childname = childname
46 self.convergence = convergence
50 return defer.succeed(self.c)
53 u = FileHandle(self.c.f, self.convergence)
54 d = self.parent.add_file(self.childname, u)
58 class NoParentError(Exception):
62 implements(ftp.IFTPShell)
63 def __init__(self, client, rootnode, username, convergence):
66 self.username = username
67 self.convergence = convergence
69 def makeDirectory(self, path):
70 d = self._get_root(path)
71 d.addCallback(lambda (root,path):
72 self._get_or_create_directories(root, path))
75 def _get_or_create_directories(self, node, path):
76 if not IDirectoryNode.providedBy(node):
77 # unfortunately it is too late to provide the name of the
78 # blocking directory in the error message.
79 raise ftp.FileExistsError("cannot create directory because there "
80 "is a file in the way")
82 return defer.succeed(node)
85 f.trap(NoSuchChildError)
86 return node.create_subdirectory(path[0])
87 d.addErrback(_maybe_create)
88 d.addCallback(self._get_or_create_directories, path[1:])
91 def _get_parent(self, path):
92 # fire with (parentnode, childname)
93 path = [unicode(p) for p in path]
97 d = self._get_root(path)
98 def _got_root((root, path)):
101 return root.get_child_at_path(path[:-1])
102 d.addCallback(_got_root)
103 def _got_parent(parent):
104 return (parent, childname)
105 d.addCallback(_got_parent)
108 def _remove_thing(self, path, must_be_directory=False, must_be_file=False):
109 d = defer.maybeDeferred(self._get_parent, path)
110 def _convert_error(f):
111 f.trap(NoParentError)
112 raise ftp.PermissionDeniedError("cannot delete root directory")
113 d.addErrback(_convert_error)
114 def _got_parent( (parent, childname) ):
115 d = parent.get(childname)
116 def _got_child(child):
117 if must_be_directory and not IDirectoryNode.providedBy(child):
118 raise ftp.IsNotADirectoryError("rmdir called on a file")
119 if must_be_file and IDirectoryNode.providedBy(child):
120 raise ftp.IsADirectoryError("rmfile called on a directory")
121 return parent.delete(childname)
122 d.addCallback(_got_child)
123 d.addErrback(self._convert_error)
125 d.addCallback(_got_parent)
128 def removeDirectory(self, path):
129 return self._remove_thing(path, must_be_directory=True)
131 def removeFile(self, path):
132 return self._remove_thing(path, must_be_file=True)
134 def rename(self, fromPath, toPath):
135 # the target directory must already exist
136 d = self._get_parent(fromPath)
137 def _got_from_parent( (fromparent, childname) ):
138 d = self._get_parent(toPath)
139 d.addCallback(lambda (toparent, tochildname):
140 fromparent.move_child_to(childname,
141 toparent, tochildname,
144 d.addCallback(_got_from_parent)
145 d.addErrback(self._convert_error)
148 def access(self, path):
149 # we allow access to everything that exists. We are required to raise
150 # an error for paths that don't exist: FTP clients (at least ncftp)
151 # uses this to decide whether to mkdir or not.
152 d = self._get_node_and_metadata_for_path(path)
153 d.addErrback(self._convert_error)
154 d.addCallback(lambda res: None)
157 def _convert_error(self, f):
158 if f.check(NoSuchChildError):
159 childname = f.value.args[0].encode("utf-8")
160 msg = "'%s' doesn't exist" % childname
161 raise ftp.FileNotFoundError(msg)
162 if f.check(ExistingChildError):
163 msg = f.value.args[0].encode("utf-8")
164 raise ftp.FileExistsError(msg)
167 def _get_root(self, path):
168 # return (root, remaining_path)
169 path = [unicode(p) for p in path]
170 if path and path[0] == "uri":
171 d = defer.maybeDeferred(self.client.create_node_from_uri,
173 d.addCallback(lambda root: (root, path[2:]))
175 d = defer.succeed((self.root,path))
178 def _get_node_and_metadata_for_path(self, path):
179 d = self._get_root(path)
180 def _got_root((root,path)):
182 return root.get_child_and_metadata_at_path(path)
185 d.addCallback(_got_root)
188 def _populate_row(self, keys, (childnode, metadata)):
190 isdir = bool(IDirectoryNode.providedBy(childnode))
196 value = childnode.get_size() or 0
197 elif key == "directory":
199 elif key == "permissions":
201 elif key == "hardlinks":
203 elif key == "modified":
204 # follow sftpd convention (i.e. linkmotime in preference to mtime)
205 if "linkmotime" in metadata.get("tahoe", {}):
206 value = metadata["tahoe"]["linkmotime"]
208 value = metadata.get("mtime", 0)
210 value = self.username
212 value = self.username
218 def stat(self, path, keys=()):
219 # for files only, I think
220 d = self._get_node_and_metadata_for_path(path)
221 def _render((node,metadata)):
222 assert not IDirectoryNode.providedBy(node)
223 return self._populate_row(keys, (node,metadata))
224 d.addCallback(_render)
225 d.addErrback(self._convert_error)
228 def list(self, path, keys=()):
229 # the interface claims that path is a list of unicodes, but in
231 d = self._get_node_and_metadata_for_path(path)
232 def _list((node, metadata)):
233 if IDirectoryNode.providedBy(node):
235 return { path[-1]: (node, metadata) } # need last-edge metadata
237 def _render(children):
239 for (name, childnode) in children.iteritems():
240 # the interface claims that the result should have a unicode
241 # object as the name, but it fails unless you give it a
243 results.append( (name.encode("utf-8"),
244 self._populate_row(keys, childnode) ) )
246 d.addCallback(_render)
247 d.addErrback(self._convert_error)
250 def openForReading(self, path):
251 d = self._get_node_and_metadata_for_path(path)
252 d.addCallback(lambda (node,metadata): ReadFile(node))
253 d.addErrback(self._convert_error)
256 def openForWriting(self, path):
257 path = [unicode(p) for p in path]
259 raise ftp.PermissionDeniedError("cannot STOR to root directory")
261 d = self._get_root(path)
262 def _got_root((root, path)):
264 raise ftp.PermissionDeniedError("cannot STOR to root directory")
265 return root.get_child_at_path(path[:-1])
266 d.addCallback(_got_root)
267 def _got_parent(parent):
268 return WriteFile(parent, childname, self.convergence)
269 d.addCallback(_got_parent)
272 from allmydata.frontends.auth import AccountURLChecker, AccountFileChecker, NeedRootcapLookupScheme
276 implements(portal.IRealm)
277 def __init__(self, client):
280 def requestAvatar(self, avatarID, mind, interface):
281 assert interface == ftp.IFTPShell
282 rootnode = self.client.create_node_from_uri(avatarID.rootcap)
283 convergence = self.client.convergence
284 s = Handler(self.client, rootnode, avatarID.username, convergence)
286 return (interface, s, None)
289 class FTPServer(service.MultiService):
290 def __init__(self, client, accountfile, accounturl, ftp_portstr):
291 precondition(isinstance(accountfile, unicode), accountfile)
292 service.MultiService.__init__(self)
294 r = Dispatcher(client)
298 c = AccountFileChecker(self, accountfile)
301 c = AccountURLChecker(self, accounturl)
303 if not accountfile and not accounturl:
304 # we could leave this anonymous, with just the /uri/CAP form
305 raise NeedRootcapLookupScheme("must provide some translation")
307 f = ftp.FTPFactory(p)
308 s = strports.service(ftp_portstr, f)
309 s.setServiceParent(self)