]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/frontends/ftpd.py
SFTP/FTP: merge user/account code, merge docs
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / frontends / ftpd.py
1
2 import tempfile
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
9
10 from allmydata.interfaces import IDirectoryNode, ExistingChildError, \
11      NoSuchChildError
12 from allmydata.immutable.download import ConsumerAdapter
13 from allmydata.immutable.upload import FileHandle
14
15 class ReadFile:
16     implements(ftp.IReadFile)
17     def __init__(self, node):
18         self.node = node
19     def send(self, consumer):
20         ad = ConsumerAdapter(consumer)
21         d = self.node.download(ad)
22         return d # when consumed
23
24 class FileWriter:
25     implements(IConsumer)
26
27     def registerProducer(self, producer, streaming):
28         if not 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()
33         return None
34
35     def unregisterProducer(self):
36         # the upload actually happens in WriteFile.close()
37         pass
38
39     def write(self, data):
40         self.f.write(data)
41
42 class WriteFile:
43     implements(ftp.IWriteFile)
44
45     def __init__(self, parent, childname, convergence):
46         self.parent = parent
47         self.childname = childname
48         self.convergence = convergence
49
50     def receive(self):
51         self.c = FileWriter()
52         return defer.succeed(self.c)
53
54     def close(self):
55         u = FileHandle(self.c.f, self.convergence)
56         d = self.parent.add_file(self.childname, u)
57         return d
58
59
60 class NoParentError(Exception):
61     pass
62
63 class Handler:
64     implements(ftp.IFTPShell)
65     def __init__(self, client, rootnode, username, convergence):
66         self.client = client
67         self.root = rootnode
68         self.username = username
69         self.convergence = convergence
70
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))
75         return d
76
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")
83         if not path:
84             return defer.succeed(node)
85         d = node.get(path[0])
86         def _maybe_create(f):
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:])
91         return d
92
93     def _get_parent(self, path):
94         # fire with (parentnode, childname)
95         path = [unicode(p) for p in path]
96         if not path:
97             raise NoParentError
98         childname = path[-1]
99         d = self._get_root(path)
100         def _got_root((root, path)):
101             if not path:
102                 raise NoParentError
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)
108         return d
109
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)
126             return d
127         d.addCallback(_got_parent)
128         return d
129
130     def removeDirectory(self, path):
131         return self._remove_thing(path, must_be_directory=True)
132
133     def removeFile(self, path):
134         return self._remove_thing(path, must_be_file=True)
135
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,
144                                                    overwrite=False))
145             return d
146         d.addCallback(_got_from_parent)
147         d.addErrback(self._convert_error)
148         return d
149
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)
157         return d
158
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)
167         return f
168
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,
174                                     str(path[1]))
175             d.addCallback(lambda root: (root, path[2:]))
176         else:
177             d = defer.succeed((self.root,path))
178         return d
179
180     def _get_node_and_metadata_for_path(self, path):
181         d = self._get_root(path)
182         def _got_root((root,path)):
183             if path:
184                 return root.get_child_and_metadata_at_path(path)
185             else:
186                 return (root,{})
187         d.addCallback(_got_root)
188         return d
189
190     def _populate_row(self, keys, (childnode, metadata)):
191         values = []
192         isdir = bool(IDirectoryNode.providedBy(childnode))
193         for key in keys:
194             if key == "size":
195                 if isdir:
196                     value = 0
197                 else:
198                     value = childnode.get_size()
199             elif key == "directory":
200                 value = isdir
201             elif key == "permissions":
202                 value = 0600
203             elif key == "hardlinks":
204                 value = 1
205             elif key == "modified":
206                 value = metadata.get("mtime", 0)
207             elif key == "owner":
208                 value = self.username
209             elif key == "group":
210                 value = self.username
211             else:
212                 value = "??"
213             values.append(value)
214         return values
215
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)
224         return d
225
226     def list(self, path, keys=()):
227         # the interface claims that path is a list of unicodes, but in
228         # practice it is not
229         d = self._get_node_and_metadata_for_path(path)
230         def _list((node, metadata)):
231             if IDirectoryNode.providedBy(node):
232                 return node.list()
233             return { path[-1]: (node, metadata) } # need last-edge metadata
234         d.addCallback(_list)
235         def _render(children):
236             results = []
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
240                 # bytestring
241                 results.append( (name.encode("utf-8"),
242                                  self._populate_row(keys, childnode) ) )
243             return results
244         d.addCallback(_render)
245         d.addErrback(self._convert_error)
246         return d
247
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)
252         return d
253
254     def openForWriting(self, path):
255         path = [unicode(p) for p in path]
256         if not path:
257             raise ftp.PermissionDeniedError("cannot STOR to root directory")
258         childname = path[-1]
259         d = self._get_root(path)
260         def _got_root((root, path)):
261             if not 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)
268         return d
269
270 from auth import AccountURLChecker, AccountFileChecker
271
272
273 class Dispatcher:
274     implements(portal.IRealm)
275     def __init__(self, client):
276         self.client = client
277
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)
283         def logout(): pass
284         return (interface, s, None)
285
286
287 class FTPServer(service.MultiService):
288     def __init__(self, client, accountfile, accounturl, ftp_portstr):
289         service.MultiService.__init__(self)
290
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"
294
295         r = Dispatcher(client)
296         p = portal.Portal(r)
297
298         if accountfile:
299             c = AccountFileChecker(self, accountfile)
300             p.registerChecker(c)
301         if accounturl:
302             c = AccountURLChecker(self, accounturl)
303             p.registerChecker(c)
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")
307
308         f = ftp.FTPFactory(p)
309         s = strports.service(ftp_portstr, f)
310         s.setServiceParent(self)