]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/web/root.py
c46a6dd5f8bc5fb9c49f8199cc74d1a742bb47a8
[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, render_time
18
19
20 class URIHandler(RenderMixin, rend.Page):
21     # I live at /uri . There are several operations defined on /uri itself,
22     # mostly involved with creation of unlinked files and directories.
23
24     def __init__(self, client):
25         rend.Page.__init__(self, client)
26         self.client = client
27
28     def render_GET(self, ctx):
29         req = IRequest(ctx)
30         uri = get_arg(req, "uri", None)
31         if uri is None:
32             raise WebError("GET /uri requires uri=")
33         there = url.URL.fromContext(ctx)
34         there = there.clear("uri")
35         # I thought about escaping the childcap that we attach to the URL
36         # here, but it seems that nevow does that for us.
37         there = there.child(uri)
38         return there
39
40     def render_PUT(self, ctx):
41         req = IRequest(ctx)
42         # either "PUT /uri" to create an unlinked file, or
43         # "PUT /uri?t=mkdir" to create an unlinked directory
44         t = get_arg(req, "t", "").strip()
45         if t == "":
46             file_format = get_format(req, "CHK")
47             mutable_type = get_mutable_type(file_format)
48             if mutable_type is not None:
49                 return unlinked.PUTUnlinkedSSK(req, self.client, mutable_type)
50             else:
51                 return unlinked.PUTUnlinkedCHK(req, self.client)
52         if t == "mkdir":
53             return unlinked.PUTUnlinkedCreateDirectory(req, self.client)
54         errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
55                   "and POST?t=mkdir")
56         raise WebError(errmsg, http.BAD_REQUEST)
57
58     def render_POST(self, ctx):
59         # "POST /uri?t=upload&file=newfile" to upload an
60         # unlinked file or "POST /uri?t=mkdir" to create a
61         # new directory
62         req = IRequest(ctx)
63         t = get_arg(req, "t", "").strip()
64         if t in ("", "upload"):
65             file_format = get_format(req)
66             mutable_type = get_mutable_type(file_format)
67             if mutable_type is not None:
68                 return unlinked.POSTUnlinkedSSK(req, self.client, mutable_type)
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 "An incident report has been saved to logs/incidents/ in the node directory."
127
128 SPACE = u"\u00A0"*2
129
130 class Root(rend.Page):
131
132     addSlash = True
133     docFactory = getxmlfile("welcome.xhtml")
134
135     _connectedalts = {
136         "not-configured": "Not Configured",
137         "yes": "Connected",
138         "no": "Disconnected",
139         }
140
141     def __init__(self, client, clock=None):
142         rend.Page.__init__(self, client)
143         self.client = client
144         # If set, clock is a twisted.internet.task.Clock that the tests
145         # use to test ophandle expiration.
146         self.child_operations = operations.OphandleTable(clock)
147         try:
148             s = client.getServiceNamed("storage")
149         except KeyError:
150             s = None
151         self.child_storage = storage.StorageStatus(s, self.client.nickname)
152
153         self.child_uri = URIHandler(client)
154         self.child_cap = URIHandler(client)
155
156         self.child_file = FileHandler(client)
157         self.child_named = FileHandler(client)
158         self.child_status = status.Status(client.get_history())
159         self.child_statistics = status.Statistics(client.stats_provider)
160         static_dir = resource_filename("allmydata.web", "static")
161         for filen in os.listdir(static_dir):
162             self.putChild(filen, nevow_File(os.path.join(static_dir, filen)))
163
164     def child_helper_status(self, ctx):
165         # the Helper isn't attached until after the Tub starts, so this child
166         # needs to created on each request
167         return status.HelperStatus(self.client.helper)
168
169     child_report_incident = IncidentReporter()
170     #child_server # let's reserve this for storage-server-over-HTTP
171
172     # FIXME: This code is duplicated in root.py and introweb.py.
173     def data_rendered_at(self, ctx, data):
174         return render_time(time.time())
175     def data_version(self, ctx, data):
176         return get_package_versions_string()
177     def data_import_path(self, ctx, data):
178         return str(allmydata)
179     def render_my_nodeid(self, ctx, data):
180         tubid_s = "TubID: "+self.client.get_long_tubid()
181         return T.td(title=tubid_s)[self.client.get_long_nodeid()]
182     def data_my_nickname(self, ctx, data):
183         return self.client.nickname
184
185     def render_services(self, ctx, data):
186         ul = T.ul()
187         try:
188             ss = self.client.getServiceNamed("storage")
189             stats = ss.get_stats()
190             if stats["storage_server.accepting_immutable_shares"]:
191                 msg = "accepting new shares"
192             else:
193                 msg = "not accepting new shares (read-only)"
194             available = stats.get("storage_server.disk_avail")
195             if available is not None:
196                 msg += ", %s available" % abbreviate_size(available)
197             ul[T.li[T.a(href="storage")["Storage Server"], ": ", msg]]
198         except KeyError:
199             ul[T.li["Not running storage server"]]
200
201         if self.client.helper:
202             stats = self.client.helper.get_stats()
203             active_uploads = stats["chk_upload_helper.active_uploads"]
204             ul[T.li["Helper: %d active uploads" % (active_uploads,)]]
205         else:
206             ul[T.li["Not running helper"]]
207
208         return ctx.tag[ul]
209
210     def data_introducer_furl_prefix(self, ctx, data):
211         ifurl = self.client.introducer_furl
212         # trim off the secret swissnum
213         (prefix, _, swissnum) = ifurl.rpartition("/")
214         if not ifurl:
215             return None
216         if swissnum == "introducer":
217             return ifurl
218         else:
219             return "%s/[censored]" % (prefix,)
220
221     def data_introducer_description(self, ctx, data):
222         if self.data_connected_to_introducer(ctx, data) == "no":
223             return "Introducer not connected"
224         return "Introducer"
225
226     def data_connected_to_introducer(self, ctx, data):
227         if self.client.connected_to_introducer():
228             return "yes"
229         return "no"
230
231     def data_connected_to_introducer_alt(self, ctx, data):
232         return self._connectedalts[self.data_connected_to_introducer(ctx, data)]
233
234     def data_helper_furl_prefix(self, ctx, data):
235         try:
236             uploader = self.client.getServiceNamed("uploader")
237         except KeyError:
238             return None
239         furl, connected = uploader.get_helper_info()
240         if not furl:
241             return None
242         # trim off the secret swissnum
243         (prefix, _, swissnum) = furl.rpartition("/")
244         return "%s/[censored]" % (prefix,)
245
246     def data_helper_description(self, ctx, data):
247         if self.data_connected_to_helper(ctx, data) == "no":
248             return "Helper not connected"
249         return "Helper"
250
251     def data_connected_to_helper(self, ctx, data):
252         try:
253             uploader = self.client.getServiceNamed("uploader")
254         except KeyError:
255             return "no" # we don't even have an Uploader
256         furl, connected = uploader.get_helper_info()
257
258         if furl is None:
259             return "not-configured"
260         if connected:
261             return "yes"
262         return "no"
263
264     def data_connected_to_helper_alt(self, ctx, data):
265         return self._connectedalts[self.data_connected_to_helper(ctx, data)]
266
267     def data_known_storage_servers(self, ctx, data):
268         sb = self.client.get_storage_broker()
269         return len(sb.get_all_serverids())
270
271     def data_connected_storage_servers(self, ctx, data):
272         sb = self.client.get_storage_broker()
273         return len(sb.get_connected_servers())
274
275     def data_services(self, ctx, data):
276         sb = self.client.get_storage_broker()
277         return sorted(sb.get_known_servers(), key=lambda s: s.get_serverid())
278
279     def render_service_row(self, ctx, server):
280         nodeid = server.get_serverid()
281
282         ctx.fillSlots("peerid", server.get_longname())
283         ctx.fillSlots("nickname", server.get_nickname())
284         rhost = server.get_remote_host()
285         if rhost:
286             if nodeid == self.client.nodeid:
287                 rhost_s = "(loopback)"
288             elif isinstance(rhost, address.IPv4Address):
289                 rhost_s = "%s:%d" % (rhost.host, rhost.port)
290             else:
291                 rhost_s = str(rhost)
292             addr = rhost_s
293             connected = "yes"
294             since = server.get_last_connect_time()
295         else:
296             addr = "N/A"
297             connected = "no"
298             since = server.get_last_loss_time()
299         announced = server.get_announcement_time()
300         announcement = server.get_announcement()
301         version = announcement["my-version"]
302         service_name = announcement["service-name"]
303         available_space = server.get_available_space()
304         if available_space is None:
305             available_space = "N/A"
306         else:
307             available_space = abbreviate_size(available_space)
308         ctx.fillSlots("address", addr)
309         ctx.fillSlots("connected", connected)
310         ctx.fillSlots("connected_alt", self._connectedalts[connected])
311         ctx.fillSlots("connected-bool", bool(rhost))
312         ctx.fillSlots("since", render_time(since))
313         ctx.fillSlots("announced", render_time(announced))
314         ctx.fillSlots("version", version)
315         ctx.fillSlots("service_name", service_name)
316         ctx.fillSlots("available_space", available_space)
317
318         return ctx.tag
319
320     def render_download_form(self, ctx, data):
321         # this is a form where users can download files by URI
322         form = T.form(action="uri", method="get",
323                       enctype="multipart/form-data")[
324             T.fieldset[
325             T.legend(class_="freeform-form-label")["Download a file"],
326             T.div["Tahoe-URI to download:"+SPACE,
327                   T.input(type="text", name="uri")],
328             T.div["Filename to download as:"+SPACE,
329                   T.input(type="text", name="filename")],
330             T.input(type="submit", value="Download!"),
331             ]]
332         return T.div[form]
333
334     def render_view_form(self, ctx, data):
335         # this is a form where users can download files by URI, or jump to a
336         # named directory
337         form = T.form(action="uri", method="get",
338                       enctype="multipart/form-data")[
339             T.fieldset[
340             T.legend(class_="freeform-form-label")["View a file or directory"],
341             "Tahoe-URI to view:"+SPACE,
342             T.input(type="text", name="uri"), SPACE*2,
343             T.input(type="submit", value="View!"),
344             ]]
345         return T.div[form]
346
347     def render_upload_form(self, ctx, data):
348         # This is a form where users can upload unlinked files.
349         # Users can choose immutable, SDMF, or MDMF from a radio button.
350
351         upload_chk  = T.input(type='radio', name='format',
352                               value='chk', id='upload-chk',
353                               checked='checked')
354         upload_sdmf = T.input(type='radio', name='format',
355                               value='sdmf', id='upload-sdmf')
356         upload_mdmf = T.input(type='radio', name='format',
357                               value='mdmf', id='upload-mdmf')
358
359         form = T.form(action="uri", method="post",
360                       enctype="multipart/form-data")[
361             T.fieldset[
362             T.legend(class_="freeform-form-label")["Upload a file"],
363             T.div["Choose a file:"+SPACE,
364                   T.input(type="file", name="file", class_="freeform-input-file")],
365             T.input(type="hidden", name="t", value="upload"),
366             T.div[upload_chk,  T.label(for_="upload-chk") [" Immutable"],           SPACE,
367                   upload_sdmf, T.label(for_="upload-sdmf")[" SDMF"],                SPACE,
368                   upload_mdmf, T.label(for_="upload-mdmf")[" MDMF (experimental)"], SPACE*2,
369                   T.input(type="submit", value="Upload!")],
370             ]]
371         return T.div[form]
372
373     def render_mkdir_form(self, ctx, data):
374         # This is a form where users can create new directories.
375         # Users can choose SDMF or MDMF from a radio button.
376
377         mkdir_sdmf = T.input(type='radio', name='format',
378                              value='sdmf', id='mkdir-sdmf',
379                              checked='checked')
380         mkdir_mdmf = T.input(type='radio', name='format',
381                              value='mdmf', id='mkdir-mdmf')
382
383         form = T.form(action="uri", method="post",
384                       enctype="multipart/form-data")[
385             T.fieldset[
386             T.legend(class_="freeform-form-label")["Create a directory"],
387             mkdir_sdmf, T.label(for_='mkdir-sdmf')[" SDMF"],                SPACE,
388             mkdir_mdmf, T.label(for_='mkdir-mdmf')[" MDMF (experimental)"], SPACE*2,
389             T.input(type="hidden", name="t", value="mkdir"),
390             T.input(type="hidden", name="redirect_to_result", value="true"),
391             T.input(type="submit", value="Create a directory"),
392             ]]
393         return T.div[form]
394
395     def render_incident_button(self, ctx, data):
396         # this button triggers a foolscap-logging "incident"
397         form = T.form(action="report_incident", method="post",
398                       enctype="multipart/form-data")[
399             T.fieldset[
400             T.input(type="hidden", name="t", value="report-incident"),
401             "What went wrong?"+SPACE,
402             T.input(type="text", name="details"), SPACE,
403             T.input(type="submit", value=u"Save \u00BB"),
404             ]]
405         return T.div[form]