]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/frontends/ftpd.py
9922a86ad8b38429c1626af65b770f5eea80f364
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / frontends / ftpd.py
1
2 from types import NoneType
3
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
10
11 from allmydata.interfaces import IDirectoryNode, ExistingChildError, \
12      NoSuchChildError
13 from allmydata.immutable.upload import FileHandle
14 from allmydata.util.fileutil import EncryptedTemporaryFile
15 from allmydata.util.assertutil import precondition
16
17 class ReadFile:
18     implements(ftp.IReadFile)
19     def __init__(self, node):
20         self.node = node
21     def send(self, consumer):
22         d = self.node.read(consumer)
23         return d # when consumed
24
25 class FileWriter:
26     implements(IConsumer)
27
28     def registerProducer(self, producer, streaming):
29         if not 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()
34         return None
35
36     def unregisterProducer(self):
37         # the upload actually happens in WriteFile.close()
38         pass
39
40     def write(self, data):
41         self.f.write(data)
42
43 class WriteFile:
44     implements(ftp.IWriteFile)
45
46     def __init__(self, parent, childname, convergence):
47         self.parent = parent
48         self.childname = childname
49         self.convergence = convergence
50
51     def receive(self):
52         self.c = FileWriter()
53         return defer.succeed(self.c)
54
55     def close(self):
56         u = FileHandle(self.c.f, self.convergence)
57         d = self.parent.add_file(self.childname, u)
58         return d
59
60
61 class NoParentError(Exception):
62     pass
63
64 class Handler:
65     implements(ftp.IFTPShell)
66     def __init__(self, client, rootnode, username, convergence):
67         self.client = client
68         self.root = rootnode
69         self.username = username
70         self.convergence = convergence
71
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))
76         return d
77
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")
84         if not path:
85             return defer.succeed(node)
86         d = node.get(path[0])
87         def _maybe_create(f):
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:])
92         return d
93
94     def _get_parent(self, path):
95         # fire with (parentnode, childname)
96         path = [unicode(p) for p in path]
97         if not path:
98             raise NoParentError
99         childname = path[-1]
100         d = self._get_root(path)
101         def _got_root((root, path)):
102             if not path:
103                 raise NoParentError
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)
109         return d
110
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)
127             return d
128         d.addCallback(_got_parent)
129         return d
130
131     def removeDirectory(self, path):
132         return self._remove_thing(path, must_be_directory=True)
133
134     def removeFile(self, path):
135         return self._remove_thing(path, must_be_file=True)
136
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,
145                                                    overwrite=False))
146             return d
147         d.addCallback(_got_from_parent)
148         d.addErrback(self._convert_error)
149         return d
150
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)
158         return d
159
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)
168         return f
169
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,
175                                     str(path[1]))
176             d.addCallback(lambda root: (root, path[2:]))
177         else:
178             d = defer.succeed((self.root,path))
179         return d
180
181     def _get_node_and_metadata_for_path(self, path):
182         d = self._get_root(path)
183         def _got_root((root,path)):
184             if path:
185                 return root.get_child_and_metadata_at_path(path)
186             else:
187                 return (root,{})
188         d.addCallback(_got_root)
189         return d
190
191     def _populate_row(self, keys, (childnode, metadata)):
192         values = []
193         isdir = bool(IDirectoryNode.providedBy(childnode))
194         for key in keys:
195             if key == "size":
196                 if isdir:
197                     value = 0
198                 else:
199                     value = childnode.get_size() or 0
200             elif key == "directory":
201                 value = isdir
202             elif key == "permissions":
203                 value = 0600
204             elif key == "hardlinks":
205                 value = 1
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"]
210                 else:
211                     value = metadata.get("mtime", 0)
212             elif key == "owner":
213                 value = self.username
214             elif key == "group":
215                 value = self.username
216             else:
217                 value = "??"
218             values.append(value)
219         return values
220
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)
229         return d
230
231     def list(self, path, keys=()):
232         # the interface claims that path is a list of unicodes, but in
233         # practice it is not
234         d = self._get_node_and_metadata_for_path(path)
235         def _list((node, metadata)):
236             if IDirectoryNode.providedBy(node):
237                 return node.list()
238             return { path[-1]: (node, metadata) } # need last-edge metadata
239         d.addCallback(_list)
240         def _render(children):
241             results = []
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
245                 # bytestring
246                 results.append( (name.encode("utf-8"),
247                                  self._populate_row(keys, childnode) ) )
248             return results
249         d.addCallback(_render)
250         d.addErrback(self._convert_error)
251         return d
252
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)
257         return d
258
259     def openForWriting(self, path):
260         path = [unicode(p) for p in path]
261         if not path:
262             raise ftp.PermissionDeniedError("cannot STOR to root directory")
263         childname = path[-1]
264         d = self._get_root(path)
265         def _got_root((root, path)):
266             if not 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)
273         return d
274
275 from allmydata.frontends.auth import AccountURLChecker, AccountFileChecker, NeedRootcapLookupScheme
276
277
278 class Dispatcher:
279     implements(portal.IRealm)
280     def __init__(self, client):
281         self.client = client
282
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)
288         def logout(): pass
289         return (interface, s, None)
290
291
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)
296
297         r = Dispatcher(client)
298         p = portal.Portal(r)
299
300         if accountfile:
301             c = AccountFileChecker(self, accountfile)
302             p.registerChecker(c)
303         if accounturl:
304             c = AccountURLChecker(self, accounturl)
305             p.registerChecker(c)
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")
309
310         f = ftp.FTPFactory(p)
311         s = strports.service(ftp_portstr, f)
312         s.setServiceParent(self)