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