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