]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/web/root.py
wui: improved columns in welcome page server list
[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, 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.util import log
13 from allmydata.interfaces import IFileNode
14 from allmydata.web import filenode, directory, unlinked, status, operations
15 from allmydata.web import storage
16 from allmydata.web.common import abbreviate_size, getxmlfile, WebError, \
17      get_arg, RenderMixin, get_format, get_mutable_type, TIME_FORMAT
18 from allmydata.util.time_format import format_delta
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 "An incident report has been saved to logs/incidents/ in the node directory."
128
129 SPACE = u"\u00A0"*2
130
131 class Root(rend.Page):
132
133     addSlash = True
134     docFactory = getxmlfile("welcome.xhtml")
135
136     def __init__(self, client, clock=None, now=None):
137         rend.Page.__init__(self, client)
138         self.client = client
139         # If set, clock is a twisted.internet.task.Clock that the tests
140         # use to test ophandle expiration.
141         self.child_operations = operations.OphandleTable(clock)
142         self.now = now
143         if self.now is None:
144             self.now = time.time
145         try:
146             s = client.getServiceNamed("storage")
147         except KeyError:
148             s = None
149         self.child_storage = storage.StorageStatus(s, self.client.nickname)
150
151         self.child_uri = URIHandler(client)
152         self.child_cap = URIHandler(client)
153
154         self.child_file = FileHandler(client)
155         self.child_named = FileHandler(client)
156         self.child_status = status.Status(client.get_history())
157         self.child_statistics = status.Statistics(client.stats_provider)
158         static_dir = resource_filename("allmydata.web", "static")
159         for filen in os.listdir(static_dir):
160             self.putChild(filen, nevow_File(os.path.join(static_dir, filen)))
161
162     def child_helper_status(self, ctx):
163         # the Helper isn't attached until after the Tub starts, so this child
164         # needs to created on each request
165         return status.HelperStatus(self.client.helper)
166
167     child_report_incident = IncidentReporter()
168     #child_server # let's reserve this for storage-server-over-HTTP
169
170     # FIXME: This code is duplicated in root.py and introweb.py.
171     def data_rendered_at(self, ctx, data):
172         return time.strftime(TIME_FORMAT, time.localtime())
173     def data_version(self, ctx, data):
174         return get_package_versions_string()
175     def data_import_path(self, ctx, data):
176         return str(allmydata)
177     def render_my_nodeid(self, ctx, data):
178         tubid_s = "TubID: "+self.client.get_long_tubid()
179         return T.td(title=tubid_s)[self.client.get_long_nodeid()]
180     def data_my_nickname(self, ctx, data):
181         return self.client.nickname
182
183     def render_services(self, ctx, data):
184         ul = T.ul()
185         try:
186             ss = self.client.getServiceNamed("storage")
187             stats = ss.get_stats()
188             if stats["storage_server.accepting_immutable_shares"]:
189                 msg = "accepting new shares"
190             else:
191                 msg = "not accepting new shares (read-only)"
192             available = stats.get("storage_server.disk_avail")
193             if available is not None:
194                 msg += ", %s available" % abbreviate_size(available)
195             ul[T.li[T.a(href="storage")["Storage Server"], ": ", msg]]
196         except KeyError:
197             ul[T.li["Not running storage server"]]
198
199         if self.client.helper:
200             stats = self.client.helper.get_stats()
201             active_uploads = stats["chk_upload_helper.active_uploads"]
202             ul[T.li["Helper: %d active uploads" % (active_uploads,)]]
203         else:
204             ul[T.li["Not running helper"]]
205
206         return ctx.tag[ul]
207
208     def data_introducer_furl_prefix(self, ctx, data):
209         ifurl = self.client.introducer_furl
210         # trim off the secret swissnum
211         (prefix, _, swissnum) = ifurl.rpartition("/")
212         if not ifurl:
213             return None
214         if swissnum == "introducer":
215             return ifurl
216         else:
217             return "%s/[censored]" % (prefix,)
218
219     def data_introducer_description(self, ctx, data):
220         if self.data_connected_to_introducer(ctx, data) == "no":
221             return "Introducer not connected"
222         return "Introducer"
223
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_prefix(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         if not furl:
236             return None
237         # trim off the secret swissnum
238         (prefix, _, swissnum) = furl.rpartition("/")
239         return "%s/[censored]" % (prefix,)
240
241     def data_helper_description(self, ctx, data):
242         if self.data_connected_to_helper(ctx, data) == "no":
243             return "Helper not connected"
244         return "Helper"
245
246     def data_connected_to_helper(self, ctx, data):
247         try:
248             uploader = self.client.getServiceNamed("uploader")
249         except KeyError:
250             return "no" # we don't even have an Uploader
251         furl, connected = uploader.get_helper_info()
252
253         if furl is None:
254             return "not-configured"
255         if connected:
256             return "yes"
257         return "no"
258
259     def data_known_storage_servers(self, ctx, data):
260         sb = self.client.get_storage_broker()
261         return len(sb.get_all_serverids())
262
263     def data_connected_storage_servers(self, ctx, data):
264         sb = self.client.get_storage_broker()
265         return len(sb.get_connected_servers())
266
267     def data_services(self, ctx, data):
268         sb = self.client.get_storage_broker()
269         return sorted(sb.get_known_servers(), key=lambda s: s.get_serverid())
270
271     def render_service_row(self, ctx, server):
272         nodeid = server.get_serverid()
273
274         ctx.fillSlots("peerid", server.get_longname())
275         ctx.fillSlots("nickname", server.get_nickname())
276         rhost = server.get_remote_host()
277         if server.is_connected():
278             if nodeid == self.client.nodeid:
279                 rhost_s = "(loopback)"
280             elif isinstance(rhost, address.IPv4Address):
281                 rhost_s = "%s:%d" % (rhost.host, rhost.port)
282             else:
283                 rhost_s = str(rhost)
284             addr = rhost_s
285             service_connection_status = "Connected"
286             service_connection_status_abs_time, service_connection_status_rel_time = format_delta(server.get_last_connect_time(), self.now())
287         else:
288             addr = "N/A"
289             service_connection_status = "Disconnected"
290             service_connection_status_abs_time, service_connection_status_rel_time = format_delta(server.get_last_loss_time(), self.now())
291
292         last_received_data_abs_time, last_received_data_rel_time = format_delta(server.get_last_received_data_time(), self.now())
293
294         announcement = server.get_announcement()
295         version = announcement["my-version"]
296         available_space = server.get_available_space()
297         if available_space is None:
298             available_space = "N/A"
299         else:
300             available_space = abbreviate_size(available_space)
301         ctx.fillSlots("address", addr)
302         ctx.fillSlots("service_connection_status", service_connection_status)
303         ctx.fillSlots("service_connection_status_abs_time", service_connection_status_abs_time)
304         ctx.fillSlots("service_connection_status_rel_time", service_connection_status_rel_time)
305         ctx.fillSlots("last_received_data_abs_time", last_received_data_abs_time)
306         ctx.fillSlots("last_received_data_rel_time", last_received_data_rel_time)
307         ctx.fillSlots("version", version)
308         ctx.fillSlots("available_space", available_space)
309
310         return ctx.tag
311
312     def render_download_form(self, ctx, data):
313         # this is a form where users can download files by URI
314         form = T.form(action="uri", method="get",
315                       enctype="multipart/form-data")[
316             T.fieldset[
317             T.legend(class_="freeform-form-label")["Download a file"],
318             T.div["Tahoe-URI to download:"+SPACE,
319                   T.input(type="text", name="uri")],
320             T.div["Filename to download as:"+SPACE,
321                   T.input(type="text", name="filename")],
322             T.input(type="submit", value="Download!"),
323             ]]
324         return T.div[form]
325
326     def render_view_form(self, ctx, data):
327         # this is a form where users can download files by URI, or jump to a
328         # named directory
329         form = T.form(action="uri", method="get",
330                       enctype="multipart/form-data")[
331             T.fieldset[
332             T.legend(class_="freeform-form-label")["View a file or directory"],
333             "Tahoe-URI to view:"+SPACE,
334             T.input(type="text", name="uri"), SPACE*2,
335             T.input(type="submit", value="View!"),
336             ]]
337         return T.div[form]
338
339     def render_upload_form(self, ctx, data):
340         # This is a form where users can upload unlinked files.
341         # Users can choose immutable, SDMF, or MDMF from a radio button.
342
343         upload_chk  = T.input(type='radio', name='format',
344                               value='chk', id='upload-chk',
345                               checked='checked')
346         upload_sdmf = T.input(type='radio', name='format',
347                               value='sdmf', id='upload-sdmf')
348         upload_mdmf = T.input(type='radio', name='format',
349                               value='mdmf', id='upload-mdmf')
350
351         form = T.form(action="uri", method="post",
352                       enctype="multipart/form-data")[
353             T.fieldset[
354             T.legend(class_="freeform-form-label")["Upload a file"],
355             T.div["Choose a file:"+SPACE,
356                   T.input(type="file", name="file", class_="freeform-input-file")],
357             T.input(type="hidden", name="t", value="upload"),
358             T.div[upload_chk,  T.label(for_="upload-chk") [" Immutable"],           SPACE,
359                   upload_sdmf, T.label(for_="upload-sdmf")[" SDMF"],                SPACE,
360                   upload_mdmf, T.label(for_="upload-mdmf")[" MDMF (experimental)"], SPACE*2,
361                   T.input(type="submit", value="Upload!")],
362             ]]
363         return T.div[form]
364
365     def render_mkdir_form(self, ctx, data):
366         # This is a form where users can create new directories.
367         # Users can choose SDMF or MDMF from a radio button.
368
369         mkdir_sdmf = T.input(type='radio', name='format',
370                              value='sdmf', id='mkdir-sdmf',
371                              checked='checked')
372         mkdir_mdmf = T.input(type='radio', name='format',
373                              value='mdmf', id='mkdir-mdmf')
374
375         form = T.form(action="uri", method="post",
376                       enctype="multipart/form-data")[
377             T.fieldset[
378             T.legend(class_="freeform-form-label")["Create a directory"],
379             mkdir_sdmf, T.label(for_='mkdir-sdmf')[" SDMF"],                SPACE,
380             mkdir_mdmf, T.label(for_='mkdir-mdmf')[" MDMF (experimental)"], SPACE*2,
381             T.input(type="hidden", name="t", value="mkdir"),
382             T.input(type="hidden", name="redirect_to_result", value="true"),
383             T.input(type="submit", value="Create a directory"),
384             ]]
385         return T.div[form]
386
387     def render_incident_button(self, ctx, data):
388         # this button triggers a foolscap-logging "incident"
389         form = T.form(action="report_incident", method="post",
390                       enctype="multipart/form-data")[
391             T.fieldset[
392             T.input(type="hidden", name="t", value="report-incident"),
393             "What went wrong?"+SPACE,
394             T.input(type="text", name="details"), SPACE,
395             T.input(type="submit", value=u"Save \u00BB"),
396             ]]
397         return T.div[form]