]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/webish.py
webish: download-results: add per-server response times
[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, IUploadStatus, IDownloadStatus
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", "on", "off")
37     return arg.lower() in ("true", "t", "1", "on")
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 _POST_mkdir(self, name):
801         d = self._check_replacement(name)
802         d.addCallback(lambda res: self._node.create_empty_directory(name))
803         d.addCallback(lambda res: "directory created")
804         return d
805
806     def _POST_uri(self, name, newuri):
807         d = self._check_replacement(name)
808         d.addCallback(lambda res: self._node.set_uri(name, newuri))
809         d.addCallback(lambda res: newuri)
810         return d
811
812     def _POST_delete(self, name):
813         if name is None:
814             # apparently an <input type="hidden" name="name" value="">
815             # won't show up in the resulting encoded form.. the 'name'
816             # field is completely missing. So to allow deletion of an
817             # empty file, we have to pretend that None means ''. The only
818             # downide of this is a slightly confusing error message if
819             # someone does a POST without a name= field. For our own HTML
820             # thisn't a big deal, because we create the 'delete' POST
821             # buttons ourselves.
822             name = ''
823         d = self._node.delete(name)
824         d.addCallback(lambda res: "thing deleted")
825         return d
826
827     def _POST_rename(self, name, from_name, to_name):
828         d = self._check_replacement(to_name)
829         d.addCallback(lambda res: self._node.get(from_name))
830         def add_dest(child):
831             uri = child.get_uri()
832             # now actually do the rename
833             return self._node.set_uri(to_name, uri)
834         d.addCallback(add_dest)
835         def rm_src(junk):
836             return self._node.delete(from_name)
837         d.addCallback(rm_src)
838         d.addCallback(lambda res: "thing renamed")
839         return d
840
841     def _POST_upload(self, contents, name, mutable, client):
842         if mutable:
843             # SDMF: files are small, and we can only upload data.
844             contents.file.seek(0)
845             data = contents.file.read()
846             #uploadable = FileHandle(contents.file)
847             d = self._check_replacement(name)
848             d.addCallback(lambda res: self._node.has_child(name))
849             def _checked(present):
850                 if present:
851                     # modify the existing one instead of creating a new
852                     # one
853                     d2 = self._node.get(name)
854                     def _got_newnode(newnode):
855                         d3 = newnode.replace(data)
856                         d3.addCallback(lambda res: newnode.get_uri())
857                         return d3
858                     d2.addCallback(_got_newnode)
859                 else:
860                     d2 = client.create_mutable_file(data)
861                     def _uploaded(newnode):
862                         d1 = self._node.set_node(name, newnode)
863                         d1.addCallback(lambda res: newnode.get_uri())
864                         return d1
865                     d2.addCallback(_uploaded)
866                 return d2
867             d.addCallback(_checked)
868         else:
869             uploadable = FileHandle(contents.file)
870             d = self._check_replacement(name)
871             d.addCallback(lambda res: self._node.add_file(name, uploadable))
872             def _done(newnode):
873                 return newnode.get_uri()
874             d.addCallback(_done)
875         return d
876
877     def _POST_overwrite(self, contents):
878         # SDMF: files are small, and we can only upload data.
879         contents.file.seek(0)
880         data = contents.file.read()
881         # TODO: 'name' handling needs review
882         d = defer.succeed(self._node)
883         def _got_child_overwrite(child_node):
884             child_node.replace(data)
885             return child_node.get_uri()
886         d.addCallback(_got_child_overwrite)
887         return d
888
889     def _POST_check(self, name):
890         d = self._node.get(name)
891         def _got_child_check(child_node):
892             d2 = child_node.check()
893             def _done(res):
894                 log.msg("checked %s, results %s" % (child_node, res),
895                         facility="tahoe.webish", level=log.NOISY)
896                 return str(res)
897             d2.addCallback(_done)
898             return d2
899         d.addCallback(_got_child_check)
900         return d
901
902     def _POST_set_children(self, children):
903         cs = []
904         for name, (file_or_dir, mddict) in children.iteritems():
905             cap = str(mddict.get('rw_uri') or mddict.get('ro_uri'))
906             cs.append((name, cap, mddict.get('metadata')))
907
908         d = self._node.set_children(cs)
909         d.addCallback(lambda res: "Okay so I did it.")
910         return d
911
912     def renderHTTP(self, ctx):
913         req = inevow.IRequest(ctx)
914
915         t = get_arg(req, "t")
916         assert t is not None
917
918         charset = get_arg(req, "_charset", "utf-8")
919
920         name = get_arg(req, "name", None)
921         if name and "/" in name:
922             req.setResponseCode(http.BAD_REQUEST)
923             req.setHeader("content-type", "text/plain")
924             return "name= may not contain a slash"
925         if name is not None:
926             name = name.strip()
927             name = name.decode(charset)
928             assert isinstance(name, unicode)
929         # we allow the user to delete an empty-named file, but not to create
930         # them, since that's an easy and confusing mistake to make
931
932         when_done = get_arg(req, "when_done", None)
933         if not boolean_of_arg(get_arg(req, "replace", "true")):
934             self._replace = False
935
936         if t == "mkdir":
937             if not name:
938                 raise RuntimeError("mkdir requires a name")
939             d = self._POST_mkdir(name)
940         elif t == "uri":
941             if not name:
942                 raise RuntimeError("set-uri requires a name")
943             newuri = get_arg(req, "uri")
944             assert newuri is not None
945             d = self._POST_uri(name, newuri)
946         elif t == "delete":
947             d = self._POST_delete(name)
948         elif t == "rename":
949             from_name = get_arg(req, "from_name")
950             if from_name is not None:
951                 from_name = from_name.strip()
952                 from_name = from_name.decode(charset)
953                 assert isinstance(from_name, unicode)
954             to_name = get_arg(req, "to_name")
955             if to_name is not None:
956                 to_name = to_name.strip()
957                 to_name = to_name.decode(charset)
958                 assert isinstance(to_name, unicode)
959             if not from_name or not to_name:
960                 raise RuntimeError("rename requires from_name and to_name")
961             if not IDirectoryNode.providedBy(self._node):
962                 raise RuntimeError("rename must only be called on directories")
963             for k,v in [ ('from_name', from_name), ('to_name', to_name) ]:
964                 if v and "/" in v:
965                     req.setResponseCode(http.BAD_REQUEST)
966                     req.setHeader("content-type", "text/plain")
967                     return "%s= may not contain a slash" % (k,)
968             d = self._POST_rename(name, from_name, to_name)
969         elif t == "upload":
970             contents = req.fields["file"]
971             name = name or contents.filename
972             if name is not None:
973                 name = name.strip()
974             if not name:
975                 # this prohibts empty, missing, and all-whitespace filenames
976                 raise RuntimeError("upload requires a name")
977             name = name.decode(charset)
978             assert isinstance(name, unicode)
979             mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
980             d = self._POST_upload(contents, name, mutable, IClient(ctx))
981         elif t == "overwrite":
982             contents = req.fields["file"]
983             d = self._POST_overwrite(contents)
984         elif t == "check":
985             d = self._POST_check(name)
986         elif t == "set_children":
987             req.content.seek(0)
988             body = req.content.read()
989             try:
990                 children = simplejson.loads(body)
991             except ValueError, le:
992                 le.args = tuple(le.args + (body,))
993                 # TODO test handling of bad JSON
994                 raise
995             d = self._POST_set_children(children)
996         else:
997             print "BAD t=%s" % t
998             return "BAD t=%s" % t
999         if when_done:
1000             d.addCallback(lambda res: url.URL.fromString(when_done))
1001         def _check_replacement(f):
1002             # TODO: make this more human-friendly: maybe send them to the
1003             # when_done page but with an extra query-arg that will display
1004             # the error message in a big box at the top of the page. The
1005             # directory page that when_done= usually points to accepts a
1006             # result= argument.. use that.
1007             f.trap(NoReplacementError)
1008             req.setResponseCode(http.CONFLICT)
1009             req.setHeader("content-type", "text/plain")
1010             return str(f.value)
1011         d.addErrback(_check_replacement)
1012         return d
1013
1014 class DELETEHandler(rend.Page):
1015     def __init__(self, node, name):
1016         self._node = node
1017         self._name = name
1018
1019     def renderHTTP(self, ctx):
1020         req = inevow.IRequest(ctx)
1021         d = self._node.delete(self._name)
1022         def _done(res):
1023             # what should this return??
1024             return "%s deleted" % self._name.encode("utf-8")
1025         d.addCallback(_done)
1026         def _trap_missing(f):
1027             f.trap(KeyError)
1028             req.setResponseCode(http.NOT_FOUND)
1029             req.setHeader("content-type", "text/plain")
1030             return "no such child %s" % self._name.encode("utf-8")
1031         d.addErrback(_trap_missing)
1032         return d
1033
1034 class PUTHandler(rend.Page):
1035     def __init__(self, node, path, t, localfile, localdir, replace):
1036         self._node = node
1037         self._path = path
1038         self._t = t
1039         self._localfile = localfile
1040         self._localdir = localdir
1041         self._replace = replace
1042
1043     def renderHTTP(self, ctx):
1044         req = inevow.IRequest(ctx)
1045         t = self._t
1046         localfile = self._localfile
1047         localdir = self._localdir
1048
1049         if t == "upload" and not (localfile or localdir):
1050             req.setResponseCode(http.BAD_REQUEST)
1051             req.setHeader("content-type", "text/plain")
1052             return "t=upload requires localfile= or localdir="
1053
1054         # we must traverse the path, creating new directories as necessary
1055         d = self._get_or_create_directories(self._node, self._path[:-1])
1056         name = self._path[-1]
1057         d.addCallback(self._check_replacement, name, self._replace)
1058         if t == "upload":
1059             if localfile:
1060                 d.addCallback(self._upload_localfile, localfile, name)
1061             else:
1062                 # localdir
1063                 # take the last step
1064                 d.addCallback(self._get_or_create_directories, self._path[-1:])
1065                 d.addCallback(self._upload_localdir, localdir)
1066         elif t == "uri":
1067             d.addCallback(self._attach_uri, req.content, name)
1068         elif t == "mkdir":
1069             d.addCallback(self._mkdir, name)
1070         else:
1071             d.addCallback(self._upload_file, req.content, name)
1072
1073         def _transform_error(f):
1074             errors = {BlockingFileError: http.BAD_REQUEST,
1075                       NoReplacementError: http.CONFLICT,
1076                       NoLocalDirectoryError: http.BAD_REQUEST,
1077                       }
1078             for k,v in errors.items():
1079                 if f.check(k):
1080                     req.setResponseCode(v)
1081                     req.setHeader("content-type", "text/plain")
1082                     return str(f.value)
1083             return f
1084         d.addErrback(_transform_error)
1085         return d
1086
1087     def _get_or_create_directories(self, node, path):
1088         if not IDirectoryNode.providedBy(node):
1089             # unfortunately it is too late to provide the name of the
1090             # blocking directory in the error message.
1091             raise BlockingFileError("cannot create directory because there "
1092                                     "is a file in the way")
1093         if not path:
1094             return defer.succeed(node)
1095         d = node.get(path[0])
1096         def _maybe_create(f):
1097             f.trap(KeyError)
1098             return node.create_empty_directory(path[0])
1099         d.addErrback(_maybe_create)
1100         d.addCallback(self._get_or_create_directories, path[1:])
1101         return d
1102
1103     def _check_replacement(self, node, name, replace):
1104         if replace:
1105             return node
1106         d = node.has_child(name)
1107         def _got(present):
1108             if present:
1109                 raise NoReplacementError("There was already a child by that "
1110                                          "name, and you asked me to not "
1111                                          "replace it.")
1112             return node
1113         d.addCallback(_got)
1114         return d
1115
1116     def _mkdir(self, node, name):
1117         d = node.create_empty_directory(name)
1118         def _done(newnode):
1119             return newnode.get_uri()
1120         d.addCallback(_done)
1121         return d
1122
1123     def _upload_file(self, node, contents, name):
1124         uploadable = FileHandle(contents)
1125         d = node.add_file(name, uploadable)
1126         def _done(filenode):
1127             log.msg("webish upload complete",
1128                     facility="tahoe.webish", level=log.NOISY)
1129             return filenode.get_uri()
1130         d.addCallback(_done)
1131         return d
1132
1133     def _upload_localfile(self, node, localfile, name):
1134         uploadable = FileName(localfile)
1135         d = node.add_file(name, uploadable)
1136         d.addCallback(lambda filenode: filenode.get_uri())
1137         return d
1138
1139     def _attach_uri(self, parentnode, contents, name):
1140         newuri = contents.read().strip()
1141         d = parentnode.set_uri(name, newuri)
1142         def _done(res):
1143             return newuri
1144         d.addCallback(_done)
1145         return d
1146
1147     def _upload_localdir(self, node, localdir):
1148         # build up a list of files to upload. TODO: for now, these files and
1149         # directories must have UTF-8 encoded filenames: anything else will
1150         # cause the upload to break.
1151         all_files = []
1152         all_dirs = []
1153         msg = "No files to upload! %s is empty" % localdir
1154         if not os.path.exists(localdir):
1155             msg = "%s doesn't exist!" % localdir
1156             raise NoLocalDirectoryError(msg)
1157         for root, dirs, files in os.walk(localdir):
1158             if root == localdir:
1159                 path = ()
1160             else:
1161                 relative_root = root[len(localdir)+1:]
1162                 path = tuple(relative_root.split(os.sep))
1163             for d in dirs:
1164                 this_dir = path + (d,)
1165                 this_dir = tuple([p.decode("utf-8") for p in this_dir])
1166                 all_dirs.append(this_dir)
1167             for f in files:
1168                 this_file = path + (f,)
1169                 this_file = tuple([p.decode("utf-8") for p in this_file])
1170                 all_files.append(this_file)
1171         d = defer.succeed(msg)
1172         for dir in all_dirs:
1173             if dir:
1174                 d.addCallback(self._makedir, node, dir)
1175         for f in all_files:
1176             d.addCallback(self._upload_one_file, node, localdir, f)
1177         return d
1178
1179     def _makedir(self, res, node, dir):
1180         d = defer.succeed(None)
1181         # get the parent. As long as os.walk gives us parents before
1182         # children, this ought to work
1183         d.addCallback(lambda res: node.get_child_at_path(dir[:-1]))
1184         # then create the child directory
1185         d.addCallback(lambda parent: parent.create_empty_directory(dir[-1]))
1186         return d
1187
1188     def _upload_one_file(self, res, node, localdir, f):
1189         # get the parent. We can be sure this exists because we already
1190         # went through and created all the directories we require.
1191         localfile = os.path.join(localdir, *f)
1192         d = node.get_child_at_path(f[:-1])
1193         d.addCallback(self._upload_localfile, localfile, f[-1])
1194         return d
1195
1196
1197 class Manifest(rend.Page):
1198     docFactory = getxmlfile("manifest.xhtml")
1199     def __init__(self, dirnode, dirpath):
1200         self._dirnode = dirnode
1201         self._dirpath = dirpath
1202
1203     def dirpath_as_string(self):
1204         return "/" + "/".join(self._dirpath)
1205
1206     def render_title(self, ctx):
1207         return T.title["Manifest of %s" % self.dirpath_as_string()]
1208
1209     def render_header(self, ctx):
1210         return T.p["Manifest of %s" % self.dirpath_as_string()]
1211
1212     def data_items(self, ctx, data):
1213         return self._dirnode.build_manifest()
1214
1215     def render_row(self, ctx, refresh_cap):
1216         ctx.fillSlots("refresh_capability", refresh_cap)
1217         return ctx.tag
1218
1219 class ChildError:
1220     implements(inevow.IResource)
1221     def renderHTTP(self, ctx):
1222         req = inevow.IRequest(ctx)
1223         req.setResponseCode(http.BAD_REQUEST)
1224         req.setHeader("content-type", "text/plain")
1225         return self.text
1226
1227 def child_error(text):
1228     ce = ChildError()
1229     ce.text = text
1230     return ce, ()
1231
1232 class VDrive(rend.Page):
1233
1234     def __init__(self, node, name):
1235         self.node = node
1236         self.name = name
1237
1238     def get_child_at_path(self, path):
1239         if path:
1240             return self.node.get_child_at_path(path)
1241         return defer.succeed(self.node)
1242
1243     def locateChild(self, ctx, segments):
1244         req = inevow.IRequest(ctx)
1245         method = req.method
1246         path = tuple([seg.decode("utf-8") for seg in segments])
1247
1248         t = get_arg(req, "t", "")
1249         localfile = get_arg(req, "localfile", None)
1250         if localfile is not None:
1251             if localfile != os.path.abspath(localfile):
1252                 return NeedAbsolutePathError(), ()
1253         localdir = get_arg(req, "localdir", None)
1254         if localdir is not None:
1255             if localdir != os.path.abspath(localdir):
1256                 return NeedAbsolutePathError(), ()
1257         if localfile or localdir:
1258             if not ILocalAccess(ctx).local_access_is_allowed():
1259                 return LocalAccessDisabledError(), ()
1260             if req.getHost().host != LOCALHOST:
1261                 return NeedLocalhostError(), ()
1262         # TODO: think about clobbering/revealing config files and node secrets
1263
1264         replace = boolean_of_arg(get_arg(req, "replace", "true"))
1265
1266         if method == "GET":
1267             # the node must exist, and our operation will be performed on the
1268             # node itself.
1269             d = self.get_child_at_path(path)
1270             def file_or_dir(node):
1271                 if (IFileNode.providedBy(node)
1272                     or IMutableFileNode.providedBy(node)):
1273                     filename = "unknown"
1274                     if path:
1275                         filename = path[-1]
1276                     filename = get_arg(req, "filename", filename)
1277                     if t == "download":
1278                         if localfile:
1279                             # write contents to a local file
1280                             return LocalFileDownloader(node, localfile), ()
1281                         # send contents as the result
1282                         return FileDownloader(node, filename), ()
1283                     elif t == "":
1284                         # send contents as the result
1285                         return FileDownloader(node, filename), ()
1286                     elif t == "json":
1287                         return FileJSONMetadata(node), ()
1288                     elif t == "uri":
1289                         return FileURI(node), ()
1290                     elif t == "readonly-uri":
1291                         return FileReadOnlyURI(node), ()
1292                     else:
1293                         return child_error("bad t=%s" % t)
1294                 elif IDirectoryNode.providedBy(node):
1295                     if t == "download":
1296                         if localdir:
1297                             # recursive download to a local directory
1298                             return LocalDirectoryDownloader(node, localdir), ()
1299                         return child_error("t=download requires localdir=")
1300                     elif t == "":
1301                         # send an HTML representation of the directory
1302                         return Directory(self.name, node, path), ()
1303                     elif t == "json":
1304                         return DirectoryJSONMetadata(node), ()
1305                     elif t == "uri":
1306                         return DirectoryURI(node), ()
1307                     elif t == "readonly-uri":
1308                         return DirectoryReadonlyURI(node), ()
1309                     elif t == "manifest":
1310                         return Manifest(node, path), ()
1311                     elif t == 'rename-form':
1312                         return RenameForm(self.name, node, path), ()
1313                     else:
1314                         return child_error("bad t=%s" % t)
1315                 else:
1316                     return child_error("unknown node type")
1317             d.addCallback(file_or_dir)
1318         elif method == "POST":
1319             # the node must exist, and our operation will be performed on the
1320             # node itself.
1321             d = self.get_child_at_path(path)
1322             def _got_POST(node):
1323                 return POSTHandler(node, replace), ()
1324             d.addCallback(_got_POST)
1325         elif method == "DELETE":
1326             # the node must exist, and our operation will be performed on its
1327             # parent node.
1328             assert path # you can't delete the root
1329             d = self.get_child_at_path(path[:-1])
1330             def _got_DELETE(node):
1331                 return DELETEHandler(node, path[-1]), ()
1332             d.addCallback(_got_DELETE)
1333         elif method in ("PUT",):
1334             # the node may or may not exist, and our operation may involve
1335             # all the ancestors of the node.
1336             return PUTHandler(self.node, path, t, localfile, localdir, replace), ()
1337         else:
1338             return rend.NotFound
1339         return d
1340
1341 class UnlinkedPUTCHKUploader(rend.Page):
1342     def renderHTTP(self, ctx):
1343         req = inevow.IRequest(ctx)
1344         assert req.method == "PUT"
1345         # "PUT /uri", to create an unlinked file. This is like PUT but
1346         # without the associated set_uri.
1347
1348         uploadable = FileHandle(req.content)
1349         d = IClient(ctx).upload(uploadable)
1350         d.addCallback(lambda results: results.uri)
1351         # that fires with the URI of the new file
1352         return d
1353
1354 class UnlinkedPUTSSKUploader(rend.Page):
1355     def renderHTTP(self, ctx):
1356         req = inevow.IRequest(ctx)
1357         assert req.method == "PUT"
1358         # SDMF: files are small, and we can only upload data
1359         req.content.seek(0)
1360         data = req.content.read()
1361         d = IClient(ctx).create_mutable_file(data)
1362         d.addCallback(lambda n: n.get_uri())
1363         return d
1364
1365 class UnlinkedPUTCreateDirectory(rend.Page):
1366     def renderHTTP(self, ctx):
1367         req = inevow.IRequest(ctx)
1368         assert req.method == "PUT"
1369         # "PUT /uri?t=mkdir", to create an unlinked directory.
1370         d = IClient(ctx).create_empty_dirnode()
1371         d.addCallback(lambda dirnode: dirnode.get_uri())
1372         # XXX add redirect_to_result
1373         return d
1374
1375 def plural(sequence):
1376     if len(sequence) == 1:
1377         return ""
1378     return "s"
1379
1380 class UploadResultsRendererMixin:
1381     # this requires a method named 'upload_results'
1382
1383     def render_sharemap(self, ctx, data):
1384         d = self.upload_results()
1385         d.addCallback(lambda res: res.sharemap)
1386         def _render(sharemap):
1387             if sharemap is None:
1388                 return "None"
1389             l = T.ul()
1390             for shnum in sorted(sharemap.keys()):
1391                 l[T.li["%d -> %s" % (shnum, sharemap[shnum])]]
1392             return l
1393         d.addCallback(_render)
1394         return d
1395
1396     def render_servermap(self, ctx, data):
1397         d = self.upload_results()
1398         d.addCallback(lambda res: res.servermap)
1399         def _render(servermap):
1400             if servermap is None:
1401                 return "None"
1402             l = T.ul()
1403             for peerid in sorted(servermap.keys()):
1404                 peerid_s = idlib.shortnodeid_b2a(peerid)
1405                 shares_s = ",".join(["#%d" % shnum
1406                                      for shnum in servermap[peerid]])
1407                 l[T.li["[%s] got share%s: %s" % (peerid_s,
1408                                                  plural(servermap[peerid]),
1409                                                  shares_s)]]
1410             return l
1411         d.addCallback(_render)
1412         return d
1413
1414     def data_file_size(self, ctx, data):
1415         d = self.upload_results()
1416         d.addCallback(lambda res: res.file_size)
1417         return d
1418
1419     def render_time(self, ctx, data):
1420         # 1.23s, 790ms, 132us
1421         if data is None:
1422             return ""
1423         s = float(data)
1424         if s >= 1.0:
1425             return "%.2fs" % s
1426         if s >= 0.01:
1427             return "%dms" % (1000*s)
1428         if s >= 0.001:
1429             return "%.1fms" % (1000*s)
1430         return "%dus" % (1000000*s)
1431
1432     def render_rate(self, ctx, data):
1433         # 21.8kBps, 554.4kBps 4.37MBps
1434         if data is None:
1435             return ""
1436         r = float(data)
1437         if r > 1000000:
1438             return "%1.2fMBps" % (r/1000000)
1439         if r > 1000:
1440             return "%.1fkBps" % (r/1000)
1441         return "%dBps" % r
1442
1443     def _get_time(self, name):
1444         d = self.upload_results()
1445         d.addCallback(lambda res: res.timings.get(name))
1446         return d
1447
1448     def data_time_total(self, ctx, data):
1449         return self._get_time("total")
1450
1451     def data_time_storage_index(self, ctx, data):
1452         return self._get_time("storage_index")
1453
1454     def data_time_contacting_helper(self, ctx, data):
1455         return self._get_time("contacting_helper")
1456
1457     def data_time_existence_check(self, ctx, data):
1458         return self._get_time("existence_check")
1459
1460     def data_time_cumulative_fetch(self, ctx, data):
1461         return self._get_time("cumulative_fetch")
1462
1463     def data_time_helper_total(self, ctx, data):
1464         return self._get_time("helper_total")
1465
1466     def data_time_peer_selection(self, ctx, data):
1467         return self._get_time("peer_selection")
1468
1469     def data_time_total_encode_and_push(self, ctx, data):
1470         return self._get_time("total_encode_and_push")
1471
1472     def data_time_cumulative_encoding(self, ctx, data):
1473         return self._get_time("cumulative_encoding")
1474
1475     def data_time_cumulative_sending(self, ctx, data):
1476         return self._get_time("cumulative_sending")
1477
1478     def data_time_hashes_and_close(self, ctx, data):
1479         return self._get_time("hashes_and_close")
1480
1481     def _get_rate(self, name):
1482         d = self.upload_results()
1483         def _convert(r):
1484             file_size = r.file_size
1485             time = r.timings.get(name)
1486             if time is None:
1487                 return None
1488             try:
1489                 return 1.0 * file_size / time
1490             except ZeroDivisionError:
1491                 return None
1492         d.addCallback(_convert)
1493         return d
1494
1495     def data_rate_total(self, ctx, data):
1496         return self._get_rate("total")
1497
1498     def data_rate_storage_index(self, ctx, data):
1499         return self._get_rate("storage_index")
1500
1501     def data_rate_encode(self, ctx, data):
1502         return self._get_rate("cumulative_encoding")
1503
1504     def data_rate_push(self, ctx, data):
1505         return self._get_rate("cumulative_sending")
1506
1507     def data_rate_encode_and_push(self, ctx, data):
1508         d = self.upload_results()
1509         def _convert(r):
1510             file_size = r.file_size
1511             if file_size is None:
1512                 return None
1513             time1 = r.timings.get("cumulative_encoding")
1514             if time1 is None:
1515                 return None
1516             time2 = r.timings.get("cumulative_sending")
1517             if time2 is None:
1518                 return None
1519             try:
1520                 return 1.0 * file_size / (time1+time2)
1521             except ZeroDivisionError:
1522                 return None
1523         d.addCallback(_convert)
1524         return d
1525
1526     def data_rate_ciphertext_fetch(self, ctx, data):
1527         d = self.upload_results()
1528         def _convert(r):
1529             fetch_size = r.ciphertext_fetched
1530             if fetch_size is None:
1531                 return None
1532             time = r.timings.get("cumulative_fetch")
1533             if time is None:
1534                 return None
1535             try:
1536                 return 1.0 * fetch_size / time
1537             except ZeroDivisionError:
1538                 return None
1539         d.addCallback(_convert)
1540         return d
1541
1542 class UnlinkedPOSTCHKUploader(UploadResultsRendererMixin, rend.Page):
1543     """'POST /uri', to create an unlinked file."""
1544     docFactory = getxmlfile("upload-results.xhtml")
1545
1546     def __init__(self, client, req):
1547         rend.Page.__init__(self)
1548         # we start the upload now, and distribute notification of its
1549         # completion to render_ methods with an ObserverList
1550         assert req.method == "POST"
1551         self._done = observer.OneShotObserverList()
1552         fileobj = req.fields["file"].file
1553         uploadable = FileHandle(fileobj)
1554         d = client.upload(uploadable)
1555         d.addBoth(self._done.fire)
1556
1557     def renderHTTP(self, ctx):
1558         req = inevow.IRequest(ctx)
1559         when_done = get_arg(req, "when_done", None)
1560         if when_done:
1561             # if when_done= is provided, return a redirect instead of our
1562             # usual upload-results page
1563             d = self._done.when_fired()
1564             d.addCallback(lambda res: url.URL.fromString(when_done))
1565             return d
1566         return rend.Page.renderHTTP(self, ctx)
1567
1568     def upload_results(self):
1569         return self._done.when_fired()
1570
1571     def data_done(self, ctx, data):
1572         d = self.upload_results()
1573         d.addCallback(lambda res: "done!")
1574         return d
1575
1576     def data_uri(self, ctx, data):
1577         d = self.upload_results()
1578         d.addCallback(lambda res: res.uri)
1579         return d
1580
1581     def render_download_link(self, ctx, data):
1582         d = self.upload_results()
1583         d.addCallback(lambda res: T.a(href="/uri/" + urllib.quote(res.uri))
1584                       ["/uri/" + res.uri])
1585         return d
1586
1587 class UnlinkedPOSTSSKUploader(rend.Page):
1588     def renderHTTP(self, ctx):
1589         req = inevow.IRequest(ctx)
1590         assert req.method == "POST"
1591
1592         # "POST /uri", to create an unlinked file.
1593         # SDMF: files are small, and we can only upload data
1594         contents = req.fields["file"]
1595         contents.file.seek(0)
1596         data = contents.file.read()
1597         d = IClient(ctx).create_mutable_file(data)
1598         d.addCallback(lambda n: n.get_uri())
1599         return d
1600
1601 class UnlinkedPOSTCreateDirectory(rend.Page):
1602     def renderHTTP(self, ctx):
1603         req = inevow.IRequest(ctx)
1604         assert req.method == "POST"
1605
1606         # "POST /uri?t=mkdir", to create an unlinked directory.
1607         d = IClient(ctx).create_empty_dirnode()
1608         redirect = get_arg(req, "redirect_to_result", "false")
1609         if boolean_of_arg(redirect):
1610             def _then_redir(res):
1611                 new_url = "uri/" + urllib.quote(res.get_uri())
1612                 req.setResponseCode(http.SEE_OTHER) # 303
1613                 req.setHeader('location', new_url)
1614                 req.finish()
1615                 return ''
1616             d.addCallback(_then_redir)
1617         else:
1618             d.addCallback(lambda dirnode: dirnode.get_uri())
1619         return d
1620
1621 class UploadStatusPage(UploadResultsRendererMixin, rend.Page):
1622     docFactory = getxmlfile("upload-status.xhtml")
1623
1624     def __init__(self, data):
1625         rend.Page.__init__(self, data)
1626         self.upload_status = data
1627
1628     def upload_results(self):
1629         return defer.maybeDeferred(self.upload_status.get_results)
1630
1631     def render_results(self, ctx, data):
1632         d = self.upload_results()
1633         def _got_results(results):
1634             if results:
1635                 return ctx.tag
1636             return ""
1637         d.addCallback(_got_results)
1638         return d
1639
1640     def render_si(self, ctx, data):
1641         si_s = base32.b2a_or_none(data.get_storage_index())
1642         if si_s is None:
1643             si_s = "(None)"
1644         return si_s
1645
1646     def render_helper(self, ctx, data):
1647         return {True: "Yes",
1648                 False: "No"}[data.using_helper()]
1649
1650     def render_total_size(self, ctx, data):
1651         size = data.get_size()
1652         if size is None:
1653             size = "(unknown)"
1654         return size
1655
1656     def render_progress_hash(self, ctx, data):
1657         progress = data.get_progress()[0]
1658         # TODO: make an ascii-art bar
1659         return "%.1f%%" % (100.0 * progress)
1660
1661     def render_progress_ciphertext(self, ctx, data):
1662         progress = data.get_progress()[1]
1663         # TODO: make an ascii-art bar
1664         return "%.1f%%" % (100.0 * progress)
1665
1666     def render_progress_encode_push(self, ctx, data):
1667         progress = data.get_progress()[2]
1668         # TODO: make an ascii-art bar
1669         return "%.1f%%" % (100.0 * progress)
1670
1671     def render_status(self, ctx, data):
1672         return data.get_status()
1673
1674 class DownloadResultsRendererMixin:
1675     # this requires a method named 'download_results'
1676
1677     def render_servermap(self, ctx, data):
1678         d = self.download_results()
1679         d.addCallback(lambda res: res.servermap)
1680         def _render(servermap):
1681             if servermap is None:
1682                 return "None"
1683             l = T.ul()
1684             for peerid in sorted(servermap.keys()):
1685                 peerid_s = idlib.shortnodeid_b2a(peerid)
1686                 shares_s = ",".join(["#%d" % shnum
1687                                      for shnum in servermap[peerid]])
1688                 l[T.li["[%s] has share%s: %s" % (peerid_s,
1689                                                  plural(servermap[peerid]),
1690                                                  shares_s)]]
1691             return l
1692         d.addCallback(_render)
1693         return d
1694
1695     def render_servers_used(self, ctx, data):
1696         d = self.download_results()
1697         d.addCallback(lambda res: res.servers_used)
1698         def _got(servers_used):
1699             if not servers_used:
1700                 return ""
1701             peerids_s = ", ".join(["[%s]" % idlib.shortnodeid_b2a(peerid)
1702                                    for peerid in servers_used])
1703             return T.li["Servers Used: ", peerids_s]
1704         d.addCallback(_got)
1705         return d
1706
1707     def render_problems(self, ctx, data):
1708         d = self.download_results()
1709         d.addCallback(lambda res: res.server_problems)
1710         def _got(server_problems):
1711             if not server_problems:
1712                 return ""
1713             l = T.ul()
1714             for peerid in sorted(server_problems.keys()):
1715                 peerid_s = idlib.shortnodeid_b2a(peerid)
1716                 l[T.li["[%s]: %s" % (peerid_s, server_problems[peerid])]]
1717             return T.li["Server Problems:", l]
1718         d.addCallback(_got)
1719         return d
1720
1721     def data_file_size(self, ctx, data):
1722         d = self.download_results()
1723         d.addCallback(lambda res: res.file_size)
1724         return d
1725
1726     def render_time(self, ctx, data):
1727         # 1.23s, 790ms, 132us
1728         if data is None:
1729             return ""
1730         s = float(data)
1731         if s >= 1.0:
1732             return "%.2fs" % s
1733         if s >= 0.01:
1734             return "%dms" % (1000*s)
1735         if s >= 0.001:
1736             return "%.1fms" % (1000*s)
1737         return "%dus" % (1000000*s)
1738
1739     def render_rate(self, ctx, data):
1740         # 21.8kBps, 554.4kBps 4.37MBps
1741         if data is None:
1742             return ""
1743         r = float(data)
1744         if r > 1000000:
1745             return "%1.2fMBps" % (r/1000000)
1746         if r > 1000:
1747             return "%.1fkBps" % (r/1000)
1748         return "%dBps" % r
1749
1750     def _get_time(self, name):
1751         d = self.download_results()
1752         d.addCallback(lambda res: res.timings.get(name))
1753         return d
1754
1755     def data_time_total(self, ctx, data):
1756         return self._get_time("total")
1757
1758     def data_time_peer_selection(self, ctx, data):
1759         return self._get_time("peer_selection")
1760
1761     def data_time_uri_extension(self, ctx, data):
1762         return self._get_time("uri_extension")
1763
1764     def data_time_hashtrees(self, ctx, data):
1765         return self._get_time("hashtrees")
1766
1767     def data_time_segments(self, ctx, data):
1768         return self._get_time("segments")
1769
1770     def data_time_cumulative_fetch(self, ctx, data):
1771         return self._get_time("cumulative_fetch")
1772
1773     def data_time_cumulative_decode(self, ctx, data):
1774         return self._get_time("cumulative_decode")
1775
1776     def data_time_cumulative_decrypt(self, ctx, data):
1777         return self._get_time("cumulative_decrypt")
1778
1779     def _get_rate(self, name):
1780         d = self.download_results()
1781         def _convert(r):
1782             file_size = r.file_size
1783             time = r.timings.get(name)
1784             if time is None:
1785                 return None
1786             try:
1787                 return 1.0 * file_size / time
1788             except ZeroDivisionError:
1789                 return None
1790         d.addCallback(_convert)
1791         return d
1792
1793     def data_rate_total(self, ctx, data):
1794         return self._get_rate("total")
1795
1796     def data_rate_segments(self, ctx, data):
1797         return self._get_rate("segments")
1798
1799     def data_rate_fetch(self, ctx, data):
1800         return self._get_rate("cumulative_fetch")
1801
1802     def data_rate_decode(self, ctx, data):
1803         return self._get_rate("cumulative_decode")
1804
1805     def data_rate_decrypt(self, ctx, data):
1806         return self._get_rate("cumulative_decrypt")
1807
1808     def render_server_timings(self, ctx, data):
1809         d = self.download_results()
1810         d.addCallback(lambda res: res.timings.get("fetch_per_server"))
1811         def _render(per_server):
1812             if per_server is None:
1813                 return ""
1814             l = T.ul()
1815             for peerid in sorted(per_server.keys()):
1816                 peerid_s = idlib.shortnodeid_b2a(peerid)
1817                 times_s = ", ".join([self.render_time(None, t)
1818                                      for t in per_server[peerid]])
1819                 l[T.li["[%s]: %s" % (peerid_s, times_s)]]
1820             return T.li["Per-Server Segment Fetch Response Times: ", l]
1821         d.addCallback(_render)
1822         return d
1823
1824 class DownloadStatusPage(DownloadResultsRendererMixin, rend.Page):
1825     docFactory = getxmlfile("download-status.xhtml")
1826
1827     def __init__(self, data):
1828         rend.Page.__init__(self, data)
1829         self.download_status = data
1830
1831     def download_results(self):
1832         return defer.maybeDeferred(self.download_status.get_results)
1833
1834     def render_results(self, ctx, data):
1835         d = self.download_results()
1836         def _got_results(results):
1837             if results:
1838                 return ctx.tag
1839             return ""
1840         d.addCallback(_got_results)
1841         return d
1842
1843     def render_si(self, ctx, data):
1844         si_s = base32.b2a_or_none(data.get_storage_index())
1845         if si_s is None:
1846             si_s = "(None)"
1847         return si_s
1848
1849     def render_helper(self, ctx, data):
1850         return {True: "Yes",
1851                 False: "No"}[data.using_helper()]
1852
1853     def render_total_size(self, ctx, data):
1854         size = data.get_size()
1855         if size is None:
1856             size = "(unknown)"
1857         return size
1858
1859     def render_progress(self, ctx, data):
1860         progress = data.get_progress()
1861         # TODO: make an ascii-art bar
1862         return "%.1f%%" % (100.0 * progress)
1863
1864     def render_status(self, ctx, data):
1865         return data.get_status()
1866
1867 class Status(rend.Page):
1868     docFactory = getxmlfile("status.xhtml")
1869     addSlash = True
1870
1871     def data_active_uploads(self, ctx, data):
1872         return [u for u in IClient(ctx).list_active_uploads()]
1873     def data_active_downloads(self, ctx, data):
1874         return [d for d in IClient(ctx).list_active_downloads()]
1875     def data_recent_uploads(self, ctx, data):
1876         return [u for u in IClient(ctx).list_recent_uploads()
1877                 if not u.get_active()]
1878     def data_recent_downloads(self, ctx, data):
1879         return [d for d in IClient(ctx).list_recent_downloads()
1880                 if not d.get_active()]
1881
1882     def childFactory(self, ctx, name):
1883         client = IClient(ctx)
1884         stype,count_s = name.split("-")
1885         count = int(count_s)
1886         if stype == "up":
1887             for s in client.list_recent_uploads():
1888                 if s.get_counter() == count:
1889                     return UploadStatusPage(s)
1890             for s in client.list_all_uploads():
1891                 if s.get_counter() == count:
1892                     return UploadStatusPage(s)
1893         if stype == "down":
1894             for s in client.list_recent_downloads():
1895                 if s.get_counter() == count:
1896                     return DownloadStatusPage(s)
1897             for s in client.list_all_downloads():
1898                 if s.get_counter() == count:
1899                     return DownloadStatusPage(s)
1900
1901     def _render_common(self, ctx, data):
1902         s = data
1903         si_s = base32.b2a_or_none(s.get_storage_index())
1904         if si_s is None:
1905             si_s = "(None)"
1906         ctx.fillSlots("si", si_s)
1907         ctx.fillSlots("helper", {True: "Yes",
1908                                  False: "No"}[s.using_helper()])
1909         size = s.get_size()
1910         if size is None:
1911             size = "(unknown)"
1912         ctx.fillSlots("total_size", size)
1913         if IUploadStatus.providedBy(data):
1914             link = "up-%d" % data.get_counter()
1915         else:
1916             assert IDownloadStatus.providedBy(data)
1917             link = "down-%d" % data.get_counter()
1918         ctx.fillSlots("status", T.a(href=link)[s.get_status()])
1919
1920     def render_row_upload(self, ctx, data):
1921         self._render_common(ctx, data)
1922         (chk, ciphertext, encandpush) = data.get_progress()
1923         # TODO: make an ascii-art bar
1924         ctx.fillSlots("progress_hash", "%.1f%%" % (100.0 * chk))
1925         ctx.fillSlots("progress_ciphertext", "%.1f%%" % (100.0 * ciphertext))
1926         ctx.fillSlots("progress_encode", "%.1f%%" % (100.0 * encandpush))
1927         return ctx.tag
1928
1929     def render_row_download(self, ctx, data):
1930         self._render_common(ctx, data)
1931         progress = data.get_progress()
1932         # TODO: make an ascii-art bar
1933         ctx.fillSlots("progress", "%.1f%%" % (100.0 * progress))
1934         return ctx.tag
1935
1936
1937 class Root(rend.Page):
1938
1939     addSlash = True
1940     docFactory = getxmlfile("welcome.xhtml")
1941
1942     def locateChild(self, ctx, segments):
1943         client = IClient(ctx)
1944         req = inevow.IRequest(ctx)
1945
1946         segments = list(segments) # XXX HELP I AM YUCKY!
1947         while segments and not segments[-1]:
1948             segments.pop()
1949         if not segments:
1950             segments.append('')
1951         segments = tuple(segments)
1952         if segments:
1953             if segments[0] == "uri":
1954                 if len(segments) == 1 or segments[1] == '':
1955                     uri = get_arg(req, "uri", None)
1956                     if uri is not None:
1957                         there = url.URL.fromContext(ctx)
1958                         there = there.clear("uri")
1959                         there = there.child("uri").child(uri)
1960                         return there, ()
1961                 if len(segments) == 1:
1962                     # /uri
1963                     if req.method == "PUT":
1964                         # either "PUT /uri" to create an unlinked file, or
1965                         # "PUT /uri?t=mkdir" to create an unlinked directory
1966                         t = get_arg(req, "t", "").strip()
1967                         if t == "":
1968                             mutable = bool(get_arg(req, "mutable", "").strip())
1969                             if mutable:
1970                                 return UnlinkedPUTSSKUploader(), ()
1971                             else:
1972                                 return UnlinkedPUTCHKUploader(), ()
1973                         if t == "mkdir":
1974                             return UnlinkedPUTCreateDirectory(), ()
1975                         errmsg = "/uri only accepts PUT and PUT?t=mkdir"
1976                         return WebError(http.BAD_REQUEST, errmsg), ()
1977
1978                     elif req.method == "POST":
1979                         # "POST /uri?t=upload&file=newfile" to upload an
1980                         # unlinked file or "POST /uri?t=mkdir" to create a
1981                         # new directory
1982                         t = get_arg(req, "t", "").strip()
1983                         if t in ("", "upload"):
1984                             mutable = bool(get_arg(req, "mutable", "").strip())
1985                             if mutable:
1986                                 return UnlinkedPOSTSSKUploader(), ()
1987                             else:
1988                                 return UnlinkedPOSTCHKUploader(client, req), ()
1989                         if t == "mkdir":
1990                             return UnlinkedPOSTCreateDirectory(), ()
1991                         errmsg = "/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, and POST?t=mkdir"
1992                         return WebError(http.BAD_REQUEST, errmsg), ()
1993                 if len(segments) < 2:
1994                     return rend.NotFound
1995                 uri = segments[1]
1996                 d = defer.maybeDeferred(client.create_node_from_uri, uri)
1997                 d.addCallback(lambda node: VDrive(node, uri))
1998                 d.addCallback(lambda vd: vd.locateChild(ctx, segments[2:]))
1999                 def _trap_KeyError(f):
2000                     f.trap(KeyError)
2001                     return rend.FourOhFour(), ()
2002                 d.addErrback(_trap_KeyError)
2003                 return d
2004             elif segments[0] == "xmlrpc":
2005                 raise NotImplementedError()
2006         return rend.Page.locateChild(self, ctx, segments)
2007
2008     child_webform_css = webform.defaultCSS
2009     child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
2010
2011     child_provisioning = provisioning.ProvisioningTool()
2012     child_status = Status()
2013
2014     def data_version(self, ctx, data):
2015         return get_package_versions_string()
2016     def data_import_path(self, ctx, data):
2017         return str(allmydata)
2018     def data_my_nodeid(self, ctx, data):
2019         return idlib.nodeid_b2a(IClient(ctx).nodeid)
2020     def data_storage(self, ctx, data):
2021         client = IClient(ctx)
2022         try:
2023             ss = client.getServiceNamed("storage")
2024         except KeyError:
2025             return "Not running"
2026         allocated = ss.allocated_size()
2027         return "about %d bytes allocated" % allocated
2028
2029     def data_introducer_furl(self, ctx, data):
2030         return IClient(ctx).introducer_furl
2031     def data_connected_to_introducer(self, ctx, data):
2032         if IClient(ctx).connected_to_introducer():
2033             return "yes"
2034         return "no"
2035
2036     def data_helper_furl(self, ctx, data):
2037         try:
2038             uploader = IClient(ctx).getServiceNamed("uploader")
2039         except KeyError:
2040             return None
2041         furl, connected = uploader.get_helper_info()
2042         return furl
2043     def data_connected_to_helper(self, ctx, data):
2044         try:
2045             uploader = IClient(ctx).getServiceNamed("uploader")
2046         except KeyError:
2047             return "no" # we don't even have an Uploader
2048         furl, connected = uploader.get_helper_info()
2049         if connected:
2050             return "yes"
2051         return "no"
2052
2053     def data_known_storage_servers(self, ctx, data):
2054         ic = IClient(ctx).introducer_client
2055         servers = [c
2056                    for c in ic.get_all_connectors().values()
2057                    if c.service_name == "storage"]
2058         return len(servers)
2059
2060     def data_connected_storage_servers(self, ctx, data):
2061         ic = IClient(ctx).introducer_client
2062         return len(ic.get_all_connections_for("storage"))
2063
2064     def data_services(self, ctx, data):
2065         ic = IClient(ctx).introducer_client
2066         c = [ (service_name, nodeid, rsc)
2067               for (nodeid, service_name), rsc
2068               in ic.get_all_connectors().items() ]
2069         c.sort()
2070         return c
2071
2072     def render_service_row(self, ctx, data):
2073         (service_name, nodeid, rsc) = data
2074         ctx.fillSlots("peerid", "%s %s" % (idlib.nodeid_b2a(nodeid),
2075                                            rsc.nickname))
2076         if rsc.rref:
2077             rhost = rsc.remote_host
2078             if nodeid == IClient(ctx).nodeid:
2079                 rhost_s = "(loopback)"
2080             elif isinstance(rhost, address.IPv4Address):
2081                 rhost_s = "%s:%d" % (rhost.host, rhost.port)
2082             else:
2083                 rhost_s = str(rhost)
2084             connected = "Yes: to " + rhost_s
2085             since = rsc.last_connect_time
2086         else:
2087             connected = "No"
2088             since = rsc.last_loss_time
2089
2090         TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
2091         ctx.fillSlots("connected", connected)
2092         ctx.fillSlots("since", time.strftime(TIME_FORMAT, time.localtime(since)))
2093         ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
2094                                                  time.localtime(rsc.announcement_time)))
2095         ctx.fillSlots("version", rsc.version)
2096         ctx.fillSlots("service_name", rsc.service_name)
2097
2098         return ctx.tag
2099
2100     def render_download_form(self, ctx, data):
2101         # this is a form where users can download files by URI
2102         form = T.form(action="uri", method="get",
2103                       enctype="multipart/form-data")[
2104             T.fieldset[
2105             T.legend(class_="freeform-form-label")["Download a file"],
2106             "URI to download: ",
2107             T.input(type="text", name="uri"), " ",
2108             "Filename to download as: ",
2109             T.input(type="text", name="filename"), " ",
2110             T.input(type="submit", value="Download!"),
2111             ]]
2112         return T.div[form]
2113
2114     def render_view_form(self, ctx, data):
2115         # this is a form where users can download files by URI, or jump to a
2116         # named directory
2117         form = T.form(action="uri", method="get",
2118                       enctype="multipart/form-data")[
2119             T.fieldset[
2120             T.legend(class_="freeform-form-label")["View a file or directory"],
2121             "URI to view: ",
2122             T.input(type="text", name="uri"), " ",
2123             T.input(type="submit", value="View!"),
2124             ]]
2125         return T.div[form]
2126
2127     def render_upload_form(self, ctx, data):
2128         # this is a form where users can upload unlinked files
2129         form = T.form(action="uri", method="post",
2130                       enctype="multipart/form-data")[
2131             T.fieldset[
2132             T.legend(class_="freeform-form-label")["Upload a file"],
2133             "Choose a file: ",
2134             T.input(type="file", name="file", class_="freeform-input-file"),
2135             T.input(type="hidden", name="t", value="upload"),
2136             " Mutable?:", T.input(type="checkbox", name="mutable"),
2137             T.input(type="submit", value="Upload!"),
2138             ]]
2139         return T.div[form]
2140
2141     def render_mkdir_form(self, ctx, data):
2142         # this is a form where users can create new directories
2143         form = T.form(action="uri", method="post",
2144                       enctype="multipart/form-data")[
2145             T.fieldset[
2146             T.legend(class_="freeform-form-label")["Create a directory"],
2147             T.input(type="hidden", name="t", value="mkdir"),
2148             T.input(type="hidden", name="redirect_to_result", value="true"),
2149             T.input(type="submit", value="Create Directory!"),
2150             ]]
2151         return T.div[form]
2152
2153
2154 class LocalAccess:
2155     implements(ILocalAccess)
2156     def __init__(self):
2157         self.local_access = False
2158     def local_access_is_allowed(self):
2159         return self.local_access
2160
2161 class WebishServer(service.MultiService):
2162     name = "webish"
2163
2164     def __init__(self, webport, nodeurl_path=None):
2165         service.MultiService.__init__(self)
2166         self.webport = webport
2167         self.root = Root()
2168         self.site = site = appserver.NevowSite(self.root)
2169         self.site.requestFactory = MyRequest
2170         self.allow_local = LocalAccess()
2171         self.site.remember(self.allow_local, ILocalAccess)
2172         s = strports.service(webport, site)
2173         s.setServiceParent(self)
2174         self.listener = s # stash it so the tests can query for the portnum
2175         self._started = defer.Deferred()
2176         if nodeurl_path:
2177             self._started.addCallback(self._write_nodeurl_file, nodeurl_path)
2178
2179     def allow_local_access(self, enable=True):
2180         self.allow_local.local_access = enable
2181
2182     def startService(self):
2183         service.MultiService.startService(self)
2184         # to make various services available to render_* methods, we stash a
2185         # reference to the client on the NevowSite. This will be available by
2186         # adapting the 'context' argument to a special marker interface named
2187         # IClient.
2188         self.site.remember(self.parent, IClient)
2189         # I thought you could do the same with an existing interface, but
2190         # apparently 'ISite' does not exist
2191         #self.site._client = self.parent
2192         self._started.callback(None)
2193
2194     def _write_nodeurl_file(self, junk, nodeurl_path):
2195         # what is our webport?
2196         s = self.listener
2197         if isinstance(s, internet.TCPServer):
2198             base_url = "http://localhost:%d" % s._port.getHost().port
2199         elif isinstance(s, internet.SSLServer):
2200             base_url = "https://localhost:%d" % s._port.getHost().port
2201         else:
2202             base_url = None
2203         if base_url:
2204             f = open(nodeurl_path, 'wb')
2205             # this file is world-readable
2206             f.write(base_url + "\n")
2207             f.close()
2208