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