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