]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/web/directory.py
Implementation, tests and docs for blacklists. This version allows listing directorie...
[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 class DirectoryAsHTML(rend.Page):
578     # The remainder of this class is to render the directory into
579     # human+browser -oriented HTML.
580     docFactory = getxmlfile("directory.xhtml")
581     addSlash = True
582
583     def __init__(self, node, default_mutable_format):
584         rend.Page.__init__(self)
585         self.node = node
586
587         assert default_mutable_format in (MDMF_VERSION, SDMF_VERSION)
588         self.default_mutable_format = default_mutable_format
589
590     def beforeRender(self, ctx):
591         # attempt to get the dirnode's children, stashing them (or the
592         # failure that results) for later use
593         d = self.node.list()
594         def _good(children):
595             # Deferreds don't optimize out tail recursion, and the way
596             # Nevow's flattener handles Deferreds doesn't take this into
597             # account. As a result, large lists of Deferreds that fire in the
598             # same turn (i.e. the output of defer.succeed) will cause a stack
599             # overflow. To work around this, we insert a turn break after
600             # every 100 items, using foolscap's fireEventually(). This gives
601             # the stack a chance to be popped. It would also work to put
602             # every item in its own turn, but that'd be a lot more
603             # inefficient. This addresses ticket #237, for which I was never
604             # able to create a failing unit test.
605             output = []
606             for i,item in enumerate(sorted(children.items())):
607                 if i % 100 == 0:
608                     output.append(fireEventually(item))
609                 else:
610                     output.append(item)
611             self.dirnode_children = output
612             return ctx
613         def _bad(f):
614             text, code = humanize_failure(f)
615             self.dirnode_children = None
616             self.dirnode_children_error = text
617             return ctx
618         d.addCallbacks(_good, _bad)
619         return d
620
621     def render_title(self, ctx, data):
622         si_s = abbreviated_dirnode(self.node)
623         header = ["Tahoe-LAFS - Directory SI=%s" % si_s]
624         if self.node.is_unknown():
625             header.append(" (unknown)")
626         elif not self.node.is_mutable():
627             header.append(" (immutable)")
628         elif self.node.is_readonly():
629             header.append(" (read-only)")
630         else:
631             header.append(" (modifiable)")
632         return ctx.tag[header]
633
634     def render_header(self, ctx, data):
635         si_s = abbreviated_dirnode(self.node)
636         header = ["Tahoe-LAFS Directory SI=", T.span(class_="data-chars")[si_s]]
637         if self.node.is_unknown():
638             header.append(" (unknown)")
639         elif not self.node.is_mutable():
640             header.append(" (immutable)")
641         elif self.node.is_readonly():
642             header.append(" (read-only)")
643         return ctx.tag[header]
644
645     def render_welcome(self, ctx, data):
646         link = get_root(ctx)
647         return ctx.tag[T.a(href=link)["Return to Welcome page"]]
648
649     def render_show_readonly(self, ctx, data):
650         if self.node.is_unknown() or self.node.is_readonly():
651             return ""
652         rocap = self.node.get_readonly_uri()
653         root = get_root(ctx)
654         uri_link = "%s/uri/%s/" % (root, urllib.quote(rocap))
655         return ctx.tag[T.a(href=uri_link)["Read-Only Version"]]
656
657     def render_try_children(self, ctx, data):
658         # if the dirnode can be retrived, render a table of children.
659         # Otherwise, render an apologetic error message.
660         if self.dirnode_children is not None:
661             return ctx.tag
662         else:
663             return T.div[T.p["Error reading directory:"],
664                          T.p[self.dirnode_children_error]]
665
666     def data_children(self, ctx, data):
667         return self.dirnode_children
668
669     def render_row(self, ctx, data):
670         name, (target, metadata) = data
671         name = name.encode("utf-8")
672         assert not isinstance(name, unicode)
673         nameurl = urllib.quote(name, safe="") # encode any slashes too
674
675         root = get_root(ctx)
676         here = "%s/uri/%s/" % (root, urllib.quote(self.node.get_uri()))
677         if self.node.is_unknown() or self.node.is_readonly():
678             unlink = "-"
679             rename = "-"
680         else:
681             # this creates a button which will cause our _POST_unlink method
682             # to be invoked, which unlinks the file and then redirects the
683             # browser back to this directory
684             unlink = T.form(action=here, method="post")[
685                 T.input(type='hidden', name='t', value='unlink'),
686                 T.input(type='hidden', name='name', value=name),
687                 T.input(type='hidden', name='when_done', value="."),
688                 T.input(type='submit', value='unlink', name="unlink"),
689                 ]
690
691             rename = T.form(action=here, method="get")[
692                 T.input(type='hidden', name='t', value='rename-form'),
693                 T.input(type='hidden', name='name', value=name),
694                 T.input(type='hidden', name='when_done', value="."),
695                 T.input(type='submit', value='rename', name="rename"),
696                 ]
697
698         ctx.fillSlots("unlink", unlink)
699         ctx.fillSlots("rename", rename)
700
701         times = []
702         linkcrtime = metadata.get('tahoe', {}).get("linkcrtime")
703         if linkcrtime is not None:
704             times.append("lcr: " + time_format.iso_local(linkcrtime))
705         else:
706             # For backwards-compatibility with links last modified by Tahoe < 1.4.0:
707             if "ctime" in metadata:
708                 ctime = time_format.iso_local(metadata["ctime"])
709                 times.append("c: " + ctime)
710         linkmotime = metadata.get('tahoe', {}).get("linkmotime")
711         if linkmotime is not None:
712             if times:
713                 times.append(T.br())
714             times.append("lmo: " + time_format.iso_local(linkmotime))
715         else:
716             # For backwards-compatibility with links last modified by Tahoe < 1.4.0:
717             if "mtime" in metadata:
718                 mtime = time_format.iso_local(metadata["mtime"])
719                 if times:
720                     times.append(T.br())
721                 times.append("m: " + mtime)
722         ctx.fillSlots("times", times)
723
724         assert IFilesystemNode.providedBy(target), target
725         target_uri = target.get_uri() or ""
726         quoted_uri = urllib.quote(target_uri, safe="") # escape slashes too
727
728         if IMutableFileNode.providedBy(target):
729             # to prevent javascript in displayed .html files from stealing a
730             # secret directory URI from the URL, send the browser to a URI-based
731             # page that doesn't know about the directory at all
732             dlurl = "%s/file/%s/@@named=/%s" % (root, quoted_uri, nameurl)
733
734             ctx.fillSlots("filename",
735                           T.a(href=dlurl)[html.escape(name)])
736             ctx.fillSlots("type", "SSK")
737
738             ctx.fillSlots("size", "?")
739
740             info_link = "%s/uri/%s?t=info" % (root, quoted_uri)
741
742         elif IImmutableFileNode.providedBy(target):
743             dlurl = "%s/file/%s/@@named=/%s" % (root, quoted_uri, nameurl)
744
745             ctx.fillSlots("filename",
746                           T.a(href=dlurl)[html.escape(name)])
747             ctx.fillSlots("type", "FILE")
748
749             ctx.fillSlots("size", target.get_size())
750
751             info_link = "%s/uri/%s?t=info" % (root, quoted_uri)
752
753         elif IDirectoryNode.providedBy(target):
754             # directory
755             uri_link = "%s/uri/%s/" % (root, urllib.quote(target_uri))
756             ctx.fillSlots("filename",
757                           T.a(href=uri_link)[html.escape(name)])
758             if not target.is_mutable():
759                 dirtype = "DIR-IMM"
760             elif target.is_readonly():
761                 dirtype = "DIR-RO"
762             else:
763                 dirtype = "DIR"
764             ctx.fillSlots("type", dirtype)
765             ctx.fillSlots("size", "-")
766             info_link = "%s/uri/%s/?t=info" % (root, quoted_uri)
767
768         elif isinstance(target, ProhibitedNode):
769             ctx.fillSlots("filename", T.strike[name])
770             if IDirectoryNode.providedBy(target.wrapped_node):
771                 blacklisted_type = "DIR-BLACKLISTED"
772             else:
773                 blacklisted_type = "BLACKLISTED"
774             ctx.fillSlots("type", blacklisted_type)
775             ctx.fillSlots("size", "-")
776             info_link = None
777             ctx.fillSlots("info", ["Access Prohibited:", T.br, target.reason])
778
779         else:
780             # unknown
781             ctx.fillSlots("filename", html.escape(name))
782             if target.get_write_uri() is not None:
783                 unknowntype = "?"
784             elif not self.node.is_mutable() or target.is_alleged_immutable():
785                 unknowntype = "?-IMM"
786             else:
787                 unknowntype = "?-RO"
788             ctx.fillSlots("type", unknowntype)
789             ctx.fillSlots("size", "-")
790             # use a directory-relative info link, so we can extract both the
791             # writecap and the readcap
792             info_link = "%s?t=info" % urllib.quote(name)
793
794         if info_link:
795             ctx.fillSlots("info", T.a(href=info_link)["More Info"])
796
797         return ctx.tag
798
799     # XXX: Duplicated from root.py.
800     def render_forms(self, ctx, data):
801         forms = []
802
803         if self.node.is_readonly():
804             return T.div["No upload forms: directory is read-only"]
805         if self.dirnode_children is None:
806             return T.div["No upload forms: directory is unreadable"]
807
808         mdmf_directory_input = T.input(type='radio', name='mutable-type',
809                                        id='mutable-directory-mdmf',
810                                        value='mdmf')
811         sdmf_directory_input = T.input(type='radio', name='mutable-type',
812                                        id='mutable-directory-sdmf',
813                                        value='sdmf', checked='checked')
814         mkdir = T.form(action=".", method="post",
815                        enctype="multipart/form-data")[
816             T.fieldset[
817             T.input(type="hidden", name="t", value="mkdir"),
818             T.input(type="hidden", name="when_done", value="."),
819             T.legend(class_="freeform-form-label")["Create a new directory in this directory"],
820             "New directory name: ",
821             T.input(type="text", name="name"), " ",
822             T.label(for_='mutable-directory-sdmf')["SDMF"],
823             sdmf_directory_input,
824             T.label(for_='mutable-directory-mdmf')["MDMF"],
825             mdmf_directory_input,
826             T.input(type="submit", value="Create"),
827             ]]
828         forms.append(T.div(class_="freeform-form")[mkdir])
829
830         # Build input elements for mutable file type. We do this outside
831         # of the list so we can check the appropriate format, based on
832         # the default configured in the client (which reflects the
833         # default configured in tahoe.cfg)
834         if self.default_mutable_format == MDMF_VERSION:
835             mdmf_input = T.input(type='radio', name='mutable-type',
836                                  id='mutable-type-mdmf', value='mdmf',
837                                  checked='checked')
838         else:
839             mdmf_input = T.input(type='radio', name='mutable-type',
840                                  id='mutable-type-mdmf', value='mdmf')
841
842         if self.default_mutable_format == SDMF_VERSION:
843             sdmf_input = T.input(type='radio', name='mutable-type',
844                                  id='mutable-type-sdmf', value='sdmf',
845                                  checked="checked")
846         else:
847             sdmf_input = T.input(type='radio', name='mutable-type',
848                                  id='mutable-type-sdmf', value='sdmf')
849
850         upload = T.form(action=".", method="post",
851                         enctype="multipart/form-data")[
852             T.fieldset[
853             T.input(type="hidden", name="t", value="upload"),
854             T.input(type="hidden", name="when_done", value="."),
855             T.legend(class_="freeform-form-label")["Upload a file to this directory"],
856             "Choose a file to upload: ",
857             T.input(type="file", name="file", class_="freeform-input-file"),
858             " ",
859             T.input(type="submit", value="Upload"),
860             " Mutable?:",
861             T.input(type="checkbox", name="mutable"),
862             sdmf_input, T.label(for_="mutable-type-sdmf")["SDMF"],
863             mdmf_input,
864             T.label(for_="mutable-type-mdmf")["MDMF (experimental)"],
865             ]]
866         forms.append(T.div(class_="freeform-form")[upload])
867
868         mount = T.form(action=".", method="post",
869                         enctype="multipart/form-data")[
870             T.fieldset[
871             T.input(type="hidden", name="t", value="uri"),
872             T.input(type="hidden", name="when_done", value="."),
873             T.legend(class_="freeform-form-label")["Add a link to a file or directory which is already in Tahoe-LAFS."],
874             "New child name: ",
875             T.input(type="text", name="name"), " ",
876             "URI of new child: ",
877             T.input(type="text", name="uri"), " ",
878             T.input(type="submit", value="Attach"),
879             ]]
880         forms.append(T.div(class_="freeform-form")[mount])
881         return forms
882
883     def render_results(self, ctx, data):
884         req = IRequest(ctx)
885         return get_arg(req, "results", "")
886
887
888 def DirectoryJSONMetadata(ctx, dirnode):
889     d = dirnode.list()
890     def _got(children):
891         kids = {}
892         for name, (childnode, metadata) in children.iteritems():
893             assert IFilesystemNode.providedBy(childnode), childnode
894             rw_uri = childnode.get_write_uri()
895             ro_uri = childnode.get_readonly_uri()
896             if IFileNode.providedBy(childnode):
897                 kiddata = ("filenode", {'size': childnode.get_size(),
898                                         'mutable': childnode.is_mutable(),
899                                         })
900                 if childnode.is_mutable() and \
901                     childnode.get_version() is not None:
902                     mutable_type = childnode.get_version()
903                     assert mutable_type in (SDMF_VERSION, MDMF_VERSION)
904
905                     if mutable_type == MDMF_VERSION:
906                         mutable_type = "mdmf"
907                     else:
908                         mutable_type = "sdmf"
909                     kiddata[1]['mutable-type'] = mutable_type
910
911             elif IDirectoryNode.providedBy(childnode):
912                 kiddata = ("dirnode", {'mutable': childnode.is_mutable()})
913             else:
914                 kiddata = ("unknown", {})
915
916             kiddata[1]["metadata"] = metadata
917             if rw_uri:
918                 kiddata[1]["rw_uri"] = rw_uri
919             if ro_uri:
920                 kiddata[1]["ro_uri"] = ro_uri
921             verifycap = childnode.get_verify_cap()
922             if verifycap:
923                 kiddata[1]['verify_uri'] = verifycap.to_string()
924
925             kids[name] = kiddata
926
927         drw_uri = dirnode.get_write_uri()
928         dro_uri = dirnode.get_readonly_uri()
929         contents = { 'children': kids }
930         if dro_uri:
931             contents['ro_uri'] = dro_uri
932         if drw_uri:
933             contents['rw_uri'] = drw_uri
934         verifycap = dirnode.get_verify_cap()
935         if verifycap:
936             contents['verify_uri'] = verifycap.to_string()
937         contents['mutable'] = dirnode.is_mutable()
938         data = ("dirnode", contents)
939         json = simplejson.dumps(data, indent=1) + "\n"
940         return json
941     d.addCallback(_got)
942     d.addCallback(text_plain, ctx)
943     return d
944
945
946 def DirectoryURI(ctx, dirnode):
947     return text_plain(dirnode.get_uri(), ctx)
948
949 def DirectoryReadonlyURI(ctx, dirnode):
950     return text_plain(dirnode.get_readonly_uri(), ctx)
951
952 class RenameForm(rend.Page):
953     addSlash = True
954     docFactory = getxmlfile("rename-form.xhtml")
955
956     def render_title(self, ctx, data):
957         return ctx.tag["Directory SI=%s" % abbreviated_dirnode(self.original)]
958
959     def render_header(self, ctx, data):
960         header = ["Rename "
961                   "in directory SI=%s" % abbreviated_dirnode(self.original),
962                   ]
963
964         if self.original.is_readonly():
965             header.append(" (readonly!)")
966         header.append(":")
967         return ctx.tag[header]
968
969     def render_when_done(self, ctx, data):
970         return T.input(type="hidden", name="when_done", value=".")
971
972     def render_get_name(self, ctx, data):
973         req = IRequest(ctx)
974         name = get_arg(req, "name", "")
975         ctx.tag.attributes['value'] = name
976         return ctx.tag
977
978
979 class ManifestResults(rend.Page, ReloadMixin):
980     docFactory = getxmlfile("manifest.xhtml")
981
982     def __init__(self, client, monitor):
983         self.client = client
984         self.monitor = monitor
985
986     def renderHTTP(self, ctx):
987         req = inevow.IRequest(ctx)
988         output = get_arg(req, "output", "html").lower()
989         if output == "text":
990             return self.text(req)
991         if output == "json":
992             return self.json(req)
993         return rend.Page.renderHTTP(self, ctx)
994
995     def slashify_path(self, path):
996         if not path:
997             return ""
998         return "/".join([p.encode("utf-8") for p in path])
999
1000     def text(self, req):
1001         req.setHeader("content-type", "text/plain")
1002         lines = []
1003         is_finished = self.monitor.is_finished()
1004         lines.append("finished: " + {True: "yes", False: "no"}[is_finished])
1005         for (path, cap) in self.monitor.get_status()["manifest"]:
1006             lines.append(self.slashify_path(path) + " " + cap)
1007         return "\n".join(lines) + "\n"
1008
1009     def json(self, req):
1010         req.setHeader("content-type", "text/plain")
1011         m = self.monitor
1012         s = m.get_status()
1013
1014         if m.origin_si:
1015             origin_base32 = base32.b2a(m.origin_si)
1016         else:
1017             origin_base32 = ""
1018         status = { "stats": s["stats"],
1019                    "finished": m.is_finished(),
1020                    "origin": origin_base32,
1021                    }
1022         if m.is_finished():
1023             # don't return manifest/verifycaps/SIs unless the operation is
1024             # done, to save on CPU/memory (both here and in the HTTP client
1025             # who has to unpack the JSON). Tests show that the ManifestWalker
1026             # needs about 1092 bytes per item, the JSON we generate here
1027             # requires about 503 bytes per item, and some internal overhead
1028             # (perhaps transport-layer buffers in twisted.web?) requires an
1029             # additional 1047 bytes per item.
1030             status.update({ "manifest": s["manifest"],
1031                             "verifycaps": [i for i in s["verifycaps"]],
1032                             "storage-index": [i for i in s["storage-index"]],
1033                             })
1034             # simplejson doesn't know how to serialize a set. We use a
1035             # generator that walks the set rather than list(setofthing) to
1036             # save a small amount of memory (4B*len) and a moderate amount of
1037             # CPU.
1038         return simplejson.dumps(status, indent=1)
1039
1040     def _si_abbrev(self):
1041         si = self.monitor.origin_si
1042         if not si:
1043             return "<LIT>"
1044         return base32.b2a(si)[:6]
1045
1046     def render_title(self, ctx):
1047         return T.title["Manifest of SI=%s" % self._si_abbrev()]
1048
1049     def render_header(self, ctx):
1050         return T.p["Manifest of SI=%s" % self._si_abbrev()]
1051
1052     def data_items(self, ctx, data):
1053         return self.monitor.get_status()["manifest"]
1054
1055     def render_row(self, ctx, (path, cap)):
1056         ctx.fillSlots("path", self.slashify_path(path))
1057         root = get_root(ctx)
1058         # TODO: we need a clean consistent way to get the type of a cap string
1059         if cap:
1060             if cap.startswith("URI:CHK") or cap.startswith("URI:SSK"):
1061                 nameurl = urllib.quote(path[-1].encode("utf-8"))
1062                 uri_link = "%s/file/%s/@@named=/%s" % (root, urllib.quote(cap),
1063                                                        nameurl)
1064             else:
1065                 uri_link = "%s/uri/%s" % (root, urllib.quote(cap, safe=""))
1066             ctx.fillSlots("cap", T.a(href=uri_link)[cap])
1067         else:
1068             ctx.fillSlots("cap", "")
1069         return ctx.tag
1070
1071 class DeepSizeResults(rend.Page):
1072     def __init__(self, client, monitor):
1073         self.client = client
1074         self.monitor = monitor
1075
1076     def renderHTTP(self, ctx):
1077         req = inevow.IRequest(ctx)
1078         output = get_arg(req, "output", "html").lower()
1079         req.setHeader("content-type", "text/plain")
1080         if output == "json":
1081             return self.json(req)
1082         # plain text
1083         is_finished = self.monitor.is_finished()
1084         output = "finished: " + {True: "yes", False: "no"}[is_finished] + "\n"
1085         if is_finished:
1086             stats = self.monitor.get_status()
1087             total = (stats.get("size-immutable-files", 0)
1088                      + stats.get("size-mutable-files", 0)
1089                      + stats.get("size-directories", 0))
1090             output += "size: %d\n" % total
1091         return output
1092
1093     def json(self, req):
1094         status = {"finished": self.monitor.is_finished(),
1095                   "size": self.monitor.get_status(),
1096                   }
1097         return simplejson.dumps(status)
1098
1099 class DeepStatsResults(rend.Page):
1100     def __init__(self, client, monitor):
1101         self.client = client
1102         self.monitor = monitor
1103
1104     def renderHTTP(self, ctx):
1105         # JSON only
1106         inevow.IRequest(ctx).setHeader("content-type", "text/plain")
1107         s = self.monitor.get_status().copy()
1108         s["finished"] = self.monitor.is_finished()
1109         return simplejson.dumps(s, indent=1)
1110
1111 class ManifestStreamer(dirnode.DeepStats):
1112     implements(IPushProducer)
1113
1114     def __init__(self, ctx, origin):
1115         dirnode.DeepStats.__init__(self, origin)
1116         self.req = IRequest(ctx)
1117
1118     def setMonitor(self, monitor):
1119         self.monitor = monitor
1120     def pauseProducing(self):
1121         pass
1122     def resumeProducing(self):
1123         pass
1124     def stopProducing(self):
1125         self.monitor.cancel()
1126
1127     def add_node(self, node, path):
1128         dirnode.DeepStats.add_node(self, node, path)
1129         d = {"path": path,
1130              "cap": node.get_uri()}
1131
1132         if IDirectoryNode.providedBy(node):
1133             d["type"] = "directory"
1134         elif IFileNode.providedBy(node):
1135             d["type"] = "file"
1136         else:
1137             d["type"] = "unknown"
1138
1139         v = node.get_verify_cap()
1140         if v:
1141             v = v.to_string()
1142         d["verifycap"] = v or ""
1143
1144         r = node.get_repair_cap()
1145         if r:
1146             r = r.to_string()
1147         d["repaircap"] = r or ""
1148
1149         si = node.get_storage_index()
1150         if si:
1151             si = base32.b2a(si)
1152         d["storage-index"] = si or ""
1153
1154         j = simplejson.dumps(d, ensure_ascii=True)
1155         assert "\n" not in j
1156         self.req.write(j+"\n")
1157
1158     def finish(self):
1159         stats = dirnode.DeepStats.get_results(self)
1160         d = {"type": "stats",
1161              "stats": stats,
1162              }
1163         j = simplejson.dumps(d, ensure_ascii=True)
1164         assert "\n" not in j
1165         self.req.write(j+"\n")
1166         return ""
1167
1168 class DeepCheckStreamer(dirnode.DeepStats):
1169     implements(IPushProducer)
1170
1171     def __init__(self, ctx, origin, verify, repair, add_lease):
1172         dirnode.DeepStats.__init__(self, origin)
1173         self.req = IRequest(ctx)
1174         self.verify = verify
1175         self.repair = repair
1176         self.add_lease = add_lease
1177
1178     def setMonitor(self, monitor):
1179         self.monitor = monitor
1180     def pauseProducing(self):
1181         pass
1182     def resumeProducing(self):
1183         pass
1184     def stopProducing(self):
1185         self.monitor.cancel()
1186
1187     def add_node(self, node, path):
1188         dirnode.DeepStats.add_node(self, node, path)
1189         data = {"path": path,
1190                 "cap": node.get_uri()}
1191
1192         if IDirectoryNode.providedBy(node):
1193             data["type"] = "directory"
1194         elif IFileNode.providedBy(node):
1195             data["type"] = "file"
1196         else:
1197             data["type"] = "unknown"
1198
1199         v = node.get_verify_cap()
1200         if v:
1201             v = v.to_string()
1202         data["verifycap"] = v or ""
1203
1204         r = node.get_repair_cap()
1205         if r:
1206             r = r.to_string()
1207         data["repaircap"] = r or ""
1208
1209         si = node.get_storage_index()
1210         if si:
1211             si = base32.b2a(si)
1212         data["storage-index"] = si or ""
1213
1214         if self.repair:
1215             d = node.check_and_repair(self.monitor, self.verify, self.add_lease)
1216             d.addCallback(self.add_check_and_repair, data)
1217         else:
1218             d = node.check(self.monitor, self.verify, self.add_lease)
1219             d.addCallback(self.add_check, data)
1220         d.addCallback(self.write_line)
1221         return d
1222
1223     def add_check_and_repair(self, crr, data):
1224         data["check-and-repair-results"] = json_check_and_repair_results(crr)
1225         return data
1226
1227     def add_check(self, cr, data):
1228         data["check-results"] = json_check_results(cr)
1229         return data
1230
1231     def write_line(self, data):
1232         j = simplejson.dumps(data, ensure_ascii=True)
1233         assert "\n" not in j
1234         self.req.write(j+"\n")
1235
1236     def finish(self):
1237         stats = dirnode.DeepStats.get_results(self)
1238         d = {"type": "stats",
1239              "stats": stats,
1240              }
1241         j = simplejson.dumps(d, ensure_ascii=True)
1242         assert "\n" not in j
1243         self.req.write(j+"\n")
1244         return ""
1245
1246
1247 class UnknownNodeHandler(RenderMixin, rend.Page):
1248     def __init__(self, client, node, parentnode=None, name=None):
1249         rend.Page.__init__(self)
1250         assert node
1251         self.node = node
1252         self.parentnode = parentnode
1253         self.name = name
1254
1255     def render_GET(self, ctx):
1256         req = IRequest(ctx)
1257         t = get_arg(req, "t", "").strip()
1258         if t == "info":
1259             return MoreInfo(self.node)
1260         if t == "json":
1261             is_parent_known_immutable = self.parentnode and not self.parentnode.is_mutable()
1262             if self.parentnode and self.name:
1263                 d = self.parentnode.get_metadata_for(self.name)
1264             else:
1265                 d = defer.succeed(None)
1266             d.addCallback(lambda md: UnknownJSONMetadata(ctx, self.node, md, is_parent_known_immutable))
1267             return d
1268         raise WebError("GET unknown URI type: can only do t=info and t=json, not t=%s.\n"
1269                        "Using a webapi server that supports a later version of Tahoe "
1270                        "may help." % t)
1271
1272 def UnknownJSONMetadata(ctx, node, edge_metadata, is_parent_known_immutable):
1273     rw_uri = node.get_write_uri()
1274     ro_uri = node.get_readonly_uri()
1275     data = ("unknown", {})
1276     if ro_uri:
1277         data[1]['ro_uri'] = ro_uri
1278     if rw_uri:
1279         data[1]['rw_uri'] = rw_uri
1280         data[1]['mutable'] = True
1281     elif is_parent_known_immutable or node.is_alleged_immutable():
1282         data[1]['mutable'] = False
1283     # else we don't know whether it is mutable.
1284
1285     if edge_metadata is not None:
1286         data[1]['metadata'] = edge_metadata
1287     return text_plain(simplejson.dumps(data, indent=1) + "\n", ctx)