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