]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blobdiff - src/allmydata/web/root.py
wui: use standard time format (#1077)
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / web / root.py
index f429e05f5befec824104c0f07dc4af443b42a042..c46a6dd5f8bc5fb9c49f8199cc74d1a742bb47a8 100644 (file)
@@ -1,28 +1,30 @@
-import time
+import time, os
 
 from twisted.internet import address
 from twisted.web import http
-from nevow import rend, url, loaders, tags as T
+from nevow import rend, url, tags as T
 from nevow.inevow import IRequest
 from nevow.static import File as nevow_File # TODO: merge with static.File?
 from nevow.util import resource_filename
-from formless import webform
 
 import allmydata # to display import path
 from allmydata import get_package_versions_string
-from allmydata import provisioning
-from allmydata.util import idlib, log
+from allmydata.util import log
 from allmydata.interfaces import IFileNode
 from allmydata.web import filenode, directory, unlinked, status, operations
-from allmydata.web import reliability
-from allmydata.web.common import abbreviate_size, IClient, \
-     getxmlfile, WebError, get_arg, RenderMixin
+from allmydata.web import storage
+from allmydata.web.common import abbreviate_size, getxmlfile, WebError, \
+     get_arg, RenderMixin, get_format, get_mutable_type, render_time
 
 
 class URIHandler(RenderMixin, rend.Page):
     # I live at /uri . There are several operations defined on /uri itself,
     # mostly involved with creation of unlinked files and directories.
 
+    def __init__(self, client):
+        rend.Page.__init__(self, client)
+        self.client = client
+
     def render_GET(self, ctx):
         req = IRequest(ctx)
         uri = get_arg(req, "uri", None)
@@ -41,13 +43,14 @@ class URIHandler(RenderMixin, rend.Page):
         # "PUT /uri?t=mkdir" to create an unlinked directory
         t = get_arg(req, "t", "").strip()
         if t == "":
-            mutable = bool(get_arg(req, "mutable", "").strip())
-            if mutable:
-                return unlinked.PUTUnlinkedSSK(ctx)
+            file_format = get_format(req, "CHK")
+            mutable_type = get_mutable_type(file_format)
+            if mutable_type is not None:
+                return unlinked.PUTUnlinkedSSK(req, self.client, mutable_type)
             else:
-                return unlinked.PUTUnlinkedCHK(ctx)
+                return unlinked.PUTUnlinkedCHK(req, self.client)
         if t == "mkdir":
-            return unlinked.PUTUnlinkedCreateDirectory(ctx)
+            return unlinked.PUTUnlinkedCreateDirectory(req, self.client)
         errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
                   "and POST?t=mkdir")
         raise WebError(errmsg, http.BAD_REQUEST)
@@ -59,23 +62,29 @@ class URIHandler(RenderMixin, rend.Page):
         req = IRequest(ctx)
         t = get_arg(req, "t", "").strip()
         if t in ("", "upload"):
-            mutable = bool(get_arg(req, "mutable", "").strip())
-            if mutable:
-                return unlinked.POSTUnlinkedSSK(ctx)
+            file_format = get_format(req)
+            mutable_type = get_mutable_type(file_format)
+            if mutable_type is not None:
+                return unlinked.POSTUnlinkedSSK(req, self.client, mutable_type)
             else:
-                return unlinked.POSTUnlinkedCHK(ctx)
+                return unlinked.POSTUnlinkedCHK(req, self.client)
         if t == "mkdir":
-            return unlinked.POSTUnlinkedCreateDirectory(ctx)
+            return unlinked.POSTUnlinkedCreateDirectory(req, self.client)
+        elif t == "mkdir-with-children":
+            return unlinked.POSTUnlinkedCreateDirectoryWithChildren(req,
+                                                                    self.client)
+        elif t == "mkdir-immutable":
+            return unlinked.POSTUnlinkedCreateImmutableDirectory(req,
+                                                                 self.client)
         errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
                   "and POST?t=mkdir")
         raise WebError(errmsg, http.BAD_REQUEST)
 
     def childFactory(self, ctx, name):
         # 'name' is expected to be a URI
-        client = IClient(ctx)
         try:
-            node = client.create_node_from_uri(name)
-            return directory.make_handler_for(node)
+            node = self.client.create_node_from_uri(name)
+            return directory.make_handler_for(node, self.client)
         except (TypeError, AssertionError):
             raise WebError("'%s' is not a valid file- or directory- cap"
                            % name)
@@ -84,20 +93,24 @@ class FileHandler(rend.Page):
     # I handle /file/$FILECAP[/IGNORED] , which provides a URL from which a
     # file can be downloaded correctly by tools like "wget".
 
+    def __init__(self, client):
+        rend.Page.__init__(self, client)
+        self.client = client
+
     def childFactory(self, ctx, name):
         req = IRequest(ctx)
         if req.method not in ("GET", "HEAD"):
             raise WebError("/file can only be used with GET or HEAD")
         # 'name' must be a file URI
-        client = IClient(ctx)
         try:
-            node = client.create_node_from_uri(name)
+            node = self.client.create_node_from_uri(name)
         except (TypeError, AssertionError):
+            # I think this can no longer be reached
             raise WebError("'%s' is not a valid file- or directory- cap"
                            % name)
         if not IFileNode.providedBy(node):
             raise WebError("'%s' is not a file-cap" % name)
-        return filenode.FileNodeDownloadHandler(node)
+        return filenode.FileNodeDownloadHandler(self.client, node)
 
     def renderHTTP(self, ctx):
         raise WebError("/file must be followed by a file-cap and a name",
@@ -110,150 +123,197 @@ class IncidentReporter(RenderMixin, rend.Page):
                 details=get_arg(req, "details", ""),
                 level=log.WEIRD, umid="LkD9Pw")
         req.setHeader("content-type", "text/plain")
-        return "Thank you for your report!"
-
-class NoReliability(rend.Page):
-    docFactory = loaders.xmlstr('''\
-<html xmlns:n="http://nevow.com/ns/nevow/0.1">
-  <head>
-    <title>AllMyData - Tahoe</title>
-    <link href="/webform_css" rel="stylesheet" type="text/css"/>
-    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
-  </head>
-  <body>
-  <h2>"Reliability" page not available</h2>
-  <p>Please install the python "NumPy" module to enable this page.</p>
-  </body>
-</html>
-''')
+        return "An incident report has been saved to logs/incidents/ in the node directory."
+
+SPACE = u"\u00A0"*2
 
 class Root(rend.Page):
 
     addSlash = True
     docFactory = getxmlfile("welcome.xhtml")
 
-    def __init__(self, original=None):
-        rend.Page.__init__(self, original)
-        self.child_operations = operations.OphandleTable()
+    _connectedalts = {
+        "not-configured": "Not Configured",
+        "yes": "Connected",
+        "no": "Disconnected",
+        }
+
+    def __init__(self, client, clock=None):
+        rend.Page.__init__(self, client)
+        self.client = client
+        # If set, clock is a twisted.internet.task.Clock that the tests
+        # use to test ophandle expiration.
+        self.child_operations = operations.OphandleTable(clock)
+        try:
+            s = client.getServiceNamed("storage")
+        except KeyError:
+            s = None
+        self.child_storage = storage.StorageStatus(s, self.client.nickname)
 
-    child_uri = URIHandler()
-    child_cap = URIHandler()
-    child_file = FileHandler()
-    child_named = FileHandler()
+        self.child_uri = URIHandler(client)
+        self.child_cap = URIHandler(client)
 
-    child_webform_css = webform.defaultCSS
-    child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
+        self.child_file = FileHandler(client)
+        self.child_named = FileHandler(client)
+        self.child_status = status.Status(client.get_history())
+        self.child_statistics = status.Statistics(client.stats_provider)
+        static_dir = resource_filename("allmydata.web", "static")
+        for filen in os.listdir(static_dir):
+            self.putChild(filen, nevow_File(os.path.join(static_dir, filen)))
 
-    child_provisioning = provisioning.ProvisioningTool()
-    if reliability.is_available():
-        child_reliability = reliability.ReliabilityTool()
-    else:
-        child_reliability = NoReliability()
-    child_status = status.Status()
-    child_helper_status = status.HelperStatus()
-    child_statistics = status.Statistics()
+    def child_helper_status(self, ctx):
+        # the Helper isn't attached until after the Tub starts, so this child
+        # needs to created on each request
+        return status.HelperStatus(self.client.helper)
 
     child_report_incident = IncidentReporter()
+    #child_server # let's reserve this for storage-server-over-HTTP
 
+    # FIXME: This code is duplicated in root.py and introweb.py.
+    def data_rendered_at(self, ctx, data):
+        return render_time(time.time())
     def data_version(self, ctx, data):
         return get_package_versions_string()
     def data_import_path(self, ctx, data):
         return str(allmydata)
-    def data_my_nodeid(self, ctx, data):
-        return idlib.nodeid_b2a(IClient(ctx).nodeid)
+    def render_my_nodeid(self, ctx, data):
+        tubid_s = "TubID: "+self.client.get_long_tubid()
+        return T.td(title=tubid_s)[self.client.get_long_nodeid()]
     def data_my_nickname(self, ctx, data):
-        return IClient(ctx).nickname
+        return self.client.nickname
 
     def render_services(self, ctx, data):
         ul = T.ul()
-        client = IClient(ctx)
         try:
-            ss = client.getServiceNamed("storage")
-            allocated_s = abbreviate_size(ss.allocated_size())
-            allocated = "about %s allocated" % allocated_s
-            reserved = "%s reserved" % abbreviate_size(ss.reserved_space)
-            ul[T.li["Storage Server: %s, %s" % (allocated, reserved)]]
+            ss = self.client.getServiceNamed("storage")
+            stats = ss.get_stats()
+            if stats["storage_server.accepting_immutable_shares"]:
+                msg = "accepting new shares"
+            else:
+                msg = "not accepting new shares (read-only)"
+            available = stats.get("storage_server.disk_avail")
+            if available is not None:
+                msg += ", %s available" % abbreviate_size(available)
+            ul[T.li[T.a(href="storage")["Storage Server"], ": ", msg]]
         except KeyError:
             ul[T.li["Not running storage server"]]
 
-        try:
-            h = client.getServiceNamed("helper")
-            stats = h.get_stats()
+        if self.client.helper:
+            stats = self.client.helper.get_stats()
             active_uploads = stats["chk_upload_helper.active_uploads"]
             ul[T.li["Helper: %d active uploads" % (active_uploads,)]]
-        except KeyError:
+        else:
             ul[T.li["Not running helper"]]
 
         return ctx.tag[ul]
 
-    def data_introducer_furl(self, ctx, data):
-        return IClient(ctx).introducer_furl
+    def data_introducer_furl_prefix(self, ctx, data):
+        ifurl = self.client.introducer_furl
+        # trim off the secret swissnum
+        (prefix, _, swissnum) = ifurl.rpartition("/")
+        if not ifurl:
+            return None
+        if swissnum == "introducer":
+            return ifurl
+        else:
+            return "%s/[censored]" % (prefix,)
+
+    def data_introducer_description(self, ctx, data):
+        if self.data_connected_to_introducer(ctx, data) == "no":
+            return "Introducer not connected"
+        return "Introducer"
+
     def data_connected_to_introducer(self, ctx, data):
-        if IClient(ctx).connected_to_introducer():
+        if self.client.connected_to_introducer():
             return "yes"
         return "no"
 
-    def data_helper_furl(self, ctx, data):
+    def data_connected_to_introducer_alt(self, ctx, data):
+        return self._connectedalts[self.data_connected_to_introducer(ctx, data)]
+
+    def data_helper_furl_prefix(self, ctx, data):
         try:
-            uploader = IClient(ctx).getServiceNamed("uploader")
+            uploader = self.client.getServiceNamed("uploader")
         except KeyError:
             return None
         furl, connected = uploader.get_helper_info()
-        return furl
+        if not furl:
+            return None
+        # trim off the secret swissnum
+        (prefix, _, swissnum) = furl.rpartition("/")
+        return "%s/[censored]" % (prefix,)
+
+    def data_helper_description(self, ctx, data):
+        if self.data_connected_to_helper(ctx, data) == "no":
+            return "Helper not connected"
+        return "Helper"
+
     def data_connected_to_helper(self, ctx, data):
         try:
-            uploader = IClient(ctx).getServiceNamed("uploader")
+            uploader = self.client.getServiceNamed("uploader")
         except KeyError:
             return "no" # we don't even have an Uploader
         furl, connected = uploader.get_helper_info()
+
+        if furl is None:
+            return "not-configured"
         if connected:
             return "yes"
         return "no"
 
+    def data_connected_to_helper_alt(self, ctx, data):
+        return self._connectedalts[self.data_connected_to_helper(ctx, data)]
+
     def data_known_storage_servers(self, ctx, data):
-        ic = IClient(ctx).introducer_client
-        servers = [c
-                   for c in ic.get_all_connectors().values()
-                   if c.service_name == "storage"]
-        return len(servers)
+        sb = self.client.get_storage_broker()
+        return len(sb.get_all_serverids())
 
     def data_connected_storage_servers(self, ctx, data):
-        ic = IClient(ctx).introducer_client
-        return len(ic.get_all_connections_for("storage"))
+        sb = self.client.get_storage_broker()
+        return len(sb.get_connected_servers())
 
     def data_services(self, ctx, data):
-        ic = IClient(ctx).introducer_client
-        c = [ (service_name, nodeid, rsc)
-              for (nodeid, service_name), rsc
-              in ic.get_all_connectors().items() ]
-        c.sort()
-        return c
-
-    def render_service_row(self, ctx, data):
-        (service_name, nodeid, rsc) = data
-        ctx.fillSlots("peerid", idlib.nodeid_b2a(nodeid))
-        ctx.fillSlots("nickname", rsc.nickname)
-        if rsc.rref:
-            rhost = rsc.remote_host
-            if nodeid == IClient(ctx).nodeid:
+        sb = self.client.get_storage_broker()
+        return sorted(sb.get_known_servers(), key=lambda s: s.get_serverid())
+
+    def render_service_row(self, ctx, server):
+        nodeid = server.get_serverid()
+
+        ctx.fillSlots("peerid", server.get_longname())
+        ctx.fillSlots("nickname", server.get_nickname())
+        rhost = server.get_remote_host()
+        if rhost:
+            if nodeid == self.client.nodeid:
                 rhost_s = "(loopback)"
             elif isinstance(rhost, address.IPv4Address):
                 rhost_s = "%s:%d" % (rhost.host, rhost.port)
             else:
                 rhost_s = str(rhost)
-            connected = "Yes: to " + rhost_s
-            since = rsc.last_connect_time
+            addr = rhost_s
+            connected = "yes"
+            since = server.get_last_connect_time()
         else:
-            connected = "No"
-            since = rsc.last_loss_time
-
-        TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
+            addr = "N/A"
+            connected = "no"
+            since = server.get_last_loss_time()
+        announced = server.get_announcement_time()
+        announcement = server.get_announcement()
+        version = announcement["my-version"]
+        service_name = announcement["service-name"]
+        available_space = server.get_available_space()
+        if available_space is None:
+            available_space = "N/A"
+        else:
+            available_space = abbreviate_size(available_space)
+        ctx.fillSlots("address", addr)
         ctx.fillSlots("connected", connected)
-        ctx.fillSlots("since", time.strftime(TIME_FORMAT, time.localtime(since)))
-        ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
-                                                 time.localtime(rsc.announcement_time)))
-        ctx.fillSlots("version", rsc.version)
-        ctx.fillSlots("service_name", rsc.service_name)
+        ctx.fillSlots("connected_alt", self._connectedalts[connected])
+        ctx.fillSlots("connected-bool", bool(rhost))
+        ctx.fillSlots("since", render_time(since))
+        ctx.fillSlots("announced", render_time(announced))
+        ctx.fillSlots("version", version)
+        ctx.fillSlots("service_name", service_name)
+        ctx.fillSlots("available_space", available_space)
 
         return ctx.tag
 
@@ -263,10 +323,10 @@ class Root(rend.Page):
                       enctype="multipart/form-data")[
             T.fieldset[
             T.legend(class_="freeform-form-label")["Download a file"],
-            "URI to download: ",
-            T.input(type="text", name="uri"), " ",
-            "Filename to download as: ",
-            T.input(type="text", name="filename"), " ",
+            T.div["Tahoe-URI to download:"+SPACE,
+                  T.input(type="text", name="uri")],
+            T.div["Filename to download as:"+SPACE,
+                  T.input(type="text", name="filename")],
             T.input(type="submit", value="Download!"),
             ]]
         return T.div[form]
@@ -278,35 +338,57 @@ class Root(rend.Page):
                       enctype="multipart/form-data")[
             T.fieldset[
             T.legend(class_="freeform-form-label")["View a file or directory"],
-            "URI to view: ",
-            T.input(type="text", name="uri"), " ",
+            "Tahoe-URI to view:"+SPACE,
+            T.input(type="text", name="uri"), SPACE*2,
             T.input(type="submit", value="View!"),
             ]]
         return T.div[form]
 
     def render_upload_form(self, ctx, data):
-        # this is a form where users can upload unlinked files
+        # This is a form where users can upload unlinked files.
+        # Users can choose immutable, SDMF, or MDMF from a radio button.
+
+        upload_chk  = T.input(type='radio', name='format',
+                              value='chk', id='upload-chk',
+                              checked='checked')
+        upload_sdmf = T.input(type='radio', name='format',
+                              value='sdmf', id='upload-sdmf')
+        upload_mdmf = T.input(type='radio', name='format',
+                              value='mdmf', id='upload-mdmf')
+
         form = T.form(action="uri", method="post",
                       enctype="multipart/form-data")[
             T.fieldset[
             T.legend(class_="freeform-form-label")["Upload a file"],
-            "Choose a file: ",
-            T.input(type="file", name="file", class_="freeform-input-file"),
+            T.div["Choose a file:"+SPACE,
+                  T.input(type="file", name="file", class_="freeform-input-file")],
             T.input(type="hidden", name="t", value="upload"),
-            " Mutable?:", T.input(type="checkbox", name="mutable"),
-            T.input(type="submit", value="Upload!"),
+            T.div[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)"], SPACE*2,
+                  T.input(type="submit", value="Upload!")],
             ]]
         return T.div[form]
 
     def render_mkdir_form(self, ctx, data):
-        # this is a form where users can create new directories
+        # This is a form where users can create new directories.
+        # Users can choose SDMF or MDMF from a radio button.
+
+        mkdir_sdmf = T.input(type='radio', name='format',
+                             value='sdmf', id='mkdir-sdmf',
+                             checked='checked')
+        mkdir_mdmf = T.input(type='radio', name='format',
+                             value='mdmf', id='mkdir-mdmf')
+
         form = T.form(action="uri", method="post",
                       enctype="multipart/form-data")[
             T.fieldset[
             T.legend(class_="freeform-form-label")["Create a directory"],
+            mkdir_sdmf, T.label(for_='mkdir-sdmf')[" SDMF"],                SPACE,
+            mkdir_mdmf, T.label(for_='mkdir-mdmf')[" MDMF (experimental)"], SPACE*2,
             T.input(type="hidden", name="t", value="mkdir"),
             T.input(type="hidden", name="redirect_to_result", value="true"),
-            T.input(type="submit", value="Create Directory!"),
+            T.input(type="submit", value="Create a directory"),
             ]]
         return T.div[form]
 
@@ -315,10 +397,9 @@ class Root(rend.Page):
         form = T.form(action="report_incident", method="post",
                       enctype="multipart/form-data")[
             T.fieldset[
-            T.legend(class_="freeform-form-label")["Report an Incident"],
             T.input(type="hidden", name="t", value="report-incident"),
-            "What went wrong?: ",
-            T.input(type="text", name="details"), " ",
-            T.input(type="submit", value="Report!"),
+            "What went wrong?"+SPACE,
+            T.input(type="text", name="details"), SPACE,
+            T.input(type="submit", value=u"Save \u00BB"),
             ]]
         return T.div[form]