]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/web/root.py
47a6e9bfbb54546850198ba4be0c1c780d22231e
[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             if file_format in ("SDMF", "MDMF"):
50                 version = get_mutable_type(file_format)
51                 return unlinked.PUTUnlinkedSSK(req, self.client, version)
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             if file_format in ("SDMF", "MDMF"):
69                 version = get_mutable_type(file_format)
70                 return unlinked.POSTUnlinkedSSK(req, self.client, version)
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("protovis-3.3.1.min.js", f("protovis-3.3.1.min.js"))
176
177     def child_helper_status(self, ctx):
178         # the Helper isn't attached until after the Tub starts, so this child
179         # needs to created on each request
180         return status.HelperStatus(self.client.helper)
181
182     child_webform_css = webform.defaultCSS
183     child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
184
185     child_provisioning = provisioning.ProvisioningTool()
186     if reliability.is_available():
187         child_reliability = reliability.ReliabilityTool()
188     else:
189         child_reliability = NoReliability()
190
191     child_report_incident = IncidentReporter()
192     #child_server # let's reserve this for storage-server-over-HTTP
193
194     # FIXME: This code is duplicated in root.py and introweb.py.
195     def data_version(self, ctx, data):
196         return get_package_versions_string()
197     def data_import_path(self, ctx, data):
198         return str(allmydata)
199     def data_my_nodeid(self, ctx, data):
200         return idlib.nodeid_b2a(self.client.nodeid)
201     def data_my_nickname(self, ctx, data):
202         return self.client.nickname
203
204     def render_services(self, ctx, data):
205         ul = T.ul()
206         try:
207             ss = self.client.getServiceNamed("storage")
208             stats = ss.get_stats()
209             if stats["storage_server.accepting_immutable_shares"]:
210                 msg = "accepting new shares"
211             else:
212                 msg = "not accepting new shares (read-only)"
213             available = stats.get("storage_server.disk_avail")
214             if available is not None:
215                 msg += ", %s available" % abbreviate_size(available)
216             ul[T.li[T.a(href="storage")["Storage Server"], ": ", msg]]
217         except KeyError:
218             ul[T.li["Not running storage server"]]
219
220         if self.client.helper:
221             stats = self.client.helper.get_stats()
222             active_uploads = stats["chk_upload_helper.active_uploads"]
223             ul[T.li["Helper: %d active uploads" % (active_uploads,)]]
224         else:
225             ul[T.li["Not running helper"]]
226
227         return ctx.tag[ul]
228
229     def data_introducer_furl(self, ctx, data):
230         return self.client.introducer_furl
231     def data_connected_to_introducer(self, ctx, data):
232         if self.client.connected_to_introducer():
233             return "yes"
234         return "no"
235
236     def data_helper_furl(self, ctx, data):
237         try:
238             uploader = self.client.getServiceNamed("uploader")
239         except KeyError:
240             return None
241         furl, connected = uploader.get_helper_info()
242         return furl
243     def data_connected_to_helper(self, ctx, data):
244         try:
245             uploader = self.client.getServiceNamed("uploader")
246         except KeyError:
247             return "no" # we don't even have an Uploader
248         furl, connected = uploader.get_helper_info()
249         if connected:
250             return "yes"
251         return "no"
252
253     def data_known_storage_servers(self, ctx, data):
254         sb = self.client.get_storage_broker()
255         return len(sb.get_all_serverids())
256
257     def data_connected_storage_servers(self, ctx, data):
258         sb = self.client.get_storage_broker()
259         return len(sb.get_connected_servers())
260
261     def data_services(self, ctx, data):
262         sb = self.client.get_storage_broker()
263         return sorted(sb.get_known_servers(), key=lambda s: s.get_serverid())
264
265     def render_service_row(self, ctx, server):
266         nodeid = server.get_serverid()
267
268         ctx.fillSlots("peerid", server.get_longname())
269         ctx.fillSlots("nickname", server.get_nickname())
270         rhost = server.get_remote_host()
271         if rhost:
272             if nodeid == self.client.nodeid:
273                 rhost_s = "(loopback)"
274             elif isinstance(rhost, address.IPv4Address):
275                 rhost_s = "%s:%d" % (rhost.host, rhost.port)
276             else:
277                 rhost_s = str(rhost)
278             connected = "Yes: to " + rhost_s
279             since = server.get_last_connect_time()
280         else:
281             connected = "No"
282             since = server.get_last_loss_time()
283         announced = server.get_announcement_time()
284         announcement = server.get_announcement()
285         version = announcement["my-version"]
286         service_name = announcement["service-name"]
287
288         TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
289         ctx.fillSlots("connected", connected)
290         ctx.fillSlots("connected-bool", bool(rhost))
291         ctx.fillSlots("since", time.strftime(TIME_FORMAT,
292                                              time.localtime(since)))
293         ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
294                                                  time.localtime(announced)))
295         ctx.fillSlots("version", version)
296         ctx.fillSlots("service_name", service_name)
297
298         return ctx.tag
299
300     def render_download_form(self, ctx, data):
301         # this is a form where users can download files by URI
302         form = T.form(action="uri", method="get",
303                       enctype="multipart/form-data")[
304             T.fieldset[
305             T.legend(class_="freeform-form-label")["Download a file"],
306             T.div["Tahoe-URI to download:"+SPACE,
307                   T.input(type="text", name="uri")],
308             T.div["Filename to download as:"+SPACE,
309                   T.input(type="text", name="filename")],
310             T.input(type="submit", value="Download!"),
311             ]]
312         return T.div[form]
313
314     def render_view_form(self, ctx, data):
315         # this is a form where users can download files by URI, or jump to a
316         # named directory
317         form = T.form(action="uri", method="get",
318                       enctype="multipart/form-data")[
319             T.fieldset[
320             T.legend(class_="freeform-form-label")["View a file or directory"],
321             "Tahoe-URI to view:"+SPACE,
322             T.input(type="text", name="uri"), SPACE*2,
323             T.input(type="submit", value="View!"),
324             ]]
325         return T.div[form]
326
327     def render_upload_form(self, ctx, data):
328         # This is a form where users can upload unlinked files.
329         # Users can choose immutable, SDMF, or MDMF from a radio button.
330
331         upload_chk  = T.input(type='radio', name='format',
332                               value='chk', id='upload-chk',
333                               checked='checked')
334         upload_sdmf = T.input(type='radio', name='format',
335                               value='sdmf', id='upload-sdmf')
336         upload_mdmf = T.input(type='radio', name='format',
337                               value='mdmf', id='upload-mdmf')
338
339         form = T.form(action="uri", method="post",
340                       enctype="multipart/form-data")[
341             T.fieldset[
342             T.legend(class_="freeform-form-label")["Upload a file"],
343             T.div["Choose a file:"+SPACE,
344                   T.input(type="file", name="file", class_="freeform-input-file")],
345             T.input(type="hidden", name="t", value="upload"),
346             T.div[upload_chk,  T.label(for_="upload-chk") [" Immutable"],           SPACE,
347                   upload_sdmf, T.label(for_="upload-sdmf")[" SDMF"],                SPACE,
348                   upload_mdmf, T.label(for_="upload-mdmf")[" MDMF (experimental)"], SPACE*2,
349                   T.input(type="submit", value="Upload!")],
350             ]]
351         return T.div[form]
352
353     def render_mkdir_form(self, ctx, data):
354         # This is a form where users can create new directories.
355         # Users can choose SDMF or MDMF from a radio button.
356
357         mkdir_sdmf = T.input(type='radio', name='format',
358                              value='sdmf', id='mkdir-sdmf',
359                              checked='checked')
360         mkdir_mdmf = T.input(type='radio', name='format',
361                              value='mdmf', id='mkdir-mdmf')
362
363         form = T.form(action="uri", method="post",
364                       enctype="multipart/form-data")[
365             T.fieldset[
366             T.legend(class_="freeform-form-label")["Create a directory"],
367             mkdir_sdmf, T.label(for_='mkdir-sdmf')[" SDMF"],                SPACE,
368             mkdir_mdmf, T.label(for_='mkdir-mdmf')[" MDMF (experimental)"], SPACE*2,
369             T.input(type="hidden", name="t", value="mkdir"),
370             T.input(type="hidden", name="redirect_to_result", value="true"),
371             T.input(type="submit", value="Create a directory"),
372             ]]
373         return T.div[form]
374
375     def render_incident_button(self, ctx, data):
376         # this button triggers a foolscap-logging "incident"
377         form = T.form(action="report_incident", method="post",
378                       enctype="multipart/form-data")[
379             T.fieldset[
380             T.legend(class_="freeform-form-label")["Report an Incident"],
381             T.input(type="hidden", name="t", value="report-incident"),
382             "What went wrong?:"+SPACE,
383             T.input(type="text", name="details"), SPACE,
384             T.input(type="submit", value="Report!"),
385             ]]
386         return T.div[form]