]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/web/directory.py
naming: finish renaming "CheckerResults" to "CheckResults"
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / web / directory.py
1
2 import simplejson
3 import urllib
4 import time
5
6 from twisted.internet import defer
7 from twisted.python.failure import Failure
8 from twisted.web import http, html
9 from nevow import url, rend, inevow, tags as T
10 from nevow.inevow import IRequest
11
12 from foolscap.eventual import fireEventually
13
14 from allmydata.util import base32
15 from allmydata.uri import from_string_dirnode
16 from allmydata.interfaces import IDirectoryNode, IFileNode, IMutableFileNode, \
17      ExistingChildError, NoSuchChildError
18 from allmydata.monitor import Monitor
19 from allmydata.web.common import text_plain, WebError, \
20      IClient, IOpHandleTable, NeedOperationHandleError, \
21      boolean_of_arg, get_arg, get_root, \
22      should_create_intermediate_directories, \
23      getxmlfile, RenderMixin
24 from allmydata.web.filenode import ReplaceMeMixin, \
25      FileNodeHandler, PlaceHolderNodeHandler
26 from allmydata.web.check_results import CheckResults, \
27      CheckAndRepairResults, DeepCheckResults, DeepCheckAndRepairResults
28 from allmydata.web.info import MoreInfo
29 from allmydata.web.operations import ReloadMixin
30
31 class BlockingFileError(Exception):
32     # TODO: catch and transform
33     """We cannot auto-create a parent directory, because there is a file in
34     the way"""
35
36 def make_handler_for(node, parentnode=None, name=None):
37     if parentnode:
38         assert IDirectoryNode.providedBy(parentnode)
39     if IMutableFileNode.providedBy(node):
40         return FileNodeHandler(node, parentnode, name)
41     if IFileNode.providedBy(node):
42         return FileNodeHandler(node, parentnode, name)
43     if IDirectoryNode.providedBy(node):
44         return DirectoryNodeHandler(node, parentnode, name)
45     raise WebError("Cannot provide handler for '%s'" % node)
46
47 class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
48     addSlash = True
49
50     def __init__(self, node, parentnode=None, name=None):
51         rend.Page.__init__(self)
52         assert node
53         self.node = node
54         self.parentnode = parentnode
55         self.name = name
56
57     def childFactory(self, ctx, name):
58         req = IRequest(ctx)
59         name = name.decode("utf-8")
60         d = self.node.get(name)
61         d.addBoth(self.got_child, ctx, name)
62         # got_child returns a handler resource: FileNodeHandler or
63         # DirectoryNodeHandler
64         return d
65
66     def got_child(self, node_or_failure, ctx, name):
67         DEBUG = False
68         if DEBUG: print "GOT_CHILD", name, node_or_failure
69         req = IRequest(ctx)
70         method = req.method
71         nonterminal = len(req.postpath) > 1
72         t = get_arg(req, "t", "").strip()
73         if isinstance(node_or_failure, Failure):
74             f = node_or_failure
75             f.trap(NoSuchChildError)
76             # No child by this name. What should we do about it?
77             if DEBUG: print "no child", name
78             if DEBUG: print "postpath", req.postpath
79             if nonterminal:
80                 if DEBUG: print " intermediate"
81                 if should_create_intermediate_directories(req):
82                     # create intermediate directories
83                     if DEBUG: print " making intermediate directory"
84                     d = self.node.create_empty_directory(name)
85                     d.addCallback(make_handler_for, self.node, name)
86                     return d
87             else:
88                 if DEBUG: print " terminal"
89                 # terminal node
90                 if (method,t) in [ ("POST","mkdir"), ("PUT","mkdir") ]:
91                     if DEBUG: print " making final directory"
92                     # final directory
93                     d = self.node.create_empty_directory(name)
94                     d.addCallback(make_handler_for, self.node, name)
95                     return d
96                 if (method,t) in ( ("PUT",""), ("PUT","uri"), ):
97                     if DEBUG: print " PUT, making leaf placeholder"
98                     # we were trying to find the leaf filenode (to put a new
99                     # file in its place), and it didn't exist. That's ok,
100                     # since that's the leaf node that we're about to create.
101                     # We make a dummy one, which will respond to the PUT
102                     # request by replacing itself.
103                     return PlaceHolderNodeHandler(self.node, name)
104             if DEBUG: print " 404"
105             # otherwise, we just return a no-such-child error
106             return rend.FourOhFour()
107
108         node = node_or_failure
109         if nonterminal and should_create_intermediate_directories(req):
110             if not IDirectoryNode.providedBy(node):
111                 # we would have put a new directory here, but there was a
112                 # file in the way.
113                 if DEBUG: print "blocking"
114                 raise WebError("Unable to create directory '%s': "
115                                "a file was in the way" % name,
116                                http.CONFLICT)
117         if DEBUG: print "good child"
118         return make_handler_for(node, self.node, name)
119
120     def render_DELETE(self, ctx):
121         assert self.parentnode and self.name
122         d = self.parentnode.delete(self.name)
123         d.addCallback(lambda res: self.node.get_uri())
124         return d
125
126     def render_GET(self, ctx):
127         client = IClient(ctx)
128         req = IRequest(ctx)
129         # This is where all of the directory-related ?t=* code goes.
130         t = get_arg(req, "t", "").strip()
131         if not t:
132             # render the directory as HTML, using the docFactory and Nevow's
133             # whole templating thing.
134             return DirectoryAsHTML(self.node)
135
136         if t == "json":
137             return DirectoryJSONMetadata(ctx, self.node)
138         if t == "info":
139             return MoreInfo(self.node)
140         if t == "uri":
141             return DirectoryURI(ctx, self.node)
142         if t == "readonly-uri":
143             return DirectoryReadonlyURI(ctx, self.node)
144         if t == 'rename-form':
145             return RenameForm(self.node)
146
147         raise WebError("GET directory: bad t=%s" % t)
148
149     def render_PUT(self, ctx):
150         req = IRequest(ctx)
151         t = get_arg(req, "t", "").strip()
152         replace = boolean_of_arg(get_arg(req, "replace", "true"))
153         if t == "mkdir":
154             # our job was done by the traversal/create-intermediate-directory
155             # process that got us here.
156             return text_plain(self.node.get_uri(), ctx) # TODO: urlencode
157         if t == "uri":
158             if not replace:
159                 # they're trying to set_uri and that name is already occupied
160                 # (by us).
161                 raise ExistingChildError()
162             d = self.replace_me_with_a_childcap(ctx, replace)
163             # TODO: results
164             return d
165
166         raise WebError("PUT to a directory")
167
168     def render_POST(self, ctx):
169         req = IRequest(ctx)
170         t = get_arg(req, "t", "").strip()
171
172         if t == "mkdir":
173             d = self._POST_mkdir(req)
174         elif t == "mkdir-p":
175             # TODO: docs, tests
176             d = self._POST_mkdir_p(req)
177         elif t == "upload":
178             d = self._POST_upload(ctx) # this one needs the context
179         elif t == "uri":
180             d = self._POST_uri(req)
181         elif t == "delete":
182             d = self._POST_delete(req)
183         elif t == "rename":
184             d = self._POST_rename(req)
185         elif t == "check":
186             d = self._POST_check(req)
187         elif t == "start-deep-check":
188             d = self._POST_start_deep_check(ctx)
189         elif t == "start-manifest":
190             d = self._POST_start_manifest(ctx)
191         elif t == "start-deep-size":
192             d = self._POST_start_deep_size(ctx)
193         elif t == "start-deep-stats":
194             d = self._POST_start_deep_stats(ctx)
195         elif t == "set_children":
196             # TODO: docs
197             d = self._POST_set_children(req)
198         else:
199             raise WebError("POST to a directory with bad t=%s" % t)
200
201         when_done = get_arg(req, "when_done", None)
202         if when_done:
203             d.addCallback(lambda res: url.URL.fromString(when_done))
204         return d
205
206     def _POST_mkdir(self, req):
207         name = get_arg(req, "name", "")
208         if not name:
209             # our job is done, it was handled by the code in got_child
210             # which created the final directory (i.e. us)
211             return defer.succeed(self.node.get_uri()) # TODO: urlencode
212         name = name.decode("utf-8")
213         replace = boolean_of_arg(get_arg(req, "replace", "true"))
214         d = self.node.create_empty_directory(name, overwrite=replace)
215         d.addCallback(lambda child: child.get_uri()) # TODO: urlencode
216         return d
217
218     def _POST_mkdir_p(self, req):
219         path = get_arg(req, "path")
220         if not path:
221             raise WebError("mkdir-p requires a path")
222         path_ = tuple([seg.decode("utf-8") for seg in path.split('/') if seg ])
223         # TODO: replace
224         d = self._get_or_create_directories(self.node, path_)
225         d.addCallback(lambda node: node.get_uri())
226         return d
227
228     def _get_or_create_directories(self, node, path):
229         if not IDirectoryNode.providedBy(node):
230             # unfortunately it is too late to provide the name of the
231             # blocking directory in the error message.
232             raise BlockingFileError("cannot create directory because there "
233                                     "is a file in the way")
234         if not path:
235             return defer.succeed(node)
236         d = node.get(path[0])
237         def _maybe_create(f):
238             f.trap(NoSuchChildError)
239             return node.create_empty_directory(path[0])
240         d.addErrback(_maybe_create)
241         d.addCallback(self._get_or_create_directories, path[1:])
242         return d
243
244     def _POST_upload(self, ctx):
245         req = IRequest(ctx)
246         charset = get_arg(req, "_charset", "utf-8")
247         contents = req.fields["file"]
248         assert contents.filename is None or isinstance(contents.filename, str)
249         name = get_arg(req, "name")
250         name = name or contents.filename
251         if name is not None:
252             name = name.strip()
253         if not name:
254             # this prohibts empty, missing, and all-whitespace filenames
255             raise WebError("upload requires a name")
256         assert isinstance(name, str)
257         name = name.decode(charset)
258         if "/" in name:
259             raise WebError("name= may not contain a slash", http.BAD_REQUEST)
260         assert isinstance(name, unicode)
261
262         # since POST /uri/path/file?t=upload is equivalent to
263         # POST /uri/path/dir?t=upload&name=foo, just do the same thing that
264         # childFactory would do. Things are cleaner if we only do a subset of
265         # them, though, so we don't do: d = self.childFactory(ctx, name)
266
267         d = self.node.get(name)
268         def _maybe_got_node(node_or_failure):
269             if isinstance(node_or_failure, Failure):
270                 f = node_or_failure
271                 f.trap(NoSuchChildError)
272                 # create a placeholder which will see POST t=upload
273                 return PlaceHolderNodeHandler(self.node, name)
274             else:
275                 node = node_or_failure
276                 return make_handler_for(node, self.node, name)
277         d.addBoth(_maybe_got_node)
278         # now we have a placeholder or a filenodehandler, and we can just
279         # delegate to it. We could return the resource back out of
280         # DirectoryNodeHandler.renderHTTP, and nevow would recurse into it,
281         # but the addCallback() that handles when_done= would break.
282         d.addCallback(lambda child: child.renderHTTP(ctx))
283         return d
284
285     def _POST_uri(self, req):
286         childcap = get_arg(req, "uri")
287         if not childcap:
288             raise WebError("set-uri requires a uri")
289         name = get_arg(req, "name")
290         if not name:
291             raise WebError("set-uri requires a name")
292         charset = get_arg(req, "_charset", "utf-8")
293         name = name.decode(charset)
294         replace = boolean_of_arg(get_arg(req, "replace", "true"))
295         d = self.node.set_uri(name, childcap, overwrite=replace)
296         d.addCallback(lambda res: childcap)
297         return d
298
299     def _POST_delete(self, req):
300         name = get_arg(req, "name")
301         if name is None:
302             # apparently an <input type="hidden" name="name" value="">
303             # won't show up in the resulting encoded form.. the 'name'
304             # field is completely missing. So to allow deletion of an
305             # empty file, we have to pretend that None means ''. The only
306             # downide of this is a slightly confusing error message if
307             # someone does a POST without a name= field. For our own HTML
308             # thisn't a big deal, because we create the 'delete' POST
309             # buttons ourselves.
310             name = ''
311         charset = get_arg(req, "_charset", "utf-8")
312         name = name.decode(charset)
313         d = self.node.delete(name)
314         d.addCallback(lambda res: "thing deleted")
315         return d
316
317     def _POST_rename(self, req):
318         charset = get_arg(req, "_charset", "utf-8")
319         from_name = get_arg(req, "from_name")
320         if from_name is not None:
321             from_name = from_name.strip()
322             from_name = from_name.decode(charset)
323             assert isinstance(from_name, unicode)
324         to_name = get_arg(req, "to_name")
325         if to_name is not None:
326             to_name = to_name.strip()
327             to_name = to_name.decode(charset)
328             assert isinstance(to_name, unicode)
329         if not from_name or not to_name:
330             raise WebError("rename requires from_name and to_name")
331         if from_name == to_name:
332             return defer.succeed("redundant rename")
333
334         # allow from_name to contain slashes, so they can fix names that were
335         # accidentally created with them. But disallow them in to_name, to
336         # discourage the practice.
337         if "/" in to_name:
338             raise WebError("to_name= may not contain a slash", http.BAD_REQUEST)
339
340         replace = boolean_of_arg(get_arg(req, "replace", "true"))
341         d = self.node.move_child_to(from_name, self.node, to_name, replace)
342         d.addCallback(lambda res: "thing renamed")
343         return d
344
345     def _POST_check(self, req):
346         # check this directory
347         verify = boolean_of_arg(get_arg(req, "verify", "false"))
348         repair = boolean_of_arg(get_arg(req, "repair", "false"))
349         if repair:
350             d = self.node.check_and_repair(Monitor(), verify)
351             d.addCallback(lambda res: CheckAndRepairResults(res))
352         else:
353             d = self.node.check(Monitor(), verify)
354             d.addCallback(lambda res: CheckResults(res))
355         return d
356
357     def _start_operation(self, monitor, renderer, ctx):
358         table = IOpHandleTable(ctx)
359         table.add_monitor(ctx, monitor, renderer)
360         return table.redirect_to(ctx)
361
362     def _POST_start_deep_check(self, ctx):
363         # check this directory and everything reachable from it
364         if not get_arg(ctx, "ophandle"):
365             raise NeedOperationHandleError("slow operation requires ophandle=")
366         verify = boolean_of_arg(get_arg(ctx, "verify", "false"))
367         repair = boolean_of_arg(get_arg(ctx, "repair", "false"))
368         if repair:
369             monitor = self.node.start_deep_check_and_repair(verify)
370             renderer = DeepCheckAndRepairResults(monitor)
371         else:
372             monitor = self.node.start_deep_check(verify)
373             renderer = DeepCheckResults(monitor)
374         return self._start_operation(monitor, renderer, ctx)
375
376     def _POST_start_manifest(self, ctx):
377         if not get_arg(ctx, "ophandle"):
378             raise NeedOperationHandleError("slow operation requires ophandle=")
379         monitor = self.node.build_manifest()
380         renderer = ManifestResults(monitor)
381         return self._start_operation(monitor, renderer, ctx)
382
383     def _POST_start_deep_size(self, ctx):
384         if not get_arg(ctx, "ophandle"):
385             raise NeedOperationHandleError("slow operation requires ophandle=")
386         monitor = self.node.start_deep_stats()
387         renderer = DeepSizeResults(monitor)
388         return self._start_operation(monitor, renderer, ctx)
389
390     def _POST_start_deep_stats(self, ctx):
391         if not get_arg(ctx, "ophandle"):
392             raise NeedOperationHandleError("slow operation requires ophandle=")
393         monitor = self.node.start_deep_stats()
394         renderer = DeepStatsResults(monitor)
395         return self._start_operation(monitor, renderer, ctx)
396
397     def _POST_set_children(self, req):
398         replace = boolean_of_arg(get_arg(req, "replace", "true"))
399         req.content.seek(0)
400         body = req.content.read()
401         try:
402             children = simplejson.loads(body)
403         except ValueError, le:
404             le.args = tuple(le.args + (body,))
405             # TODO test handling of bad JSON
406             raise
407         cs = []
408         for name, (file_or_dir, mddict) in children.iteritems():
409             name = unicode(name) # simplejson-2.0.1 returns str *or* unicode
410             cap = str(mddict.get('rw_uri') or mddict.get('ro_uri'))
411             cs.append((name, cap, mddict.get('metadata')))
412         d = self.node.set_children(cs, replace)
413         d.addCallback(lambda res: "Okay so I did it.")
414         # TODO: results
415         return d
416
417 def abbreviated_dirnode(dirnode):
418     u = from_string_dirnode(dirnode.get_uri())
419     return u.abbrev()
420
421 class DirectoryAsHTML(rend.Page):
422     # The remainder of this class is to render the directory into
423     # human+browser -oriented HTML.
424     docFactory = getxmlfile("directory.xhtml")
425     addSlash = True
426
427     def __init__(self, node):
428         rend.Page.__init__(self)
429         self.node = node
430
431     def render_title(self, ctx, data):
432         si_s = abbreviated_dirnode(self.node)
433         header = ["Directory SI=%s" % si_s]
434         return ctx.tag[header]
435
436     def render_header(self, ctx, data):
437         si_s = abbreviated_dirnode(self.node)
438         header = ["Directory SI=%s" % si_s]
439         if self.node.is_readonly():
440             header.append(" (readonly)")
441         return ctx.tag[header]
442
443     def render_welcome(self, ctx, data):
444         link = get_root(ctx)
445         return T.div[T.a(href=link)["Return to Welcome page"]]
446
447     def data_children(self, ctx, data):
448         d = self.node.list()
449         d.addCallback(lambda dict: sorted(dict.items()))
450         def _stall_some(items):
451             # Deferreds don't optimize out tail recursion, and the way
452             # Nevow's flattener handles Deferreds doesn't take this into
453             # account. As a result, large lists of Deferreds that fire in the
454             # same turn (i.e. the output of defer.succeed) will cause a stack
455             # overflow. To work around this, we insert a turn break after
456             # every 100 items, using foolscap's fireEventually(). This gives
457             # the stack a chance to be popped. It would also work to put
458             # every item in its own turn, but that'd be a lot more
459             # inefficient. This addresses ticket #237, for which I was never
460             # able to create a failing unit test.
461             output = []
462             for i,item in enumerate(items):
463                 if i % 100 == 0:
464                     output.append(fireEventually(item))
465                 else:
466                     output.append(item)
467             return output
468         d.addCallback(_stall_some)
469         return d
470
471     def render_row(self, ctx, data):
472         name, (target, metadata) = data
473         name = name.encode("utf-8")
474         assert not isinstance(name, unicode)
475         nameurl = urllib.quote(name, safe="") # encode any slashes too
476
477         root = get_root(ctx)
478         here = "%s/uri/%s/" % (root, urllib.quote(self.node.get_uri()))
479         if self.node.is_readonly():
480             delete = "-"
481             rename = "-"
482         else:
483             # this creates a button which will cause our child__delete method
484             # to be invoked, which deletes the file and then redirects the
485             # browser back to this directory
486             delete = T.form(action=here, method="post")[
487                 T.input(type='hidden', name='t', value='delete'),
488                 T.input(type='hidden', name='name', value=name),
489                 T.input(type='hidden', name='when_done', value="."),
490                 T.input(type='submit', value='del', name="del"),
491                 ]
492
493             rename = T.form(action=here, method="get")[
494                 T.input(type='hidden', name='t', value='rename-form'),
495                 T.input(type='hidden', name='name', value=name),
496                 T.input(type='hidden', name='when_done', value="."),
497                 T.input(type='submit', value='rename', name="rename"),
498                 ]
499
500         ctx.fillSlots("delete", delete)
501         ctx.fillSlots("rename", rename)
502
503         times = []
504         TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
505         if "ctime" in metadata:
506             ctime = time.strftime(TIME_FORMAT,
507                                   time.localtime(metadata["ctime"]))
508             times.append("c: " + ctime)
509         if "mtime" in metadata:
510             mtime = time.strftime(TIME_FORMAT,
511                                   time.localtime(metadata["mtime"]))
512             if times:
513                 times.append(T.br())
514                 times.append("m: " + mtime)
515         ctx.fillSlots("times", times)
516
517         assert (IFileNode.providedBy(target)
518                 or IDirectoryNode.providedBy(target)
519                 or IMutableFileNode.providedBy(target)), target
520
521         quoted_uri = urllib.quote(target.get_uri())
522
523         if IMutableFileNode.providedBy(target):
524             # to prevent javascript in displayed .html files from stealing a
525             # secret directory URI from the URL, send the browser to a URI-based
526             # page that doesn't know about the directory at all
527             dlurl = "%s/file/%s/@@named=/%s" % (root, quoted_uri, nameurl)
528
529             ctx.fillSlots("filename",
530                           T.a(href=dlurl)[html.escape(name)])
531             ctx.fillSlots("type", "SSK")
532
533             ctx.fillSlots("size", "?")
534
535             info_link = "%s/uri/%s?t=info" % (root, quoted_uri)
536
537         elif IFileNode.providedBy(target):
538             dlurl = "%s/file/%s/@@named=/%s" % (root, quoted_uri, nameurl)
539
540             ctx.fillSlots("filename",
541                           T.a(href=dlurl)[html.escape(name)])
542             ctx.fillSlots("type", "FILE")
543
544             ctx.fillSlots("size", target.get_size())
545
546             info_link = "%s/uri/%s?t=info" % (root, quoted_uri)
547
548         elif IDirectoryNode.providedBy(target):
549             # directory
550             uri_link = "%s/uri/%s/" % (root, urllib.quote(target.get_uri()))
551             ctx.fillSlots("filename",
552                           T.a(href=uri_link)[html.escape(name)])
553             if target.is_readonly():
554                 dirtype = "DIR-RO"
555             else:
556                 dirtype = "DIR"
557             ctx.fillSlots("type", dirtype)
558             ctx.fillSlots("size", "-")
559             info_link = "%s/uri/%s/?t=info" % (root, quoted_uri)
560
561         ctx.fillSlots("info", T.a(href=info_link)["More Info"])
562
563         return ctx.tag
564
565     def render_forms(self, ctx, data):
566         forms = []
567
568         if self.node.is_readonly():
569             forms.append(T.div["No upload forms: directory is read-only"])
570             return forms
571
572         mkdir = T.form(action=".", method="post",
573                        enctype="multipart/form-data")[
574             T.fieldset[
575             T.input(type="hidden", name="t", value="mkdir"),
576             T.input(type="hidden", name="when_done", value="."),
577             T.legend(class_="freeform-form-label")["Create a new directory"],
578             "New directory name: ",
579             T.input(type="text", name="name"), " ",
580             T.input(type="submit", value="Create"),
581             ]]
582         forms.append(T.div(class_="freeform-form")[mkdir])
583
584         upload = T.form(action=".", method="post",
585                         enctype="multipart/form-data")[
586             T.fieldset[
587             T.input(type="hidden", name="t", value="upload"),
588             T.input(type="hidden", name="when_done", value="."),
589             T.legend(class_="freeform-form-label")["Upload a file to this directory"],
590             "Choose a file to upload: ",
591             T.input(type="file", name="file", class_="freeform-input-file"),
592             " ",
593             T.input(type="submit", value="Upload"),
594             " Mutable?:",
595             T.input(type="checkbox", name="mutable"),
596             ]]
597         forms.append(T.div(class_="freeform-form")[upload])
598
599         mount = T.form(action=".", method="post",
600                         enctype="multipart/form-data")[
601             T.fieldset[
602             T.input(type="hidden", name="t", value="uri"),
603             T.input(type="hidden", name="when_done", value="."),
604             T.legend(class_="freeform-form-label")["Attach a file or directory"
605                                                    " (by URI) to this"
606                                                    " directory"],
607             "New child name: ",
608             T.input(type="text", name="name"), " ",
609             "URI of new child: ",
610             T.input(type="text", name="uri"), " ",
611             T.input(type="submit", value="Attach"),
612             ]]
613         forms.append(T.div(class_="freeform-form")[mount])
614         return forms
615
616     def render_results(self, ctx, data):
617         req = IRequest(ctx)
618         return get_arg(req, "results", "")
619
620
621 def DirectoryJSONMetadata(ctx, dirnode):
622     d = dirnode.list()
623     def _got(children):
624         kids = {}
625         for name, (childnode, metadata) in children.iteritems():
626             if childnode.is_readonly():
627                 rw_uri = None
628                 ro_uri = childnode.get_uri()
629             else:
630                 rw_uri = childnode.get_uri()
631                 ro_uri = childnode.get_readonly_uri()
632             if IFileNode.providedBy(childnode):
633                 kiddata = ("filenode", {'size': childnode.get_size(),
634                                         'metadata': metadata,
635                                         })
636             else:
637                 assert IDirectoryNode.providedBy(childnode), (childnode,
638                                                               children,)
639                 kiddata = ("dirnode", {'metadata': metadata})
640             if ro_uri:
641                 kiddata[1]["ro_uri"] = ro_uri
642             if rw_uri:
643                 kiddata[1]["rw_uri"] = rw_uri
644             kiddata[1]['mutable'] = childnode.is_mutable()
645             kids[name] = kiddata
646         if dirnode.is_readonly():
647             drw_uri = None
648             dro_uri = dirnode.get_uri()
649         else:
650             drw_uri = dirnode.get_uri()
651             dro_uri = dirnode.get_readonly_uri()
652         contents = { 'children': kids }
653         if dro_uri:
654             contents['ro_uri'] = dro_uri
655         if drw_uri:
656             contents['rw_uri'] = drw_uri
657         contents['mutable'] = dirnode.is_mutable()
658         data = ("dirnode", contents)
659         return simplejson.dumps(data, indent=1) + "\n"
660     d.addCallback(_got)
661     d.addCallback(text_plain, ctx)
662     return d
663
664
665
666 def DirectoryURI(ctx, dirnode):
667     return text_plain(dirnode.get_uri(), ctx)
668
669 def DirectoryReadonlyURI(ctx, dirnode):
670     return text_plain(dirnode.get_readonly_uri(), ctx)
671
672 class RenameForm(rend.Page):
673     addSlash = True
674     docFactory = getxmlfile("rename-form.xhtml")
675
676     def render_title(self, ctx, data):
677         return ctx.tag["Directory SI=%s" % abbreviated_dirnode(self.original)]
678
679     def render_header(self, ctx, data):
680         header = ["Rename "
681                   "in directory SI=%s" % abbreviated_dirnode(self.original),
682                   ]
683
684         if self.original.is_readonly():
685             header.append(" (readonly!)")
686         header.append(":")
687         return ctx.tag[header]
688
689     def render_when_done(self, ctx, data):
690         return T.input(type="hidden", name="when_done", value=".")
691
692     def render_get_name(self, ctx, data):
693         req = IRequest(ctx)
694         name = get_arg(req, "name", "")
695         ctx.tag.attributes['value'] = name
696         return ctx.tag
697
698
699 class ManifestResults(rend.Page, ReloadMixin):
700     docFactory = getxmlfile("manifest.xhtml")
701
702     def __init__(self, monitor):
703         self.monitor = monitor
704
705     def renderHTTP(self, ctx):
706         output = get_arg(inevow.IRequest(ctx), "output", "html").lower()
707         if output == "text":
708             return self.text(ctx)
709         if output == "json":
710             return self.json(ctx)
711         return rend.Page.renderHTTP(self, ctx)
712
713     def slashify_path(self, path):
714         if not path:
715             return ""
716         return "/".join([p.encode("utf-8") for p in path])
717
718     def text(self, ctx):
719         inevow.IRequest(ctx).setHeader("content-type", "text/plain")
720         lines = []
721         is_finished = self.monitor.is_finished()
722         lines.append("finished: " + {True: "yes", False: "no"}[is_finished])
723         for (path, cap) in self.monitor.get_status()["manifest"]:
724             lines.append(self.slashify_path(path) + " " + cap)
725         return "\n".join(lines) + "\n"
726
727     def json(self, ctx):
728         inevow.IRequest(ctx).setHeader("content-type", "text/plain")
729         m = self.monitor
730         s = m.get_status()
731
732         status = { "stats": s["stats"],
733                    "finished": m.is_finished(),
734                    "origin": base32.b2a(m.origin_si),
735                    }
736         if m.is_finished():
737             # don't return manifest/verifycaps/SIs unless the operation is
738             # done, to save on CPU/memory (both here and in the HTTP client
739             # who has to unpack the JSON). Tests show that the ManifestWalker
740             # needs about 1092 bytes per item, the JSON we generate here
741             # requires about 503 bytes per item, and some internal overhead
742             # (perhaps transport-layer buffers in twisted.web?) requires an
743             # additional 1047 bytes per item.
744             status.update({ "manifest": s["manifest"],
745                             "verifycaps": [i for i in s["verifycaps"]],
746                             "storage-index": [i for i in s["storage-index"]],
747                             })
748             # simplejson doesn't know how to serialize a set. We use a
749             # generator that walks the set rather than list(setofthing) to
750             # save a small amount of memory (4B*len) and a moderate amount of
751             # CPU.
752         return simplejson.dumps(status, indent=1)
753
754     def _si_abbrev(self):
755         return base32.b2a(self.monitor.origin_si)[:6]
756
757     def render_title(self, ctx):
758         return T.title["Manifest of SI=%s" % self._si_abbrev()]
759
760     def render_header(self, ctx):
761         return T.p["Manifest of SI=%s" % self._si_abbrev()]
762
763     def data_items(self, ctx, data):
764         return self.monitor.get_status()["manifest"]
765
766     def render_row(self, ctx, (path, cap)):
767         ctx.fillSlots("path", self.slashify_path(path))
768         root = get_root(ctx)
769         # TODO: we need a clean consistent way to get the type of a cap string
770         if cap.startswith("URI:CHK") or cap.startswith("URI:SSK"):
771             nameurl = urllib.quote(path[-1].encode("utf-8"))
772             uri_link = "%s/file/%s/@@named=/%s" % (root, urllib.quote(cap),
773                                                    nameurl)
774         else:
775             uri_link = "%s/uri/%s" % (root, urllib.quote(cap))
776         ctx.fillSlots("cap", T.a(href=uri_link)[cap])
777         return ctx.tag
778
779 class DeepSizeResults(rend.Page):
780     def __init__(self, monitor):
781         self.monitor = monitor
782
783     def renderHTTP(self, ctx):
784         output = get_arg(inevow.IRequest(ctx), "output", "html").lower()
785         inevow.IRequest(ctx).setHeader("content-type", "text/plain")
786         if output == "json":
787             return self.json(ctx)
788         # plain text
789         is_finished = self.monitor.is_finished()
790         output = "finished: " + {True: "yes", False: "no"}[is_finished] + "\n"
791         if is_finished:
792             stats = self.monitor.get_status()
793             total = (stats.get("size-immutable-files", 0)
794                      + stats.get("size-mutable-files", 0)
795                      + stats.get("size-directories", 0))
796             output += "size: %d\n" % total
797         return output
798
799     def json(self, ctx):
800         status = {"finished": self.monitor.is_finished(),
801                   "size": self.monitor.get_status(),
802                   }
803         return simplejson.dumps(status)
804
805 class DeepStatsResults(rend.Page):
806     def __init__(self, monitor):
807         self.monitor = monitor
808
809     def renderHTTP(self, ctx):
810         # JSON only
811         inevow.IRequest(ctx).setHeader("content-type", "text/plain")
812         s = self.monitor.get_status().copy()
813         s["finished"] = self.monitor.is_finished()
814         return simplejson.dumps(s, indent=1)