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