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