]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/web/directory.py
webapi: add verifycap (spelled 'verify_url') to the t=json output on files and direct...
[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_si()
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 render_show_readonly(self, ctx, data):
470         if self.node.is_readonly():
471             return ""
472         rocap = self.node.get_readonly_uri()
473         root = get_root(ctx)
474         uri_link = "%s/uri/%s/" % (root, urllib.quote(rocap))
475         return ctx.tag[T.a(href=uri_link)["Read-Only Version"]]
476
477     def data_children(self, ctx, data):
478         d = self.node.list()
479         d.addCallback(lambda dict: sorted(dict.items()))
480         def _stall_some(items):
481             # Deferreds don't optimize out tail recursion, and the way
482             # Nevow's flattener handles Deferreds doesn't take this into
483             # account. As a result, large lists of Deferreds that fire in the
484             # same turn (i.e. the output of defer.succeed) will cause a stack
485             # overflow. To work around this, we insert a turn break after
486             # every 100 items, using foolscap's fireEventually(). This gives
487             # the stack a chance to be popped. It would also work to put
488             # every item in its own turn, but that'd be a lot more
489             # inefficient. This addresses ticket #237, for which I was never
490             # able to create a failing unit test.
491             output = []
492             for i,item in enumerate(items):
493                 if i % 100 == 0:
494                     output.append(fireEventually(item))
495                 else:
496                     output.append(item)
497             return output
498         d.addCallback(_stall_some)
499         return d
500
501     def render_row(self, ctx, data):
502         name, (target, metadata) = data
503         name = name.encode("utf-8")
504         assert not isinstance(name, unicode)
505         nameurl = urllib.quote(name, safe="") # encode any slashes too
506
507         root = get_root(ctx)
508         here = "%s/uri/%s/" % (root, urllib.quote(self.node.get_uri()))
509         if self.node.is_readonly():
510             delete = "-"
511             rename = "-"
512         else:
513             # this creates a button which will cause our child__delete method
514             # to be invoked, which deletes the file and then redirects the
515             # browser back to this directory
516             delete = T.form(action=here, method="post")[
517                 T.input(type='hidden', name='t', value='delete'),
518                 T.input(type='hidden', name='name', value=name),
519                 T.input(type='hidden', name='when_done', value="."),
520                 T.input(type='submit', value='del', name="del"),
521                 ]
522
523             rename = T.form(action=here, method="get")[
524                 T.input(type='hidden', name='t', value='rename-form'),
525                 T.input(type='hidden', name='name', value=name),
526                 T.input(type='hidden', name='when_done', value="."),
527                 T.input(type='submit', value='rename', name="rename"),
528                 ]
529
530         ctx.fillSlots("delete", delete)
531         ctx.fillSlots("rename", rename)
532
533         times = []
534         TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
535         if "ctime" in metadata:
536             ctime = time.strftime(TIME_FORMAT,
537                                   time.localtime(metadata["ctime"]))
538             times.append("c: " + ctime)
539         if "mtime" in metadata:
540             mtime = time.strftime(TIME_FORMAT,
541                                   time.localtime(metadata["mtime"]))
542             if times:
543                 times.append(T.br())
544                 times.append("m: " + mtime)
545         ctx.fillSlots("times", times)
546
547         assert (IFileNode.providedBy(target)
548                 or IDirectoryNode.providedBy(target)
549                 or IMutableFileNode.providedBy(target)), target
550
551         quoted_uri = urllib.quote(target.get_uri())
552
553         if IMutableFileNode.providedBy(target):
554             # to prevent javascript in displayed .html files from stealing a
555             # secret directory URI from the URL, send the browser to a URI-based
556             # page that doesn't know about the directory at all
557             dlurl = "%s/file/%s/@@named=/%s" % (root, quoted_uri, nameurl)
558
559             ctx.fillSlots("filename",
560                           T.a(href=dlurl)[html.escape(name)])
561             ctx.fillSlots("type", "SSK")
562
563             ctx.fillSlots("size", "?")
564
565             info_link = "%s/uri/%s?t=info" % (root, quoted_uri)
566
567         elif IFileNode.providedBy(target):
568             dlurl = "%s/file/%s/@@named=/%s" % (root, quoted_uri, nameurl)
569
570             ctx.fillSlots("filename",
571                           T.a(href=dlurl)[html.escape(name)])
572             ctx.fillSlots("type", "FILE")
573
574             ctx.fillSlots("size", target.get_size())
575
576             info_link = "%s/uri/%s?t=info" % (root, quoted_uri)
577
578         elif IDirectoryNode.providedBy(target):
579             # directory
580             uri_link = "%s/uri/%s/" % (root, urllib.quote(target.get_uri()))
581             ctx.fillSlots("filename",
582                           T.a(href=uri_link)[html.escape(name)])
583             if target.is_readonly():
584                 dirtype = "DIR-RO"
585             else:
586                 dirtype = "DIR"
587             ctx.fillSlots("type", dirtype)
588             ctx.fillSlots("size", "-")
589             info_link = "%s/uri/%s/?t=info" % (root, quoted_uri)
590
591         ctx.fillSlots("info", T.a(href=info_link)["More Info"])
592
593         return ctx.tag
594
595     def render_forms(self, ctx, data):
596         forms = []
597
598         if self.node.is_readonly():
599             forms.append(T.div["No upload forms: directory is read-only"])
600             return forms
601
602         mkdir = T.form(action=".", method="post",
603                        enctype="multipart/form-data")[
604             T.fieldset[
605             T.input(type="hidden", name="t", value="mkdir"),
606             T.input(type="hidden", name="when_done", value="."),
607             T.legend(class_="freeform-form-label")["Create a new directory"],
608             "New directory name: ",
609             T.input(type="text", name="name"), " ",
610             T.input(type="submit", value="Create"),
611             ]]
612         forms.append(T.div(class_="freeform-form")[mkdir])
613
614         upload = T.form(action=".", method="post",
615                         enctype="multipart/form-data")[
616             T.fieldset[
617             T.input(type="hidden", name="t", value="upload"),
618             T.input(type="hidden", name="when_done", value="."),
619             T.legend(class_="freeform-form-label")["Upload a file to this directory"],
620             "Choose a file to upload: ",
621             T.input(type="file", name="file", class_="freeform-input-file"),
622             " ",
623             T.input(type="submit", value="Upload"),
624             " Mutable?:",
625             T.input(type="checkbox", name="mutable"),
626             ]]
627         forms.append(T.div(class_="freeform-form")[upload])
628
629         mount = T.form(action=".", method="post",
630                         enctype="multipart/form-data")[
631             T.fieldset[
632             T.input(type="hidden", name="t", value="uri"),
633             T.input(type="hidden", name="when_done", value="."),
634             T.legend(class_="freeform-form-label")["Attach a file or directory"
635                                                    " (by URI) to this"
636                                                    " directory"],
637             "New child name: ",
638             T.input(type="text", name="name"), " ",
639             "URI of new child: ",
640             T.input(type="text", name="uri"), " ",
641             T.input(type="submit", value="Attach"),
642             ]]
643         forms.append(T.div(class_="freeform-form")[mount])
644         return forms
645
646     def render_results(self, ctx, data):
647         req = IRequest(ctx)
648         return get_arg(req, "results", "")
649
650
651 def DirectoryJSONMetadata(ctx, dirnode):
652     d = dirnode.list()
653     def _got(children):
654         kids = {}
655         for name, (childnode, metadata) in children.iteritems():
656             if childnode.is_readonly():
657                 rw_uri = None
658                 ro_uri = childnode.get_uri()
659             else:
660                 rw_uri = childnode.get_uri()
661                 ro_uri = childnode.get_readonly_uri()
662             if IFileNode.providedBy(childnode):
663                 kiddata = ("filenode", {'size': childnode.get_size(),
664                                         'metadata': metadata,
665                                         })
666             else:
667                 assert IDirectoryNode.providedBy(childnode), (childnode,
668                                                               children,)
669                 kiddata = ("dirnode", {'metadata': metadata})
670             if ro_uri:
671                 kiddata[1]["ro_uri"] = ro_uri
672             if rw_uri:
673                 kiddata[1]["rw_uri"] = rw_uri
674             verifycap = childnode.get_verify_cap()
675             if verifycap:
676                 kiddata[1]['verify_uri'] = verifycap.to_string()
677             kiddata[1]['mutable'] = childnode.is_mutable()
678             kids[name] = kiddata
679         if dirnode.is_readonly():
680             drw_uri = None
681             dro_uri = dirnode.get_uri()
682         else:
683             drw_uri = dirnode.get_uri()
684             dro_uri = dirnode.get_readonly_uri()
685         contents = { 'children': kids }
686         if dro_uri:
687             contents['ro_uri'] = dro_uri
688         if drw_uri:
689             contents['rw_uri'] = drw_uri
690         verifycap = dirnode.get_verify_cap()
691         if verifycap:
692             contents['verify_uri'] = verifycap.to_string()
693         contents['mutable'] = dirnode.is_mutable()
694         data = ("dirnode", contents)
695         return simplejson.dumps(data, indent=1) + "\n"
696     d.addCallback(_got)
697     d.addCallback(text_plain, ctx)
698     return d
699
700
701
702 def DirectoryURI(ctx, dirnode):
703     return text_plain(dirnode.get_uri(), ctx)
704
705 def DirectoryReadonlyURI(ctx, dirnode):
706     return text_plain(dirnode.get_readonly_uri(), ctx)
707
708 class RenameForm(rend.Page):
709     addSlash = True
710     docFactory = getxmlfile("rename-form.xhtml")
711
712     def render_title(self, ctx, data):
713         return ctx.tag["Directory SI=%s" % abbreviated_dirnode(self.original)]
714
715     def render_header(self, ctx, data):
716         header = ["Rename "
717                   "in directory SI=%s" % abbreviated_dirnode(self.original),
718                   ]
719
720         if self.original.is_readonly():
721             header.append(" (readonly!)")
722         header.append(":")
723         return ctx.tag[header]
724
725     def render_when_done(self, ctx, data):
726         return T.input(type="hidden", name="when_done", value=".")
727
728     def render_get_name(self, ctx, data):
729         req = IRequest(ctx)
730         name = get_arg(req, "name", "")
731         ctx.tag.attributes['value'] = name
732         return ctx.tag
733
734
735 class ManifestResults(rend.Page, ReloadMixin):
736     docFactory = getxmlfile("manifest.xhtml")
737
738     def __init__(self, monitor):
739         self.monitor = monitor
740
741     def renderHTTP(self, ctx):
742         output = get_arg(inevow.IRequest(ctx), "output", "html").lower()
743         if output == "text":
744             return self.text(ctx)
745         if output == "json":
746             return self.json(ctx)
747         return rend.Page.renderHTTP(self, ctx)
748
749     def slashify_path(self, path):
750         if not path:
751             return ""
752         return "/".join([p.encode("utf-8") for p in path])
753
754     def text(self, ctx):
755         inevow.IRequest(ctx).setHeader("content-type", "text/plain")
756         lines = []
757         is_finished = self.monitor.is_finished()
758         lines.append("finished: " + {True: "yes", False: "no"}[is_finished])
759         for (path, cap) in self.monitor.get_status()["manifest"]:
760             lines.append(self.slashify_path(path) + " " + cap)
761         return "\n".join(lines) + "\n"
762
763     def json(self, ctx):
764         inevow.IRequest(ctx).setHeader("content-type", "text/plain")
765         m = self.monitor
766         s = m.get_status()
767
768         status = { "stats": s["stats"],
769                    "finished": m.is_finished(),
770                    "origin": base32.b2a(m.origin_si),
771                    }
772         if m.is_finished():
773             # don't return manifest/verifycaps/SIs unless the operation is
774             # done, to save on CPU/memory (both here and in the HTTP client
775             # who has to unpack the JSON). Tests show that the ManifestWalker
776             # needs about 1092 bytes per item, the JSON we generate here
777             # requires about 503 bytes per item, and some internal overhead
778             # (perhaps transport-layer buffers in twisted.web?) requires an
779             # additional 1047 bytes per item.
780             status.update({ "manifest": s["manifest"],
781                             "verifycaps": [i for i in s["verifycaps"]],
782                             "storage-index": [i for i in s["storage-index"]],
783                             })
784             # simplejson doesn't know how to serialize a set. We use a
785             # generator that walks the set rather than list(setofthing) to
786             # save a small amount of memory (4B*len) and a moderate amount of
787             # CPU.
788         return simplejson.dumps(status, indent=1)
789
790     def _si_abbrev(self):
791         return base32.b2a(self.monitor.origin_si)[:6]
792
793     def render_title(self, ctx):
794         return T.title["Manifest of SI=%s" % self._si_abbrev()]
795
796     def render_header(self, ctx):
797         return T.p["Manifest of SI=%s" % self._si_abbrev()]
798
799     def data_items(self, ctx, data):
800         return self.monitor.get_status()["manifest"]
801
802     def render_row(self, ctx, (path, cap)):
803         ctx.fillSlots("path", self.slashify_path(path))
804         root = get_root(ctx)
805         # TODO: we need a clean consistent way to get the type of a cap string
806         if cap.startswith("URI:CHK") or cap.startswith("URI:SSK"):
807             nameurl = urllib.quote(path[-1].encode("utf-8"))
808             uri_link = "%s/file/%s/@@named=/%s" % (root, urllib.quote(cap),
809                                                    nameurl)
810         else:
811             uri_link = "%s/uri/%s" % (root, urllib.quote(cap))
812         ctx.fillSlots("cap", T.a(href=uri_link)[cap])
813         return ctx.tag
814
815 class DeepSizeResults(rend.Page):
816     def __init__(self, monitor):
817         self.monitor = monitor
818
819     def renderHTTP(self, ctx):
820         output = get_arg(inevow.IRequest(ctx), "output", "html").lower()
821         inevow.IRequest(ctx).setHeader("content-type", "text/plain")
822         if output == "json":
823             return self.json(ctx)
824         # plain text
825         is_finished = self.monitor.is_finished()
826         output = "finished: " + {True: "yes", False: "no"}[is_finished] + "\n"
827         if is_finished:
828             stats = self.monitor.get_status()
829             total = (stats.get("size-immutable-files", 0)
830                      + stats.get("size-mutable-files", 0)
831                      + stats.get("size-directories", 0))
832             output += "size: %d\n" % total
833         return output
834
835     def json(self, ctx):
836         status = {"finished": self.monitor.is_finished(),
837                   "size": self.monitor.get_status(),
838                   }
839         return simplejson.dumps(status)
840
841 class DeepStatsResults(rend.Page):
842     def __init__(self, monitor):
843         self.monitor = monitor
844
845     def renderHTTP(self, ctx):
846         # JSON only
847         inevow.IRequest(ctx).setHeader("content-type", "text/plain")
848         s = self.monitor.get_status().copy()
849         s["finished"] = self.monitor.is_finished()
850         return simplejson.dumps(s, indent=1)
851
852 class ManifestStreamer(dirnode.DeepStats):
853     implements(IPushProducer)
854
855     def __init__(self, ctx, origin):
856         dirnode.DeepStats.__init__(self, origin)
857         self.req = IRequest(ctx)
858
859     def setMonitor(self, monitor):
860         self.monitor = monitor
861     def pauseProducing(self):
862         pass
863     def resumeProducing(self):
864         pass
865     def stopProducing(self):
866         self.monitor.cancel()
867
868     def add_node(self, node, path):
869         dirnode.DeepStats.add_node(self, node, path)
870         d = {"path": path,
871              "cap": node.get_uri()}
872
873         if IDirectoryNode.providedBy(node):
874             d["type"] = "directory"
875         else:
876             d["type"] = "file"
877
878         v = node.get_verify_cap()
879         if v:
880             v = v.to_string()
881         d["verifycap"] = v
882
883         r = node.get_repair_cap()
884         if r:
885             r = r.to_string()
886         d["repaircap"] = r
887
888         si = node.get_storage_index()
889         if si:
890             si = base32.b2a(si)
891         d["storage-index"] = si
892
893         j = simplejson.dumps(d, ensure_ascii=True)
894         assert "\n" not in j
895         self.req.write(j+"\n")
896
897     def finish(self):
898         stats = dirnode.DeepStats.get_results(self)
899         d = {"type": "stats",
900              "stats": stats,
901              }
902         j = simplejson.dumps(d, ensure_ascii=True)
903         assert "\n" not in j
904         self.req.write(j+"\n")
905         return ""