]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/web/root.py
Rewrite download-status-timeline visualizer ('viz') with d3.js
[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, get_format, get_mutable_type
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             file_format = get_format(req, "CHK")
49             mutable_type = get_mutable_type(file_format)
50             if mutable_type is not None:
51                 return unlinked.PUTUnlinkedSSK(req, self.client, mutable_type)
52             else:
53                 return unlinked.PUTUnlinkedCHK(req, self.client)
54         if t == "mkdir":
55             return unlinked.PUTUnlinkedCreateDirectory(req, self.client)
56         errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
57                   "and POST?t=mkdir")
58         raise WebError(errmsg, http.BAD_REQUEST)
59
60     def render_POST(self, ctx):
61         # "POST /uri?t=upload&file=newfile" to upload an
62         # unlinked file or "POST /uri?t=mkdir" to create a
63         # new directory
64         req = IRequest(ctx)
65         t = get_arg(req, "t", "").strip()
66         if t in ("", "upload"):
67             file_format = get_format(req)
68             mutable_type = get_mutable_type(file_format)
69             if mutable_type is not None:
70                 return unlinked.POSTUnlinkedSSK(req, self.client, mutable_type)
71             else:
72                 return unlinked.POSTUnlinkedCHK(req, self.client)
73         if t == "mkdir":
74             return unlinked.POSTUnlinkedCreateDirectory(req, self.client)
75         elif t == "mkdir-with-children":
76             return unlinked.POSTUnlinkedCreateDirectoryWithChildren(req,
77                                                                     self.client)
78         elif t == "mkdir-immutable":
79             return unlinked.POSTUnlinkedCreateImmutableDirectory(req,
80                                                                  self.client)
81         errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
82                   "and POST?t=mkdir")
83         raise WebError(errmsg, http.BAD_REQUEST)
84
85     def childFactory(self, ctx, name):
86         # 'name' is expected to be a URI
87         try:
88             node = self.client.create_node_from_uri(name)
89             return directory.make_handler_for(node, self.client)
90         except (TypeError, AssertionError):
91             raise WebError("'%s' is not a valid file- or directory- cap"
92                            % name)
93
94 class FileHandler(rend.Page):
95     # I handle /file/$FILECAP[/IGNORED] , which provides a URL from which a
96     # file can be downloaded correctly by tools like "wget".
97
98     def __init__(self, client):
99         rend.Page.__init__(self, client)
100         self.client = client
101
102     def childFactory(self, ctx, name):
103         req = IRequest(ctx)
104         if req.method not in ("GET", "HEAD"):
105             raise WebError("/file can only be used with GET or HEAD")
106         # 'name' must be a file URI
107         try:
108             node = self.client.create_node_from_uri(name)
109         except (TypeError, AssertionError):
110             # I think this can no longer be reached
111             raise WebError("'%s' is not a valid file- or directory- cap"
112                            % name)
113         if not IFileNode.providedBy(node):
114             raise WebError("'%s' is not a file-cap" % name)
115         return filenode.FileNodeDownloadHandler(self.client, node)
116
117     def renderHTTP(self, ctx):
118         raise WebError("/file must be followed by a file-cap and a name",
119                        http.NOT_FOUND)
120
121 class IncidentReporter(RenderMixin, rend.Page):
122     def render_POST(self, ctx):
123         req = IRequest(ctx)
124         log.msg(format="User reports incident through web page: %(details)s",
125                 details=get_arg(req, "details", ""),
126                 level=log.WEIRD, umid="LkD9Pw")
127         req.setHeader("content-type", "text/plain")
128         return "Thank you for your report!"
129
130 class NoReliability(rend.Page):
131     docFactory = loaders.xmlstr('''\
132 <html xmlns:n="http://nevow.com/ns/nevow/0.1">
133   <head>
134     <title>AllMyData - Tahoe</title>
135     <link href="/webform_css" rel="stylesheet" type="text/css"/>
136     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
137   </head>
138   <body>
139   <h2>"Reliability" page not available</h2>
140   <p>Please install the python "NumPy" module to enable this page.</p>
141   </body>
142 </html>
143 ''')
144
145 SPACE = u"\u00A0"*2
146
147 class Root(rend.Page):
148
149     addSlash = True
150     docFactory = getxmlfile("welcome.xhtml")
151
152     def __init__(self, client, clock=None):
153         rend.Page.__init__(self, client)
154         self.client = client
155         # If set, clock is a twisted.internet.task.Clock that the tests
156         # use to test ophandle expiration.
157         self.child_operations = operations.OphandleTable(clock)
158         try:
159             s = client.getServiceNamed("storage")
160         except KeyError:
161             s = None
162         self.child_storage = storage.StorageStatus(s)
163
164         self.child_uri = URIHandler(client)
165         self.child_cap = URIHandler(client)
166
167         self.child_file = FileHandler(client)
168         self.child_named = FileHandler(client)
169         self.child_status = status.Status(client.get_history())
170         self.child_statistics = status.Statistics(client.stats_provider)
171         def f(name):
172             return nevow_File(resource_filename('allmydata.web', name))
173         self.putChild("download_status_timeline.js", f("download_status_timeline.js"))
174         self.putChild("jquery-1.6.1.min.js", f("jquery-1.6.1.min.js"))
175         self.putChild("d3-2.4.6.min.js", f("d3-2.4.6.min.js"))
176         self.putChild("d3-2.4.6.time.min.js", f("d3-2.4.6.time.min.js"))
177
178     def child_helper_status(self, ctx):
179         # the Helper isn't attached until after the Tub starts, so this child
180         # needs to created on each request
181         return status.HelperStatus(self.client.helper)
182
183     child_webform_css = webform.defaultCSS
184     child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
185
186     child_provisioning = provisioning.ProvisioningTool()
187     if reliability.is_available():
188         child_reliability = reliability.ReliabilityTool()
189     else:
190         child_reliability = NoReliability()
191
192     child_report_incident = IncidentReporter()
193     #child_server # let's reserve this for storage-server-over-HTTP
194
195     # FIXME: This code is duplicated in root.py and introweb.py.
196     def data_version(self, ctx, data):
197         return get_package_versions_string()
198     def data_import_path(self, ctx, data):
199         return str(allmydata)
200     def data_my_nodeid(self, ctx, data):
201         return idlib.nodeid_b2a(self.client.nodeid)
202     def data_my_nickname(self, ctx, data):
203         return self.client.nickname
204
205     def render_services(self, ctx, data):
206         ul = T.ul()
207         try:
208             ss = self.client.getServiceNamed("storage")
209             stats = ss.get_stats()
210             if stats["storage_server.accepting_immutable_shares"]:
211                 msg = "accepting new shares"
212             else:
213                 msg = "not accepting new shares (read-only)"
214             available = stats.get("storage_server.disk_avail")
215             if available is not None:
216                 msg += ", %s available" % abbreviate_size(available)
217             ul[T.li[T.a(href="storage")["Storage Server"], ": ", msg]]
218         except KeyError:
219             ul[T.li["Not running storage server"]]
220
221         if self.client.helper:
222             stats = self.client.helper.get_stats()
223             active_uploads = stats["chk_upload_helper.active_uploads"]
224             ul[T.li["Helper: %d active uploads" % (active_uploads,)]]
225         else:
226             ul[T.li["Not running helper"]]
227
228         return ctx.tag[ul]
229
230     def data_introducer_furl(self, ctx, data):
231         return self.client.introducer_furl
232     def data_connected_to_introducer(self, ctx, data):
233         if self.client.connected_to_introducer():
234             return "yes"
235         return "no"
236
237     def data_helper_furl(self, ctx, data):
238         try:
239             uploader = self.client.getServiceNamed("uploader")
240         except KeyError:
241             return None
242         furl, connected = uploader.get_helper_info()
243         return furl
244     def data_connected_to_helper(self, ctx, data):
245         try:
246             uploader = self.client.getServiceNamed("uploader")
247         except KeyError:
248             return "no" # we don't even have an Uploader
249         furl, connected = uploader.get_helper_info()
250         if connected:
251             return "yes"
252         return "no"
253
254     def data_known_storage_servers(self, ctx, data):
255         sb = self.client.get_storage_broker()
256         return len(sb.get_all_serverids())
257
258     def data_connected_storage_servers(self, ctx, data):
259         sb = self.client.get_storage_broker()
260         return len(sb.get_connected_servers())
261
262     def data_services(self, ctx, data):
263         sb = self.client.get_storage_broker()
264         return sorted(sb.get_known_servers(), key=lambda s: s.get_serverid())
265
266     def render_service_row(self, ctx, server):
267         nodeid = server.get_serverid()
268
269         ctx.fillSlots("peerid", server.get_longname())
270         ctx.fillSlots("nickname", server.get_nickname())
271         rhost = server.get_remote_host()
272         if rhost:
273             if nodeid == self.client.nodeid:
274                 rhost_s = "(loopback)"
275             elif isinstance(rhost, address.IPv4Address):
276                 rhost_s = "%s:%d" % (rhost.host, rhost.port)
277             else:
278                 rhost_s = str(rhost)
279             connected = "Yes: to " + rhost_s
280             since = server.get_last_connect_time()
281         else:
282             connected = "No"
283             since = server.get_last_loss_time()
284         announced = server.get_announcement_time()
285         announcement = server.get_announcement()
286         version = announcement["my-version"]
287         service_name = announcement["service-name"]
288
289         TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
290         ctx.fillSlots("connected", connected)
291         ctx.fillSlots("connected-bool", bool(rhost))
292         ctx.fillSlots("since", time.strftime(TIME_FORMAT,
293                                              time.localtime(since)))
294         ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
295                                                  time.localtime(announced)))
296         ctx.fillSlots("version", version)
297         ctx.fillSlots("service_name", service_name)
298
299         return ctx.tag
300
301     def render_download_form(self, ctx, data):
302         # this is a form where users can download files by URI
303         form = T.form(action="uri", method="get",
304                       enctype="multipart/form-data")[
305             T.fieldset[
306             T.legend(class_="freeform-form-label")["Download a file"],
307             T.div["Tahoe-URI to download:"+SPACE,
308                   T.input(type="text", name="uri")],
309             T.div["Filename to download as:"+SPACE,
310                   T.input(type="text", name="filename")],
311             T.input(type="submit", value="Download!"),
312             ]]
313         return T.div[form]
314
315     def render_view_form(self, ctx, data):
316         # this is a form where users can download files by URI, or jump to a
317         # named directory
318         form = T.form(action="uri", method="get",
319                       enctype="multipart/form-data")[
320             T.fieldset[
321             T.legend(class_="freeform-form-label")["View a file or directory"],
322             "Tahoe-URI to view:"+SPACE,
323             T.input(type="text", name="uri"), SPACE*2,
324             T.input(type="submit", value="View!"),
325             ]]
326         return T.div[form]
327
328     def render_upload_form(self, ctx, data):
329         # This is a form where users can upload unlinked files.
330         # Users can choose immutable, SDMF, or MDMF from a radio button.
331
332         upload_chk  = T.input(type='radio', name='format',
333                               value='chk', id='upload-chk',
334                               checked='checked')
335         upload_sdmf = T.input(type='radio', name='format',
336                               value='sdmf', id='upload-sdmf')
337         upload_mdmf = T.input(type='radio', name='format',
338                               value='mdmf', id='upload-mdmf')
339
340         form = T.form(action="uri", method="post",
341                       enctype="multipart/form-data")[
342             T.fieldset[
343             T.legend(class_="freeform-form-label")["Upload a file"],
344             T.div["Choose a file:"+SPACE,
345                   T.input(type="file", name="file", class_="freeform-input-file")],
346             T.input(type="hidden", name="t", value="upload"),
347             T.div[upload_chk,  T.label(for_="upload-chk") [" Immutable"],           SPACE,
348                   upload_sdmf, T.label(for_="upload-sdmf")[" SDMF"],                SPACE,
349                   upload_mdmf, T.label(for_="upload-mdmf")[" MDMF (experimental)"], SPACE*2,
350                   T.input(type="submit", value="Upload!")],
351             ]]
352         return T.div[form]
353
354     def render_mkdir_form(self, ctx, data):
355         # This is a form where users can create new directories.
356         # Users can choose SDMF or MDMF from a radio button.
357
358         mkdir_sdmf = T.input(type='radio', name='format',
359                              value='sdmf', id='mkdir-sdmf',
360                              checked='checked')
361         mkdir_mdmf = T.input(type='radio', name='format',
362                              value='mdmf', id='mkdir-mdmf')
363
364         form = T.form(action="uri", method="post",
365                       enctype="multipart/form-data")[
366             T.fieldset[
367             T.legend(class_="freeform-form-label")["Create a directory"],
368             mkdir_sdmf, T.label(for_='mkdir-sdmf')[" SDMF"],                SPACE,
369             mkdir_mdmf, T.label(for_='mkdir-mdmf')[" MDMF (experimental)"], SPACE*2,
370             T.input(type="hidden", name="t", value="mkdir"),
371             T.input(type="hidden", name="redirect_to_result", value="true"),
372             T.input(type="submit", value="Create a directory"),
373             ]]
374         return T.div[form]
375
376     def render_incident_button(self, ctx, data):
377         # this button triggers a foolscap-logging "incident"
378         form = T.form(action="report_incident", method="post",
379                       enctype="multipart/form-data")[
380             T.fieldset[
381             T.legend(class_="freeform-form-label")["Report an Incident"],
382             T.input(type="hidden", name="t", value="report-incident"),
383             "What went wrong?:"+SPACE,
384             T.input(type="text", name="details"), SPACE,
385             T.input(type="submit", value="Report!"),
386             ]]
387         return T.div[form]