]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blobdiff - src/allmydata/web/directory.py
wui: use standard time format (#1077)
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / web / directory.py
index d4f88de29125d403ae536a2c56911b5892ae3841..29b57620ba2048aa8d3ad55f7802b1878cc0492c 100644 (file)
@@ -6,14 +6,15 @@ from zope.interface import implements
 from twisted.internet import defer
 from twisted.internet.interfaces import IPushProducer
 from twisted.python.failure import Failure
-from twisted.web import http, html
+from twisted.web import http
 from nevow import url, rend, inevow, tags as T
 from nevow.inevow import IRequest
 
 from foolscap.api import fireEventually
 
-from allmydata.util import base32, time_format
-from allmydata.uri import from_string_dirnode, is_writeable_directory_uri
+from allmydata.util import base32
+from allmydata.util.encodingutil import to_str
+from allmydata.uri import from_string_dirnode
 from allmydata.interfaces import IDirectoryNode, IFileNode, IFilesystemNode, \
      IImmutableFileNode, IMutableFileNode, ExistingChildError, \
      NoSuchChildError, EmptyPathnameComponentError, SDMF_VERSION, MDMF_VERSION
@@ -25,7 +26,7 @@ from allmydata.web.common import text_plain, WebError, \
      boolean_of_arg, get_arg, get_root, parse_replace_arg, \
      should_create_intermediate_directories, \
      getxmlfile, RenderMixin, humanize_failure, convert_children_json, \
-     get_format, get_mutable_type
+     get_format, get_mutable_type, get_filenode_metadata, render_time
 from allmydata.web.filenode import ReplaceMeMixin, \
      FileNodeHandler, PlaceHolderNodeHandler
 from allmydata.web.check_results import CheckResultsRenderer, \
@@ -153,6 +154,15 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
         req = IRequest(ctx)
         # This is where all of the directory-related ?t=* code goes.
         t = get_arg(req, "t", "").strip()
+
+        # t=info contains variable ophandles, t=rename-form contains the name
+        # of the child being renamed. Neither is allowed an ETag.
+        FIXED_OUTPUT_TYPES =  ["", "json", "uri", "readonly-uri"]
+        if not self.node.is_mutable() and t in FIXED_OUTPUT_TYPES:
+            si = self.node.get_storage_index()
+            if si and req.setETag('DIR:%s-%s' % (base32.b2a(si), t or "")):
+                return ""
+
         if not t:
             # render the directory as HTML, using the docFactory and Nevow's
             # whole templating thing.
@@ -169,8 +179,6 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
             return DirectoryReadonlyURI(ctx, self.node)
         if t == 'rename-form':
             return RenameForm(self.node)
-        if t == 'move-form':
-            return MoveForm(self.node)
 
         raise WebError("GET directory: bad t=%s" % t)
 
@@ -204,9 +212,6 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
             d = self._POST_mkdir_with_children(req)
         elif t == "mkdir-immutable":
             d = self._POST_mkdir_immutable(req)
-        elif t == "mkdir-p":
-            # TODO: docs, tests
-            d = self._POST_mkdir_p(req)
         elif t == "upload":
             d = self._POST_upload(ctx) # this one needs the context
         elif t == "uri":
@@ -215,8 +220,8 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
             d = self._POST_unlink(req)
         elif t == "rename":
             d = self._POST_rename(req)
-        elif t == "move":
-            d = self._POST_move(req)
+        elif t == "relink":
+            d = self._POST_relink(req)
         elif t == "check":
             d = self._POST_check(req)
         elif t == "start-deep-check":
@@ -290,32 +295,6 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
         d.addCallback(lambda child: child.get_uri()) # TODO: urlencode
         return d
 
-    def _POST_mkdir_p(self, req):
-        path = get_arg(req, "path")
-        if not path:
-            raise WebError("mkdir-p requires a path")
-        path_ = tuple([seg.decode("utf-8") for seg in path.split('/') if seg ])
-        # TODO: replace
-        d = self._get_or_create_directories(self.node, path_)
-        d.addCallback(lambda node: node.get_uri())
-        return d
-
-    def _get_or_create_directories(self, node, path):
-        if not IDirectoryNode.providedBy(node):
-            # unfortunately it is too late to provide the name of the
-            # blocking directory in the error message.
-            raise BlockingFileError("cannot create directory because there "
-                                    "is a file in the way")
-        if not path:
-            return defer.succeed(node)
-        d = node.get(path[0])
-        def _maybe_create(f):
-            f.trap(NoSuchChildError)
-            return node.create_subdirectory(path[0])
-        d.addErrback(_maybe_create)
-        d.addCallback(self._get_or_create_directories, path[1:])
-        return d
-
     def _POST_upload(self, ctx):
         req = IRequest(ctx)
         charset = get_arg(req, "_charset", "utf-8")
@@ -366,7 +345,7 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
             raise WebError("set-uri requires a name")
         charset = get_arg(req, "_charset", "utf-8")
         name = name.decode(charset)
-        replace = boolean_of_arg(get_arg(req, "replace", "true"))
+        replace = parse_replace_arg(get_arg(req, "replace", "true"))
 
         # We mustn't pass childcap for the readcap argument because we don't
         # know whether it is a read cap. Passing a read cap as the writecap
@@ -395,76 +374,63 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
         return d
 
     def _POST_rename(self, req):
+        # rename is identical to relink, but to_dir is not allowed
+        # and to_name is required.
+        if get_arg(req, "to_dir") is not None:
+            raise WebError("to_dir= is not valid for rename")
+        if get_arg(req, "to_name") is None:
+            raise WebError("to_name= is required for rename")
+        return self._POST_relink(req)
+
+    def _POST_relink(self, req):
         charset = get_arg(req, "_charset", "utf-8")
-        from_name = get_arg(req, "from_name")
-        if from_name is not None:
-            from_name = from_name.strip()
-            from_name = from_name.decode(charset)
-            assert isinstance(from_name, unicode)
-        to_name = get_arg(req, "to_name")
-        if to_name is not None:
-            to_name = to_name.strip()
-            to_name = to_name.decode(charset)
-            assert isinstance(to_name, unicode)
-        if not from_name or not to_name:
-            raise WebError("rename requires from_name and to_name")
-        if from_name == to_name:
-            return defer.succeed("redundant rename")
-
-        # allow from_name to contain slashes, so they can fix names that were
-        # accidentally created with them. But disallow them in to_name, to
-        # discourage the practice.
-        if "/" in to_name:
-            raise WebError("to_name= may not contain a slash", http.BAD_REQUEST)
-
-        replace = boolean_of_arg(get_arg(req, "replace", "true"))
-        d = self.node.move_child_to(from_name, self.node, to_name, replace)
-        d.addCallback(lambda res: "thing renamed")
-        return d
+        replace = parse_replace_arg(get_arg(req, "replace", "true"))
 
-    def _POST_move(self, req):
-        charset = get_arg(req, "_charset", "utf-8")
         from_name = get_arg(req, "from_name")
         if from_name is not None:
             from_name = from_name.strip()
             from_name = from_name.decode(charset)
             assert isinstance(from_name, unicode)
+        else:
+            raise WebError("from_name= is required")
+
         to_name = get_arg(req, "to_name")
         if to_name is not None:
             to_name = to_name.strip()
             to_name = to_name.decode(charset)
             assert isinstance(to_name, unicode)
-        if not to_name:
+        else:
             to_name = from_name
+
+        # Disallow slashes in both from_name and to_name, that would only
+        # cause confusion.
+        if "/" in from_name:
+            raise WebError("from_name= may not contain a slash",
+                           http.BAD_REQUEST)
+        if "/" in to_name:
+            raise WebError("to_name= may not contain a slash",
+                           http.BAD_REQUEST)
+
         to_dir = get_arg(req, "to_dir")
-        if to_dir is not None:
+        if to_dir is not None and to_dir != self.node.get_write_uri():
             to_dir = to_dir.strip()
             to_dir = to_dir.decode(charset)
             assert isinstance(to_dir, unicode)
-        if not from_name or not to_dir:
-            raise WebError("move requires from_name and to_dir")
-        replace = boolean_of_arg(get_arg(req, "replace", "true"))
+            to_path = to_dir.split(u"/")
+            to_root = self.client.nodemaker.create_from_cap(to_str(to_path[0]))
+            if not IDirectoryNode.providedBy(to_root):
+                raise WebError("to_dir is not a directory", http.BAD_REQUEST)
+            d = to_root.get_child_at_path(to_path[1:])
+        else:
+            d = defer.succeed(self.node)
 
-        # allow from_name to contain slashes, so they can fix names that
-        # were accidentally created with them. But disallow them in to_name
-        # (if it's specified), to discourage the practice.
-        if to_name and "/" in to_name:
-            raise WebError("to_name= may not contain a slash", http.BAD_REQUEST)
+        def _got_new_parent(new_parent):
+            if not IDirectoryNode.providedBy(new_parent):
+                raise WebError("to_dir is not a directory", http.BAD_REQUEST)
 
-        d = self.node.has_child(to_dir.split('/')[0])
-        def get_target_node(isname):
-            if isname or not is_writeable_directory_uri(str(to_dir)):
-                return self.node.get_child_at_path(to_dir)
-            else:
-                return self.client.create_node_from_uri(str(to_dir))
-        d.addCallback(get_target_node)
-        def is_target_node_usable(target_node):
-            if not IDirectoryNode.providedBy(target_node):
-                raise WebError("to_dir is not a usable directory", http.GONE)
-            return target_node
-        d.addCallback(is_target_node_usable)
-        d.addCallback(lambda new_parent: self.node.move_child_to(
-                      from_name, new_parent, to_name, replace))
+            return self.node.move_child_to(from_name, new_parent,
+                                           to_name, replace)
+        d.addCallback(_got_new_parent)
         d.addCallback(lambda res: "thing moved")
         return d
 
@@ -579,7 +545,7 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
         return d
 
     def _POST_set_children(self, req):
-        replace = boolean_of_arg(get_arg(req, "replace", "true"))
+        replace = parse_replace_arg(get_arg(req, "replace", "true"))
         req.content.seek(0)
         body = req.content.read()
         try:
@@ -712,7 +678,6 @@ class DirectoryAsHTML(rend.Page):
         if self.node.is_unknown() or self.node.is_readonly():
             unlink = "-"
             rename = "-"
-            move = "-"
         else:
             # this creates a button which will cause our _POST_unlink method
             # to be invoked, which unlinks the file and then redirects the
@@ -721,45 +686,37 @@ class DirectoryAsHTML(rend.Page):
                 T.input(type='hidden', name='t', value='unlink'),
                 T.input(type='hidden', name='name', value=name),
                 T.input(type='hidden', name='when_done', value="."),
-                T.input(type='submit', value='unlink', name="unlink"),
+                T.input(type='submit', _class='btn', value='unlink', name="unlink"),
                 ]
 
             rename = T.form(action=here, method="get")[
                 T.input(type='hidden', name='t', value='rename-form'),
                 T.input(type='hidden', name='name', value=name),
                 T.input(type='hidden', name='when_done', value="."),
-                T.input(type='submit', value='rename', name="rename"),
-                ]
-
-            move = T.form(action=here, method="get")[
-                T.input(type='hidden', name='t', value='move-form'),
-                T.input(type='hidden', name='name', value=name),
-                T.input(type='hidden', name='when_done', value="."),
-                T.input(type='submit', value='move', name="move"),
+                T.input(type='submit', _class='btn', value='rename/relink', name="rename"),
                 ]
 
         ctx.fillSlots("unlink", unlink)
         ctx.fillSlots("rename", rename)
-        ctx.fillSlots("move", move)
 
         times = []
         linkcrtime = metadata.get('tahoe', {}).get("linkcrtime")
         if linkcrtime is not None:
-            times.append("lcr: " + time_format.iso_local(linkcrtime))
+            times.append("lcr: " + render_time(linkcrtime))
         else:
             # For backwards-compatibility with links last modified by Tahoe < 1.4.0:
             if "ctime" in metadata:
-                ctime = time_format.iso_local(metadata["ctime"])
+                ctime = render_time(metadata["ctime"])
                 times.append("c: " + ctime)
         linkmotime = metadata.get('tahoe', {}).get("linkmotime")
         if linkmotime is not None:
             if times:
                 times.append(T.br())
-            times.append("lmo: " + time_format.iso_local(linkmotime))
+            times.append("lmo: " + render_time(linkmotime))
         else:
             # For backwards-compatibility with links last modified by Tahoe < 1.4.0:
             if "mtime" in metadata:
-                mtime = time_format.iso_local(metadata["mtime"])
+                mtime = render_time(metadata["mtime"])
                 if times:
                     times.append(T.br())
                 times.append("m: " + mtime)
@@ -775,8 +732,7 @@ class DirectoryAsHTML(rend.Page):
             # page that doesn't know about the directory at all
             dlurl = "%s/file/%s/@@named=/%s" % (root, quoted_uri, nameurl)
 
-            ctx.fillSlots("filename",
-                          T.a(href=dlurl)[html.escape(name)])
+            ctx.fillSlots("filename", T.a(href=dlurl)[name])
             ctx.fillSlots("type", "SSK")
 
             ctx.fillSlots("size", "?")
@@ -786,8 +742,7 @@ class DirectoryAsHTML(rend.Page):
         elif IImmutableFileNode.providedBy(target):
             dlurl = "%s/file/%s/@@named=/%s" % (root, quoted_uri, nameurl)
 
-            ctx.fillSlots("filename",
-                          T.a(href=dlurl)[html.escape(name)])
+            ctx.fillSlots("filename", T.a(href=dlurl)[name])
             ctx.fillSlots("type", "FILE")
 
             ctx.fillSlots("size", target.get_size())
@@ -797,8 +752,7 @@ class DirectoryAsHTML(rend.Page):
         elif IDirectoryNode.providedBy(target):
             # directory
             uri_link = "%s/uri/%s/" % (root, urllib.quote(target_uri))
-            ctx.fillSlots("filename",
-                          T.a(href=uri_link)[html.escape(name)])
+            ctx.fillSlots("filename", T.a(href=uri_link)[name])
             if not target.is_mutable():
                 dirtype = "DIR-IMM"
             elif target.is_readonly():
@@ -822,7 +776,7 @@ class DirectoryAsHTML(rend.Page):
 
         else:
             # unknown
-            ctx.fillSlots("filename", html.escape(name))
+            ctx.fillSlots("filename", name)
             if target.get_write_uri() is not None:
                 unknowntype = "?"
             elif not self.node.is_mutable() or target.is_alleged_immutable():
@@ -861,11 +815,13 @@ class DirectoryAsHTML(rend.Page):
             T.input(type="hidden", name="t", value="mkdir"),
             T.input(type="hidden", name="when_done", value="."),
             T.legend(class_="freeform-form-label")["Create a new directory in this directory"],
-            "New directory name:"+SPACE,
+            "New directory name:"+SPACE, T.br,
             T.input(type="text", name="name"), SPACE,
-            T.input(type="submit", value="Create"), SPACE*2,
-            mkdir_sdmf, T.label(for_='mutable-directory-sdmf')[" SDMF"], SPACE,
-            mkdir_mdmf, T.label(for_='mutable-directory-mdmf')[" MDMF (experimental)"],
+            T.div(class_="form-inline")[
+                mkdir_sdmf, T.label(for_='mutable-directory-sdmf')[SPACE, "SDMF"], SPACE*2,
+                mkdir_mdmf, T.label(for_='mutable-directory-mdmf')[SPACE, "MDMF (experimental)"]
+            ],
+            T.input(type="submit", class_="btn", value="Create")
             ]]
         forms.append(T.div(class_="freeform-form")[mkdir_form])
 
@@ -885,25 +841,27 @@ class DirectoryAsHTML(rend.Page):
             T.legend(class_="freeform-form-label")["Upload a file to this directory"],
             "Choose a file to upload:"+SPACE,
             T.input(type="file", name="file", class_="freeform-input-file"), SPACE,
-            T.input(type="submit", value="Upload"),                          SPACE*2,
-            upload_chk,  T.label(for_="upload-chk") [" Immutable"],          SPACE,
-            upload_sdmf, T.label(for_="upload-sdmf")[" SDMF"],               SPACE,
-            upload_mdmf, T.label(for_="upload-mdmf")[" MDMF (experimental)"],
+            T.div(class_="form-inline")[
+                upload_chk,  T.label(for_="upload-chk") [SPACE, "Immutable"], SPACE*2,
+                upload_sdmf, T.label(for_="upload-sdmf")[SPACE, "SDMF"], SPACE*2,
+                upload_mdmf, T.label(for_="upload-mdmf")[SPACE, "MDMF (experimental)"]
+            ],
+            T.input(type="submit", class_="btn", value="Upload"),             SPACE*2,
             ]]
         forms.append(T.div(class_="freeform-form")[upload_form])
 
         attach_form = T.form(action=".", method="post",
                              enctype="multipart/form-data")[
-            T.fieldset[
-            T.input(type="hidden", name="t", value="uri"),
-            T.input(type="hidden", name="when_done", value="."),
-            T.legend(class_="freeform-form-label")["Add a link to a file or directory which is already in Tahoe-LAFS."],
-            "New child name:"+SPACE,
-            T.input(type="text", name="name"), SPACE*2,
-            "URI of new child:"+SPACE,
-            T.input(type="text", name="uri"), SPACE,
-            T.input(type="submit", value="Attach"),
-            ]]
+            T.fieldset[ T.div(class_="form-inline")[
+                T.input(type="hidden", name="t", value="uri"),
+                T.input(type="hidden", name="when_done", value="."),
+                T.legend(class_="freeform-form-label")["Add a link to a file or directory which is already in Tahoe-LAFS."],
+                "New child name:"+SPACE,
+                T.input(type="text", name="name"), SPACE*2, T.br,
+                "URI of new child:"+SPACE,
+                T.input(type="text", name="uri"), SPACE,
+                T.input(type="submit", class_="btn", value="Attach"),
+            ]]]
         forms.append(T.div(class_="freeform-form")[attach_form])
         return forms
 
@@ -911,7 +869,6 @@ class DirectoryAsHTML(rend.Page):
         req = IRequest(ctx)
         return get_arg(req, "results", "")
 
-
 def DirectoryJSONMetadata(ctx, dirnode):
     d = dirnode.list()
     def _got(children):
@@ -921,20 +878,7 @@ def DirectoryJSONMetadata(ctx, dirnode):
             rw_uri = childnode.get_write_uri()
             ro_uri = childnode.get_readonly_uri()
             if IFileNode.providedBy(childnode):
-                kiddata = ("filenode", {'size': childnode.get_size(),
-                                        'mutable': childnode.is_mutable(),
-                                        })
-                if childnode.is_mutable():
-                    mutable_type = childnode.get_version()
-                    assert mutable_type in (SDMF_VERSION, MDMF_VERSION)
-                    if mutable_type == MDMF_VERSION:
-                        file_format = "MDMF"
-                    else:
-                        file_format = "SDMF"
-                else:
-                    file_format = "CHK"
-                kiddata[1]['format'] = file_format
-
+                kiddata = ("filenode", get_filenode_metadata(childnode))
             elif IDirectoryNode.providedBy(childnode):
                 kiddata = ("dirnode", {'mutable': childnode.is_mutable()})
             else:
@@ -1002,33 +946,6 @@ class RenameForm(rend.Page):
         ctx.tag.attributes['value'] = name
         return ctx.tag
 
-class MoveForm(rend.Page):
-    addSlash = True
-    docFactory = getxmlfile("move-form.xhtml")
-
-    def render_title(self, ctx, data):
-        return ctx.tag["Directory SI=%s" % abbreviated_dirnode(self.original)]
-
-    def render_header(self, ctx, data):
-        header = ["Move "
-                  "from directory SI=%s" % abbreviated_dirnode(self.original),
-                  ]
-
-        if self.original.is_readonly():
-            header.append(" (readonly!)")
-        header.append(":")
-        return ctx.tag[header]
-
-    def render_when_done(self, ctx, data):
-        return T.input(type="hidden", name="when_done", value=".")
-
-    def render_get_name(self, ctx, data):
-        req = IRequest(ctx)
-        name = get_arg(req, "name", "")
-        ctx.tag.attributes['value'] = name
-        return ctx.tag
-
-
 class ManifestResults(rend.Page, ReloadMixin):
     docFactory = getxmlfile("manifest.xhtml")