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