]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/web/root.py
Modify markup of Tahoe web pages to be more amenable to styling; some minor changes...
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / web / root.py
1 import time
2
3 from twisted.internet import address
4 from twisted.web import http
5 from nevow import rend, url, loaders, tags as T
6 from nevow.inevow import IRequest
7 from nevow.static import File as nevow_File # TODO: merge with static.File?
8 from nevow.util import resource_filename
9 from formless import webform
10
11 import allmydata # to display import path
12 from allmydata import get_package_versions_string
13 from allmydata import provisioning
14 from allmydata.util import idlib, log
15 from allmydata.interfaces import IFileNode
16 from allmydata.web import filenode, directory, unlinked, status, operations
17 from allmydata.web import reliability, storage
18 from allmydata.web.common import abbreviate_size, getxmlfile, WebError, \
19      get_arg, RenderMixin, boolean_of_arg
20
21
22 class URIHandler(RenderMixin, rend.Page):
23     # I live at /uri . There are several operations defined on /uri itself,
24     # mostly involved with creation of unlinked files and directories.
25
26     def __init__(self, client):
27         rend.Page.__init__(self, client)
28         self.client = client
29
30     def render_GET(self, ctx):
31         req = IRequest(ctx)
32         uri = get_arg(req, "uri", None)
33         if uri is None:
34             raise WebError("GET /uri requires uri=")
35         there = url.URL.fromContext(ctx)
36         there = there.clear("uri")
37         # I thought about escaping the childcap that we attach to the URL
38         # here, but it seems that nevow does that for us.
39         there = there.child(uri)
40         return there
41
42     def render_PUT(self, ctx):
43         req = IRequest(ctx)
44         # either "PUT /uri" to create an unlinked file, or
45         # "PUT /uri?t=mkdir" to create an unlinked directory
46         t = get_arg(req, "t", "").strip()
47         if t == "":
48             mutable = boolean_of_arg(get_arg(req, "mutable", "false").strip())
49             if mutable:
50                 return unlinked.PUTUnlinkedSSK(req, self.client)
51             else:
52                 return unlinked.PUTUnlinkedCHK(req, self.client)
53         if t == "mkdir":
54             return unlinked.PUTUnlinkedCreateDirectory(req, self.client)
55         errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
56                   "and POST?t=mkdir")
57         raise WebError(errmsg, http.BAD_REQUEST)
58
59     def render_POST(self, ctx):
60         # "POST /uri?t=upload&file=newfile" to upload an
61         # unlinked file or "POST /uri?t=mkdir" to create a
62         # new directory
63         req = IRequest(ctx)
64         t = get_arg(req, "t", "").strip()
65         if t in ("", "upload"):
66             mutable = bool(get_arg(req, "mutable", "").strip())
67             if mutable:
68                 return unlinked.POSTUnlinkedSSK(req, self.client)
69             else:
70                 return unlinked.POSTUnlinkedCHK(req, self.client)
71         if t == "mkdir":
72             return unlinked.POSTUnlinkedCreateDirectory(req, self.client)
73         errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
74                   "and POST?t=mkdir")
75         raise WebError(errmsg, http.BAD_REQUEST)
76
77     def childFactory(self, ctx, name):
78         # 'name' is expected to be a URI
79         try:
80             node = self.client.create_node_from_uri(name)
81             return directory.make_handler_for(node, self.client)
82         except (TypeError, AssertionError):
83             raise WebError("'%s' is not a valid file- or directory- cap"
84                            % name)
85
86 class FileHandler(rend.Page):
87     # I handle /file/$FILECAP[/IGNORED] , which provides a URL from which a
88     # file can be downloaded correctly by tools like "wget".
89
90     def __init__(self, client):
91         rend.Page.__init__(self, client)
92         self.client = client
93
94     def childFactory(self, ctx, name):
95         req = IRequest(ctx)
96         if req.method not in ("GET", "HEAD"):
97             raise WebError("/file can only be used with GET or HEAD")
98         # 'name' must be a file URI
99         try:
100             node = self.client.create_node_from_uri(name)
101         except (TypeError, AssertionError):
102             raise WebError("'%s' is not a valid file- or directory- cap"
103                            % name)
104         if not IFileNode.providedBy(node):
105             raise WebError("'%s' is not a file-cap" % name)
106         return filenode.FileNodeDownloadHandler(self.client, node)
107
108     def renderHTTP(self, ctx):
109         raise WebError("/file must be followed by a file-cap and a name",
110                        http.NOT_FOUND)
111
112 class IncidentReporter(RenderMixin, rend.Page):
113     def render_POST(self, ctx):
114         req = IRequest(ctx)
115         log.msg(format="User reports incident through web page: %(details)s",
116                 details=get_arg(req, "details", ""),
117                 level=log.WEIRD, umid="LkD9Pw")
118         req.setHeader("content-type", "text/plain")
119         return "Thank you for your report!"
120
121 class NoReliability(rend.Page):
122     docFactory = loaders.xmlstr('''\
123 <html xmlns:n="http://nevow.com/ns/nevow/0.1">
124   <head>
125     <title>AllMyData - Tahoe</title>
126     <link href="/webform_css" rel="stylesheet" type="text/css"/>
127     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
128   </head>
129   <body>
130   <h2>"Reliability" page not available</h2>
131   <p>Please install the python "NumPy" module to enable this page.</p>
132   </body>
133 </html>
134 ''')
135
136 class Root(rend.Page):
137
138     addSlash = True
139     docFactory = getxmlfile("welcome.xhtml")
140
141     def __init__(self, client):
142         rend.Page.__init__(self, client)
143         self.client = client
144         self.child_operations = operations.OphandleTable()
145         try:
146             s = client.getServiceNamed("storage")
147         except KeyError:
148             s = None
149         self.child_storage = storage.StorageStatus(s)
150
151         self.child_uri = URIHandler(client)
152         self.child_cap = URIHandler(client)
153
154         self.child_file = FileHandler(client)
155         self.child_named = FileHandler(client)
156         self.child_status = status.Status(client) # TODO: use client.history
157         self.child_statistics = status.Statistics(client.stats_provider)
158
159     def child_helper_status(self, ctx):
160         # the Helper isn't attached until after the Tub starts, so this child
161         # needs to created on each request
162         try:
163             helper = self.client.getServiceNamed("helper")
164         except KeyError:
165             helper = None
166         return status.HelperStatus(helper)
167
168     child_webform_css = webform.defaultCSS
169     child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
170
171     child_provisioning = provisioning.ProvisioningTool()
172     if reliability.is_available():
173         child_reliability = reliability.ReliabilityTool()
174     else:
175         child_reliability = NoReliability()
176
177     child_report_incident = IncidentReporter()
178     #child_server # let's reserve this for storage-server-over-HTTP
179
180     # FIXME: This code is duplicated in root.py and introweb.py.
181     def data_version(self, ctx, data):
182         return get_package_versions_string()
183     def data_import_path(self, ctx, data):
184         return str(allmydata)
185     def data_my_nodeid(self, ctx, data):
186         return idlib.nodeid_b2a(self.client.nodeid)
187     def data_my_nickname(self, ctx, data):
188         return self.client.nickname
189
190     def render_services(self, ctx, data):
191         ul = T.ul()
192         try:
193             ss = self.client.getServiceNamed("storage")
194             stats = ss.get_stats()
195             if stats["storage_server.accepting_immutable_shares"]:
196                 msg = "accepting new shares"
197             else:
198                 msg = "not accepting new shares (read-only)"
199             available = stats.get("storage_server.disk_avail")
200             if available is not None:
201                 msg += ", %s available" % abbreviate_size(available)
202             ul[T.li[T.a(href="storage")["Storage Server"], ": ", msg]]
203         except KeyError:
204             ul[T.li["Not running storage server"]]
205
206         try:
207             h = self.client.getServiceNamed("helper")
208             stats = h.get_stats()
209             active_uploads = stats["chk_upload_helper.active_uploads"]
210             ul[T.li["Helper: %d active uploads" % (active_uploads,)]]
211         except KeyError:
212             ul[T.li["Not running helper"]]
213
214         return ctx.tag[ul]
215
216     def data_introducer_furl(self, ctx, data):
217         return self.client.introducer_furl
218     def data_connected_to_introducer(self, ctx, data):
219         if self.client.connected_to_introducer():
220             return "yes"
221         return "no"
222
223     def data_helper_furl(self, ctx, data):
224         try:
225             uploader = self.client.getServiceNamed("uploader")
226         except KeyError:
227             return None
228         furl, connected = uploader.get_helper_info()
229         return furl
230     def data_connected_to_helper(self, ctx, data):
231         try:
232             uploader = self.client.getServiceNamed("uploader")
233         except KeyError:
234             return "no" # we don't even have an Uploader
235         furl, connected = uploader.get_helper_info()
236         if connected:
237             return "yes"
238         return "no"
239
240     def data_known_storage_servers(self, ctx, data):
241         ic = self.client.introducer_client
242         servers = [c
243                    for c in ic.get_all_connectors().values()
244                    if c.service_name == "storage"]
245         return len(servers)
246
247     def data_connected_storage_servers(self, ctx, data):
248         ic = self.client.introducer_client
249         return len(ic.get_all_connections_for("storage"))
250
251     def data_services(self, ctx, data):
252         ic = self.client.introducer_client
253         c = [ (service_name, nodeid, rsc)
254               for (nodeid, service_name), rsc
255               in ic.get_all_connectors().items() ]
256         c.sort()
257         return c
258
259     def render_service_row(self, ctx, data):
260         (service_name, nodeid, rsc) = data
261         ctx.fillSlots("peerid", idlib.nodeid_b2a(nodeid))
262         ctx.fillSlots("nickname", rsc.nickname)
263         if rsc.rref:
264             rhost = rsc.remote_host
265             if nodeid == self.client.nodeid:
266                 rhost_s = "(loopback)"
267             elif isinstance(rhost, address.IPv4Address):
268                 rhost_s = "%s:%d" % (rhost.host, rhost.port)
269             else:
270                 rhost_s = str(rhost)
271             connected = "Yes: to " + rhost_s
272             since = rsc.last_connect_time
273         else:
274             connected = "No"
275             since = rsc.last_loss_time
276
277         TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
278         ctx.fillSlots("connected", connected)
279         ctx.fillSlots("connected-bool", not not rsc.rref)
280         ctx.fillSlots("since", time.strftime(TIME_FORMAT, time.localtime(since)))
281         ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
282                                                  time.localtime(rsc.announcement_time)))
283         ctx.fillSlots("version", rsc.version)
284         ctx.fillSlots("service_name", rsc.service_name)
285
286         return ctx.tag
287
288     def render_download_form(self, ctx, data):
289         # this is a form where users can download files by URI
290         form = T.form(action="uri", method="get",
291                       enctype="multipart/form-data")[
292             T.fieldset[
293             T.legend(class_="freeform-form-label")["Download a file"],
294             T.div["Tahoe-URI to download: ",
295                   T.input(type="text", name="uri")],
296             T.div["Filename to download as: ",
297                   T.input(type="text", name="filename")],
298             T.input(type="submit", value="Download!"),
299             ]]
300         return T.div[form]
301
302     def render_view_form(self, ctx, data):
303         # this is a form where users can download files by URI, or jump to a
304         # named directory
305         form = T.form(action="uri", method="get",
306                       enctype="multipart/form-data")[
307             T.fieldset[
308             T.legend(class_="freeform-form-label")["View a file or directory"],
309             "Tahoe-URI to view: ",
310             T.input(type="text", name="uri"), " ",
311             T.input(type="submit", value="View!"),
312             ]]
313         return T.div[form]
314
315     def render_upload_form(self, ctx, data):
316         # this is a form where users can upload unlinked files
317         form = T.form(action="uri", method="post",
318                       enctype="multipart/form-data")[
319             T.fieldset[
320             T.legend(class_="freeform-form-label")["Upload a file"],
321             T.div["Choose a file: ",
322                   T.input(type="file", name="file", class_="freeform-input-file")],
323             T.input(type="hidden", name="t", value="upload"),
324             T.div[T.input(type="checkbox", name="mutable"), T.label(for_="mutable")["Create mutable file"],
325                   " ", T.input(type="submit", value="Upload!")],
326             ]]
327         return T.div[form]
328
329     def render_mkdir_form(self, ctx, data):
330         # this is a form where users can create new directories
331         form = T.form(action="uri", method="post",
332                       enctype="multipart/form-data")[
333             T.fieldset[
334             T.legend(class_="freeform-form-label")["Create a directory"],
335             T.input(type="hidden", name="t", value="mkdir"),
336             T.input(type="hidden", name="redirect_to_result", value="true"),
337             T.input(type="submit", value="Create a directory"),
338             ]]
339         return T.div[form]
340
341     def render_incident_button(self, ctx, data):
342         # this button triggers a foolscap-logging "incident"
343         form = T.form(action="report_incident", method="post",
344                       enctype="multipart/form-data")[
345             T.fieldset[
346             T.legend(class_="freeform-form-label")["Report an Incident"],
347             T.input(type="hidden", name="t", value="report-incident"),
348             "What went wrong?: ",
349             T.input(type="text", name="details"), " ",
350             T.input(type="submit", value="Report!"),
351             ]]
352         return T.div[form]