source: trunk/src/allmydata/web/directory.py @ 65ec2127

Last change on this file since 65ec2127 was 65ec2127, checked in by meejah <meejah@…>, at 2020-04-18T07:52:17Z

unused imports

  • Property mode set to 100644
File size: 54.7 KB
Line 
1
2import json
3import urllib
4from datetime import timedelta
5
6from zope.interface import implementer
7from twisted.internet import defer
8from twisted.internet.interfaces import IPushProducer
9from twisted.python.failure import Failure
10from twisted.web import http
11from twisted.web.resource import ErrorPage
12from twisted.web.resource import Resource
13from twisted.web.template import (
14    Element,
15    XMLFile,
16    renderElement,
17    renderer,
18    tags,
19)
20from hyperlink import URL
21from twisted.python.filepath import FilePath
22
23from allmydata.util import base32
24from allmydata.util.encodingutil import to_str
25from allmydata.uri import (
26    from_string_dirnode,
27    from_string,
28    CHKFileURI,
29    WriteableSSKFileURI,
30    ReadonlySSKFileURI,
31)
32from allmydata.interfaces import IDirectoryNode, IFileNode, IFilesystemNode, \
33     IImmutableFileNode, IMutableFileNode, ExistingChildError, \
34     NoSuchChildError, EmptyPathnameComponentError, SDMF_VERSION, MDMF_VERSION
35from allmydata.blacklist import ProhibitedNode
36from allmydata.monitor import Monitor, OperationCancelledError
37from allmydata import dirnode
38from allmydata.web.common import (
39    text_plain,
40    WebError,
41    NeedOperationHandleError,
42    boolean_of_arg,
43    get_arg,
44    get_root,
45    parse_replace_arg,
46    should_create_intermediate_directories,
47    RenderMixin,
48    humanize_failure,
49    convert_children_json,
50    get_format,
51    get_mutable_type,
52    get_filenode_metadata,
53    render_time,
54    MultiFormatPage,
55    MultiFormatResource,
56    SlotsSequenceElement,
57)
58from allmydata.web.filenode import ReplaceMeMixin, \
59     FileNodeHandler, PlaceHolderNodeHandler
60from allmydata.web.check_results import CheckResultsRenderer, \
61     CheckAndRepairResultsRenderer, DeepCheckResultsRenderer, \
62     DeepCheckAndRepairResultsRenderer, LiteralCheckResultsRenderer
63from allmydata.web.info import MoreInfo
64from allmydata.web.operations import ReloadMixin
65from allmydata.web.check_results import json_check_results, \
66     json_check_and_repair_results
67
68
69class BlockingFileError(Exception):
70    # TODO: catch and transform
71    """We cannot auto-create a parent directory, because there is a file in
72    the way"""
73
74
75def make_handler_for(node, client, parentnode=None, name=None):
76    if parentnode:
77        assert IDirectoryNode.providedBy(parentnode)
78    if IFileNode.providedBy(node):
79        return FileNodeHandler(client, node, parentnode, name)
80    if IDirectoryNode.providedBy(node):
81        return DirectoryNodeHandler(client, node, parentnode, name)
82    return UnknownNodeHandler(client, node, parentnode, name)
83
84
85# did inherit from: RenderMixin, rend.Page, ReplaceMeMixin
86# XXX is MultiFormatResource appropriate? this probably *should*
87# support ?t=json but I don't know that all the variants already *did*
88# support that..
89class DirectoryNodeHandler(ReplaceMeMixin, Resource, object):
90
91    def __init__(self, client, node, parentnode=None, name=None):
92        super(DirectoryNodeHandler, self).__init__()
93        self.client = client
94        assert node
95        self.node = node
96        self.parentnode = parentnode
97        self.name = name
98        self._operations = client.get_web_service().get_operations()
99
100    def getChild(self, name, req):
101        """
102        Dynamically create a child for the given request and name
103        """
104        # trying to replicate what I have observed as Nevow behavior
105        # for these nodes, which is that a URI like
106        # "/uri/URI%3ADIR2%3Aj...vq/" (that is, with a trailing slash
107        # or no further children) renders "this" page
108        name = name.decode('utf8')
109        if not name:
110            # replicating Nevow behavior that complains about "empty
111            # path segments" .. but twisted.web sends in "name=None"
112            # for a URL like "/foo/bar/" as well as "/foo//bar"
113            # (i.e. a trailing slash means "name=None" as well)
114            if b'//' in req.path:
115                raise EmptyPathnameComponentError(
116                    u"The webapi does not allow empty pathname components, i.e. a double slash",
117                )
118            return self
119        d = self.node.get(name)
120        d.addBoth(self._got_child, req, name)
121        return d
122
123    def _got_child(self, node_or_failure, req, name):
124        """
125        Callback when self.node.get has returned
126        """
127        # we used to determine if a request was 'terminal' by
128        # examining req.postpath .. but that's a Nevow-ism, so we
129        # determine if this is a "terminal" request using only Twisted
130        # Web APIs, which means finding where "name" is in the path
131        u = URL.from_text(unicode(req.path)).to_iri()
132        assert name in u.path, "Can't find our child-name in the path"
133        nonterminal = (u.path[-1] != name)
134        t = get_arg(req, "t", "").strip()  # XXX looking like MultiFormatResource..
135        if isinstance(node_or_failure, Failure):
136            f = node_or_failure
137            f.trap(NoSuchChildError)
138            # No child by this name. What should we do about it?
139            if nonterminal:
140                if should_create_intermediate_directories(req):
141                    # create intermediate directories
142                    d = self.node.create_subdirectory(name)
143                    d.addCallback(make_handler_for,
144                                  self.client, self.node, name)
145                    return d
146            else:
147                # terminal node
148                terminal_requests = (
149                    ("POST", "mkdir"),
150                    ("PUT", "mkdir"),
151                    ("POST", "mkdir-with-children"),
152                    ("POST", "mkdir-immutable")
153                )
154                if (req.method, t) in terminal_requests:
155                    # final directory
156                    kids = {}
157                    if t in ("mkdir-with-children", "mkdir-immutable"):
158                        req.content.seek(0)
159                        kids_json = req.content.read()
160                        kids = convert_children_json(
161                            self.client.nodemaker,
162                            kids_json,
163                        )
164                    file_format = get_format(req, None)
165                    mutable = True
166                    mt = get_mutable_type(file_format)
167                    if t == "mkdir-immutable":
168                        mutable = False
169
170                    d = self.node.create_subdirectory(
171                        name, kids,
172                        mutable=mutable,
173                        mutable_version=mt,
174                    )
175                    d.addCallback(
176                        make_handler_for,
177                        self.client, self.node, name,
178                    )
179                    return d
180                leaf_requests = (
181                    ("PUT",""),
182                    ("PUT","uri"),
183                )
184                if (req.method, t) in leaf_requests:
185                    # we were trying to find the leaf filenode (to put a new
186                    # file in its place), and it didn't exist. That's ok,
187                    # since that's the leaf node that we're about to create.
188                    # We make a dummy one, which will respond to the PUT
189                    # request by replacing itself.
190                    return PlaceHolderNodeHandler(self.client, self.node, name)
191            # otherwise, we just return a no-such-child error
192            return f
193
194        node = node_or_failure
195        if nonterminal and should_create_intermediate_directories(req):
196            if not IDirectoryNode.providedBy(node):
197                # we would have put a new directory here, but there was a
198                # file in the way.
199                return ErrorPage(
200                    http.CONFLICT,
201                    "Unable to create directory '{}': a file was in the way".format(name),
202                    "no details",
203                )
204        return make_handler_for(node, self.client, self.node, name)
205
206    def render_DELETE(self, req):
207        assert self.parentnode and self.name
208        d = self.parentnode.delete(self.name)
209        d.addCallback(lambda res: self.node.get_uri())
210        return d
211
212    def render_GET(self, req):
213        # This is where all of the directory-related ?t=* code goes.
214        t = get_arg(req, "t", "").strip()
215
216        # t=info contains variable ophandles, t=rename-form contains the name
217        # of the child being renamed. Neither is allowed an ETag.
218        FIXED_OUTPUT_TYPES =  ["", "json", "uri", "readonly-uri"]
219        if not self.node.is_mutable() and t in FIXED_OUTPUT_TYPES:
220            si = self.node.get_storage_index()
221            if si and req.setETag('DIR:%s-%s' % (base32.b2a(si), t or "")):
222                return ""
223
224        if not t:
225            # render the directory as HTML, using the docFactory and Nevow's
226            # whole templating thing.
227            return renderElement(
228                req,
229                DirectoryAsHTML(
230                    self.node,
231                    self.client.mutable_file_default,
232                )
233            )
234
235        if t == "json":
236            return DirectoryJSONMetadata(req, self.node)
237        if t == "info":
238            return MoreInfo(self.node)
239        if t == "uri":
240            return DirectoryURI(req, self.node)
241        if t == "readonly-uri":
242            return DirectoryReadonlyURI(req, self.node)
243        if t == 'rename-form':
244            return renderElement(
245                req,
246                RenameForm(self.node),
247            )
248
249        raise WebError("GET directory: bad t=%s" % t)
250
251    def render_PUT(self, req):
252        t = get_arg(req, "t", "").strip()
253        replace = parse_replace_arg(get_arg(req, "replace", "true"))
254
255        if t == "mkdir":
256            # our job was done by the traversal/create-intermediate-directory
257            # process that got us here.
258            return text_plain(self.node.get_uri(), req) # TODO: urlencode
259        if t == "uri":
260            if not replace:
261                # they're trying to set_uri and that name is already occupied
262                # (by us).
263                raise ExistingChildError()
264            d = self.replace_me_with_a_childcap(req, self.client, replace)
265            # TODO: results
266            return d
267
268        raise WebError("PUT to a directory")
269
270    def render_POST(self, req):
271        t = get_arg(req, "t", "").strip()
272
273        if t == "mkdir":
274            d = self._POST_mkdir(req)
275        elif t == "mkdir-with-children":
276            d = self._POST_mkdir_with_children(req)
277        elif t == "mkdir-immutable":
278            d = self._POST_mkdir_immutable(req)
279        elif t == "upload":
280            d = self._POST_upload(req) # this one needs the context
281        elif t == "uri":
282            d = self._POST_uri(req)
283        elif t == "delete" or t == "unlink":
284            d = self._POST_unlink(req)
285        elif t == "rename":
286            d = self._POST_rename(req)
287        elif t == "relink":
288            d = self._POST_relink(req)
289        elif t == "check":
290            d = self._POST_check(req)
291        elif t == "start-deep-check":
292            d = self._POST_start_deep_check(req)
293        elif t == "stream-deep-check":
294            d = self._POST_stream_deep_check(req)
295        elif t == "start-manifest":
296            d = self._POST_start_manifest(req)
297        elif t == "start-deep-size":
298            d = self._POST_start_deep_size(req)
299        elif t == "start-deep-stats":
300            d = self._POST_start_deep_stats(req)
301        elif t == "stream-manifest":
302            d = self._POST_stream_manifest(req)
303        elif t == "set_children" or t == "set-children":
304            d = self._POST_set_children(req)
305        else:
306            raise WebError("POST to a directory with bad t=%s" % t)
307
308        when_done = get_arg(req, "when_done", None)
309        if when_done:
310            def done(res):
311                req.redirect(when_done)
312                return res
313            d.addCallback(done)
314        return d
315
316    def _POST_mkdir(self, req):
317        name = get_arg(req, "name", "")
318        if not name:
319            # our job is done, it was handled by the code in got_child
320            # which created the final directory (i.e. us)
321            return defer.succeed(self.node.get_uri()) # TODO: urlencode
322        name = name.decode("utf-8")
323        replace = boolean_of_arg(get_arg(req, "replace", "true"))
324        kids = {}
325        mt = get_mutable_type(get_format(req, None))
326        d = self.node.create_subdirectory(name, kids, overwrite=replace,
327                                          mutable_version=mt)
328        d.addCallback(lambda child: child.get_uri()) # TODO: urlencode
329        return d
330
331    def _POST_mkdir_with_children(self, req):
332        name = get_arg(req, "name", "")
333        if not name:
334            # our job is done, it was handled by the code in got_child
335            # which created the final directory (i.e. us)
336            return defer.succeed(self.node.get_uri()) # TODO: urlencode
337        name = name.decode("utf-8")
338        # TODO: decide on replace= behavior, see #903
339        #replace = boolean_of_arg(get_arg(req, "replace", "false"))
340        req.content.seek(0)
341        kids_json = req.content.read()
342        kids = convert_children_json(self.client.nodemaker, kids_json)
343        mt = get_mutable_type(get_format(req, None))
344        d = self.node.create_subdirectory(name, kids, overwrite=False,
345                                          mutable_version=mt)
346        d.addCallback(lambda child: child.get_uri()) # TODO: urlencode
347        return d
348
349    def _POST_mkdir_immutable(self, req):
350        name = get_arg(req, "name", "")
351        if not name:
352            # our job is done, it was handled by the code in got_child
353            # which created the final directory (i.e. us)
354            return defer.succeed(self.node.get_uri()) # TODO: urlencode
355        name = name.decode("utf-8")
356        # TODO: decide on replace= behavior, see #903
357        #replace = boolean_of_arg(get_arg(req, "replace", "false"))
358        req.content.seek(0)
359        kids_json = req.content.read()
360        kids = convert_children_json(self.client.nodemaker, kids_json)
361        d = self.node.create_subdirectory(name, kids, overwrite=False, mutable=False)
362        d.addCallback(lambda child: child.get_uri()) # TODO: urlencode
363        return d
364
365    def _POST_upload(self, req):
366        charset = get_arg(req, "_charset", "utf-8")
367        contents = req.fields["file"]
368        assert contents.filename is None or isinstance(contents.filename, str)
369        name = get_arg(req, "name")
370        name = name or contents.filename
371        if name is not None:
372            name = name.strip()
373        if not name:
374            # this prohibts empty, missing, and all-whitespace filenames
375            raise WebError("upload requires a name")
376        assert isinstance(name, str)
377        name = name.decode(charset)
378        if "/" in name:
379            raise WebError("name= may not contain a slash", http.BAD_REQUEST)
380        assert isinstance(name, unicode)
381
382        # since POST /uri/path/file?t=upload is equivalent to
383        # POST /uri/path/dir?t=upload&name=foo, just do the same thing that
384        # childFactory would do. Things are cleaner if we only do a subset of
385        # them, though, so we don't do: d = self.childFactory(req, name)
386
387        d = self.node.get(name)
388        def _maybe_got_node(node_or_failure):
389            if isinstance(node_or_failure, Failure):
390                f = node_or_failure
391                f.trap(NoSuchChildError)
392                # create a placeholder which will see POST t=upload
393                return PlaceHolderNodeHandler(self.client, self.node, name)
394            else:
395                node = node_or_failure
396                return make_handler_for(node, self.client, self.node, name)
397        d.addBoth(_maybe_got_node)
398        # now we have a placeholder or a filenodehandler, and we can just
399        # delegate to it. We could return the resource back out of
400        # DirectoryNodeHandler.renderHTTP, and nevow would recurse into it,
401        # but the addCallback() that handles when_done= would break.
402        d.addCallback(lambda child: child.render(req))
403        return d
404
405    def _POST_uri(self, req):
406        childcap = get_arg(req, "uri")
407        if not childcap:
408            raise WebError("set-uri requires a uri")
409        name = get_arg(req, "name")
410        if not name:
411            raise WebError("set-uri requires a name")
412        charset = get_arg(req, "_charset", "utf-8")
413        name = name.decode(charset)
414        replace = parse_replace_arg(get_arg(req, "replace", "true"))
415
416        # We mustn't pass childcap for the readcap argument because we don't
417        # know whether it is a read cap. Passing a read cap as the writecap
418        # argument will work (it ends up calling NodeMaker.create_from_cap,
419        # which derives a readcap if necessary and possible).
420        d = self.node.set_uri(name, childcap, None, overwrite=replace)
421        d.addCallback(lambda res: childcap)
422        return d
423
424    def _POST_unlink(self, req):
425        name = get_arg(req, "name")
426        if name is None:
427            # apparently an <input type="hidden" name="name" value="">
428            # won't show up in the resulting encoded form.. the 'name'
429            # field is completely missing. So to allow unlinking of a
430            # child with a name that is the empty string, we have to
431            # pretend that None means ''. The only downside of this is
432            # a slightly confusing error message if someone does a POST
433            # without a name= field. For our own HTML this isn't a big
434            # deal, because we create the 'unlink' POST buttons ourselves.
435            name = ''
436        charset = get_arg(req, "_charset", "utf-8")
437        name = name.decode(charset)
438        d = self.node.delete(name)
439        d.addCallback(lambda res: "thing unlinked")
440        return d
441
442    def _POST_rename(self, req):
443        # rename is identical to relink, but to_dir is not allowed
444        # and to_name is required.
445        if get_arg(req, "to_dir") is not None:
446            raise WebError("to_dir= is not valid for rename")
447        if get_arg(req, "to_name") is None:
448            raise WebError("to_name= is required for rename")
449        return self._POST_relink(req)
450
451    def _POST_relink(self, req):
452        charset = get_arg(req, "_charset", "utf-8")
453        replace = parse_replace_arg(get_arg(req, "replace", "true"))
454
455        from_name = get_arg(req, "from_name")
456        if from_name is not None:
457            from_name = from_name.strip()
458            from_name = from_name.decode(charset)
459            assert isinstance(from_name, unicode)
460        else:
461            raise WebError("from_name= is required")
462
463        to_name = get_arg(req, "to_name")
464        if to_name is not None:
465            to_name = to_name.strip()
466            to_name = to_name.decode(charset)
467            assert isinstance(to_name, unicode)
468        else:
469            to_name = from_name
470
471        # Disallow slashes in both from_name and to_name, that would only
472        # cause confusion.
473        if "/" in from_name:
474            raise WebError("from_name= may not contain a slash",
475                           http.BAD_REQUEST)
476        if "/" in to_name:
477            raise WebError("to_name= may not contain a slash",
478                           http.BAD_REQUEST)
479
480        to_dir = get_arg(req, "to_dir")
481        if to_dir is not None and to_dir != self.node.get_write_uri():
482            to_dir = to_dir.strip()
483            to_dir = to_dir.decode(charset)
484            assert isinstance(to_dir, unicode)
485            to_path = to_dir.split(u"/")
486            to_root = self.client.nodemaker.create_from_cap(to_str(to_path[0]))
487            if not IDirectoryNode.providedBy(to_root):
488                raise WebError("to_dir is not a directory", http.BAD_REQUEST)
489            d = to_root.get_child_at_path(to_path[1:])
490        else:
491            d = defer.succeed(self.node)
492
493        def _got_new_parent(new_parent):
494            if not IDirectoryNode.providedBy(new_parent):
495                raise WebError("to_dir is not a directory", http.BAD_REQUEST)
496
497            return self.node.move_child_to(from_name, new_parent,
498                                           to_name, replace)
499        d.addCallback(_got_new_parent)
500        d.addCallback(lambda res: "thing moved")
501        return d
502
503    def _maybe_literal(self, res, Results_Class):
504        if res:
505            return Results_Class(self.client, res)
506        return LiteralCheckResultsRenderer(self.client)
507
508    def _POST_check(self, req):
509        # check this directory
510        verify = boolean_of_arg(get_arg(req, "verify", "false"))
511        repair = boolean_of_arg(get_arg(req, "repair", "false"))
512        add_lease = boolean_of_arg(get_arg(req, "add-lease", "false"))
513        if repair:
514            d = self.node.check_and_repair(Monitor(), verify, add_lease)
515            d.addCallback(self._maybe_literal, CheckAndRepairResultsRenderer)
516        else:
517            d = self.node.check(Monitor(), verify, add_lease)
518            d.addCallback(self._maybe_literal, CheckResultsRenderer)
519        return d
520
521    def _start_operation(self, monitor, renderer, ctx):
522        self._operations.add_monitor(ctx, monitor, renderer)
523        return self._operations.redirect_to(ctx)
524
525    def _POST_start_deep_check(self, req):
526        # check this directory and everything reachable from it
527        if not get_arg(req, "ophandle"):
528            raise NeedOperationHandleError("slow operation requires ophandle=")
529        verify = boolean_of_arg(get_arg(req, "verify", "false"))
530        repair = boolean_of_arg(get_arg(req, "repair", "false"))
531        add_lease = boolean_of_arg(get_arg(req, "add-lease", "false"))
532        if repair:
533            monitor = self.node.start_deep_check_and_repair(verify, add_lease)
534            renderer = DeepCheckAndRepairResultsRenderer(self.client, monitor)
535        else:
536            monitor = self.node.start_deep_check(verify, add_lease)
537            renderer = DeepCheckResultsRenderer(self.client, monitor)
538        return self._start_operation(monitor, renderer, req)
539
540    def _POST_stream_deep_check(self, req):
541        verify = boolean_of_arg(get_arg(req, "verify", "false"))
542        repair = boolean_of_arg(get_arg(req, "repair", "false"))
543        add_lease = boolean_of_arg(get_arg(req, "add-lease", "false"))
544        walker = DeepCheckStreamer(req, self.node, verify, repair, add_lease)
545        monitor = self.node.deep_traverse(walker)
546        walker.setMonitor(monitor)
547        # register to hear stopProducing. The walker ignores pauseProducing.
548        req.registerProducer(walker, True)
549        d = monitor.when_done()
550        def _done(res):
551            req.unregisterProducer()
552            return res
553        d.addBoth(_done)
554        def _cancelled(f):
555            f.trap(OperationCancelledError)
556            return "Operation Cancelled"
557        d.addErrback(_cancelled)
558        def _error(f):
559            # signal the error as a non-JSON "ERROR:" line, plus exception
560            msg = "ERROR: %s(%s)\n" % (f.value.__class__.__name__,
561                                       ", ".join([str(a) for a in f.value.args]))
562            msg += str(f)
563            return msg
564        d.addErrback(_error)
565        return d
566
567    def _POST_start_manifest(self, req):
568        if not get_arg(req, "ophandle"):
569            raise NeedOperationHandleError("slow operation requires ophandle=")
570        monitor = self.node.build_manifest()
571        renderer = ManifestResults(self.client, monitor)
572        return self._start_operation(monitor, renderer, req)
573
574    def _POST_start_deep_size(self, req):
575        if not get_arg(req, "ophandle"):
576            raise NeedOperationHandleError("slow operation requires ophandle=")
577        monitor = self.node.start_deep_stats()
578        renderer = DeepSizeResults(self.client, monitor)
579        return self._start_operation(monitor, renderer, req)
580
581    def _POST_start_deep_stats(self, req):
582        if not get_arg(req, "ophandle"):
583            raise NeedOperationHandleError("slow operation requires ophandle=")
584        monitor = self.node.start_deep_stats()
585        renderer = DeepStatsResults(self.client, monitor)
586        return self._start_operation(monitor, renderer, req)
587
588    def _POST_stream_manifest(self, req):
589        walker = ManifestStreamer(req, self.node)
590        monitor = self.node.deep_traverse(walker)
591        walker.setMonitor(monitor)
592        # register to hear stopProducing. The walker ignores pauseProducing.
593        req.registerProducer(walker, True)
594        d = monitor.when_done()
595        def _done(res):
596            req.unregisterProducer()
597            return res
598        d.addBoth(_done)
599        def _cancelled(f):
600            f.trap(OperationCancelledError)
601            return "Operation Cancelled"
602        d.addErrback(_cancelled)
603        def _error(f):
604            # signal the error as a non-JSON "ERROR:" line, plus exception
605            msg = "ERROR: %s(%s)\n" % (f.value.__class__.__name__,
606                                       ", ".join([str(a) for a in f.value.args]))
607            msg += str(f)
608            return msg
609        d.addErrback(_error)
610        return d
611
612    def _POST_set_children(self, req):
613        replace = parse_replace_arg(get_arg(req, "replace", "true"))
614        req.content.seek(0)
615        body = req.content.read()
616        try:
617            children = json.loads(body)
618        except ValueError as le:
619            le.args = tuple(le.args + (body,))
620            # TODO test handling of bad JSON
621            raise
622        cs = {}
623        for name, (file_or_dir, mddict) in children.iteritems():
624            name = unicode(name) # json returns str *or* unicode
625            writecap = mddict.get('rw_uri')
626            if writecap is not None:
627                writecap = str(writecap)
628            readcap = mddict.get('ro_uri')
629            if readcap is not None:
630                readcap = str(readcap)
631            cs[name] = (writecap, readcap, mddict.get('metadata'))
632        d = self.node.set_children(cs, replace)
633        d.addCallback(lambda res: "Okay so I did it.")
634        # TODO: results
635        return d
636
637def abbreviated_dirnode(dirnode):
638    u = from_string_dirnode(dirnode.get_uri())
639    return u.abbrev_si()
640
641SPACE = u"\u00A0"*2
642
643class DirectoryAsHTML(Element):
644    # The remainder of this class is to render the directory into
645    # human+browser -oriented HTML.
646    loader = XMLFile(FilePath(__file__).sibling("directory.xhtml"))
647
648    def __init__(self, node, default_mutable_format):
649        super(DirectoryAsHTML, self).__init__()
650        self.node = node
651        if default_mutable_format not in (MDMF_VERSION, SDMF_VERSION):
652            raise ValueError(
653                "Uknown mutable format '{}'".format(default_mutable_format)
654            )
655        self.default_mutable_format = default_mutable_format
656
657    @defer.inlineCallbacks
658    def render(self, request):
659        """
660        Override Element.render .. we have async work to do before we flatten our template
661        """
662        # this could be improved; doing a more-straightforward port
663        # here, which used to be in "beforeRender" .. so lots of the
664        # renderers don't yield/wait properly, they expect the self.*
665        # state that is set up here to "just be there" :/
666        yield self._get_children(request)
667        template = Element.render(self, request)
668        defer.returnValue(template)
669
670    @defer.inlineCallbacks
671    def _get_children(self, req):
672        # attempt to get the dirnode's children, stashing them (or the
673        # failure that results) for later use
674        try:
675            children = yield self.node.list()
676        except Exception as e:
677            text, code = humanize_failure(Failure(e))
678            children = None
679            self.dirnode_children_error = text
680
681        # XXX there was a big comment here about tail-recursion etc
682        # .. do we need to yield sometimes in this loop, or ..? (I'm
683        # not sure the former implementation actually did what the
684        # comment claimed, though..)
685        self.dirnode_children = children
686        defer.returnValue(self.dirnode_children)
687
688    @renderer
689    def children(self, req, tag):
690        return SlotsSequenceElement(
691            tag,
692            [
693                self._child_slots(req, fname, data[0], data[1])
694                for fname, data in self.dirnode_children.items()
695            ]
696        )
697
698    @renderer
699    def title(self, req, tag):
700        si_s = abbreviated_dirnode(self.node)
701        header = ["Tahoe-LAFS - Directory SI=%s" % si_s]
702        if self.node.is_unknown():
703            header.append(" (unknown)")
704        elif not self.node.is_mutable():
705            header.append(" (immutable)")
706        elif self.node.is_readonly():
707            header.append(" (read-only)")
708        else:
709            header.append(" (modifiable)")
710        return tag(header)
711
712    @renderer
713    def header(self, req, tag):
714        si_s = abbreviated_dirnode(self.node)
715        header = ["Tahoe-LAFS Directory SI=", tags.span(si_s, class_="data-chars")]
716        if self.node.is_unknown():
717            header.append(" (unknown)")
718        elif not self.node.is_mutable():
719            header.append(" (immutable)")
720        elif self.node.is_readonly():
721            header.append(" (read-only)")
722        return tag(header)
723
724    @renderer
725    def welcome(self, req, tag):
726        link = get_root(req)
727        return tag(tags.a("Return to Welcome page", href=link))
728
729    @renderer
730    def show_readonly(self, req, tag):
731        if self.node.is_unknown() or self.node.is_readonly():
732            return ""
733        rocap = self.node.get_readonly_uri()
734        root = get_root(req)
735        uri_link = "%s/uri/%s/" % (root, urllib.quote(rocap))
736        return tag(tags.a("Read-Only Version", href=uri_link))
737
738    @renderer
739    def try_children(self, req, tag):
740        # if the dirnode can be retrived, render a table of children.
741        # Otherwise, render an apologetic error message.
742        if self.dirnode_children is not None:
743            return tag
744        else:
745            return tags.div(
746                tags.p("Error reading directory:"),
747                tags.p(self.dirnode_children_error),
748            )
749
750    def _child_slots(self, req, name, target, metadata):
751        """
752        :returns: a dict of key/values to give to each item in the table
753            of sub-items that directory.xhtml defines (this method is
754            called by the 'children' renderer)
755        """
756        name = name.encode("utf-8")
757        nameurl = urllib.quote(name, safe="") # encode any slashes too
758
759        root = get_root(req)
760        here = "{}/uri/{}/".format(root, urllib.quote(self.node.get_uri()))
761        if self.node.is_unknown() or self.node.is_readonly():
762            unlink = "-"
763            rename = "-"
764        else:
765            # this creates a button which will cause our _POST_unlink method
766            # to be invoked, which unlinks the file and then redirects the
767            # browser back to this directory
768            unlink = tags.form(
769                [
770                    tags.input(type='hidden', name='t', value='unlink'),
771                    tags.input(type='hidden', name='name', value=name),
772                    tags.input(type='hidden', name='when_done', value="."),
773                    tags.input(type='submit', class_='btn', value='unlink', name="unlink"),
774                ],
775                action=here, method="post"
776            )
777            rename = tags.form(
778                [
779                    tags.input(type='hidden', name='t', value='rename-form'),
780                    tags.input(type='hidden', name='name', value=name),
781                    tags.input(type='hidden', name='when_done', value="."),
782                    tags.input(type='submit', class_='btn', value='rename/relink', name="rename"),
783                ],
784                action=here, method="get",
785            )
786
787        slots = {
788            "unlink": unlink,
789            "rename": rename,
790        }
791
792        times = []
793        linkcrtime = metadata.get('tahoe', {}).get("linkcrtime")
794        if linkcrtime is not None:
795            times.append("lcr: " + render_time(linkcrtime))
796        else:
797            # For backwards-compatibility with links last modified by Tahoe < 1.4.0:
798            if "ctime" in metadata:
799                ctime = render_time(metadata["ctime"])
800                times.append("c: " + ctime)
801        linkmotime = metadata.get('tahoe', {}).get("linkmotime")
802        if linkmotime is not None:
803            if times:
804                times.append(tags.br())
805            times.append("lmo: " + render_time(linkmotime))
806        else:
807            # For backwards-compatibility with links last modified by Tahoe < 1.4.0:
808            if "mtime" in metadata:
809                mtime = render_time(metadata["mtime"])
810                if times:
811                    times.append(tags.br())
812                times.append("m: " + mtime)
813        slots["times"] = times
814
815        assert IFilesystemNode.providedBy(target), target
816        target_uri = target.get_uri() or ""
817        quoted_uri = urllib.quote(target_uri, safe="") # escape slashes too
818
819        if IMutableFileNode.providedBy(target):
820            # to prevent javascript in displayed .html files from stealing a
821            # secret directory URI from the URL, send the browser to a URI-based
822            # page that doesn't know about the directory at all
823            dlurl = "%s/file/%s/@@named=/%s" % (root, quoted_uri, nameurl)
824            slots["filename"] = tags.a(name, href=dlurl, rel="noreferrer")
825            slots["type"] = "SSK"
826            slots["size"] = "?"
827            info_link = "{}/uri/{}?t=info".format(root, quoted_uri)
828
829        elif IImmutableFileNode.providedBy(target):
830            dlurl = "%s/file/%s/@@named=/%s" % (root, quoted_uri, nameurl)
831            slots["filename"] = tags.a(name, href=dlurl, rel="noreferrer")
832            slots["type"] = "FILE"
833            slots["size"] = str(target.get_size())
834            info_link = "{}/uri/{}?t=info".format(root, quoted_uri)
835
836        elif IDirectoryNode.providedBy(target):
837            # directory
838            uri_link = "%s/uri/%s/" % (root, urllib.quote(target_uri))
839            slots["filename"] = tags.a(name, href=uri_link)
840            if not target.is_mutable():
841                dirtype = "DIR-IMM"
842            elif target.is_readonly():
843                dirtype = "DIR-RO"
844            else:
845                dirtype = "DIR"
846            slots["type"] = dirtype
847            slots["size"] = "-"
848            info_link = "%s/uri/%s/?t=info" % (root, quoted_uri)
849
850        elif isinstance(target, ProhibitedNode):
851            if IDirectoryNode.providedBy(target.wrapped_node):
852                blacklisted_type = "DIR-BLACKLISTED"
853            else:
854                blacklisted_type = "BLACKLISTED"
855            slots["type"] = blacklisted_type
856            slots["size"] = "-"
857            slots["info"] = ["Access Prohibited:", tags.br, target.reason]
858            slots["filename"] = tags.strike(name)
859            info_link = None
860
861        else:
862            # unknown
863            if target.get_write_uri() is not None:
864                unknowntype = "?"
865            elif not self.node.is_mutable() or target.is_alleged_immutable():
866                unknowntype = "?-IMM"
867            else:
868                unknowntype = "?-RO"
869            slots["filename"] = name
870            slots["type"] = unknowntype
871            slots["size"] = "-"
872            # use a directory-relative info link, so we can extract both the
873            # writecap and the readcap
874            info_link = "%s?t=info" % urllib.quote(name)
875
876        if info_link:
877            slots["info"] = tags.a("More Info", href=info_link)
878
879        return slots
880
881    # XXX: similar to render_upload_form and render_mkdir_form in root.py.
882    # XXX: also, is generating so much HTML in code a great idea? -> templates?
883    @renderer
884    def forms(self, req, data):
885        forms = []
886
887        # making this consistent with the other forms, and also
888        # because action="." doesn't get us back to the dir page (but
889        # instead /uri itself)
890        root = get_root(req)
891        here = "{}/uri/{}/".format(root, urllib.quote(self.node.get_uri()))
892
893        if self.node.is_readonly():
894            return tags.div("No upload forms: directory is read-only")
895        if self.dirnode_children is None:
896            return tags.div("No upload forms: directory is unreadable")
897
898        mkdir_sdmf = tags.input(
899            type='radio',
900            name='format',
901            value='sdmf',
902            id='mkdir-sdmf',
903            checked='checked',
904        )
905        mkdir_mdmf = tags.input(
906            type='radio',
907            name='format',
908            value='mdmf',
909            id='mkdir-mdmf',
910        )
911
912        mkdir_form = tags.form(
913            [
914                tags.fieldset([
915                    tags.input(type="hidden", name="t", value="mkdir"),
916                    tags.input(type="hidden", name="when_done", value="."),
917                    tags.legend("Create a new directory in this directory", class_="freeform-form-label"),
918                    "New directory name:"+SPACE, tags.br,
919                    tags.input(type="text", name="name"), SPACE,
920                    tags.div(
921                        [
922                            mkdir_sdmf, tags.label(SPACE, "SDMF", for_='mutable-directory-sdmf'), SPACE*2,
923                            mkdir_mdmf, tags.label(SPACE, "MDMF (experimental)", for_='mutable-directory-mdmf'),
924                        ],
925                        class_="form-inline",
926                    ),
927                    tags.input(
928                        type="submit",
929                        class_="btn",
930                        value="Create",
931                    ),
932                ])
933            ],
934            action=here,
935            method="post",
936            enctype="multipart/form-data",
937        )
938        forms.append(tags.div(mkdir_form, class_="freeform-form"))
939
940        upload_chk = tags.input(
941            type='radio',
942            name='format',
943            value='chk',
944            id='upload-chk',
945            checked='checked',
946        )
947        upload_sdmf = tags.input(
948            type='radio',
949            name='format',
950            value='sdmf',
951            id='upload-sdmf',
952        )
953        upload_mdmf = tags.input(
954            type='radio',
955            name='format',
956            value='mdmf',
957            id='upload-mdmf',
958        )
959
960        upload_form = tags.form(
961            tags.fieldset([
962                tags.input(type="hidden", name="t", value="upload"),
963                tags.input(type="hidden", name="when_done", value=req.uri),
964                tags.legend("Upload a file to this directory", class_="freeform-form-label"),
965                "Choose a file to upload:"+SPACE,
966                tags.input(type="file", name="file", class_="freeform-input-file"), SPACE,
967                tags.div([
968                    upload_chk,  tags.label(SPACE, "Immutable", for_="upload-chk"), SPACE*2,
969                    upload_sdmf, tags.label(SPACE, "SDMF", for_="upload-sdmf"), SPACE*2,
970                    upload_mdmf, tags.label(SPACE, "MDMF (experimental)", for_="upload-mdmf"),
971                ], class_="form-inline"),
972                tags.input(type="submit", class_="btn", value="Upload"),             SPACE*2,
973            ]),
974            action=req.uri,
975            method="post",
976            enctype="multipart/form-data",
977        )
978        forms.append(tags.div(upload_form, class_="freeform-form"))
979
980        attach_form = tags.form(
981            tags.fieldset(
982                tags.div([
983                    tags.input(type="hidden", name="t", value="uri"),
984                    tags.input(type="hidden", name="when_done", value="."),
985                    tags.legend("Add a link to a file or directory which is already in Tahoe-LAFS.", class_="freeform-form-label"),
986                    "New child name:"+SPACE,
987                    tags.input(type="text", name="name"), SPACE*2, tags.br,
988                    "URI of new child:"+SPACE,
989                    tags.input(type="text", name="uri"), SPACE,
990                    tags.input(type="submit", class_="btn", value="Attach"),
991                    ], class_="form-inline"),
992            ),
993            action=here,
994            method="post",
995            enctype="multipart/form-data",
996        )
997        forms.append(tags.div(attach_form, class_="freeform-form"))
998        return forms
999
1000    @renderer
1001    def results(self, req, tag):
1002        return get_arg(req, "results", "")
1003
1004def DirectoryJSONMetadata(req, dirnode):
1005    d = dirnode.list()
1006    def _got(children):
1007        kids = {}
1008        for name, (childnode, metadata) in children.iteritems():
1009            assert IFilesystemNode.providedBy(childnode), childnode
1010            rw_uri = childnode.get_write_uri()
1011            ro_uri = childnode.get_readonly_uri()
1012            if IFileNode.providedBy(childnode):
1013                kiddata = ("filenode", get_filenode_metadata(childnode))
1014            elif IDirectoryNode.providedBy(childnode):
1015                kiddata = ("dirnode", {'mutable': childnode.is_mutable()})
1016            else:
1017                kiddata = ("unknown", {})
1018
1019            kiddata[1]["metadata"] = metadata
1020            if rw_uri:
1021                kiddata[1]["rw_uri"] = rw_uri
1022            if ro_uri:
1023                kiddata[1]["ro_uri"] = ro_uri
1024            verifycap = childnode.get_verify_cap()
1025            if verifycap:
1026                kiddata[1]['verify_uri'] = verifycap.to_string()
1027
1028            kids[name] = kiddata
1029
1030        drw_uri = dirnode.get_write_uri()
1031        dro_uri = dirnode.get_readonly_uri()
1032        contents = { 'children': kids }
1033        if dro_uri:
1034            contents['ro_uri'] = dro_uri
1035        if drw_uri:
1036            contents['rw_uri'] = drw_uri
1037        verifycap = dirnode.get_verify_cap()
1038        if verifycap:
1039            contents['verify_uri'] = verifycap.to_string()
1040        contents['mutable'] = dirnode.is_mutable()
1041        data = ("dirnode", contents)
1042        return json.dumps(data, indent=1) + "\n"
1043    d.addCallback(_got)
1044    d.addCallback(text_plain, req)
1045
1046    def error(f):
1047        message, code = humanize_failure(f)
1048        req.setResponseCode(code)
1049        return json.dumps({
1050            "error": message,
1051        })
1052    d.addErrback(error)
1053    return d
1054
1055
1056def DirectoryURI(req, dirnode):
1057    return text_plain(dirnode.get_uri(), req)
1058
1059def DirectoryReadonlyURI(req, dirnode):
1060    return text_plain(dirnode.get_readonly_uri(), req)
1061
1062class RenameForm(Element, object):
1063    addSlash = True
1064    loader = XMLFile(FilePath(__file__).sibling("rename-form.xhtml"))
1065
1066    def __init__(self, original):
1067        self.original = original
1068        super(RenameForm, self).__init__()
1069
1070    @renderer
1071    def title(self, req, tag):
1072        return tag("Directory SI={}".format(abbreviated_dirnode(self.original)))
1073
1074    @renderer
1075    def header(self, req, tag):
1076        header = [
1077            "Rename "
1078            "in directory SI=%s" % abbreviated_dirnode(self.original),
1079        ]
1080
1081        if self.original.is_readonly():
1082            header.append(" (readonly!)")
1083        header.append(":")
1084        return tag(header)
1085
1086    @renderer
1087    def when_done(self, req, tag):
1088        return tags.input(type="hidden", name="when_done", value=".")
1089
1090    @renderer
1091    def get_name(self, req, tag):
1092        name = get_arg(req, "name", "")
1093        tag.attributes['value'] = name
1094        return tag
1095
1096
1097class ReloadableMonitorElement(Element):
1098    """
1099    Like 'ReloadMixin', but for twisted.web.template style. This
1100    provides renderers for "reload" and "refesh" and a self.monitor
1101    attribute (which is an instance of IMonitor)
1102    """
1103    refresh_time = timedelta(seconds=60)
1104
1105    def __init__(self, monitor):
1106        self.monitor = monitor
1107        super(ReloadableMonitorElement, self).__init__()
1108
1109    @renderer
1110    def refresh(self, req, tag):
1111        if self.monitor.is_finished():
1112            return u""
1113        tag.attributes[u"http-equiv"] = u"refresh"
1114        tag.attributes[u"content"] = u"{}".format(self.refresh_time.seconds)
1115        return tag
1116
1117    @renderer
1118    def reload(self, req, tag):
1119        if self.monitor.is_finished():
1120            return u""
1121        reload_url = URL.from_text(u"{}".format(req.path))
1122        cancel_button = tags.form(
1123            [
1124                tags.input(type=u"submit", value=u"Cancel"),
1125            ],
1126            action=reload_url.replace(query={u"t": u"cancel"}).to_uri().to_text(),
1127            method=u"POST",
1128            enctype=u"multipart/form-data",
1129        )
1130
1131        return tag([
1132            u"Operation still running: ",
1133            tags.a(
1134                u"Reload",
1135                href=reload_url.replace(query={u"output": u"html"}).to_uri().to_text(),
1136            ),
1137            cancel_button,
1138        ])
1139
1140
1141def _slashify_path(path):
1142    """
1143    Converts a tuple from a 'manifest' path into a string with slashes
1144    in it
1145    """
1146    if not path:
1147        return ""
1148    return "/".join([p.encode("utf-8") for p in path])
1149
1150
1151def _cap_to_link(root, path, cap):
1152    """
1153    Turns a capability-string into a WebAPI link tag
1154
1155    :param text root: the root piece of the URI
1156
1157    :param text cap: the capability-string
1158
1159    :returns: something suitable for `IRenderable`, specifically
1160        either a valid local link (tags.a instance) to the capability
1161        or an empty string.
1162    """
1163    if cap:
1164        root_url = URL.from_text(u"{}".format(root))
1165        cap_obj = from_string(cap)
1166        if isinstance(cap_obj, (CHKFileURI, WriteableSSKFileURI, ReadonlySSKFileURI)):
1167            uri_link = root_url.child(
1168                u"file",
1169                u"{}".format(urllib.quote(cap)),
1170                u"{}".format(urllib.quote(path[-1])),
1171            )
1172        else:
1173            uri_link = root_url.child(
1174                u"uri",
1175                u"{}".format(urllib.quote(cap, safe="")),
1176            )
1177        return tags.a(cap, href=uri_link.to_text())
1178    else:
1179        return u""
1180
1181
1182class ManifestElement(ReloadableMonitorElement):
1183    loader = XMLFile(FilePath(__file__).sibling("manifest.xhtml"))
1184
1185    def _si_abbrev(self):
1186        si = self.monitor.origin_si
1187        if not si:
1188            return "<LIT>"
1189        return base32.b2a(si)[:6]
1190
1191    @renderer
1192    def title(self, req, tag):
1193        return tag(
1194            "Manifest of SI={}".format(self._si_abbrev())
1195        )
1196
1197    @renderer
1198    def header(self, req, tag):
1199        return tag(
1200            "Manifest of SI={}".format(self._si_abbrev())
1201        )
1202
1203    @renderer
1204    def items(self, req, tag):
1205        manifest = self.monitor.get_status()["manifest"]
1206        root = get_root(req)
1207        rows = [
1208            {
1209                "path": _slashify_path(path),
1210                "cap": _cap_to_link(root, path, cap),
1211            }
1212            for path, cap in manifest
1213        ]
1214        return SlotsSequenceElement(tag, rows)
1215
1216
1217class ManifestResults(MultiFormatResource, ReloadMixin):
1218
1219    # Control MultiFormatPage
1220    formatArgument = "output"
1221    formatDefault = "html"
1222
1223    def __init__(self, client, monitor):
1224        self.client = client
1225        self.monitor = monitor
1226
1227    def render_HTML(self, req):
1228        return renderElement(
1229            req,
1230            ManifestElement(self.monitor)
1231        )
1232
1233    def render_TEXT(self, req):
1234        req.setHeader("content-type", "text/plain")
1235        lines = []
1236        is_finished = self.monitor.is_finished()
1237        lines.append("finished: " + {True: "yes", False: "no"}[is_finished])
1238        for path, cap in self.monitor.get_status()["manifest"]:
1239            lines.append(_slashify_path(path) + " " + cap)
1240        return "\n".join(lines) + "\n"
1241
1242    def render_JSON(self, req):
1243        req.setHeader("content-type", "text/plain")
1244        m = self.monitor
1245        s = m.get_status()
1246
1247        if m.origin_si:
1248            origin_base32 = base32.b2a(m.origin_si)
1249        else:
1250            origin_base32 = ""
1251        status = { "stats": s["stats"],
1252                   "finished": m.is_finished(),
1253                   "origin": origin_base32,
1254                   }
1255        if m.is_finished():
1256            # don't return manifest/verifycaps/SIs unless the operation is
1257            # done, to save on CPU/memory (both here and in the HTTP client
1258            # who has to unpack the JSON). Tests show that the ManifestWalker
1259            # needs about 1092 bytes per item, the JSON we generate here
1260            # requires about 503 bytes per item, and some internal overhead
1261            # (perhaps transport-layer buffers in twisted.web?) requires an
1262            # additional 1047 bytes per item.
1263            status.update({ "manifest": s["manifest"],
1264                            "verifycaps": [i for i in s["verifycaps"]],
1265                            "storage-index": [i for i in s["storage-index"]],
1266                            })
1267            # simplejson doesn't know how to serialize a set. We use a
1268            # generator that walks the set rather than list(setofthing) to
1269            # save a small amount of memory (4B*len) and a moderate amount of
1270            # CPU.
1271        return json.dumps(status, indent=1)
1272
1273
1274class DeepSizeResults(MultiFormatPage):
1275    # Control MultiFormatPage
1276    formatArgument = "output"
1277    formatDefault = "html"
1278
1279    def __init__(self, client, monitor):
1280        self.client = client
1281        self.monitor = monitor
1282
1283    def render_HTML(self, req):
1284        is_finished = self.monitor.is_finished()
1285        output = "finished: " + {True: "yes", False: "no"}[is_finished] + "\n"
1286        if is_finished:
1287            stats = self.monitor.get_status()
1288            total = (stats.get("size-immutable-files", 0)
1289                     + stats.get("size-mutable-files", 0)
1290                     + stats.get("size-directories", 0))
1291            output += "size: %d\n" % total
1292        return output
1293    render_TEXT = render_HTML
1294
1295    def render_JSON(self, req):
1296        req.setHeader("content-type", "text/plain")
1297        status = {"finished": self.monitor.is_finished(),
1298                  "size": self.monitor.get_status(),
1299                  }
1300        return json.dumps(status)
1301
1302
1303class DeepStatsResults(Resource, object):
1304    """
1305    Renders the results of a 'deep-stats' operation on a directory
1306    capability.
1307    """
1308    def __init__(self, client, monitor):
1309        self.client = client
1310        self.monitor = monitor
1311
1312    def render(self, req):
1313        # JSON only
1314        req.setHeader("content-type", "text/plain")
1315        s = self.monitor.get_status().copy()
1316        s["finished"] = self.monitor.is_finished()
1317        return json.dumps(s, indent=1)
1318
1319
1320@implementer(IPushProducer)
1321class ManifestStreamer(dirnode.DeepStats):
1322
1323    def __init__(self, req, origin):
1324        dirnode.DeepStats.__init__(self, origin)
1325        self.req = req
1326
1327    def setMonitor(self, monitor):
1328        self.monitor = monitor
1329    def pauseProducing(self):
1330        pass
1331    def resumeProducing(self):
1332        pass
1333    def stopProducing(self):
1334        self.monitor.cancel()
1335
1336    def add_node(self, node, path):
1337        dirnode.DeepStats.add_node(self, node, path)
1338        d = {"path": path,
1339             "cap": node.get_uri()}
1340
1341        if IDirectoryNode.providedBy(node):
1342            d["type"] = "directory"
1343        elif IFileNode.providedBy(node):
1344            d["type"] = "file"
1345        else:
1346            d["type"] = "unknown"
1347
1348        v = node.get_verify_cap()
1349        if v:
1350            v = v.to_string()
1351        d["verifycap"] = v or ""
1352
1353        r = node.get_repair_cap()
1354        if r:
1355            r = r.to_string()
1356        d["repaircap"] = r or ""
1357
1358        si = node.get_storage_index()
1359        if si:
1360            si = base32.b2a(si)
1361        d["storage-index"] = si or ""
1362
1363        j = json.dumps(d, ensure_ascii=True)
1364        assert "\n" not in j
1365        self.req.write(j+"\n")
1366
1367    def finish(self):
1368        stats = dirnode.DeepStats.get_results(self)
1369        d = {"type": "stats",
1370             "stats": stats,
1371             }
1372        j = json.dumps(d, ensure_ascii=True)
1373        assert "\n" not in j
1374        self.req.write(j+"\n")
1375        return ""
1376
1377@implementer(IPushProducer)
1378class DeepCheckStreamer(dirnode.DeepStats):
1379
1380    def __init__(self, req, origin, verify, repair, add_lease):
1381        dirnode.DeepStats.__init__(self, origin)
1382        self.req = req
1383        self.verify = verify
1384        self.repair = repair
1385        self.add_lease = add_lease
1386
1387    def setMonitor(self, monitor):
1388        self.monitor = monitor
1389    def pauseProducing(self):
1390        pass
1391    def resumeProducing(self):
1392        pass
1393    def stopProducing(self):
1394        self.monitor.cancel()
1395
1396    def add_node(self, node, path):
1397        dirnode.DeepStats.add_node(self, node, path)
1398        data = {"path": path,
1399                "cap": node.get_uri()}
1400
1401        if IDirectoryNode.providedBy(node):
1402            data["type"] = "directory"
1403        elif IFileNode.providedBy(node):
1404            data["type"] = "file"
1405        else:
1406            data["type"] = "unknown"
1407
1408        v = node.get_verify_cap()
1409        if v:
1410            v = v.to_string()
1411        data["verifycap"] = v or ""
1412
1413        r = node.get_repair_cap()
1414        if r:
1415            r = r.to_string()
1416        data["repaircap"] = r or ""
1417
1418        si = node.get_storage_index()
1419        if si:
1420            si = base32.b2a(si)
1421        data["storage-index"] = si or ""
1422
1423        if self.repair:
1424            d = node.check_and_repair(self.monitor, self.verify, self.add_lease)
1425            d.addCallback(self.add_check_and_repair, data)
1426        else:
1427            d = node.check(self.monitor, self.verify, self.add_lease)
1428            d.addCallback(self.add_check, data)
1429        d.addCallback(self.write_line)
1430        return d
1431
1432    def add_check_and_repair(self, crr, data):
1433        data["check-and-repair-results"] = json_check_and_repair_results(crr)
1434        return data
1435
1436    def add_check(self, cr, data):
1437        data["check-results"] = json_check_results(cr)
1438        return data
1439
1440    def write_line(self, data):
1441        j = json.dumps(data, ensure_ascii=True)
1442        assert "\n" not in j
1443        self.req.write(j+"\n")
1444
1445    def finish(self):
1446        stats = dirnode.DeepStats.get_results(self)
1447        d = {"type": "stats",
1448             "stats": stats,
1449             }
1450        j = json.dumps(d, ensure_ascii=True)
1451        assert "\n" not in j
1452        self.req.write(j+"\n")
1453        return ""
1454
1455
1456class UnknownNodeHandler(RenderMixin, Resource):
1457    def __init__(self, client, node, parentnode=None, name=None):
1458        super(UnknownNodeHandler, self).__init__()
1459        assert node
1460        self.node = node
1461        self.parentnode = parentnode
1462        self.name = name
1463
1464    def render_GET(self, req):
1465        t = get_arg(req, "t", "").strip()
1466        if t == "info":
1467            return MoreInfo(self.node)
1468        if t == "json":
1469            is_parent_known_immutable = self.parentnode and not self.parentnode.is_mutable()
1470            if self.parentnode and self.name:
1471                d = self.parentnode.get_metadata_for(self.name)
1472            else:
1473                d = defer.succeed(None)
1474            d.addCallback(lambda md: UnknownJSONMetadata(req, self.node, md, is_parent_known_immutable))
1475            return d
1476        raise WebError("GET unknown URI type: can only do t=info and t=json, not t=%s.\n"
1477                       "Using a webapi server that supports a later version of Tahoe "
1478                       "may help." % t)
1479
1480def UnknownJSONMetadata(req, node, edge_metadata, is_parent_known_immutable):
1481    rw_uri = node.get_write_uri()
1482    ro_uri = node.get_readonly_uri()
1483    data = ("unknown", {})
1484    if ro_uri:
1485        data[1]['ro_uri'] = ro_uri
1486    if rw_uri:
1487        data[1]['rw_uri'] = rw_uri
1488        data[1]['mutable'] = True
1489    elif is_parent_known_immutable or node.is_alleged_immutable():
1490        data[1]['mutable'] = False
1491    # else we don't know whether it is mutable.
1492
1493    if edge_metadata is not None:
1494        data[1]['metadata'] = edge_metadata
1495    return text_plain(json.dumps(data, indent=1) + "\n", req)
Note: See TracBrowser for help on using the repository browser.