]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/web/root.py
00495d894987ec116eb058fde33d1a052c475b7c
[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, MDMF_VERSION, SDMF_VERSION
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 class Root(rend.Page):
154
155     addSlash = True
156     docFactory = getxmlfile("welcome.xhtml")
157
158     def __init__(self, client, clock=None):
159         rend.Page.__init__(self, client)
160         self.client = client
161         # If set, clock is a twisted.internet.task.Clock that the tests
162         # use to test ophandle expiration.
163         self.child_operations = operations.OphandleTable(clock)
164         try:
165             s = client.getServiceNamed("storage")
166         except KeyError:
167             s = None
168         self.child_storage = storage.StorageStatus(s)
169
170         self.child_uri = URIHandler(client)
171         self.child_cap = URIHandler(client)
172
173         self.child_file = FileHandler(client)
174         self.child_named = FileHandler(client)
175         self.child_status = status.Status(client.get_history())
176         self.child_statistics = status.Statistics(client.stats_provider)
177         def f(name):
178             return nevow_File(resource_filename('allmydata.web', name))
179         self.putChild("download_status_timeline.js", f("download_status_timeline.js"))
180         self.putChild("jquery-1.6.1.min.js", f("jquery-1.6.1.min.js"))
181         self.putChild("protovis-3.3.1.min.js", f("protovis-3.3.1.min.js"))
182
183     def child_helper_status(self, ctx):
184         # the Helper isn't attached until after the Tub starts, so this child
185         # needs to created on each request
186         return status.HelperStatus(self.client.helper)
187
188     child_webform_css = webform.defaultCSS
189     child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
190
191     child_provisioning = provisioning.ProvisioningTool()
192     if reliability.is_available():
193         child_reliability = reliability.ReliabilityTool()
194     else:
195         child_reliability = NoReliability()
196
197     child_report_incident = IncidentReporter()
198     #child_server # let's reserve this for storage-server-over-HTTP
199
200     # FIXME: This code is duplicated in root.py and introweb.py.
201     def data_version(self, ctx, data):
202         return get_package_versions_string()
203     def data_import_path(self, ctx, data):
204         return str(allmydata)
205     def data_my_nodeid(self, ctx, data):
206         return idlib.nodeid_b2a(self.client.nodeid)
207     def data_my_nickname(self, ctx, data):
208         return self.client.nickname
209
210     def render_services(self, ctx, data):
211         ul = T.ul()
212         try:
213             ss = self.client.getServiceNamed("storage")
214             stats = ss.get_stats()
215             if stats["storage_server.accepting_immutable_shares"]:
216                 msg = "accepting new shares"
217             else:
218                 msg = "not accepting new shares (read-only)"
219             available = stats.get("storage_server.disk_avail")
220             if available is not None:
221                 msg += ", %s available" % abbreviate_size(available)
222             ul[T.li[T.a(href="storage")["Storage Server"], ": ", msg]]
223         except KeyError:
224             ul[T.li["Not running storage server"]]
225
226         if self.client.helper:
227             stats = self.client.helper.get_stats()
228             active_uploads = stats["chk_upload_helper.active_uploads"]
229             ul[T.li["Helper: %d active uploads" % (active_uploads,)]]
230         else:
231             ul[T.li["Not running helper"]]
232
233         return ctx.tag[ul]
234
235     def data_introducer_furl(self, ctx, data):
236         return self.client.introducer_furl
237     def data_connected_to_introducer(self, ctx, data):
238         if self.client.connected_to_introducer():
239             return "yes"
240         return "no"
241
242     def data_helper_furl(self, ctx, data):
243         try:
244             uploader = self.client.getServiceNamed("uploader")
245         except KeyError:
246             return None
247         furl, connected = uploader.get_helper_info()
248         return furl
249     def data_connected_to_helper(self, ctx, data):
250         try:
251             uploader = self.client.getServiceNamed("uploader")
252         except KeyError:
253             return "no" # we don't even have an Uploader
254         furl, connected = uploader.get_helper_info()
255         if connected:
256             return "yes"
257         return "no"
258
259     def data_known_storage_servers(self, ctx, data):
260         sb = self.client.get_storage_broker()
261         return len(sb.get_all_serverids())
262
263     def data_connected_storage_servers(self, ctx, data):
264         sb = self.client.get_storage_broker()
265         return len(sb.get_connected_servers())
266
267     def data_services(self, ctx, data):
268         sb = self.client.get_storage_broker()
269         return sorted(sb.get_known_servers(), key=lambda s: s.get_serverid())
270
271     def render_service_row(self, ctx, server):
272         nodeid = server.get_serverid()
273
274         ctx.fillSlots("peerid", server.get_longname())
275         ctx.fillSlots("nickname", server.get_nickname())
276         rhost = server.get_remote_host()
277         if rhost:
278             if nodeid == self.client.nodeid:
279                 rhost_s = "(loopback)"
280             elif isinstance(rhost, address.IPv4Address):
281                 rhost_s = "%s:%d" % (rhost.host, rhost.port)
282             else:
283                 rhost_s = str(rhost)
284             connected = "Yes: to " + rhost_s
285             since = server.get_last_connect_time()
286         else:
287             connected = "No"
288             since = server.get_last_loss_time()
289         announced = server.get_announcement_time()
290         announcement = server.get_announcement()
291         version = announcement["my-version"]
292         service_name = announcement["service-name"]
293
294         TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
295         ctx.fillSlots("connected", connected)
296         ctx.fillSlots("connected-bool", bool(rhost))
297         ctx.fillSlots("since", time.strftime(TIME_FORMAT,
298                                              time.localtime(since)))
299         ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
300                                                  time.localtime(announced)))
301         ctx.fillSlots("version", version)
302         ctx.fillSlots("service_name", service_name)
303
304         return ctx.tag
305
306     def render_download_form(self, ctx, data):
307         # this is a form where users can download files by URI
308         form = T.form(action="uri", method="get",
309                       enctype="multipart/form-data")[
310             T.fieldset[
311             T.legend(class_="freeform-form-label")["Download a file"],
312             T.div["Tahoe-URI to download: ",
313                   T.input(type="text", name="uri")],
314             T.div["Filename to download as: ",
315                   T.input(type="text", name="filename")],
316             T.input(type="submit", value="Download!"),
317             ]]
318         return T.div[form]
319
320     def render_view_form(self, ctx, data):
321         # this is a form where users can download files by URI, or jump to a
322         # named directory
323         form = T.form(action="uri", method="get",
324                       enctype="multipart/form-data")[
325             T.fieldset[
326             T.legend(class_="freeform-form-label")["View a file or directory"],
327             "Tahoe-URI to view: ",
328             T.input(type="text", name="uri"), " ",
329             T.input(type="submit", value="View!"),
330             ]]
331         return T.div[form]
332
333     def render_upload_form(self, ctx, data):
334         # this is a form where users can upload unlinked files
335         #
336         # for mutable files, users can choose the format by selecting
337         # MDMF or SDMF from a radio button. They can also configure a
338         # default format in tahoe.cfg, which they rightly expect us to
339         # obey. we convey to them that we are obeying their choice by
340         # ensuring that the one that they've chosen is selected in the
341         # interface.
342         if self.client.mutable_file_default == MDMF_VERSION:
343             mdmf_input = T.input(type='radio', name='mutable-type',
344                                  value='mdmf', id='mutable-type-mdmf',
345                                  checked='checked')
346         else:
347             mdmf_input = T.input(type='radio', name='mutable-type',
348                                  value='mdmf', id='mutable-type-mdmf')
349
350         if self.client.mutable_file_default == SDMF_VERSION:
351             sdmf_input = T.input(type='radio', name='mutable-type',
352                                  value='sdmf', id='mutable-type-sdmf',
353                                  checked='checked')
354         else:
355             sdmf_input = T.input(type='radio', name='mutable-type',
356                                  value='sdmf', id='mutable-type-sdmf')
357
358
359         form = T.form(action="uri", method="post",
360                       enctype="multipart/form-data")[
361             T.fieldset[
362             T.legend(class_="freeform-form-label")["Upload a file"],
363             T.div["Choose a file: ",
364                   T.input(type="file", name="file", class_="freeform-input-file")],
365             T.input(type="hidden", name="t", value="upload"),
366             T.div[T.input(type="checkbox", name="mutable"), T.label(for_="mutable")["Create mutable file"],
367                   sdmf_input, T.label(for_="mutable-type-sdmf")["SDMF"],
368                   mdmf_input,
369                   T.label(for_='mutable-type-mdmf')['MDMF (experimental)'],
370                   " ", T.input(type="submit", value="Upload!")],
371             ]]
372         return T.div[form]
373
374     def render_mkdir_form(self, ctx, data):
375         # this is a form where users can create new directories
376         mdmf_input = T.input(type='radio', name='mutable-type',
377                              value='mdmf', id='mutable-directory-mdmf')
378         sdmf_input = T.input(type='radio', name='mutable-type',
379                              value='sdmf', id='mutable-directory-sdmf',
380                              checked='checked')
381         form = T.form(action="uri", method="post",
382                       enctype="multipart/form-data")[
383             T.fieldset[
384             T.legend(class_="freeform-form-label")["Create a directory"],
385             T.label(for_='mutable-directory-sdmf')["SDMF"],
386             sdmf_input,
387             T.label(for_='mutable-directory-mdmf')["MDMF"],
388             mdmf_input,
389             T.input(type="hidden", name="t", value="mkdir"),
390             T.input(type="hidden", name="redirect_to_result", value="true"),
391             T.input(type="submit", value="Create a directory"),
392             ]]
393         return T.div[form]
394
395     def render_incident_button(self, ctx, data):
396         # this button triggers a foolscap-logging "incident"
397         form = T.form(action="report_incident", method="post",
398                       enctype="multipart/form-data")[
399             T.fieldset[
400             T.legend(class_="freeform-form-label")["Report an Incident"],
401             T.input(type="hidden", name="t", value="report-incident"),
402             "What went wrong?: ",
403             T.input(type="text", name="details"), " ",
404             T.input(type="submit", value="Report!"),
405             ]]
406         return T.div[form]