source: trunk/src/allmydata/web/directory.py

Last change on this file was 1cfe843d, checked in by Alexandre Detiste <alexandre.detiste@…>, at 2024-02-22T23:40:25Z

more python2 removal

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