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