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