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