]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/webish.py
96e8c352d3cb1a9e183f7d8f9a0428bb74996773
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / webish.py
1
2 import time, os.path
3 from twisted.application import service, strports, internet
4 from twisted.web import static, resource, server, html, http
5 from twisted.internet import defer, address
6 from twisted.internet.interfaces import IConsumer
7 from nevow import inevow, rend, loaders, appserver, url, tags as T
8 from nevow.static import File as nevow_File # TODO: merge with static.File?
9 from allmydata.util import fileutil, idlib, observer, log
10 import simplejson
11 from allmydata.interfaces import IDownloadTarget, IDirectoryNode, IFileNode, \
12      IMutableFileNode
13 import allmydata # to display import path
14 from allmydata import download
15 from allmydata.upload import FileHandle, FileName
16 from allmydata import provisioning
17 from allmydata import get_package_versions_string
18 from zope.interface import implements, Interface
19 import urllib
20 from formless import webform
21 from foolscap.eventual import fireEventually
22
23 from nevow.util import resource_filename
24
25 from allmydata.web import status
26 from allmydata.web.common import IClient
27
28 def getxmlfile(name):
29     return loaders.xmlfile(resource_filename('allmydata.web', '%s' % name))
30
31 class ILocalAccess(Interface):
32     def local_access_is_allowed():
33         """Return True if t=upload&localdir= is allowed, giving anyone who
34         can talk to the webserver control over the local (disk) filesystem."""
35
36 def boolean_of_arg(arg):
37     assert arg.lower() in ("true", "t", "1", "false", "f", "0", "on", "off")
38     return arg.lower() in ("true", "t", "1", "on")
39
40 def get_arg(req, argname, default=None, multiple=False):
41     """Extract an argument from either the query args (req.args) or the form
42     body fields (req.fields). If multiple=False, this returns a single value
43     (or the default, which defaults to None), and the query args take
44     precedence. If multiple=True, this returns a tuple of arguments (possibly
45     empty), starting with all those in the query args.
46     """
47     results = []
48     if argname in req.args:
49         results.extend(req.args[argname])
50     if req.fields and argname in req.fields:
51         results.append(req.fields[argname].value)
52     if multiple:
53         return tuple(results)
54     if results:
55         return results[0]
56     return default
57
58 # we must override twisted.web.http.Request.requestReceived with a version
59 # that doesn't use cgi.parse_multipart() . Since we actually use Nevow, we
60 # override the nevow-specific subclass, nevow.appserver.NevowRequest . This
61 # is an exact copy of twisted.web.http.Request (from SVN HEAD on 10-Aug-2007)
62 # that modifies the way form arguments are parsed. Note that this sort of
63 # surgery may induce a dependency upon a particular version of twisted.web
64
65 parse_qs = http.parse_qs
66 class MyRequest(appserver.NevowRequest):
67     fields = None
68     def requestReceived(self, command, path, version):
69         """Called by channel when all data has been received.
70
71         This method is not intended for users.
72         """
73         self.content.seek(0,0)
74         self.args = {}
75         self.stack = []
76
77         self.method, self.uri = command, path
78         self.clientproto = version
79         x = self.uri.split('?', 1)
80
81         if len(x) == 1:
82             self.path = self.uri
83         else:
84             self.path, argstring = x
85             self.args = parse_qs(argstring, 1)
86
87         # cache the client and server information, we'll need this later to be
88         # serialized and sent with the request so CGIs will work remotely
89         self.client = self.channel.transport.getPeer()
90         self.host = self.channel.transport.getHost()
91
92         # Argument processing.
93
94 ##      The original twisted.web.http.Request.requestReceived code parsed the
95 ##      content and added the form fields it found there to self.args . It
96 ##      did this with cgi.parse_multipart, which holds the arguments in RAM
97 ##      and is thus unsuitable for large file uploads. The Nevow subclass
98 ##      (nevow.appserver.NevowRequest) uses cgi.FieldStorage instead (putting
99 ##      the results in self.fields), which is much more memory-efficient.
100 ##      Since we know we're using Nevow, we can anticipate these arguments
101 ##      appearing in self.fields instead of self.args, and thus skip the
102 ##      parse-content-into-self.args step.
103
104 ##      args = self.args
105 ##      ctype = self.getHeader('content-type')
106 ##      if self.method == "POST" and ctype:
107 ##          mfd = 'multipart/form-data'
108 ##          key, pdict = cgi.parse_header(ctype)
109 ##          if key == 'application/x-www-form-urlencoded':
110 ##              args.update(parse_qs(self.content.read(), 1))
111 ##          elif key == mfd:
112 ##              try:
113 ##                  args.update(cgi.parse_multipart(self.content, pdict))
114 ##              except KeyError, e:
115 ##                  if e.args[0] == 'content-disposition':
116 ##                      # Parse_multipart can't cope with missing
117 ##                      # content-dispostion headers in multipart/form-data
118 ##                      # parts, so we catch the exception and tell the client
119 ##                      # it was a bad request.
120 ##                      self.channel.transport.write(
121 ##                              "HTTP/1.1 400 Bad Request\r\n\r\n")
122 ##                      self.channel.transport.loseConnection()
123 ##                      return
124 ##                  raise
125
126         self.process()
127
128     def _logger(self):
129         # we build up a log string that hides most of the cap, to preserve
130         # user privacy. We retain the query args so we can identify things
131         # like t=json. Then we send it to the flog. We make no attempt to
132         # match apache formatting. TODO: when we move to DSA dirnodes and
133         # shorter caps, consider exposing a few characters of the cap, or
134         # maybe a few characters of its hash.
135         x = self.uri.split("?", 1)
136         if len(x) == 1:
137             # no query args
138             path = self.uri
139             queryargs = ""
140         else:
141             path, queryargs = x
142             # there is a form handler which redirects POST /uri?uri=FOO into
143             # GET /uri/FOO so folks can paste in non-HTTP-prefixed uris. Make
144             # sure we censor these too.
145             if queryargs.startswith("uri="):
146                 queryargs = "[uri=CENSORED]"
147             queryargs = "?" + queryargs
148         if path.startswith("/uri"):
149             path = "/uri/[CENSORED].."
150         uri = path + queryargs
151
152         log.msg(format="web: %(clientip)s %(method)s %(uri)s %(code)s %(length)s",
153                 clientip=self.getClientIP(),
154                 method=self.method,
155                 uri=uri,
156                 code=self.code,
157                 length=(self.sentLength or "-"),
158                 facility="tahoe.webish",
159                 level=log.OPERATIONAL,
160                 )
161
162 class Directory(rend.Page):
163     addSlash = True
164     docFactory = getxmlfile("directory.xhtml")
165
166     def __init__(self, rootname, dirnode, dirpath):
167         self._rootname = rootname
168         self._dirnode = dirnode
169         self._dirpath = dirpath
170
171     def dirpath_as_string(self):
172         return "/" + "/".join(self._dirpath)
173
174     def render_title(self, ctx, data):
175         return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
176
177     def render_header(self, ctx, data):
178         parent_directories = ("<%s>" % self._rootname,) + self._dirpath
179         num_dirs = len(parent_directories)
180
181         header = ["Directory '"]
182         for i,d in enumerate(parent_directories):
183             upness = num_dirs - i - 1
184             if upness:
185                 link = "/".join( ("..",) * upness )
186             else:
187                 link = "."
188             header.append(T.a(href=link)[d])
189             if upness != 0:
190                 header.append("/")
191         header.append("'")
192
193         if self._dirnode.is_readonly():
194             header.append(" (readonly)")
195         header.append(":")
196         return ctx.tag[header]
197
198     def render_welcome(self, ctx, data):
199         depth = len(self._dirpath) + 2
200         link = "/".join([".."] * depth)
201         return T.div[T.a(href=link)["Return to Welcome page"]]
202
203     def data_children(self, ctx, data):
204         d = self._dirnode.list()
205         d.addCallback(lambda dict: sorted(dict.items()))
206         def _stall_some(items):
207             # Deferreds don't optimize out tail recursion, and the way
208             # Nevow's flattener handles Deferreds doesn't take this into
209             # account. As a result, large lists of Deferreds that fire in the
210             # same turn (i.e. the output of defer.succeed) will cause a stack
211             # overflow. To work around this, we insert a turn break after
212             # every 100 items, using foolscap's fireEventually(). This gives
213             # the stack a chance to be popped. It would also work to put
214             # every item in its own turn, but that'd be a lot more
215             # inefficient. This addresses ticket #237, for which I was never
216             # able to create a failing unit test.
217             output = []
218             for i,item in enumerate(items):
219                 if i % 100 == 0:
220                     output.append(fireEventually(item))
221                 else:
222                     output.append(item)
223             return output
224         d.addCallback(_stall_some)
225         return d
226
227     def render_row(self, ctx, data):
228         name, (target, metadata) = data
229         name = name.encode("utf-8")
230         assert not isinstance(name, unicode)
231
232         if self._dirnode.is_readonly():
233             delete = "-"
234             rename = "-"
235         else:
236             # this creates a button which will cause our child__delete method
237             # to be invoked, which deletes the file and then redirects the
238             # browser back to this directory
239             delete = T.form(action=url.here, method="post")[
240                 T.input(type='hidden', name='t', value='delete'),
241                 T.input(type='hidden', name='name', value=name),
242                 T.input(type='hidden', name='when_done', value=url.here),
243                 T.input(type='submit', value='del', name="del"),
244                 ]
245
246             rename = T.form(action=url.here, method="get")[
247                 T.input(type='hidden', name='t', value='rename-form'),
248                 T.input(type='hidden', name='name', value=name),
249                 T.input(type='hidden', name='when_done', value=url.here),
250                 T.input(type='submit', value='rename', name="rename"),
251                 ]
252
253         ctx.fillSlots("delete", delete)
254         ctx.fillSlots("rename", rename)
255         check = T.form(action=url.here, method="post")[
256             T.input(type='hidden', name='t', value='check'),
257             T.input(type='hidden', name='name', value=name),
258             T.input(type='hidden', name='when_done', value=url.here),
259             T.input(type='submit', value='check', name="check"),
260             ]
261         ctx.fillSlots("overwrite", self.build_overwrite(ctx, (name, target)))
262         ctx.fillSlots("check", check)
263
264         times = []
265         TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
266         if "ctime" in metadata:
267             ctime = time.strftime(TIME_FORMAT,
268                                   time.localtime(metadata["ctime"]))
269             times.append("c: " + ctime)
270         if "mtime" in metadata:
271             mtime = time.strftime(TIME_FORMAT,
272                                   time.localtime(metadata["mtime"]))
273             if times:
274                 times.append(T.br())
275                 times.append("m: " + mtime)
276         ctx.fillSlots("times", times)
277
278
279         # build the base of the uri_link link url
280         uri_link = "/uri/" + urllib.quote(target.get_uri())
281
282         assert (IFileNode.providedBy(target)
283                 or IDirectoryNode.providedBy(target)
284                 or IMutableFileNode.providedBy(target)), target
285
286         if IMutableFileNode.providedBy(target):
287             # file
288
289             # add the filename to the uri_link url
290             uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
291
292             # to prevent javascript in displayed .html files from stealing a
293             # secret directory URI from the URL, send the browser to a URI-based
294             # page that doesn't know about the directory at all
295             #dlurl = urllib.quote(name)
296             dlurl = uri_link
297
298             ctx.fillSlots("filename",
299                           T.a(href=dlurl)[html.escape(name)])
300             ctx.fillSlots("type", "SSK")
301
302             ctx.fillSlots("size", "?")
303
304             text_plain_link = uri_link + "?filename=foo.txt"
305             text_plain_tag = T.a(href=text_plain_link)["text/plain"]
306
307         elif IFileNode.providedBy(target):
308             # file
309
310             # add the filename to the uri_link url
311             uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
312
313             # to prevent javascript in displayed .html files from stealing a
314             # secret directory URI from the URL, send the browser to a URI-based
315             # page that doesn't know about the directory at all
316             #dlurl = urllib.quote(name)
317             dlurl = uri_link
318
319             ctx.fillSlots("filename",
320                           T.a(href=dlurl)[html.escape(name)])
321             ctx.fillSlots("type", "FILE")
322
323             ctx.fillSlots("size", target.get_size())
324
325             text_plain_link = uri_link + "?filename=foo.txt"
326             text_plain_tag = T.a(href=text_plain_link)["text/plain"]
327
328         elif IDirectoryNode.providedBy(target):
329             # directory
330             ctx.fillSlots("filename",
331                           T.a(href=uri_link)[html.escape(name)])
332             if target.is_readonly():
333                 dirtype = "DIR-RO"
334             else:
335                 dirtype = "DIR"
336             ctx.fillSlots("type", dirtype)
337             ctx.fillSlots("size", "-")
338             text_plain_tag = None
339
340         childdata = [T.a(href="%s?t=json" % name)["JSON"], ", ",
341                      T.a(href="%s?t=uri" % name)["URI"], ", ",
342                      T.a(href="%s?t=readonly-uri" % name)["readonly-URI"],
343                      ]
344         if text_plain_tag:
345             childdata.extend([", ", text_plain_tag])
346
347         ctx.fillSlots("data", childdata)
348
349         try:
350             checker = IClient(ctx).getServiceNamed("checker")
351         except KeyError:
352             checker = None
353         if checker:
354             d = defer.maybeDeferred(checker.checker_results_for,
355                                     target.get_verifier())
356             def _got(checker_results):
357                 recent_results = reversed(checker_results[-5:])
358                 if IFileNode.providedBy(target):
359                     results = ("[" +
360                                ", ".join(["%d/%d" % (found, needed)
361                                           for (when,
362                                                (needed, total, found, sharemap))
363                                           in recent_results]) +
364                                "]")
365                 elif IDirectoryNode.providedBy(target):
366                     results = ("[" +
367                                "".join([{True:"+",False:"-"}[res]
368                                         for (when, res) in recent_results]) +
369                                "]")
370                 else:
371                     results = "%d results" % len(checker_results)
372                 return results
373             d.addCallback(_got)
374             results = d
375         else:
376             results = "--"
377         # TODO: include a link to see more results, including timestamps
378         # TODO: use a sparkline
379         ctx.fillSlots("checker_results", results)
380
381         return ctx.tag
382
383     def render_forms(self, ctx, data):
384         if self._dirnode.is_readonly():
385             return T.div["No upload forms: directory is read-only"]
386         mkdir = T.form(action=".", method="post",
387                        enctype="multipart/form-data")[
388             T.fieldset[
389             T.input(type="hidden", name="t", value="mkdir"),
390             T.input(type="hidden", name="when_done", value=url.here),
391             T.legend(class_="freeform-form-label")["Create a new directory"],
392             "New directory name: ",
393             T.input(type="text", name="name"), " ",
394             T.input(type="submit", value="Create"),
395             ]]
396
397         upload = T.form(action=".", method="post",
398                         enctype="multipart/form-data")[
399             T.fieldset[
400             T.input(type="hidden", name="t", value="upload"),
401             T.input(type="hidden", name="when_done", value=url.here),
402             T.legend(class_="freeform-form-label")["Upload a file to this directory"],
403             "Choose a file to upload: ",
404             T.input(type="file", name="file", class_="freeform-input-file"),
405             " ",
406             T.input(type="submit", value="Upload"),
407             " Mutable?:",
408             T.input(type="checkbox", name="mutable"),
409             ]]
410
411         mount = T.form(action=".", method="post",
412                         enctype="multipart/form-data")[
413             T.fieldset[
414             T.input(type="hidden", name="t", value="uri"),
415             T.input(type="hidden", name="when_done", value=url.here),
416             T.legend(class_="freeform-form-label")["Attach a file or directory"
417                                                    " (by URI) to this"
418                                                    " directory"],
419             "New child name: ",
420             T.input(type="text", name="name"), " ",
421             "URI of new child: ",
422             T.input(type="text", name="uri"), " ",
423             T.input(type="submit", value="Attach"),
424             ]]
425         return [T.div(class_="freeform-form")[mkdir],
426                 T.div(class_="freeform-form")[upload],
427                 T.div(class_="freeform-form")[mount],
428                 ]
429
430     def build_overwrite(self, ctx, data):
431         name, target = data
432         if IMutableFileNode.providedBy(target) and not target.is_readonly():
433             action="/uri/" + urllib.quote(target.get_uri())
434             overwrite = T.form(action=action, method="post",
435                                enctype="multipart/form-data")[
436                 T.fieldset[
437                 T.input(type="hidden", name="t", value="overwrite"),
438                 T.input(type='hidden', name='name', value=name),
439                 T.input(type='hidden', name='when_done', value=url.here),
440                 T.legend(class_="freeform-form-label")["Overwrite"],
441                 "Choose new file: ",
442                 T.input(type="file", name="file", class_="freeform-input-file"),
443                 " ",
444                 T.input(type="submit", value="Overwrite")
445                 ]]
446             return [T.div(class_="freeform-form")[overwrite],]
447         else:
448             return []
449
450     def render_results(self, ctx, data):
451         req = inevow.IRequest(ctx)
452         return get_arg(req, "results", "")
453
454 class WebDownloadTarget:
455     implements(IDownloadTarget, IConsumer)
456     def __init__(self, req, content_type, content_encoding, save_to_file):
457         self._req = req
458         self._content_type = content_type
459         self._content_encoding = content_encoding
460         self._opened = False
461         self._producer = None
462         self._save_to_file = save_to_file
463
464     def registerProducer(self, producer, streaming):
465         self._req.registerProducer(producer, streaming)
466     def unregisterProducer(self):
467         self._req.unregisterProducer()
468
469     def open(self, size):
470         self._opened = True
471         self._req.setHeader("content-type", self._content_type)
472         if self._content_encoding:
473             self._req.setHeader("content-encoding", self._content_encoding)
474         self._req.setHeader("content-length", str(size))
475         if self._save_to_file is not None:
476             # tell the browser to save the file rather display it
477             # TODO: quote save_to_file properly
478             filename = self._save_to_file.encode("utf-8")
479             self._req.setHeader("content-disposition",
480                                 'attachment; filename="%s"'
481                                 % filename)
482
483     def write(self, data):
484         self._req.write(data)
485     def close(self):
486         self._req.finish()
487
488     def fail(self, why):
489         if self._opened:
490             # The content-type is already set, and the response code
491             # has already been sent, so we can't provide a clean error
492             # indication. We can emit text (which a browser might interpret
493             # as something else), and if we sent a Size header, they might
494             # notice that we've truncated the data. Keep the error message
495             # small to improve the chances of having our error response be
496             # shorter than the intended results.
497             #
498             # We don't have a lot of options, unfortunately.
499             self._req.write("problem during download\n")
500         else:
501             # We haven't written anything yet, so we can provide a sensible
502             # error message.
503             msg = str(why.type)
504             msg.replace("\n", "|")
505             self._req.setResponseCode(http.GONE, msg)
506             self._req.setHeader("content-type", "text/plain")
507             # TODO: HTML-formatted exception?
508             self._req.write(str(why))
509         self._req.finish()
510
511     def register_canceller(self, cb):
512         pass
513     def finish(self):
514         pass
515
516 class FileDownloader(resource.Resource):
517     def __init__(self, filenode, name):
518         assert (IFileNode.providedBy(filenode)
519                 or IMutableFileNode.providedBy(filenode))
520         self._filenode = filenode
521         self._name = name
522
523     def render(self, req):
524         gte = static.getTypeAndEncoding
525         ctype, encoding = gte(self._name,
526                               static.File.contentTypes,
527                               static.File.contentEncodings,
528                               defaultType="text/plain")
529         save_to_file = None
530         if get_arg(req, "save", False):
531             # TODO: make the API specification clear: should "save=" or
532             # "save=false" count?
533             save_to_file = self._name
534         wdt = WebDownloadTarget(req, ctype, encoding, save_to_file)
535         d = self._filenode.download(wdt)
536         # exceptions during download are handled by the WebDownloadTarget
537         d.addErrback(lambda why: None)
538         return server.NOT_DONE_YET
539
540 class BlockingFileError(Exception):
541     """We cannot auto-create a parent directory, because there is a file in
542     the way"""
543 class NoReplacementError(Exception):
544     """There was already a child by that name, and you asked me to not replace it"""
545 class NoLocalDirectoryError(Exception):
546     """The localdir= directory didn't exist"""
547
548 LOCALHOST = "127.0.0.1"
549
550 class NeedLocalhostError:
551     implements(inevow.IResource)
552
553     def renderHTTP(self, ctx):
554         req = inevow.IRequest(ctx)
555         req.setResponseCode(http.FORBIDDEN)
556         req.setHeader("content-type", "text/plain")
557         return "localfile= or localdir= requires a local connection"
558
559 class NeedAbsolutePathError:
560     implements(inevow.IResource)
561
562     def renderHTTP(self, ctx):
563         req = inevow.IRequest(ctx)
564         req.setResponseCode(http.FORBIDDEN)
565         req.setHeader("content-type", "text/plain")
566         return "localfile= or localdir= requires an absolute path"
567
568 class LocalAccessDisabledError:
569     implements(inevow.IResource)
570
571     def renderHTTP(self, ctx):
572         req = inevow.IRequest(ctx)
573         req.setResponseCode(http.FORBIDDEN)
574         req.setHeader("content-type", "text/plain")
575         return "local file access is disabled"
576
577 class WebError:
578     implements(inevow.IResource)
579     def __init__(self, response_code, errmsg):
580         self._response_code = response_code
581         self._errmsg = errmsg
582
583     def renderHTTP(self, ctx):
584         req = inevow.IRequest(ctx)
585         req.setResponseCode(self._response_code)
586         req.setHeader("content-type", "text/plain")
587         return self._errmsg
588
589
590 class LocalFileDownloader(resource.Resource):
591     def __init__(self, filenode, local_filename):
592         self._local_filename = local_filename
593         IFileNode(filenode)
594         self._filenode = filenode
595
596     def render(self, req):
597         target = download.FileName(self._local_filename)
598         d = self._filenode.download(target)
599         def _done(res):
600             req.write(self._filenode.get_uri())
601             req.finish()
602         d.addCallback(_done)
603         return server.NOT_DONE_YET
604
605
606 class FileJSONMetadata(rend.Page):
607     def __init__(self, filenode):
608         self._filenode = filenode
609
610     def renderHTTP(self, ctx):
611         req = inevow.IRequest(ctx)
612         req.setHeader("content-type", "text/plain")
613         return self.renderNode(self._filenode)
614
615     def renderNode(self, filenode):
616         file_uri = filenode.get_uri()
617         data = ("filenode",
618                 {'ro_uri': file_uri,
619                  'size': filenode.get_size(),
620                  })
621         return simplejson.dumps(data, indent=1)
622
623 class FileURI(FileJSONMetadata):
624     def renderNode(self, filenode):
625         file_uri = filenode.get_uri()
626         return file_uri
627
628 class FileReadOnlyURI(FileJSONMetadata):
629     def renderNode(self, filenode):
630         if filenode.is_readonly():
631             return filenode.get_uri()
632         else:
633             return filenode.get_readonly().get_uri()
634
635 class DirnodeWalkerMixin:
636     """Visit all nodes underneath (and including) the rootnode, one at a
637     time. For each one, call the visitor. The visitor will see the
638     IDirectoryNode before it sees any of the IFileNodes inside. If the
639     visitor returns a Deferred, I do not call the visitor again until it has
640     fired.
641     """
642
643 ##    def _walk_if_we_could_use_generators(self, rootnode, rootpath=()):
644 ##        # this is what we'd be doing if we didn't have the Deferreds and
645 ##        # thus could use generators
646 ##        yield rootpath, rootnode
647 ##        for childname, childnode in rootnode.list().items():
648 ##            childpath = rootpath + (childname,)
649 ##            if IFileNode.providedBy(childnode):
650 ##                yield childpath, childnode
651 ##            elif IDirectoryNode.providedBy(childnode):
652 ##                for res in self._walk_if_we_could_use_generators(childnode,
653 ##                                                                 childpath):
654 ##                    yield res
655
656     def walk(self, rootnode, visitor, rootpath=()):
657         d = rootnode.list()
658         def _listed(listing):
659             return listing.items()
660         d.addCallback(_listed)
661         d.addCallback(self._handle_items, visitor, rootpath)
662         return d
663
664     def _handle_items(self, items, visitor, rootpath):
665         if not items:
666             return
667         childname, (childnode, metadata) = items[0]
668         childpath = rootpath + (childname,)
669         d = defer.maybeDeferred(visitor, childpath, childnode, metadata)
670         if IDirectoryNode.providedBy(childnode):
671             d.addCallback(lambda res: self.walk(childnode, visitor, childpath))
672         d.addCallback(lambda res:
673                       self._handle_items(items[1:], visitor, rootpath))
674         return d
675
676 class LocalDirectoryDownloader(resource.Resource, DirnodeWalkerMixin):
677     def __init__(self, dirnode, localdir):
678         self._dirnode = dirnode
679         self._localdir = localdir
680
681     def _handle(self, path, node, metadata):
682         path = tuple([p.encode("utf-8") for p in path])
683         localfile = os.path.join(self._localdir, os.sep.join(path))
684         if IDirectoryNode.providedBy(node):
685             fileutil.make_dirs(localfile)
686         elif IFileNode.providedBy(node):
687             target = download.FileName(localfile)
688             return node.download(target)
689
690     def render(self, req):
691         d = self.walk(self._dirnode, self._handle)
692         def _done(res):
693             req.setHeader("content-type", "text/plain")
694             return "operation complete"
695         d.addCallback(_done)
696         return d
697
698 class DirectoryJSONMetadata(rend.Page):
699     def __init__(self, dirnode):
700         self._dirnode = dirnode
701
702     def renderHTTP(self, ctx):
703         req = inevow.IRequest(ctx)
704         req.setHeader("content-type", "text/plain")
705         return self.renderNode(self._dirnode)
706
707     def renderNode(self, node):
708         d = node.list()
709         def _got(children):
710             kids = {}
711             for name, (childnode, metadata) in children.iteritems():
712                 if IFileNode.providedBy(childnode):
713                     kiduri = childnode.get_uri()
714                     kiddata = ("filenode",
715                                {'ro_uri': kiduri,
716                                 'size': childnode.get_size(),
717                                 'metadata': metadata,
718                                 })
719                 else:
720                     assert IDirectoryNode.providedBy(childnode), (childnode, children,)
721                     kiddata = ("dirnode",
722                                {'ro_uri': childnode.get_readonly_uri(),
723                                 'metadata': metadata,
724                                 })
725                     if not childnode.is_readonly():
726                         kiddata[1]['rw_uri'] = childnode.get_uri()
727                 kids[name] = kiddata
728             contents = { 'children': kids,
729                          'ro_uri': node.get_readonly_uri(),
730                          }
731             if not node.is_readonly():
732                 contents['rw_uri'] = node.get_uri()
733             data = ("dirnode", contents)
734             return simplejson.dumps(data, indent=1)
735         d.addCallback(_got)
736         return d
737
738 class DirectoryURI(DirectoryJSONMetadata):
739     def renderNode(self, node):
740         return node.get_uri()
741
742 class DirectoryReadonlyURI(DirectoryJSONMetadata):
743     def renderNode(self, node):
744         return node.get_readonly_uri()
745
746 class RenameForm(rend.Page):
747     addSlash = True
748     docFactory = getxmlfile("rename-form.xhtml")
749
750     def __init__(self, rootname, dirnode, dirpath):
751         self._rootname = rootname
752         self._dirnode = dirnode
753         self._dirpath = dirpath
754
755     def dirpath_as_string(self):
756         return "/" + "/".join(self._dirpath)
757
758     def render_title(self, ctx, data):
759         return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
760
761     def render_header(self, ctx, data):
762         parent_directories = ("<%s>" % self._rootname,) + self._dirpath
763         num_dirs = len(parent_directories)
764
765         header = [ "Rename in directory '",
766                    "<%s>/" % self._rootname,
767                    "/".join(self._dirpath),
768                    "':", ]
769
770         if self._dirnode.is_readonly():
771             header.append(" (readonly)")
772         return ctx.tag[header]
773
774     def render_when_done(self, ctx, data):
775         return T.input(type="hidden", name="when_done", value=url.here)
776
777     def render_get_name(self, ctx, data):
778         req = inevow.IRequest(ctx)
779         name = get_arg(req, "name", "")
780         ctx.tag.attributes['value'] = name
781         return ctx.tag
782
783 class POSTHandler(rend.Page):
784     def __init__(self, node, replace):
785         self._node = node
786         self._replace = replace
787
788     def _check_replacement(self, name):
789         if self._replace:
790             return defer.succeed(None)
791         d = self._node.has_child(name)
792         def _got(present):
793             if present:
794                 raise NoReplacementError("There was already a child by that "
795                                          "name, and you asked me to not "
796                                          "replace it.")
797             return None
798         d.addCallback(_got)
799         return d
800
801     def _POST_mkdir(self, name):
802         d = self._check_replacement(name)
803         d.addCallback(lambda res: self._node.create_empty_directory(name))
804         d.addCallback(lambda res: "directory created")
805         return d
806
807     def _POST_uri(self, name, newuri):
808         d = self._check_replacement(name)
809         d.addCallback(lambda res: self._node.set_uri(name, newuri))
810         d.addCallback(lambda res: newuri)
811         return d
812
813     def _POST_delete(self, name):
814         if name is None:
815             # apparently an <input type="hidden" name="name" value="">
816             # won't show up in the resulting encoded form.. the 'name'
817             # field is completely missing. So to allow deletion of an
818             # empty file, we have to pretend that None means ''. The only
819             # downide of this is a slightly confusing error message if
820             # someone does a POST without a name= field. For our own HTML
821             # thisn't a big deal, because we create the 'delete' POST
822             # buttons ourselves.
823             name = ''
824         d = self._node.delete(name)
825         d.addCallback(lambda res: "thing deleted")
826         return d
827
828     def _POST_rename(self, name, from_name, to_name):
829         d = self._check_replacement(to_name)
830         d.addCallback(lambda res: self._node.get(from_name))
831         def add_dest(child):
832             uri = child.get_uri()
833             # now actually do the rename
834             return self._node.set_uri(to_name, uri)
835         d.addCallback(add_dest)
836         def rm_src(junk):
837             return self._node.delete(from_name)
838         d.addCallback(rm_src)
839         d.addCallback(lambda res: "thing renamed")
840         return d
841
842     def _POST_upload(self, contents, name, mutable, client):
843         if mutable:
844             # SDMF: files are small, and we can only upload data.
845             contents.file.seek(0)
846             data = contents.file.read()
847             #uploadable = FileHandle(contents.file)
848             d = self._check_replacement(name)
849             d.addCallback(lambda res: self._node.has_child(name))
850             def _checked(present):
851                 if present:
852                     # modify the existing one instead of creating a new
853                     # one
854                     d2 = self._node.get(name)
855                     def _got_newnode(newnode):
856                         d3 = newnode.replace(data)
857                         d3.addCallback(lambda res: newnode.get_uri())
858                         return d3
859                     d2.addCallback(_got_newnode)
860                 else:
861                     d2 = client.create_mutable_file(data)
862                     def _uploaded(newnode):
863                         d1 = self._node.set_node(name, newnode)
864                         d1.addCallback(lambda res: newnode.get_uri())
865                         return d1
866                     d2.addCallback(_uploaded)
867                 return d2
868             d.addCallback(_checked)
869         else:
870             uploadable = FileHandle(contents.file)
871             d = self._check_replacement(name)
872             d.addCallback(lambda res: self._node.add_file(name, uploadable))
873             def _done(newnode):
874                 return newnode.get_uri()
875             d.addCallback(_done)
876         return d
877
878     def _POST_overwrite(self, contents):
879         # SDMF: files are small, and we can only upload data.
880         contents.file.seek(0)
881         data = contents.file.read()
882         # TODO: 'name' handling needs review
883         d = defer.succeed(self._node)
884         def _got_child_overwrite(child_node):
885             child_node.replace(data)
886             return child_node.get_uri()
887         d.addCallback(_got_child_overwrite)
888         return d
889
890     def _POST_check(self, name):
891         d = self._node.get(name)
892         def _got_child_check(child_node):
893             d2 = child_node.check()
894             def _done(res):
895                 log.msg("checked %s, results %s" % (child_node, res),
896                         facility="tahoe.webish", level=log.NOISY)
897                 return str(res)
898             d2.addCallback(_done)
899             return d2
900         d.addCallback(_got_child_check)
901         return d
902
903     def _POST_set_children(self, children):
904         cs = []
905         for name, (file_or_dir, mddict) in children.iteritems():
906             cap = str(mddict.get('rw_uri') or mddict.get('ro_uri'))
907             cs.append((name, cap, mddict.get('metadata')))
908
909         d = self._node.set_children(cs)
910         d.addCallback(lambda res: "Okay so I did it.")
911         return d
912
913     def renderHTTP(self, ctx):
914         req = inevow.IRequest(ctx)
915
916         t = get_arg(req, "t")
917         assert t is not None
918
919         charset = get_arg(req, "_charset", "utf-8")
920
921         name = get_arg(req, "name", None)
922         if name and "/" in name:
923             req.setResponseCode(http.BAD_REQUEST)
924             req.setHeader("content-type", "text/plain")
925             return "name= may not contain a slash"
926         if name is not None:
927             name = name.strip()
928             name = name.decode(charset)
929             assert isinstance(name, unicode)
930         # we allow the user to delete an empty-named file, but not to create
931         # them, since that's an easy and confusing mistake to make
932
933         when_done = get_arg(req, "when_done", None)
934         if not boolean_of_arg(get_arg(req, "replace", "true")):
935             self._replace = False
936
937         if t == "mkdir":
938             if not name:
939                 raise RuntimeError("mkdir requires a name")
940             d = self._POST_mkdir(name)
941         elif t == "uri":
942             if not name:
943                 raise RuntimeError("set-uri requires a name")
944             newuri = get_arg(req, "uri")
945             assert newuri is not None
946             d = self._POST_uri(name, newuri)
947         elif t == "delete":
948             d = self._POST_delete(name)
949         elif t == "rename":
950             from_name = get_arg(req, "from_name")
951             if from_name is not None:
952                 from_name = from_name.strip()
953                 from_name = from_name.decode(charset)
954                 assert isinstance(from_name, unicode)
955             to_name = get_arg(req, "to_name")
956             if to_name is not None:
957                 to_name = to_name.strip()
958                 to_name = to_name.decode(charset)
959                 assert isinstance(to_name, unicode)
960             if not from_name or not to_name:
961                 raise RuntimeError("rename requires from_name and to_name")
962             if not IDirectoryNode.providedBy(self._node):
963                 raise RuntimeError("rename must only be called on directories")
964             for k,v in [ ('from_name', from_name), ('to_name', to_name) ]:
965                 if v and "/" in v:
966                     req.setResponseCode(http.BAD_REQUEST)
967                     req.setHeader("content-type", "text/plain")
968                     return "%s= may not contain a slash" % (k,)
969             d = self._POST_rename(name, from_name, to_name)
970         elif t == "upload":
971             contents = req.fields["file"]
972             name = name or contents.filename
973             if name is not None:
974                 name = name.strip()
975             if not name:
976                 # this prohibts empty, missing, and all-whitespace filenames
977                 raise RuntimeError("upload requires a name")
978             name = name.decode(charset)
979             assert isinstance(name, unicode)
980             mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
981             d = self._POST_upload(contents, name, mutable, IClient(ctx))
982         elif t == "overwrite":
983             contents = req.fields["file"]
984             d = self._POST_overwrite(contents)
985         elif t == "check":
986             d = self._POST_check(name)
987         elif t == "set_children":
988             req.content.seek(0)
989             body = req.content.read()
990             try:
991                 children = simplejson.loads(body)
992             except ValueError, le:
993                 le.args = tuple(le.args + (body,))
994                 # TODO test handling of bad JSON
995                 raise
996             d = self._POST_set_children(children)
997         else:
998             print "BAD t=%s" % t
999             return "BAD t=%s" % t
1000         if when_done:
1001             d.addCallback(lambda res: url.URL.fromString(when_done))
1002         def _check_replacement(f):
1003             # TODO: make this more human-friendly: maybe send them to the
1004             # when_done page but with an extra query-arg that will display
1005             # the error message in a big box at the top of the page. The
1006             # directory page that when_done= usually points to accepts a
1007             # result= argument.. use that.
1008             f.trap(NoReplacementError)
1009             req.setResponseCode(http.CONFLICT)
1010             req.setHeader("content-type", "text/plain")
1011             return str(f.value)
1012         d.addErrback(_check_replacement)
1013         return d
1014
1015 class DELETEHandler(rend.Page):
1016     def __init__(self, node, name):
1017         self._node = node
1018         self._name = name
1019
1020     def renderHTTP(self, ctx):
1021         req = inevow.IRequest(ctx)
1022         d = self._node.delete(self._name)
1023         def _done(res):
1024             # what should this return??
1025             return "%s deleted" % self._name.encode("utf-8")
1026         d.addCallback(_done)
1027         def _trap_missing(f):
1028             f.trap(KeyError)
1029             req.setResponseCode(http.NOT_FOUND)
1030             req.setHeader("content-type", "text/plain")
1031             return "no such child %s" % self._name.encode("utf-8")
1032         d.addErrback(_trap_missing)
1033         return d
1034
1035 class PUTHandler(rend.Page):
1036     def __init__(self, node, path, t, localfile, localdir, replace):
1037         self._node = node
1038         self._path = path
1039         self._t = t
1040         self._localfile = localfile
1041         self._localdir = localdir
1042         self._replace = replace
1043
1044     def renderHTTP(self, ctx):
1045         req = inevow.IRequest(ctx)
1046         t = self._t
1047         localfile = self._localfile
1048         localdir = self._localdir
1049
1050         if t == "upload" and not (localfile or localdir):
1051             req.setResponseCode(http.BAD_REQUEST)
1052             req.setHeader("content-type", "text/plain")
1053             return "t=upload requires localfile= or localdir="
1054
1055         # we must traverse the path, creating new directories as necessary
1056         d = self._get_or_create_directories(self._node, self._path[:-1])
1057         name = self._path[-1]
1058         d.addCallback(self._check_replacement, name, self._replace)
1059         if t == "upload":
1060             if localfile:
1061                 d.addCallback(self._upload_localfile, localfile, name)
1062             else:
1063                 # localdir
1064                 # take the last step
1065                 d.addCallback(self._get_or_create_directories, self._path[-1:])
1066                 d.addCallback(self._upload_localdir, localdir)
1067         elif t == "uri":
1068             d.addCallback(self._attach_uri, req.content, name)
1069         elif t == "mkdir":
1070             d.addCallback(self._mkdir, name)
1071         else:
1072             d.addCallback(self._upload_file, req.content, name)
1073
1074         def _transform_error(f):
1075             errors = {BlockingFileError: http.BAD_REQUEST,
1076                       NoReplacementError: http.CONFLICT,
1077                       NoLocalDirectoryError: http.BAD_REQUEST,
1078                       }
1079             for k,v in errors.items():
1080                 if f.check(k):
1081                     req.setResponseCode(v)
1082                     req.setHeader("content-type", "text/plain")
1083                     return str(f.value)
1084             return f
1085         d.addErrback(_transform_error)
1086         return d
1087
1088     def _get_or_create_directories(self, node, path):
1089         if not IDirectoryNode.providedBy(node):
1090             # unfortunately it is too late to provide the name of the
1091             # blocking directory in the error message.
1092             raise BlockingFileError("cannot create directory because there "
1093                                     "is a file in the way")
1094         if not path:
1095             return defer.succeed(node)
1096         d = node.get(path[0])
1097         def _maybe_create(f):
1098             f.trap(KeyError)
1099             return node.create_empty_directory(path[0])
1100         d.addErrback(_maybe_create)
1101         d.addCallback(self._get_or_create_directories, path[1:])
1102         return d
1103
1104     def _check_replacement(self, node, name, replace):
1105         if replace:
1106             return node
1107         d = node.has_child(name)
1108         def _got(present):
1109             if present:
1110                 raise NoReplacementError("There was already a child by that "
1111                                          "name, and you asked me to not "
1112                                          "replace it.")
1113             return node
1114         d.addCallback(_got)
1115         return d
1116
1117     def _mkdir(self, node, name):
1118         d = node.create_empty_directory(name)
1119         def _done(newnode):
1120             return newnode.get_uri()
1121         d.addCallback(_done)
1122         return d
1123
1124     def _upload_file(self, node, contents, name):
1125         uploadable = FileHandle(contents)
1126         d = node.add_file(name, uploadable)
1127         def _done(filenode):
1128             log.msg("webish upload complete",
1129                     facility="tahoe.webish", level=log.NOISY)
1130             return filenode.get_uri()
1131         d.addCallback(_done)
1132         return d
1133
1134     def _upload_localfile(self, node, localfile, name):
1135         uploadable = FileName(localfile)
1136         d = node.add_file(name, uploadable)
1137         d.addCallback(lambda filenode: filenode.get_uri())
1138         return d
1139
1140     def _attach_uri(self, parentnode, contents, name):
1141         newuri = contents.read().strip()
1142         d = parentnode.set_uri(name, newuri)
1143         def _done(res):
1144             return newuri
1145         d.addCallback(_done)
1146         return d
1147
1148     def _upload_localdir(self, node, localdir):
1149         # build up a list of files to upload. TODO: for now, these files and
1150         # directories must have UTF-8 encoded filenames: anything else will
1151         # cause the upload to break.
1152         all_files = []
1153         all_dirs = []
1154         msg = "No files to upload! %s is empty" % localdir
1155         if not os.path.exists(localdir):
1156             msg = "%s doesn't exist!" % localdir
1157             raise NoLocalDirectoryError(msg)
1158         for root, dirs, files in os.walk(localdir):
1159             if root == localdir:
1160                 path = ()
1161             else:
1162                 relative_root = root[len(localdir)+1:]
1163                 path = tuple(relative_root.split(os.sep))
1164             for d in dirs:
1165                 this_dir = path + (d,)
1166                 this_dir = tuple([p.decode("utf-8") for p in this_dir])
1167                 all_dirs.append(this_dir)
1168             for f in files:
1169                 this_file = path + (f,)
1170                 this_file = tuple([p.decode("utf-8") for p in this_file])
1171                 all_files.append(this_file)
1172         d = defer.succeed(msg)
1173         for dir in all_dirs:
1174             if dir:
1175                 d.addCallback(self._makedir, node, dir)
1176         for f in all_files:
1177             d.addCallback(self._upload_one_file, node, localdir, f)
1178         return d
1179
1180     def _makedir(self, res, node, dir):
1181         d = defer.succeed(None)
1182         # get the parent. As long as os.walk gives us parents before
1183         # children, this ought to work
1184         d.addCallback(lambda res: node.get_child_at_path(dir[:-1]))
1185         # then create the child directory
1186         d.addCallback(lambda parent: parent.create_empty_directory(dir[-1]))
1187         return d
1188
1189     def _upload_one_file(self, res, node, localdir, f):
1190         # get the parent. We can be sure this exists because we already
1191         # went through and created all the directories we require.
1192         localfile = os.path.join(localdir, *f)
1193         d = node.get_child_at_path(f[:-1])
1194         d.addCallback(self._upload_localfile, localfile, f[-1])
1195         return d
1196
1197
1198 class Manifest(rend.Page):
1199     docFactory = getxmlfile("manifest.xhtml")
1200     def __init__(self, dirnode, dirpath):
1201         self._dirnode = dirnode
1202         self._dirpath = dirpath
1203
1204     def dirpath_as_string(self):
1205         return "/" + "/".join(self._dirpath)
1206
1207     def render_title(self, ctx):
1208         return T.title["Manifest of %s" % self.dirpath_as_string()]
1209
1210     def render_header(self, ctx):
1211         return T.p["Manifest of %s" % self.dirpath_as_string()]
1212
1213     def data_items(self, ctx, data):
1214         return self._dirnode.build_manifest()
1215
1216     def render_row(self, ctx, refresh_cap):
1217         ctx.fillSlots("refresh_capability", refresh_cap)
1218         return ctx.tag
1219
1220 class ChildError:
1221     implements(inevow.IResource)
1222     def renderHTTP(self, ctx):
1223         req = inevow.IRequest(ctx)
1224         req.setResponseCode(http.BAD_REQUEST)
1225         req.setHeader("content-type", "text/plain")
1226         return self.text
1227
1228 def child_error(text):
1229     ce = ChildError()
1230     ce.text = text
1231     return ce, ()
1232
1233 class VDrive(rend.Page):
1234
1235     def __init__(self, node, name):
1236         self.node = node
1237         self.name = name
1238
1239     def get_child_at_path(self, path):
1240         if path:
1241             return self.node.get_child_at_path(path)
1242         return defer.succeed(self.node)
1243
1244     def locateChild(self, ctx, segments):
1245         req = inevow.IRequest(ctx)
1246         method = req.method
1247         path = tuple([seg.decode("utf-8") for seg in segments])
1248
1249         t = get_arg(req, "t", "")
1250         localfile = get_arg(req, "localfile", None)
1251         if localfile is not None:
1252             if localfile != os.path.abspath(localfile):
1253                 return NeedAbsolutePathError(), ()
1254         localdir = get_arg(req, "localdir", None)
1255         if localdir is not None:
1256             if localdir != os.path.abspath(localdir):
1257                 return NeedAbsolutePathError(), ()
1258         if localfile or localdir:
1259             if not ILocalAccess(ctx).local_access_is_allowed():
1260                 return LocalAccessDisabledError(), ()
1261             if req.getHost().host != LOCALHOST:
1262                 return NeedLocalhostError(), ()
1263         # TODO: think about clobbering/revealing config files and node secrets
1264
1265         replace = boolean_of_arg(get_arg(req, "replace", "true"))
1266
1267         if method == "GET":
1268             # the node must exist, and our operation will be performed on the
1269             # node itself.
1270             d = self.get_child_at_path(path)
1271             def file_or_dir(node):
1272                 if (IFileNode.providedBy(node)
1273                     or IMutableFileNode.providedBy(node)):
1274                     filename = "unknown"
1275                     if path:
1276                         filename = path[-1]
1277                     filename = get_arg(req, "filename", filename)
1278                     if t == "download":
1279                         if localfile:
1280                             # write contents to a local file
1281                             return LocalFileDownloader(node, localfile), ()
1282                         # send contents as the result
1283                         return FileDownloader(node, filename), ()
1284                     elif t == "":
1285                         # send contents as the result
1286                         return FileDownloader(node, filename), ()
1287                     elif t == "json":
1288                         return FileJSONMetadata(node), ()
1289                     elif t == "uri":
1290                         return FileURI(node), ()
1291                     elif t == "readonly-uri":
1292                         return FileReadOnlyURI(node), ()
1293                     else:
1294                         return child_error("bad t=%s" % t)
1295                 elif IDirectoryNode.providedBy(node):
1296                     if t == "download":
1297                         if localdir:
1298                             # recursive download to a local directory
1299                             return LocalDirectoryDownloader(node, localdir), ()
1300                         return child_error("t=download requires localdir=")
1301                     elif t == "":
1302                         # send an HTML representation of the directory
1303                         return Directory(self.name, node, path), ()
1304                     elif t == "json":
1305                         return DirectoryJSONMetadata(node), ()
1306                     elif t == "uri":
1307                         return DirectoryURI(node), ()
1308                     elif t == "readonly-uri":
1309                         return DirectoryReadonlyURI(node), ()
1310                     elif t == "manifest":
1311                         return Manifest(node, path), ()
1312                     elif t == 'rename-form':
1313                         return RenameForm(self.name, node, path), ()
1314                     else:
1315                         return child_error("bad t=%s" % t)
1316                 else:
1317                     return child_error("unknown node type")
1318             d.addCallback(file_or_dir)
1319         elif method == "POST":
1320             # the node must exist, and our operation will be performed on the
1321             # node itself.
1322             d = self.get_child_at_path(path)
1323             def _got_POST(node):
1324                 return POSTHandler(node, replace), ()
1325             d.addCallback(_got_POST)
1326         elif method == "DELETE":
1327             # the node must exist, and our operation will be performed on its
1328             # parent node.
1329             assert path # you can't delete the root
1330             d = self.get_child_at_path(path[:-1])
1331             def _got_DELETE(node):
1332                 return DELETEHandler(node, path[-1]), ()
1333             d.addCallback(_got_DELETE)
1334         elif method in ("PUT",):
1335             # the node may or may not exist, and our operation may involve
1336             # all the ancestors of the node.
1337             return PUTHandler(self.node, path, t, localfile, localdir, replace), ()
1338         else:
1339             return rend.NotFound
1340         return d
1341
1342 class UnlinkedPUTCHKUploader(rend.Page):
1343     def renderHTTP(self, ctx):
1344         req = inevow.IRequest(ctx)
1345         assert req.method == "PUT"
1346         # "PUT /uri", to create an unlinked file. This is like PUT but
1347         # without the associated set_uri.
1348
1349         uploadable = FileHandle(req.content)
1350         d = IClient(ctx).upload(uploadable)
1351         d.addCallback(lambda results: results.uri)
1352         # that fires with the URI of the new file
1353         return d
1354
1355 class UnlinkedPUTSSKUploader(rend.Page):
1356     def renderHTTP(self, ctx):
1357         req = inevow.IRequest(ctx)
1358         assert req.method == "PUT"
1359         # SDMF: files are small, and we can only upload data
1360         req.content.seek(0)
1361         data = req.content.read()
1362         d = IClient(ctx).create_mutable_file(data)
1363         d.addCallback(lambda n: n.get_uri())
1364         return d
1365
1366 class UnlinkedPUTCreateDirectory(rend.Page):
1367     def renderHTTP(self, ctx):
1368         req = inevow.IRequest(ctx)
1369         assert req.method == "PUT"
1370         # "PUT /uri?t=mkdir", to create an unlinked directory.
1371         d = IClient(ctx).create_empty_dirnode()
1372         d.addCallback(lambda dirnode: dirnode.get_uri())
1373         # XXX add redirect_to_result
1374         return d
1375
1376 class UnlinkedPOSTCHKUploader(status.UploadResultsRendererMixin, rend.Page):
1377     """'POST /uri', to create an unlinked file."""
1378     docFactory = getxmlfile("upload-results.xhtml")
1379
1380     def __init__(self, client, req):
1381         rend.Page.__init__(self)
1382         # we start the upload now, and distribute notification of its
1383         # completion to render_ methods with an ObserverList
1384         assert req.method == "POST"
1385         self._done = observer.OneShotObserverList()
1386         fileobj = req.fields["file"].file
1387         uploadable = FileHandle(fileobj)
1388         d = client.upload(uploadable)
1389         d.addBoth(self._done.fire)
1390
1391     def renderHTTP(self, ctx):
1392         req = inevow.IRequest(ctx)
1393         when_done = get_arg(req, "when_done", None)
1394         if when_done:
1395             # if when_done= is provided, return a redirect instead of our
1396             # usual upload-results page
1397             d = self._done.when_fired()
1398             d.addCallback(lambda res: url.URL.fromString(when_done))
1399             return d
1400         return rend.Page.renderHTTP(self, ctx)
1401
1402     def upload_results(self):
1403         return self._done.when_fired()
1404
1405     def data_done(self, ctx, data):
1406         d = self.upload_results()
1407         d.addCallback(lambda res: "done!")
1408         return d
1409
1410     def data_uri(self, ctx, data):
1411         d = self.upload_results()
1412         d.addCallback(lambda res: res.uri)
1413         return d
1414
1415     def render_download_link(self, ctx, data):
1416         d = self.upload_results()
1417         d.addCallback(lambda res: T.a(href="/uri/" + urllib.quote(res.uri))
1418                       ["/uri/" + res.uri])
1419         return d
1420
1421 class UnlinkedPOSTSSKUploader(rend.Page):
1422     def renderHTTP(self, ctx):
1423         req = inevow.IRequest(ctx)
1424         assert req.method == "POST"
1425
1426         # "POST /uri", to create an unlinked file.
1427         # SDMF: files are small, and we can only upload data
1428         contents = req.fields["file"]
1429         contents.file.seek(0)
1430         data = contents.file.read()
1431         d = IClient(ctx).create_mutable_file(data)
1432         d.addCallback(lambda n: n.get_uri())
1433         return d
1434
1435 class UnlinkedPOSTCreateDirectory(rend.Page):
1436     def renderHTTP(self, ctx):
1437         req = inevow.IRequest(ctx)
1438         assert req.method == "POST"
1439
1440         # "POST /uri?t=mkdir", to create an unlinked directory.
1441         d = IClient(ctx).create_empty_dirnode()
1442         redirect = get_arg(req, "redirect_to_result", "false")
1443         if boolean_of_arg(redirect):
1444             def _then_redir(res):
1445                 new_url = "uri/" + urllib.quote(res.get_uri())
1446                 req.setResponseCode(http.SEE_OTHER) # 303
1447                 req.setHeader('location', new_url)
1448                 req.finish()
1449                 return ''
1450             d.addCallback(_then_redir)
1451         else:
1452             d.addCallback(lambda dirnode: dirnode.get_uri())
1453         return d
1454
1455 class Root(rend.Page):
1456
1457     addSlash = True
1458     docFactory = getxmlfile("welcome.xhtml")
1459
1460     def locateChild(self, ctx, segments):
1461         client = IClient(ctx)
1462         req = inevow.IRequest(ctx)
1463
1464         segments = list(segments) # XXX HELP I AM YUCKY!
1465         while segments and not segments[-1]:
1466             segments.pop()
1467         if not segments:
1468             segments.append('')
1469         segments = tuple(segments)
1470         if segments:
1471             if segments[0] == "uri":
1472                 if len(segments) == 1 or segments[1] == '':
1473                     uri = get_arg(req, "uri", None)
1474                     if uri is not None:
1475                         there = url.URL.fromContext(ctx)
1476                         there = there.clear("uri")
1477                         there = there.child("uri").child(uri)
1478                         return there, ()
1479                 if len(segments) == 1:
1480                     # /uri
1481                     if req.method == "PUT":
1482                         # either "PUT /uri" to create an unlinked file, or
1483                         # "PUT /uri?t=mkdir" to create an unlinked directory
1484                         t = get_arg(req, "t", "").strip()
1485                         if t == "":
1486                             mutable = bool(get_arg(req, "mutable", "").strip())
1487                             if mutable:
1488                                 return UnlinkedPUTSSKUploader(), ()
1489                             else:
1490                                 return UnlinkedPUTCHKUploader(), ()
1491                         if t == "mkdir":
1492                             return UnlinkedPUTCreateDirectory(), ()
1493                         errmsg = "/uri only accepts PUT and PUT?t=mkdir"
1494                         return WebError(http.BAD_REQUEST, errmsg), ()
1495
1496                     elif req.method == "POST":
1497                         # "POST /uri?t=upload&file=newfile" to upload an
1498                         # unlinked file or "POST /uri?t=mkdir" to create a
1499                         # new directory
1500                         t = get_arg(req, "t", "").strip()
1501                         if t in ("", "upload"):
1502                             mutable = bool(get_arg(req, "mutable", "").strip())
1503                             if mutable:
1504                                 return UnlinkedPOSTSSKUploader(), ()
1505                             else:
1506                                 return UnlinkedPOSTCHKUploader(client, req), ()
1507                         if t == "mkdir":
1508                             return UnlinkedPOSTCreateDirectory(), ()
1509                         errmsg = "/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, and POST?t=mkdir"
1510                         return WebError(http.BAD_REQUEST, errmsg), ()
1511                 if len(segments) < 2:
1512                     return rend.NotFound
1513                 uri = segments[1]
1514                 d = defer.maybeDeferred(client.create_node_from_uri, uri)
1515                 d.addCallback(lambda node: VDrive(node, uri))
1516                 d.addCallback(lambda vd: vd.locateChild(ctx, segments[2:]))
1517                 def _trap_KeyError(f):
1518                     f.trap(KeyError)
1519                     return rend.FourOhFour(), ()
1520                 d.addErrback(_trap_KeyError)
1521                 return d
1522             elif segments[0] == "xmlrpc":
1523                 raise NotImplementedError()
1524         return rend.Page.locateChild(self, ctx, segments)
1525
1526     child_webform_css = webform.defaultCSS
1527     child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
1528
1529     child_provisioning = provisioning.ProvisioningTool()
1530     child_status = status.Status()
1531
1532     def data_version(self, ctx, data):
1533         return get_package_versions_string()
1534     def data_import_path(self, ctx, data):
1535         return str(allmydata)
1536     def data_my_nodeid(self, ctx, data):
1537         return idlib.nodeid_b2a(IClient(ctx).nodeid)
1538     def data_storage(self, ctx, data):
1539         client = IClient(ctx)
1540         try:
1541             ss = client.getServiceNamed("storage")
1542         except KeyError:
1543             return "Not running"
1544         allocated = ss.allocated_size()
1545         return "about %d bytes allocated" % allocated
1546
1547     def data_introducer_furl(self, ctx, data):
1548         return IClient(ctx).introducer_furl
1549     def data_connected_to_introducer(self, ctx, data):
1550         if IClient(ctx).connected_to_introducer():
1551             return "yes"
1552         return "no"
1553
1554     def data_helper_furl(self, ctx, data):
1555         try:
1556             uploader = IClient(ctx).getServiceNamed("uploader")
1557         except KeyError:
1558             return None
1559         furl, connected = uploader.get_helper_info()
1560         return furl
1561     def data_connected_to_helper(self, ctx, data):
1562         try:
1563             uploader = IClient(ctx).getServiceNamed("uploader")
1564         except KeyError:
1565             return "no" # we don't even have an Uploader
1566         furl, connected = uploader.get_helper_info()
1567         if connected:
1568             return "yes"
1569         return "no"
1570
1571     def data_known_storage_servers(self, ctx, data):
1572         ic = IClient(ctx).introducer_client
1573         servers = [c
1574                    for c in ic.get_all_connectors().values()
1575                    if c.service_name == "storage"]
1576         return len(servers)
1577
1578     def data_connected_storage_servers(self, ctx, data):
1579         ic = IClient(ctx).introducer_client
1580         return len(ic.get_all_connections_for("storage"))
1581
1582     def data_services(self, ctx, data):
1583         ic = IClient(ctx).introducer_client
1584         c = [ (service_name, nodeid, rsc)
1585               for (nodeid, service_name), rsc
1586               in ic.get_all_connectors().items() ]
1587         c.sort()
1588         return c
1589
1590     def render_service_row(self, ctx, data):
1591         (service_name, nodeid, rsc) = data
1592         ctx.fillSlots("peerid", "%s %s" % (idlib.nodeid_b2a(nodeid),
1593                                            rsc.nickname))
1594         if rsc.rref:
1595             rhost = rsc.remote_host
1596             if nodeid == IClient(ctx).nodeid:
1597                 rhost_s = "(loopback)"
1598             elif isinstance(rhost, address.IPv4Address):
1599                 rhost_s = "%s:%d" % (rhost.host, rhost.port)
1600             else:
1601                 rhost_s = str(rhost)
1602             connected = "Yes: to " + rhost_s
1603             since = rsc.last_connect_time
1604         else:
1605             connected = "No"
1606             since = rsc.last_loss_time
1607
1608         TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
1609         ctx.fillSlots("connected", connected)
1610         ctx.fillSlots("since", time.strftime(TIME_FORMAT, time.localtime(since)))
1611         ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
1612                                                  time.localtime(rsc.announcement_time)))
1613         ctx.fillSlots("version", rsc.version)
1614         ctx.fillSlots("service_name", rsc.service_name)
1615
1616         return ctx.tag
1617
1618     def render_download_form(self, ctx, data):
1619         # this is a form where users can download files by URI
1620         form = T.form(action="uri", method="get",
1621                       enctype="multipart/form-data")[
1622             T.fieldset[
1623             T.legend(class_="freeform-form-label")["Download a file"],
1624             "URI to download: ",
1625             T.input(type="text", name="uri"), " ",
1626             "Filename to download as: ",
1627             T.input(type="text", name="filename"), " ",
1628             T.input(type="submit", value="Download!"),
1629             ]]
1630         return T.div[form]
1631
1632     def render_view_form(self, ctx, data):
1633         # this is a form where users can download files by URI, or jump to a
1634         # named directory
1635         form = T.form(action="uri", method="get",
1636                       enctype="multipart/form-data")[
1637             T.fieldset[
1638             T.legend(class_="freeform-form-label")["View a file or directory"],
1639             "URI to view: ",
1640             T.input(type="text", name="uri"), " ",
1641             T.input(type="submit", value="View!"),
1642             ]]
1643         return T.div[form]
1644
1645     def render_upload_form(self, ctx, data):
1646         # this is a form where users can upload unlinked files
1647         form = T.form(action="uri", method="post",
1648                       enctype="multipart/form-data")[
1649             T.fieldset[
1650             T.legend(class_="freeform-form-label")["Upload a file"],
1651             "Choose a file: ",
1652             T.input(type="file", name="file", class_="freeform-input-file"),
1653             T.input(type="hidden", name="t", value="upload"),
1654             " Mutable?:", T.input(type="checkbox", name="mutable"),
1655             T.input(type="submit", value="Upload!"),
1656             ]]
1657         return T.div[form]
1658
1659     def render_mkdir_form(self, ctx, data):
1660         # this is a form where users can create new directories
1661         form = T.form(action="uri", method="post",
1662                       enctype="multipart/form-data")[
1663             T.fieldset[
1664             T.legend(class_="freeform-form-label")["Create a directory"],
1665             T.input(type="hidden", name="t", value="mkdir"),
1666             T.input(type="hidden", name="redirect_to_result", value="true"),
1667             T.input(type="submit", value="Create Directory!"),
1668             ]]
1669         return T.div[form]
1670
1671
1672 class LocalAccess:
1673     implements(ILocalAccess)
1674     def __init__(self):
1675         self.local_access = False
1676     def local_access_is_allowed(self):
1677         return self.local_access
1678
1679 class WebishServer(service.MultiService):
1680     name = "webish"
1681
1682     def __init__(self, webport, nodeurl_path=None):
1683         service.MultiService.__init__(self)
1684         self.webport = webport
1685         self.root = Root()
1686         self.site = site = appserver.NevowSite(self.root)
1687         self.site.requestFactory = MyRequest
1688         self.allow_local = LocalAccess()
1689         self.site.remember(self.allow_local, ILocalAccess)
1690         s = strports.service(webport, site)
1691         s.setServiceParent(self)
1692         self.listener = s # stash it so the tests can query for the portnum
1693         self._started = defer.Deferred()
1694         if nodeurl_path:
1695             self._started.addCallback(self._write_nodeurl_file, nodeurl_path)
1696
1697     def allow_local_access(self, enable=True):
1698         self.allow_local.local_access = enable
1699
1700     def startService(self):
1701         service.MultiService.startService(self)
1702         # to make various services available to render_* methods, we stash a
1703         # reference to the client on the NevowSite. This will be available by
1704         # adapting the 'context' argument to a special marker interface named
1705         # IClient.
1706         self.site.remember(self.parent, IClient)
1707         # I thought you could do the same with an existing interface, but
1708         # apparently 'ISite' does not exist
1709         #self.site._client = self.parent
1710         self._started.callback(None)
1711
1712     def _write_nodeurl_file(self, junk, nodeurl_path):
1713         # what is our webport?
1714         s = self.listener
1715         if isinstance(s, internet.TCPServer):
1716             base_url = "http://localhost:%d" % s._port.getHost().port
1717         elif isinstance(s, internet.SSLServer):
1718             base_url = "https://localhost:%d" % s._port.getHost().port
1719         else:
1720             base_url = None
1721         if base_url:
1722             f = open(nodeurl_path, 'wb')
1723             # this file is world-readable
1724             f.write(base_url + "\n")
1725             f.close()
1726