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