]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/frontends/ftpd.py
Change relative imports to absolute
[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.upload import FileHandle
13
14 class ReadFile:
15     implements(ftp.IReadFile)
16     def __init__(self, node):
17         self.node = node
18     def send(self, consumer):
19         d = self.node.read(consumer)
20         return d # when consumed
21
22 class FileWriter:
23     implements(IConsumer)
24
25     def registerProducer(self, producer, streaming):
26         if not 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 = tempfile.TemporaryFile()
31         return None
32
33     def unregisterProducer(self):
34         # the upload actually happens in WriteFile.close()
35         pass
36
37     def write(self, data):
38         self.f.write(data)
39
40 class WriteFile:
41     implements(ftp.IWriteFile)
42
43     def __init__(self, parent, childname, convergence):
44         self.parent = parent
45         self.childname = childname
46         self.convergence = convergence
47
48     def receive(self):
49         self.c = FileWriter()
50         return defer.succeed(self.c)
51
52     def close(self):
53         u = FileHandle(self.c.f, self.convergence)
54         d = self.parent.add_file(self.childname, u)
55         return d
56
57
58 class NoParentError(Exception):
59     pass
60
61 class Handler:
62     implements(ftp.IFTPShell)
63     def __init__(self, client, rootnode, username, convergence):
64         self.client = client
65         self.root = rootnode
66         self.username = username
67         self.convergence = convergence
68
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))
73         return d
74
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")
81         if not path:
82             return defer.succeed(node)
83         d = node.get(path[0])
84         def _maybe_create(f):
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:])
89         return d
90
91     def _get_parent(self, path):
92         # fire with (parentnode, childname)
93         path = [unicode(p) for p in path]
94         if not path:
95             raise NoParentError
96         childname = path[-1]
97         d = self._get_root(path)
98         def _got_root((root, path)):
99             if not path:
100                 raise NoParentError
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)
106         return d
107
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)
124             return d
125         d.addCallback(_got_parent)
126         return d
127
128     def removeDirectory(self, path):
129         return self._remove_thing(path, must_be_directory=True)
130
131     def removeFile(self, path):
132         return self._remove_thing(path, must_be_file=True)
133
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,
142                                                    overwrite=False))
143             return d
144         d.addCallback(_got_from_parent)
145         d.addErrback(self._convert_error)
146         return d
147
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)
155         return d
156
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)
165         return f
166
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,
172                                     str(path[1]))
173             d.addCallback(lambda root: (root, path[2:]))
174         else:
175             d = defer.succeed((self.root,path))
176         return d
177
178     def _get_node_and_metadata_for_path(self, path):
179         d = self._get_root(path)
180         def _got_root((root,path)):
181             if path:
182                 return root.get_child_and_metadata_at_path(path)
183             else:
184                 return (root,{})
185         d.addCallback(_got_root)
186         return d
187
188     def _populate_row(self, keys, (childnode, metadata)):
189         values = []
190         isdir = bool(IDirectoryNode.providedBy(childnode))
191         for key in keys:
192             if key == "size":
193                 if isdir:
194                     value = 0
195                 else:
196                     value = childnode.get_size()
197             elif key == "directory":
198                 value = isdir
199             elif key == "permissions":
200                 value = 0600
201             elif key == "hardlinks":
202                 value = 1
203             elif key == "modified":
204                 value = metadata.get("mtime", 0)
205             elif key == "owner":
206                 value = self.username
207             elif key == "group":
208                 value = self.username
209             else:
210                 value = "??"
211             values.append(value)
212         return values
213
214     def stat(self, path, keys=()):
215         # for files only, I think
216         d = self._get_node_and_metadata_for_path(path)
217         def _render((node,metadata)):
218             assert not IDirectoryNode.providedBy(node)
219             return self._populate_row(keys, (node,metadata))
220         d.addCallback(_render)
221         d.addErrback(self._convert_error)
222         return d
223
224     def list(self, path, keys=()):
225         # the interface claims that path is a list of unicodes, but in
226         # practice it is not
227         d = self._get_node_and_metadata_for_path(path)
228         def _list((node, metadata)):
229             if IDirectoryNode.providedBy(node):
230                 return node.list()
231             return { path[-1]: (node, metadata) } # need last-edge metadata
232         d.addCallback(_list)
233         def _render(children):
234             results = []
235             for (name, childnode) in children.iteritems():
236                 # the interface claims that the result should have a unicode
237                 # object as the name, but it fails unless you give it a
238                 # bytestring
239                 results.append( (name.encode("utf-8"),
240                                  self._populate_row(keys, childnode) ) )
241             return results
242         d.addCallback(_render)
243         d.addErrback(self._convert_error)
244         return d
245
246     def openForReading(self, path):
247         d = self._get_node_and_metadata_for_path(path)
248         d.addCallback(lambda (node,metadata): ReadFile(node))
249         d.addErrback(self._convert_error)
250         return d
251
252     def openForWriting(self, path):
253         path = [unicode(p) for p in path]
254         if not path:
255             raise ftp.PermissionDeniedError("cannot STOR to root directory")
256         childname = path[-1]
257         d = self._get_root(path)
258         def _got_root((root, path)):
259             if not path:
260                 raise ftp.PermissionDeniedError("cannot STOR to root directory")
261             return root.get_child_at_path(path[:-1])
262         d.addCallback(_got_root)
263         def _got_parent(parent):
264             return WriteFile(parent, childname, self.convergence)
265         d.addCallback(_got_parent)
266         return d
267
268 from allmydata.frontends.auth import AccountURLChecker, AccountFileChecker, NeedRootcapLookupScheme
269
270
271 class Dispatcher:
272     implements(portal.IRealm)
273     def __init__(self, client):
274         self.client = client
275
276     def requestAvatar(self, avatarID, mind, interface):
277         assert interface == ftp.IFTPShell
278         rootnode = self.client.create_node_from_uri(avatarID.rootcap)
279         convergence = self.client.convergence
280         s = Handler(self.client, rootnode, avatarID.username, convergence)
281         def logout(): pass
282         return (interface, s, None)
283
284
285 class FTPServer(service.MultiService):
286     def __init__(self, client, accountfile, accounturl, ftp_portstr):
287         service.MultiService.__init__(self)
288
289         # make sure we're using a patched Twisted that uses IWriteFile.close:
290         # see docs/frontends/FTP-and-SFTP.txt and
291         # http://twistedmatrix.com/trac/ticket/3462 for details.
292         if "close" not in ftp.IWriteFile.names():
293             raise AssertionError("your twisted is lacking a vital patch, see docs/frontends/FTP-and-SFTP.txt")
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 NeedRootcapLookupScheme("must provide some translation")
307
308         f = ftp.FTPFactory(p)
309         s = strports.service(ftp_portstr, f)
310         s.setServiceParent(self)