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.protocols import ftp
11 from allmydata.interfaces import IDirectoryNode, ExistingChildError, \
13 from allmydata.immutable.upload import FileHandle
14 from allmydata.util.fileutil import EncryptedTemporaryFile
15 from allmydata.util.assertutil import precondition
18 implements(ftp.IReadFile)
19 def __init__(self, node):
21 def send(self, consumer):
22 d = self.node.read(consumer)
23 return d # when consumed
28 def registerProducer(self, producer, streaming):
30 raise NotImplementedError("Non-streaming producer not supported.")
31 # we write the data to a temporary file, since Tahoe can't do
32 # streaming upload yet.
33 self.f = EncryptedTemporaryFile()
36 def unregisterProducer(self):
37 # the upload actually happens in WriteFile.close()
40 def write(self, data):
44 implements(ftp.IWriteFile)
46 def __init__(self, parent, childname, convergence):
48 self.childname = childname
49 self.convergence = convergence
53 return defer.succeed(self.c)
56 u = FileHandle(self.c.f, self.convergence)
57 d = self.parent.add_file(self.childname, u)
61 class NoParentError(Exception):
65 implements(ftp.IFTPShell)
66 def __init__(self, client, rootnode, username, convergence):
69 self.username = username
70 self.convergence = convergence
72 def makeDirectory(self, path):
73 d = self._get_root(path)
74 d.addCallback(lambda (root,path):
75 self._get_or_create_directories(root, path))
78 def _get_or_create_directories(self, node, path):
79 if not IDirectoryNode.providedBy(node):
80 # unfortunately it is too late to provide the name of the
81 # blocking directory in the error message.
82 raise ftp.FileExistsError("cannot create directory because there "
83 "is a file in the way")
85 return defer.succeed(node)
88 f.trap(NoSuchChildError)
89 return node.create_subdirectory(path[0])
90 d.addErrback(_maybe_create)
91 d.addCallback(self._get_or_create_directories, path[1:])
94 def _get_parent(self, path):
95 # fire with (parentnode, childname)
96 path = [unicode(p) for p in path]
100 d = self._get_root(path)
101 def _got_root((root, path)):
104 return root.get_child_at_path(path[:-1])
105 d.addCallback(_got_root)
106 def _got_parent(parent):
107 return (parent, childname)
108 d.addCallback(_got_parent)
111 def _remove_thing(self, path, must_be_directory=False, must_be_file=False):
112 d = defer.maybeDeferred(self._get_parent, path)
113 def _convert_error(f):
114 f.trap(NoParentError)
115 raise ftp.PermissionDeniedError("cannot delete root directory")
116 d.addErrback(_convert_error)
117 def _got_parent( (parent, childname) ):
118 d = parent.get(childname)
119 def _got_child(child):
120 if must_be_directory and not IDirectoryNode.providedBy(child):
121 raise ftp.IsNotADirectoryError("rmdir called on a file")
122 if must_be_file and IDirectoryNode.providedBy(child):
123 raise ftp.IsADirectoryError("rmfile called on a directory")
124 return parent.delete(childname)
125 d.addCallback(_got_child)
126 d.addErrback(self._convert_error)
128 d.addCallback(_got_parent)
131 def removeDirectory(self, path):
132 return self._remove_thing(path, must_be_directory=True)
134 def removeFile(self, path):
135 return self._remove_thing(path, must_be_file=True)
137 def rename(self, fromPath, toPath):
138 # the target directory must already exist
139 d = self._get_parent(fromPath)
140 def _got_from_parent( (fromparent, childname) ):
141 d = self._get_parent(toPath)
142 d.addCallback(lambda (toparent, tochildname):
143 fromparent.move_child_to(childname,
144 toparent, tochildname,
147 d.addCallback(_got_from_parent)
148 d.addErrback(self._convert_error)
151 def access(self, path):
152 # we allow access to everything that exists. We are required to raise
153 # an error for paths that don't exist: FTP clients (at least ncftp)
154 # uses this to decide whether to mkdir or not.
155 d = self._get_node_and_metadata_for_path(path)
156 d.addErrback(self._convert_error)
157 d.addCallback(lambda res: None)
160 def _convert_error(self, f):
161 if f.check(NoSuchChildError):
162 childname = f.value.args[0].encode("utf-8")
163 msg = "'%s' doesn't exist" % childname
164 raise ftp.FileNotFoundError(msg)
165 if f.check(ExistingChildError):
166 msg = f.value.args[0].encode("utf-8")
167 raise ftp.FileExistsError(msg)
170 def _get_root(self, path):
171 # return (root, remaining_path)
172 path = [unicode(p) for p in path]
173 if path and path[0] == "uri":
174 d = defer.maybeDeferred(self.client.create_node_from_uri,
176 d.addCallback(lambda root: (root, path[2:]))
178 d = defer.succeed((self.root,path))
181 def _get_node_and_metadata_for_path(self, path):
182 d = self._get_root(path)
183 def _got_root((root,path)):
185 return root.get_child_and_metadata_at_path(path)
188 d.addCallback(_got_root)
191 def _populate_row(self, keys, (childnode, metadata)):
193 isdir = bool(IDirectoryNode.providedBy(childnode))
199 value = childnode.get_size() or 0
200 elif key == "directory":
202 elif key == "permissions":
204 elif key == "hardlinks":
206 elif key == "modified":
207 # follow sftpd convention (i.e. linkmotime in preference to mtime)
208 if "linkmotime" in metadata.get("tahoe", {}):
209 value = metadata["tahoe"]["linkmotime"]
211 value = metadata.get("mtime", 0)
213 value = self.username
215 value = self.username
221 def stat(self, path, keys=()):
222 # for files only, I think
223 d = self._get_node_and_metadata_for_path(path)
224 def _render((node,metadata)):
225 assert not IDirectoryNode.providedBy(node)
226 return self._populate_row(keys, (node,metadata))
227 d.addCallback(_render)
228 d.addErrback(self._convert_error)
231 def list(self, path, keys=()):
232 # the interface claims that path is a list of unicodes, but in
234 d = self._get_node_and_metadata_for_path(path)
235 def _list((node, metadata)):
236 if IDirectoryNode.providedBy(node):
238 return { path[-1]: (node, metadata) } # need last-edge metadata
240 def _render(children):
242 for (name, childnode) in children.iteritems():
243 # the interface claims that the result should have a unicode
244 # object as the name, but it fails unless you give it a
246 results.append( (name.encode("utf-8"),
247 self._populate_row(keys, childnode) ) )
249 d.addCallback(_render)
250 d.addErrback(self._convert_error)
253 def openForReading(self, path):
254 d = self._get_node_and_metadata_for_path(path)
255 d.addCallback(lambda (node,metadata): ReadFile(node))
256 d.addErrback(self._convert_error)
259 def openForWriting(self, path):
260 path = [unicode(p) for p in path]
262 raise ftp.PermissionDeniedError("cannot STOR to root directory")
264 d = self._get_root(path)
265 def _got_root((root, path)):
267 raise ftp.PermissionDeniedError("cannot STOR to root directory")
268 return root.get_child_at_path(path[:-1])
269 d.addCallback(_got_root)
270 def _got_parent(parent):
271 return WriteFile(parent, childname, self.convergence)
272 d.addCallback(_got_parent)
275 from allmydata.frontends.auth import AccountURLChecker, AccountFileChecker, NeedRootcapLookupScheme
279 implements(portal.IRealm)
280 def __init__(self, client):
283 def requestAvatar(self, avatarID, mind, interface):
284 assert interface == ftp.IFTPShell
285 rootnode = self.client.create_node_from_uri(avatarID.rootcap)
286 convergence = self.client.convergence
287 s = Handler(self.client, rootnode, avatarID.username, convergence)
289 return (interface, s, None)
292 class FTPServer(service.MultiService):
293 def __init__(self, client, accountfile, accounturl, ftp_portstr):
294 precondition(isinstance(accountfile, (unicode, NoneType)), accountfile)
295 service.MultiService.__init__(self)
297 r = Dispatcher(client)
301 c = AccountFileChecker(self, accountfile)
304 c = AccountURLChecker(self, accounturl)
306 if not accountfile and not accounturl:
307 # we could leave this anonymous, with just the /uri/CAP form
308 raise NeedRootcapLookupScheme("must provide some translation")
310 f = ftp.FTPFactory(p)
311 s = strports.service(ftp_portstr, f)
312 s.setServiceParent(self)