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