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