source file: /home/buildslave/tahoe/edgy/build/src/allmydata/web/directory.py
file stats: 759 lines, 731 executed: 96.3% covered
coverage versus previous test: 0 lines added, 0 lines removed
    1. 
    2. import simplejson
    3. import urllib
    4. 
    5. from zope.interface import implements
    6. from twisted.internet import defer
    7. from twisted.internet.interfaces import IPushProducer
    8. from twisted.python.failure import Failure
    9. from twisted.web import http, html
   10. from nevow import url, rend, inevow, tags as T
   11. from nevow.inevow import IRequest
   12. 
   13. from foolscap.api import fireEventually
   14. 
   15. from allmydata.util import base32, time_format
   16. from allmydata.uri import from_string_dirnode
   17. from allmydata.interfaces import IDirectoryNode, IFileNode, IMutableFileNode, \
   18.      IFilesystemNode, ExistingChildError, NoSuchChildError
   19. from allmydata.monitor import Monitor, OperationCancelledError
   20. from allmydata import dirnode
   21. from allmydata.web.common import text_plain, WebError, \
   22.      IOpHandleTable, NeedOperationHandleError, \
   23.      boolean_of_arg, get_arg, get_root, parse_replace_arg, \
   24.      should_create_intermediate_directories, \
   25.      getxmlfile, RenderMixin, humanize_failure
   26. from allmydata.web.filenode import ReplaceMeMixin, \
   27.      FileNodeHandler, PlaceHolderNodeHandler
   28. from allmydata.web.check_results import CheckResults, \
   29.      CheckAndRepairResults, DeepCheckResults, DeepCheckAndRepairResults
   30. from allmydata.web.info import MoreInfo
   31. from allmydata.web.operations import ReloadMixin
   32. from allmydata.web.check_results import json_check_results, \
   33.      json_check_and_repair_results
   34. 
   35. class BlockingFileError(Exception):
   36.     # TODO: catch and transform
   37.     """We cannot auto-create a parent directory, because there is a file in
   38.     the way"""
   39. 
   40. def make_handler_for(node, client, parentnode=None, name=None):
   41.     if parentnode:
   42.         assert IDirectoryNode.providedBy(parentnode)
   43.     if IMutableFileNode.providedBy(node):
   44.         return FileNodeHandler(client, node, parentnode, name)
   45.     if IFileNode.providedBy(node):
   46.         return FileNodeHandler(client, node, parentnode, name)
   47.     if IDirectoryNode.providedBy(node):
   48.         return DirectoryNodeHandler(client, node, parentnode, name)
   49.     return UnknownNodeHandler(client, node, parentnode, name)
   50. 
   51. class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
   52.     addSlash = True
   53. 
   54.     def __init__(self, client, node, parentnode=None, name=None):
   55.         rend.Page.__init__(self)
   56.         self.client = client
   57.         assert node
   58.         self.node = node
   59.         self.parentnode = parentnode
   60.         self.name = name
   61. 
   62.     def childFactory(self, ctx, name):
   63.         req = IRequest(ctx)
   64.         name = name.decode("utf-8")
   65.         d = self.node.get(name)
   66.         d.addBoth(self.got_child, ctx, name)
   67.         # got_child returns a handler resource: FileNodeHandler or
   68.         # DirectoryNodeHandler
   69.         return d
   70. 
   71.     def got_child(self, node_or_failure, ctx, name):
   72.         DEBUG = False
   73.         if DEBUG: print "GOT_CHILD", name, node_or_failure
   74.         req = IRequest(ctx)
   75.         method = req.method
   76.         nonterminal = len(req.postpath) > 1
   77.         t = get_arg(req, "t", "").strip()
   78.         if isinstance(node_or_failure, Failure):
   79.             f = node_or_failure
   80.             f.trap(NoSuchChildError)
   81.             # No child by this name. What should we do about it?
   82.             if DEBUG: print "no child", name
   83.             if DEBUG: print "postpath", req.postpath
   84.             if nonterminal:
   85.                 if DEBUG: print " intermediate"
   86.                 if should_create_intermediate_directories(req):
   87.                     # create intermediate directories
   88.                     if DEBUG: print " making intermediate directory"
   89.                     d = self.node.create_empty_directory(name)
   90.                     d.addCallback(make_handler_for,
   91.                                   self.client, self.node, name)
   92.                     return d
   93.             else:
   94.                 if DEBUG: print " terminal"
   95.                 # terminal node
   96.                 if (method,t) in [ ("POST","mkdir"), ("PUT","mkdir") ]:
   97.                     if DEBUG: print " making final directory"
   98.                     # final directory
   99.                     d = self.node.create_empty_directory(name)
  100.                     d.addCallback(make_handler_for,
  101.                                   self.client, self.node, name)
  102.                     return d
  103.                 if (method,t) in ( ("PUT",""), ("PUT","uri"), ):
  104.                     if DEBUG: print " PUT, making leaf placeholder"
  105.                     # we were trying to find the leaf filenode (to put a new
  106.                     # file in its place), and it didn't exist. That's ok,
  107.                     # since that's the leaf node that we're about to create.
  108.                     # We make a dummy one, which will respond to the PUT
  109.                     # request by replacing itself.
  110.                     return PlaceHolderNodeHandler(self.client, self.node, name)
  111.             if DEBUG: print " 404"
  112.             # otherwise, we just return a no-such-child error
  113.             return f
  114. 
  115.         node = node_or_failure
  116.         if nonterminal and should_create_intermediate_directories(req):
  117.             if not IDirectoryNode.providedBy(node):
  118.                 # we would have put a new directory here, but there was a
  119.                 # file in the way.
  120.                 if DEBUG: print "blocking"
  121.                 raise WebError("Unable to create directory '%s': "
  122.                                "a file was in the way" % name,
  123.                                http.CONFLICT)
  124.         if DEBUG: print "good child"
  125.         return make_handler_for(node, self.client, self.node, name)
  126. 
  127.     def render_DELETE(self, ctx):
  128.         assert self.parentnode and self.name
  129.         d = self.parentnode.delete(self.name)
  130.         d.addCallback(lambda res: self.node.get_uri())
  131.         return d
  132. 
  133.     def render_GET(self, ctx):
  134.         req = IRequest(ctx)
  135.         # This is where all of the directory-related ?t=* code goes.
  136.         t = get_arg(req, "t", "").strip()
  137.         if not t:
  138.             # render the directory as HTML, using the docFactory and Nevow's
  139.             # whole templating thing.
  140.             return DirectoryAsHTML(self.node)
  141. 
  142.         if t == "json":
  143.             return DirectoryJSONMetadata(ctx, self.node)
  144.         if t == "info":
  145.             return MoreInfo(self.node)
  146.         if t == "uri":
  147.             return DirectoryURI(ctx, self.node)
  148.         if t == "readonly-uri":
  149.             return DirectoryReadonlyURI(ctx, self.node)
  150.         if t == 'rename-form':
  151.             return RenameForm(self.node)
  152. 
  153.         raise WebError("GET directory: bad t=%s" % t)
  154. 
  155.     def render_PUT(self, ctx):
  156.         req = IRequest(ctx)
  157.         t = get_arg(req, "t", "").strip()
  158.         replace = parse_replace_arg(get_arg(req, "replace", "true"))
  159. 
  160.         if t == "mkdir":
  161.             # our job was done by the traversal/create-intermediate-directory
  162.             # process that got us here.
  163.             return text_plain(self.node.get_uri(), ctx) # TODO: urlencode
  164.         if t == "uri":
  165.             if not replace:
  166.                 # they're trying to set_uri and that name is already occupied
  167.                 # (by us).
  168.                 raise ExistingChildError()
  169.             d = self.replace_me_with_a_childcap(req, self.client, replace)
  170.             # TODO: results
  171.             return d
  172. 
  173.         raise WebError("PUT to a directory")
  174. 
  175.     def render_POST(self, ctx):
  176.         req = IRequest(ctx)
  177.         t = get_arg(req, "t", "").strip()
  178. 
  179.         if t == "mkdir":
  180.             d = self._POST_mkdir(req)
  181.         elif t == "mkdir-p":
  182.             # TODO: docs, tests
  183.             d = self._POST_mkdir_p(req)
  184.         elif t == "upload":
  185.             d = self._POST_upload(ctx) # this one needs the context
  186.         elif t == "uri":
  187.             d = self._POST_uri(req)
  188.         elif t == "delete":
  189.             d = self._POST_delete(req)
  190.         elif t == "rename":
  191.             d = self._POST_rename(req)
  192.         elif t == "check":
  193.             d = self._POST_check(req)
  194.         elif t == "start-deep-check":
  195.             d = self._POST_start_deep_check(ctx)
  196.         elif t == "stream-deep-check":
  197.             d = self._POST_stream_deep_check(ctx)
  198.         elif t == "start-manifest":
  199.             d = self._POST_start_manifest(ctx)
  200.         elif t == "start-deep-size":
  201.             d = self._POST_start_deep_size(ctx)
  202.         elif t == "start-deep-stats":
  203.             d = self._POST_start_deep_stats(ctx)
  204.         elif t == "stream-manifest":
  205.             d = self._POST_stream_manifest(ctx)
  206.         elif t == "set_children":
  207.             # TODO: docs
  208.             d = self._POST_set_children(req)
  209.         else:
  210.             raise WebError("POST to a directory with bad t=%s" % t)
  211. 
  212.         when_done = get_arg(req, "when_done", None)
  213.         if when_done:
  214.             d.addCallback(lambda res: url.URL.fromString(when_done))
  215.         return d
  216. 
  217.     def _POST_mkdir(self, req):
  218.         name = get_arg(req, "name", "")
  219.         if not name:
  220.             # our job is done, it was handled by the code in got_child
  221.             # which created the final directory (i.e. us)
  222.             return defer.succeed(self.node.get_uri()) # TODO: urlencode
  223.         name = name.decode("utf-8")
  224.         replace = boolean_of_arg(get_arg(req, "replace", "true"))
  225.         d = self.node.create_empty_directory(name, overwrite=replace)
  226.         d.addCallback(lambda child: child.get_uri()) # TODO: urlencode
  227.         return d
  228. 
  229.     def _POST_mkdir_p(self, req):
  230.         path = get_arg(req, "path")
  231.         if not path:
  232.             raise WebError("mkdir-p requires a path")
  233.         path_ = tuple([seg.decode("utf-8") for seg in path.split('/') if seg ])
  234.         # TODO: replace
  235.         d = self._get_or_create_directories(self.node, path_)
  236.         d.addCallback(lambda node: node.get_uri())
  237.         return d
  238. 
  239.     def _get_or_create_directories(self, node, path):
  240.         if not IDirectoryNode.providedBy(node):
  241.             # unfortunately it is too late to provide the name of the
  242.             # blocking directory in the error message.
  243.             raise BlockingFileError("cannot create directory because there "
  244.                                     "is a file in the way")
  245.         if not path:
  246.             return defer.succeed(node)
  247.         d = node.get(path[0])
  248.         def _maybe_create(f):
  249.             f.trap(NoSuchChildError)
  250.             return node.create_empty_directory(path[0])
  251.         d.addErrback(_maybe_create)
  252.         d.addCallback(self._get_or_create_directories, path[1:])
  253.         return d
  254. 
  255.     def _POST_upload(self, ctx):
  256.         req = IRequest(ctx)
  257.         charset = get_arg(req, "_charset", "utf-8")
  258.         contents = req.fields["file"]
  259.         assert contents.filename is None or isinstance(contents.filename, str)
  260.         name = get_arg(req, "name")
  261.         name = name or contents.filename
  262.         if name is not None:
  263.             name = name.strip()
  264.         if not name:
  265.             # this prohibts empty, missing, and all-whitespace filenames
  266.             raise WebError("upload requires a name")
  267.         assert isinstance(name, str)
  268.         name = name.decode(charset)
  269.         if "/" in name:
  270.             raise WebError("name= may not contain a slash", http.BAD_REQUEST)
  271.         assert isinstance(name, unicode)
  272. 
  273.         # since POST /uri/path/file?t=upload is equivalent to
  274.         # POST /uri/path/dir?t=upload&name=foo, just do the same thing that
  275.         # childFactory would do. Things are cleaner if we only do a subset of
  276.         # them, though, so we don't do: d = self.childFactory(ctx, name)
  277. 
  278.         d = self.node.get(name)
  279.         def _maybe_got_node(node_or_failure):
  280.             if isinstance(node_or_failure, Failure):
  281.                 f = node_or_failure
  282.                 f.trap(NoSuchChildError)
  283.                 # create a placeholder which will see POST t=upload
  284.                 return PlaceHolderNodeHandler(self.client, self.node, name)
  285.             else:
  286.                 node = node_or_failure
  287.                 return make_handler_for(node, self.client, self.node, name)
  288.         d.addBoth(_maybe_got_node)
  289.         # now we have a placeholder or a filenodehandler, and we can just
  290.         # delegate to it. We could return the resource back out of
  291.         # DirectoryNodeHandler.renderHTTP, and nevow would recurse into it,
  292.         # but the addCallback() that handles when_done= would break.
  293.         d.addCallback(lambda child: child.renderHTTP(ctx))
  294.         return d
  295. 
  296.     def _POST_uri(self, req):
  297.         childcap = get_arg(req, "uri")
  298.         if not childcap:
  299.             raise WebError("set-uri requires a uri")
  300.         name = get_arg(req, "name")
  301.         if not name:
  302.             raise WebError("set-uri requires a name")
  303.         charset = get_arg(req, "_charset", "utf-8")
  304.         name = name.decode(charset)
  305.         replace = boolean_of_arg(get_arg(req, "replace", "true"))
  306.         d = self.node.set_uri(name, childcap, overwrite=replace)
  307.         d.addCallback(lambda res: childcap)
  308.         return d
  309. 
  310.     def _POST_delete(self, req):
  311.         name = get_arg(req, "name")
  312.         if name is None:
  313.             # apparently an <input type="hidden" name="name" value="">
  314.             # won't show up in the resulting encoded form.. the 'name'
  315.             # field is completely missing. So to allow deletion of an
  316.             # empty file, we have to pretend that None means ''. The only
  317.             # downide of this is a slightly confusing error message if
  318.             # someone does a POST without a name= field. For our own HTML
  319.             # thisn't a big deal, because we create the 'delete' POST
  320.             # buttons ourselves.
  321.             name = ''
  322.         charset = get_arg(req, "_charset", "utf-8")
  323.         name = name.decode(charset)
  324.         d = self.node.delete(name)
  325.         d.addCallback(lambda res: "thing deleted")
  326.         return d
  327. 
  328.     def _POST_rename(self, req):
  329.         charset = get_arg(req, "_charset", "utf-8")
  330.         from_name = get_arg(req, "from_name")
  331.         if from_name is not None:
  332.             from_name = from_name.strip()
  333.             from_name = from_name.decode(charset)
  334.             assert isinstance(from_name, unicode)
  335.         to_name = get_arg(req, "to_name")
  336.         if to_name is not None:
  337.             to_name = to_name.strip()
  338.             to_name = to_name.decode(charset)
  339.             assert isinstance(to_name, unicode)
  340.         if not from_name or not to_name:
  341.             raise WebError("rename requires from_name and to_name")
  342.         if from_name == to_name:
  343.             return defer.succeed("redundant rename")
  344. 
  345.         # allow from_name to contain slashes, so they can fix names that were
  346.         # accidentally created with them. But disallow them in to_name, to
  347.         # discourage the practice.
  348.         if "/" in to_name:
  349.             raise WebError("to_name= may not contain a slash", http.BAD_REQUEST)
  350. 
  351.         replace = boolean_of_arg(get_arg(req, "replace", "true"))
  352.         d = self.node.move_child_to(from_name, self.node, to_name, replace)
  353.         d.addCallback(lambda res: "thing renamed")
  354.         return d
  355. 
  356.     def _POST_check(self, req):
  357.         # check this directory
  358.         verify = boolean_of_arg(get_arg(req, "verify", "false"))
  359.         repair = boolean_of_arg(get_arg(req, "repair", "false"))
  360.         add_lease = boolean_of_arg(get_arg(req, "add-lease", "false"))
  361.         if repair:
  362.             d = self.node.check_and_repair(Monitor(), verify, add_lease)
  363.             d.addCallback(lambda res: CheckAndRepairResults(self.client, res))
  364.         else:
  365.             d = self.node.check(Monitor(), verify, add_lease)
  366.             d.addCallback(lambda res: CheckResults(self.client, res))
  367.         return d
  368. 
  369.     def _start_operation(self, monitor, renderer, ctx):
  370.         table = IOpHandleTable(ctx)
  371.         table.add_monitor(ctx, monitor, renderer)
  372.         return table.redirect_to(ctx)
  373. 
  374.     def _POST_start_deep_check(self, ctx):
  375.         # check this directory and everything reachable from it
  376.         if not get_arg(ctx, "ophandle"):
  377.             raise NeedOperationHandleError("slow operation requires ophandle=")
  378.         verify = boolean_of_arg(get_arg(ctx, "verify", "false"))
  379.         repair = boolean_of_arg(get_arg(ctx, "repair", "false"))
  380.         add_lease = boolean_of_arg(get_arg(ctx, "add-lease", "false"))
  381.         if repair:
  382.             monitor = self.node.start_deep_check_and_repair(verify, add_lease)
  383.             renderer = DeepCheckAndRepairResults(self.client, monitor)
  384.         else:
  385.             monitor = self.node.start_deep_check(verify, add_lease)
  386.             renderer = DeepCheckResults(self.client, monitor)
  387.         return self._start_operation(monitor, renderer, ctx)
  388. 
  389.     def _POST_stream_deep_check(self, ctx):
  390.         verify = boolean_of_arg(get_arg(ctx, "verify", "false"))
  391.         repair = boolean_of_arg(get_arg(ctx, "repair", "false"))
  392.         add_lease = boolean_of_arg(get_arg(ctx, "add-lease", "false"))
  393.         walker = DeepCheckStreamer(ctx, self.node, verify, repair, add_lease)
  394.         monitor = self.node.deep_traverse(walker)
  395.         walker.setMonitor(monitor)
  396.         # register to hear stopProducing. The walker ignores pauseProducing.
  397.         IRequest(ctx).registerProducer(walker, True)
  398.         d = monitor.when_done()
  399.         def _done(res):
  400.             IRequest(ctx).unregisterProducer()
  401.             return res
  402.         d.addBoth(_done)
  403.         def _cancelled(f):
  404.             f.trap(OperationCancelledError)
  405.             return "Operation Cancelled"
  406.         d.addErrback(_cancelled)
  407.         def _error(f):
  408.             # signal the error as a non-JSON "ERROR:" line, plus exception
  409.             msg = "ERROR: %s(%s)\n" % (f.value.__class__.__name__,
  410.                                        ", ".join([str(a) for a in f.value.args]))
  411.             msg += str(f)
  412.             return msg
  413.         d.addErrback(_error)
  414.         return d
  415. 
  416.     def _POST_start_manifest(self, ctx):
  417.         if not get_arg(ctx, "ophandle"):
  418.             raise NeedOperationHandleError("slow operation requires ophandle=")
  419.         monitor = self.node.build_manifest()
  420.         renderer = ManifestResults(self.client, monitor)
  421.         return self._start_operation(monitor, renderer, ctx)
  422. 
  423.     def _POST_start_deep_size(self, ctx):
  424.         if not get_arg(ctx, "ophandle"):
  425.             raise NeedOperationHandleError("slow operation requires ophandle=")
  426.         monitor = self.node.start_deep_stats()
  427.         renderer = DeepSizeResults(self.client, monitor)
  428.         return self._start_operation(monitor, renderer, ctx)
  429. 
  430.     def _POST_start_deep_stats(self, ctx):
  431.         if not get_arg(ctx, "ophandle"):
  432.             raise NeedOperationHandleError("slow operation requires ophandle=")
  433.         monitor = self.node.start_deep_stats()
  434.         renderer = DeepStatsResults(self.client, monitor)
  435.         return self._start_operation(monitor, renderer, ctx)
  436. 
  437.     def _POST_stream_manifest(self, ctx):
  438.         walker = ManifestStreamer(ctx, self.node)
  439.         monitor = self.node.deep_traverse(walker)
  440.         walker.setMonitor(monitor)
  441.         # register to hear stopProducing. The walker ignores pauseProducing.
  442.         IRequest(ctx).registerProducer(walker, True)
  443.         d = monitor.when_done()
  444.         def _done(res):
  445.             IRequest(ctx).unregisterProducer()
  446.             return res
  447.         d.addBoth(_done)
  448.         def _cancelled(f):
  449.             f.trap(OperationCancelledError)
  450.             return "Operation Cancelled"
  451.         d.addErrback(_cancelled)
  452.         def _error(f):
  453.             # signal the error as a non-JSON "ERROR:" line, plus exception
  454.             msg = "ERROR: %s(%s)\n" % (f.value.__class__.__name__,
  455.                                        ", ".join([str(a) for a in f.value.args]))
  456.             msg += str(f)
  457.             return msg
  458.         d.addErrback(_error)
  459.         return d
  460. 
  461.     def _POST_set_children(self, req):
  462.         replace = boolean_of_arg(get_arg(req, "replace", "true"))
  463.         req.content.seek(0)
  464.         body = req.content.read()
  465.         try:
  466.             children = simplejson.loads(body)
  467.         except ValueError, le:
  468.             le.args = tuple(le.args + (body,))
  469.             # TODO test handling of bad JSON
  470.             raise
  471.         cs = []
  472.         for name, (file_or_dir, mddict) in children.iteritems():
  473.             name = unicode(name) # simplejson-2.0.1 returns str *or* unicode
  474.             cap = str(mddict.get('rw_uri') or mddict.get('ro_uri'))
  475.             cs.append((name, cap, mddict.get('metadata')))
  476.         d = self.node.set_children(cs, replace)
  477.         d.addCallback(lambda res: "Okay so I did it.")
  478.         # TODO: results
  479.         return d
  480. 
  481. def abbreviated_dirnode(dirnode):
  482.     u = from_string_dirnode(dirnode.get_uri())
  483.     return u.abbrev_si()
  484. 
  485. class DirectoryAsHTML(rend.Page):
  486.     # The remainder of this class is to render the directory into
  487.     # human+browser -oriented HTML.
  488.     docFactory = getxmlfile("directory.xhtml")
  489.     addSlash = True
  490. 
  491.     def __init__(self, node):
  492.         rend.Page.__init__(self)
  493.         self.node = node
  494. 
  495.     def beforeRender(self, ctx):
  496.         # attempt to get the dirnode's children, stashing them (or the
  497.         # failure that results) for later use
  498.         d = self.node.list()
  499.         def _good(children):
  500.             # Deferreds don't optimize out tail recursion, and the way
  501.             # Nevow's flattener handles Deferreds doesn't take this into
  502.             # account. As a result, large lists of Deferreds that fire in the
  503.             # same turn (i.e. the output of defer.succeed) will cause a stack
  504.             # overflow. To work around this, we insert a turn break after
  505.             # every 100 items, using foolscap's fireEventually(). This gives
  506.             # the stack a chance to be popped. It would also work to put
  507.             # every item in its own turn, but that'd be a lot more
  508.             # inefficient. This addresses ticket #237, for which I was never
  509.             # able to create a failing unit test.
  510.             output = []
  511.             for i,item in enumerate(sorted(children.items())):
  512.                 if i % 100 == 0:
  513.                     output.append(fireEventually(item))
  514.                 else:
  515.                     output.append(item)
  516.             self.dirnode_children = output
  517.             return ctx
  518.         def _bad(f):
  519.             text, code = humanize_failure(f)
  520.             self.dirnode_children = None
  521.             self.dirnode_children_error = text
  522.             return ctx
  523.         d.addCallbacks(_good, _bad)
  524.         return d
  525. 
  526.     def render_title(self, ctx, data):
  527.         si_s = abbreviated_dirnode(self.node)
  528.         header = ["TahoeLAFS - Directory SI=%s" % si_s]
  529.         if self.node.is_readonly():
  530.             header.append(" (read-only)")
  531.         else:
  532.             header.append(" (modifiable)")
  533.         return ctx.tag[header]
  534. 
  535.     def render_header(self, ctx, data):
  536.         si_s = abbreviated_dirnode(self.node)
  537.         header = ["TahoeLAFS Directory SI=", T.span(class_="data-chars")[si_s]]
  538.         if self.node.is_readonly():
  539.             header.append(" (read-only)")
  540.         return ctx.tag[header]
  541. 
  542.     def render_welcome(self, ctx, data):
  543.         link = get_root(ctx)
  544.         return T.div[T.a(href=link)["Return to Welcome page"]]
  545. 
  546.     def render_show_readonly(self, ctx, data):
  547.         if self.node.is_readonly():
  548.             return ""
  549.         rocap = self.node.get_readonly_uri()
  550.         root = get_root(ctx)
  551.         uri_link = "%s/uri/%s/" % (root, urllib.quote(rocap))
  552.         return ctx.tag[T.a(href=uri_link)["Read-Only Version"]]
  553. 
  554.     def render_try_children(self, ctx, data):
  555.         # if the dirnode can be retrived, render a table of children.
  556.         # Otherwise, render an apologetic error message.
  557.         if self.dirnode_children is not None:
  558.             return ctx.tag
  559.         else:
  560.             return T.div[T.p["Error reading directory:"],
  561.                          T.p[self.dirnode_children_error]]
  562. 
  563.     def data_children(self, ctx, data):
  564.         return self.dirnode_children
  565. 
  566.     def render_row(self, ctx, data):
  567.         name, (target, metadata) = data
  568.         name = name.encode("utf-8")
  569.         assert not isinstance(name, unicode)
  570.         nameurl = urllib.quote(name, safe="") # encode any slashes too
  571. 
  572.         root = get_root(ctx)
  573.         here = "%s/uri/%s/" % (root, urllib.quote(self.node.get_uri()))
  574.         if self.node.is_readonly():
  575.             delete = "-"
  576.             rename = "-"
  577.         else:
  578.             # this creates a button which will cause our child__delete method
  579.             # to be invoked, which deletes the file and then redirects the
  580.             # browser back to this directory
  581.             delete = T.form(action=here, method="post")[
  582.                 T.input(type='hidden', name='t', value='delete'),
  583.                 T.input(type='hidden', name='name', value=name),
  584.                 T.input(type='hidden', name='when_done', value="."),
  585.                 T.input(type='submit', value='del', name="del"),
  586.                 ]
  587. 
  588.             rename = T.form(action=here, method="get")[
  589.                 T.input(type='hidden', name='t', value='rename-form'),
  590.                 T.input(type='hidden', name='name', value=name),
  591.                 T.input(type='hidden', name='when_done', value="."),
  592.                 T.input(type='submit', value='rename', name="rename"),
  593.                 ]
  594. 
  595.         ctx.fillSlots("delete", delete)
  596.         ctx.fillSlots("rename", rename)
  597. 
  598.         times = []
  599.         linkcrtime = metadata.get('tahoe', {}).get("linkcrtime")
  600.         if linkcrtime is not None:
  601.             times.append("lcr: " + time_format.iso_local(linkcrtime))
  602.         else:
  603.             # For backwards-compatibility with links last modified by Tahoe < 1.4.0:
  604.             if "ctime" in metadata:
  605.                 ctime = time_format.iso_local(metadata["ctime"])
  606.                 times.append("c: " + ctime)
  607.         linkmotime = metadata.get('tahoe', {}).get("linkmotime")
  608.         if linkmotime is not None:
  609.             if times:
  610.                 times.append(T.br())
  611.             times.append("lmo: " + time_format.iso_local(linkmotime))
  612.         else:
  613.             # For backwards-compatibility with links last modified by Tahoe < 1.4.0:
  614.             if "mtime" in metadata:
  615.                 mtime = time_format.iso_local(metadata["mtime"])
  616.                 if times:
  617.                     times.append(T.br())
  618.                 times.append("m: " + mtime)
  619.         ctx.fillSlots("times", times)
  620. 
  621.         assert IFilesystemNode.providedBy(target), target
  622.         writecap = target.get_uri() or ""
  623.         quoted_uri = urllib.quote(writecap, safe="") # escape slashes too
  624. 
  625.         if IMutableFileNode.providedBy(target):
  626.             # to prevent javascript in displayed .html files from stealing a
  627.             # secret directory URI from the URL, send the browser to a URI-based
  628.             # page that doesn't know about the directory at all
  629.             dlurl = "%s/file/%s/@@named=/%s" % (root, quoted_uri, nameurl)
  630. 
  631.             ctx.fillSlots("filename",
  632.                           T.a(href=dlurl)[html.escape(name)])
  633.             ctx.fillSlots("type", "SSK")
  634. 
  635.             ctx.fillSlots("size", "?")
  636. 
  637.             info_link = "%s/uri/%s?t=info" % (root, quoted_uri)
  638. 
  639.         elif IFileNode.providedBy(target):
  640.             dlurl = "%s/file/%s/@@named=/%s" % (root, quoted_uri, nameurl)
  641. 
  642.             ctx.fillSlots("filename",
  643.                           T.a(href=dlurl)[html.escape(name)])
  644.             ctx.fillSlots("type", "FILE")
  645. 
  646.             ctx.fillSlots("size", target.get_size())
  647. 
  648.             info_link = "%s/uri/%s?t=info" % (root, quoted_uri)
  649. 
  650.         elif IDirectoryNode.providedBy(target):
  651.             # directory
  652.             uri_link = "%s/uri/%s/" % (root, urllib.quote(writecap))
  653.             ctx.fillSlots("filename",
  654.                           T.a(href=uri_link)[html.escape(name)])
  655.             if target.is_readonly():
  656.                 dirtype = "DIR-RO"
  657.             else:
  658.                 dirtype = "DIR"
  659.             ctx.fillSlots("type", dirtype)
  660.             ctx.fillSlots("size", "-")
  661.             info_link = "%s/uri/%s/?t=info" % (root, quoted_uri)
  662. 
  663.         else:
  664.             # unknown
  665.             ctx.fillSlots("filename", html.escape(name))
  666.             ctx.fillSlots("type", "?")
  667.             ctx.fillSlots("size", "-")
  668.             # use a directory-relative info link, so we can extract both the
  669.             # writecap and the readcap
  670.             info_link = "%s?t=info" % urllib.quote(name)
  671. 
  672.         ctx.fillSlots("info", T.a(href=info_link)["More Info"])
  673. 
  674.         return ctx.tag
  675. 
  676.     def render_forms(self, ctx, data):
  677.         forms = []
  678. 
  679.         if self.node.is_readonly():
  680.             return T.div["No upload forms: directory is read-only"]
  681.         if self.dirnode_children is None:
  682.             return T.div["No upload forms: directory is unreadable"]
  683. 
  684.         mkdir = T.form(action=".", method="post",
  685.                        enctype="multipart/form-data")[
  686.             T.fieldset[
  687.             T.input(type="hidden", name="t", value="mkdir"),
  688.             T.input(type="hidden", name="when_done", value="."),
  689.             T.legend(class_="freeform-form-label")["Create a new directory in this directory"],
  690.             "New directory name: ",
  691.             T.input(type="text", name="name"), " ",
  692.             T.input(type="submit", value="Create"),
  693.             ]]
  694.         forms.append(T.div(class_="freeform-form")[mkdir])
  695. 
  696.         upload = T.form(action=".", method="post",
  697.                         enctype="multipart/form-data")[
  698.             T.fieldset[
  699.             T.input(type="hidden", name="t", value="upload"),
  700.             T.input(type="hidden", name="when_done", value="."),
  701.             T.legend(class_="freeform-form-label")["Upload a file to this directory"],
  702.             "Choose a file to upload: ",
  703.             T.input(type="file", name="file", class_="freeform-input-file"),
  704.             " ",
  705.             T.input(type="submit", value="Upload"),
  706.             " Mutable?:",
  707.             T.input(type="checkbox", name="mutable"),
  708.             ]]
  709.         forms.append(T.div(class_="freeform-form")[upload])
  710. 
  711.         mount = T.form(action=".", method="post",
  712.                         enctype="multipart/form-data")[
  713.             T.fieldset[
  714.             T.input(type="hidden", name="t", value="uri"),
  715.             T.input(type="hidden", name="when_done", value="."),
  716.             T.legend(class_="freeform-form-label")["Add a link to a file or directory which is already in TahoeLAFS."],
  717.             "New child name: ",
  718.             T.input(type="text", name="name"), " ",
  719.             "URI of new child: ",
  720.             T.input(type="text", name="uri"), " ",
  721.             T.input(type="submit", value="Attach"),
  722.             ]]
  723.         forms.append(T.div(class_="freeform-form")[mount])
  724.         return forms
  725. 
  726.     def render_results(self, ctx, data):
  727.         req = IRequest(ctx)
  728.         return get_arg(req, "results", "")
  729. 
  730. 
  731. def DirectoryJSONMetadata(ctx, dirnode):
  732.     d = dirnode.list()
  733.     def _got(children):
  734.         kids = {}
  735.         for name, (childnode, metadata) in children.iteritems():
  736.             assert IFilesystemNode.providedBy(childnode), childnode
  737.             rw_uri = childnode.get_uri()
  738.             ro_uri = childnode.get_readonly_uri()
  739.             if (IDirectoryNode.providedBy(childnode)
  740.                 or IFileNode.providedBy(childnode)):
  741.                 if childnode.is_readonly():
  742.                     rw_uri = None
  743.             if IFileNode.providedBy(childnode):
  744.                 kiddata = ("filenode", {'size': childnode.get_size(),
  745.                                         'mutable': childnode.is_mutable(),
  746.                                         })
  747.             elif IDirectoryNode.providedBy(childnode):
  748.                 kiddata = ("dirnode", {'mutable': childnode.is_mutable(),
  749.                                        })
  750.             else:
  751.                 kiddata = ("unknown", {})
  752.             kiddata[1]["metadata"] = metadata
  753.             if ro_uri:
  754.                 kiddata[1]["ro_uri"] = ro_uri
  755.             if rw_uri:
  756.                 kiddata[1]["rw_uri"] = rw_uri
  757.             verifycap = childnode.get_verify_cap()
  758.             if verifycap:
  759.                 kiddata[1]['verify_uri'] = verifycap.to_string()
  760.             kids[name] = kiddata
  761.         if dirnode.is_readonly():
  762.             drw_uri = None
  763.             dro_uri = dirnode.get_uri()
  764.         else:
  765.             drw_uri = dirnode.get_uri()
  766.             dro_uri = dirnode.get_readonly_uri()
  767.         contents = { 'children': kids }
  768.         if dro_uri:
  769.             contents['ro_uri'] = dro_uri
  770.         if drw_uri:
  771.             contents['rw_uri'] = drw_uri
  772.         verifycap = dirnode.get_verify_cap()
  773.         if verifycap:
  774.             contents['verify_uri'] = verifycap.to_string()
  775.         contents['mutable'] = dirnode.is_mutable()
  776.         data = ("dirnode", contents)
  777.         return simplejson.dumps(data, indent=1) + "\n"
  778.     d.addCallback(_got)
  779.     d.addCallback(text_plain, ctx)
  780.     return d
  781. 
  782. 
  783. 
  784. def DirectoryURI(ctx, dirnode):
  785.     return text_plain(dirnode.get_uri(), ctx)
  786. 
  787. def DirectoryReadonlyURI(ctx, dirnode):
  788.     return text_plain(dirnode.get_readonly_uri(), ctx)
  789. 
  790. class RenameForm(rend.Page):
  791.     addSlash = True
  792.     docFactory = getxmlfile("rename-form.xhtml")
  793. 
  794.     def render_title(self, ctx, data):
  795.         return ctx.tag["Directory SI=%s" % abbreviated_dirnode(self.original)]
  796. 
  797.     def render_header(self, ctx, data):
  798.         header = ["Rename "
  799.                   "in directory SI=%s" % abbreviated_dirnode(self.original),
  800.                   ]
  801. 
  802.         if self.original.is_readonly():
  803.             header.append(" (readonly!)")
  804.         header.append(":")
  805.         return ctx.tag[header]
  806. 
  807.     def render_when_done(self, ctx, data):
  808.         return T.input(type="hidden", name="when_done", value=".")
  809. 
  810.     def render_get_name(self, ctx, data):
  811.         req = IRequest(ctx)
  812.         name = get_arg(req, "name", "")
  813.         ctx.tag.attributes['value'] = name
  814.         return ctx.tag
  815. 
  816. 
  817. class ManifestResults(rend.Page, ReloadMixin):
  818.     docFactory = getxmlfile("manifest.xhtml")
  819. 
  820.     def __init__(self, client, monitor):
  821.         self.client = client
  822.         self.monitor = monitor
  823. 
  824.     def renderHTTP(self, ctx):
  825.         req = inevow.IRequest(ctx)
  826.         output = get_arg(req, "output", "html").lower()
  827.         if output == "text":
  828.             return self.text(req)
  829.         if output == "json":
  830.             return self.json(req)
  831.         return rend.Page.renderHTTP(self, ctx)
  832. 
  833.     def slashify_path(self, path):
  834.         if not path:
  835.             return ""
  836.         return "/".join([p.encode("utf-8") for p in path])
  837. 
  838.     def text(self, req):
  839.         req.setHeader("content-type", "text/plain")
  840.         lines = []
  841.         is_finished = self.monitor.is_finished()
  842.         lines.append("finished: " + {True: "yes", False: "no"}[is_finished])
  843.         for (path, cap) in self.monitor.get_status()["manifest"]:
  844.             lines.append(self.slashify_path(path) + " " + cap)
  845.         return "\n".join(lines) + "\n"
  846. 
  847.     def json(self, req):
  848.         req.setHeader("content-type", "text/plain")
  849.         m = self.monitor
  850.         s = m.get_status()
  851. 
  852.         status = { "stats": s["stats"],
  853.                    "finished": m.is_finished(),
  854.                    "origin": base32.b2a(m.origin_si),
  855.                    }
  856.         if m.is_finished():
  857.             # don't return manifest/verifycaps/SIs unless the operation is
  858.             # done, to save on CPU/memory (both here and in the HTTP client
  859.             # who has to unpack the JSON). Tests show that the ManifestWalker
  860.             # needs about 1092 bytes per item, the JSON we generate here
  861.             # requires about 503 bytes per item, and some internal overhead
  862.             # (perhaps transport-layer buffers in twisted.web?) requires an
  863.             # additional 1047 bytes per item.
  864.             status.update({ "manifest": s["manifest"],
  865.                             "verifycaps": [i for i in s["verifycaps"]],
  866.                             "storage-index": [i for i in s["storage-index"]],
  867.                             })
  868.             # simplejson doesn't know how to serialize a set. We use a
  869.             # generator that walks the set rather than list(setofthing) to
  870.             # save a small amount of memory (4B*len) and a moderate amount of
  871.             # CPU.
  872.         return simplejson.dumps(status, indent=1)
  873. 
  874.     def _si_abbrev(self):
  875.         return base32.b2a(self.monitor.origin_si)[:6]
  876. 
  877.     def render_title(self, ctx):
  878.         return T.title["Manifest of SI=%s" % self._si_abbrev()]
  879. 
  880.     def render_header(self, ctx):
  881.         return T.p["Manifest of SI=%s" % self._si_abbrev()]
  882. 
  883.     def data_items(self, ctx, data):
  884.         return self.monitor.get_status()["manifest"]
  885. 
  886.     def render_row(self, ctx, (path, cap)):
  887.         ctx.fillSlots("path", self.slashify_path(path))
  888.         root = get_root(ctx)
  889.         # TODO: we need a clean consistent way to get the type of a cap string
  890.         if cap:
  891.             if cap.startswith("URI:CHK") or cap.startswith("URI:SSK"):
  892.                 nameurl = urllib.quote(path[-1].encode("utf-8"))
  893.                 uri_link = "%s/file/%s/@@named=/%s" % (root, urllib.quote(cap),
  894.                                                        nameurl)
  895.             else:
  896.                 uri_link = "%s/uri/%s" % (root, urllib.quote(cap, safe=""))
  897.             ctx.fillSlots("cap", T.a(href=uri_link)[cap])
  898.         else:
  899.             ctx.fillSlots("cap", "")
  900.         return ctx.tag
  901. 
  902. class DeepSizeResults(rend.Page):
  903.     def __init__(self, client, monitor):
  904.         self.client = client
  905.         self.monitor = monitor
  906. 
  907.     def renderHTTP(self, ctx):
  908.         req = inevow.IRequest(ctx)
  909.         output = get_arg(req, "output", "html").lower()
  910.         req.setHeader("content-type", "text/plain")
  911.         if output == "json":
  912.             return self.json(req)
  913.         # plain text
  914.         is_finished = self.monitor.is_finished()
  915.         output = "finished: " + {True: "yes", False: "no"}[is_finished] + "\n"
  916.         if is_finished:
  917.             stats = self.monitor.get_status()
  918.             total = (stats.get("size-immutable-files", 0)
  919.                      + stats.get("size-mutable-files", 0)
  920.                      + stats.get("size-directories", 0))
  921.             output += "size: %d\n" % total
  922.         return output
  923. 
  924.     def json(self, req):
  925.         status = {"finished": self.monitor.is_finished(),
  926.                   "size": self.monitor.get_status(),
  927.                   }
  928.         return simplejson.dumps(status)
  929. 
  930. class DeepStatsResults(rend.Page):
  931.     def __init__(self, client, monitor):
  932.         self.client = client
  933.         self.monitor = monitor
  934. 
  935.     def renderHTTP(self, ctx):
  936.         # JSON only
  937.         inevow.IRequest(ctx).setHeader("content-type", "text/plain")
  938.         s = self.monitor.get_status().copy()
  939.         s["finished"] = self.monitor.is_finished()
  940.         return simplejson.dumps(s, indent=1)
  941. 
  942. class ManifestStreamer(dirnode.DeepStats):
  943.     implements(IPushProducer)
  944. 
  945.     def __init__(self, ctx, origin):
  946.         dirnode.DeepStats.__init__(self, origin)
  947.         self.req = IRequest(ctx)
  948. 
  949.     def setMonitor(self, monitor):
  950.         self.monitor = monitor
  951.     def pauseProducing(self):
  952.         pass
  953.     def resumeProducing(self):
  954.         pass
  955.     def stopProducing(self):
  956.         self.monitor.cancel()
  957. 
  958.     def add_node(self, node, path):
  959.         dirnode.DeepStats.add_node(self, node, path)
  960.         d = {"path": path,
  961.              "cap": node.get_uri()}
  962. 
  963.         if IDirectoryNode.providedBy(node):
  964.             d["type"] = "directory"
  965.         elif IFileNode.providedBy(node):
  966.             d["type"] = "file"
  967.         else:
  968.             d["type"] = "unknown"
  969. 
  970.         v = node.get_verify_cap()
  971.         if v:
  972.             v = v.to_string()
  973.         d["verifycap"] = v
  974. 
  975.         r = node.get_repair_cap()
  976.         if r:
  977.             r = r.to_string()
  978.         d["repaircap"] = r
  979. 
  980.         si = node.get_storage_index()
  981.         if si:
  982.             si = base32.b2a(si)
  983.         d["storage-index"] = si
  984. 
  985.         j = simplejson.dumps(d, ensure_ascii=True)
  986.         assert "\n" not in j
  987.         self.req.write(j+"\n")
  988. 
  989.     def finish(self):
  990.         stats = dirnode.DeepStats.get_results(self)
  991.         d = {"type": "stats",
  992.              "stats": stats,
  993.              }
  994.         j = simplejson.dumps(d, ensure_ascii=True)
  995.         assert "\n" not in j
  996.         self.req.write(j+"\n")
  997.         return ""
  998. 
  999. class DeepCheckStreamer(dirnode.DeepStats):
 1000.     implements(IPushProducer)
 1001. 
 1002.     def __init__(self, ctx, origin, verify, repair, add_lease):
 1003.         dirnode.DeepStats.__init__(self, origin)
 1004.         self.req = IRequest(ctx)
 1005.         self.verify = verify
 1006.         self.repair = repair
 1007.         self.add_lease = add_lease
 1008. 
 1009.     def setMonitor(self, monitor):
 1010.         self.monitor = monitor
 1011.     def pauseProducing(self):
 1012.         pass
 1013.     def resumeProducing(self):
 1014.         pass
 1015.     def stopProducing(self):
 1016.         self.monitor.cancel()
 1017. 
 1018.     def add_node(self, node, path):
 1019.         dirnode.DeepStats.add_node(self, node, path)
 1020.         data = {"path": path,
 1021.                 "cap": node.get_uri()}
 1022. 
 1023.         if IDirectoryNode.providedBy(node):
 1024.             data["type"] = "directory"
 1025.         else:
 1026.             data["type"] = "file"
 1027. 
 1028.         v = node.get_verify_cap()
 1029.         if v:
 1030.             v = v.to_string()
 1031.         data["verifycap"] = v
 1032. 
 1033.         r = node.get_repair_cap()
 1034.         if r:
 1035.             r = r.to_string()
 1036.         data["repaircap"] = r
 1037. 
 1038.         si = node.get_storage_index()
 1039.         if si:
 1040.             si = base32.b2a(si)
 1041.         data["storage-index"] = si
 1042. 
 1043.         if self.repair:
 1044.             d = node.check_and_repair(self.monitor, self.verify, self.add_lease)
 1045.             d.addCallback(self.add_check_and_repair, data)
 1046.         else:
 1047.             d = node.check(self.monitor, self.verify, self.add_lease)
 1048.             d.addCallback(self.add_check, data)
 1049.         d.addCallback(self.write_line)
 1050.         return d
 1051. 
 1052.     def add_check_and_repair(self, crr, data):
 1053.         data["check-and-repair-results"] = json_check_and_repair_results(crr)
 1054.         return data
 1055. 
 1056.     def add_check(self, cr, data):
 1057.         data["check-results"] = json_check_results(cr)
 1058.         return data
 1059. 
 1060.     def write_line(self, data):
 1061.         j = simplejson.dumps(data, ensure_ascii=True)
 1062.         assert "\n" not in j
 1063.         self.req.write(j+"\n")
 1064. 
 1065.     def finish(self):
 1066.         stats = dirnode.DeepStats.get_results(self)
 1067.         d = {"type": "stats",
 1068.              "stats": stats,
 1069.              }
 1070.         j = simplejson.dumps(d, ensure_ascii=True)
 1071.         assert "\n" not in j
 1072.         self.req.write(j+"\n")
 1073.         return ""
 1074. 
 1075. class UnknownNodeHandler(RenderMixin, rend.Page):
 1076. 
 1077.     def __init__(self, client, node, parentnode=None, name=None):
 1078.         rend.Page.__init__(self)
 1079.         assert node
 1080.         self.node = node
 1081. 
 1082.     def render_GET(self, ctx):
 1083.         req = IRequest(ctx)
 1084.         t = get_arg(req, "t", "").strip()
 1085.         if t == "info":
 1086.             return MoreInfo(self.node)
 1087.         raise WebError("GET unknown URI type: can only do t=info, not t=%s" % t)
 1088. 
 1089.