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