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
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_delta, render_time, render_time_attr
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.
24 def __init__(self, client):
25 rend.Page.__init__(self, client)
28 def render_GET(self, ctx):
30 uri = get_arg(req, "uri", 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)
40 def render_PUT(self, 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()
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)
51 return unlinked.PUTUnlinkedCHK(req, self.client)
53 return unlinked.PUTUnlinkedCreateDirectory(req, self.client)
54 errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
56 raise WebError(errmsg, http.BAD_REQUEST)
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
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)
70 return unlinked.POSTUnlinkedCHK(req, self.client)
72 return unlinked.POSTUnlinkedCreateDirectory(req, self.client)
73 elif t == "mkdir-with-children":
74 return unlinked.POSTUnlinkedCreateDirectoryWithChildren(req,
76 elif t == "mkdir-immutable":
77 return unlinked.POSTUnlinkedCreateImmutableDirectory(req,
79 errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
81 raise WebError(errmsg, http.BAD_REQUEST)
83 def childFactory(self, ctx, name):
84 # 'name' is expected to be a URI
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"
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".
96 def __init__(self, client):
97 rend.Page.__init__(self, client)
100 def childFactory(self, ctx, name):
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
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"
111 if not IFileNode.providedBy(node):
112 raise WebError("'%s' is not a file-cap" % name)
113 return filenode.FileNodeDownloadHandler(self.client, node)
115 def renderHTTP(self, ctx):
116 raise WebError("/file must be followed by a file-cap and a name",
119 class IncidentReporter(RenderMixin, rend.Page):
120 def render_POST(self, 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."
130 class Root(rend.Page):
133 docFactory = getxmlfile("welcome.xhtml")
136 "not-configured": "Not Configured",
138 "no": "Disconnected",
141 def __init__(self, client, clock=None, now_fn=None):
142 rend.Page.__init__(self, 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)
149 s = client.getServiceNamed("storage")
152 self.child_storage = storage.StorageStatus(s, self.client.nickname)
154 self.child_uri = URIHandler(client)
155 self.child_cap = URIHandler(client)
157 self.child_file = FileHandler(client)
158 self.child_named = FileHandler(client)
159 self.child_status = status.Status(client.get_history())
160 self.child_statistics = status.Statistics(client.stats_provider)
161 static_dir = resource_filename("allmydata.web", "static")
162 for filen in os.listdir(static_dir):
163 self.putChild(filen, nevow_File(os.path.join(static_dir, filen)))
165 def child_helper_status(self, ctx):
166 # the Helper isn't attached until after the Tub starts, so this child
167 # needs to created on each request
168 return status.HelperStatus(self.client.helper)
170 child_report_incident = IncidentReporter()
171 #child_server # let's reserve this for storage-server-over-HTTP
173 # FIXME: This code is duplicated in root.py and introweb.py.
174 def data_rendered_at(self, ctx, data):
175 return render_time(time.time())
176 def data_version(self, ctx, data):
177 return get_package_versions_string()
178 def data_import_path(self, ctx, data):
179 return str(allmydata)
180 def render_my_nodeid(self, ctx, data):
181 tubid_s = "TubID: "+self.client.get_long_tubid()
182 return T.td(title=tubid_s)[self.client.get_long_nodeid()]
183 def data_my_nickname(self, ctx, data):
184 return self.client.nickname
186 def render_services(self, ctx, data):
189 ss = self.client.getServiceNamed("storage")
190 stats = ss.get_stats()
191 if stats["storage_server.accepting_immutable_shares"]:
192 msg = "accepting new shares"
194 msg = "not accepting new shares (read-only)"
195 available = stats.get("storage_server.disk_avail")
196 if available is not None:
197 msg += ", %s available" % abbreviate_size(available)
198 ul[T.li[T.a(href="storage")["Storage Server"], ": ", msg]]
200 ul[T.li["Not running storage server"]]
202 if self.client.helper:
203 stats = self.client.helper.get_stats()
204 active_uploads = stats["chk_upload_helper.active_uploads"]
205 ul[T.li["Helper: %d active uploads" % (active_uploads,)]]
207 ul[T.li["Not running helper"]]
211 def data_introducer_furl_prefix(self, ctx, data):
212 ifurl = self.client.introducer_furl
213 # trim off the secret swissnum
214 (prefix, _, swissnum) = ifurl.rpartition("/")
217 if swissnum == "introducer":
220 return "%s/[censored]" % (prefix,)
222 def data_introducer_description(self, ctx, data):
223 if self.data_connected_to_introducer(ctx, data) == "no":
224 return "Introducer not connected"
227 def data_connected_to_introducer(self, ctx, data):
228 if self.client.connected_to_introducer():
232 def data_connected_to_introducer_alt(self, ctx, data):
233 return self._connectedalts[self.data_connected_to_introducer(ctx, data)]
235 def data_helper_furl_prefix(self, ctx, data):
237 uploader = self.client.getServiceNamed("uploader")
240 furl, connected = uploader.get_helper_info()
243 # trim off the secret swissnum
244 (prefix, _, swissnum) = furl.rpartition("/")
245 return "%s/[censored]" % (prefix,)
247 def data_helper_description(self, ctx, data):
248 if self.data_connected_to_helper(ctx, data) == "no":
249 return "Helper not connected"
252 def data_connected_to_helper(self, ctx, data):
254 uploader = self.client.getServiceNamed("uploader")
256 return "no" # we don't even have an Uploader
257 furl, connected = uploader.get_helper_info()
260 return "not-configured"
265 def data_connected_to_helper_alt(self, ctx, data):
266 return self._connectedalts[self.data_connected_to_helper(ctx, data)]
268 def data_known_storage_servers(self, ctx, data):
269 sb = self.client.get_storage_broker()
270 return len(sb.get_all_serverids())
272 def data_connected_storage_servers(self, ctx, data):
273 sb = self.client.get_storage_broker()
274 return len(sb.get_connected_servers())
276 def data_services(self, ctx, data):
277 sb = self.client.get_storage_broker()
278 return sorted(sb.get_known_servers(), key=lambda s: s.get_serverid())
280 def render_service_row(self, ctx, server):
281 nodeid = server.get_serverid()
283 ctx.fillSlots("peerid", server.get_longname())
284 ctx.fillSlots("nickname", server.get_nickname())
285 rhost = server.get_remote_host()
286 if server.is_connected():
287 if nodeid == self.client.nodeid:
288 rhost_s = "(loopback)"
289 elif isinstance(rhost, address.IPv4Address):
290 rhost_s = "%s:%d" % (rhost.host, rhost.port)
294 service_connection_status = "yes"
295 last_connect_time = server.get_last_connect_time()
296 service_connection_status_rel_time = render_time_delta(last_connect_time, self.now_fn())
297 service_connection_status_abs_time = render_time_attr(last_connect_time)
300 service_connection_status = "no"
301 last_loss_time = server.get_last_loss_time()
302 service_connection_status_rel_time = render_time_delta(last_loss_time, self.now_fn())
303 service_connection_status_abs_time = render_time_attr(last_loss_time)
305 last_received_data_time = server.get_last_received_data_time()
306 last_received_data_rel_time = render_time_delta(last_received_data_time, self.now_fn())
307 last_received_data_abs_time = render_time_attr(last_received_data_time)
309 announcement = server.get_announcement()
310 version = announcement["my-version"]
311 available_space = server.get_available_space()
312 if available_space is None:
313 available_space = "N/A"
315 available_space = abbreviate_size(available_space)
316 ctx.fillSlots("address", addr)
317 ctx.fillSlots("service_connection_status", service_connection_status)
318 ctx.fillSlots("service_connection_status_alt", self._connectedalts[service_connection_status])
319 ctx.fillSlots("connected-bool", bool(rhost))
320 ctx.fillSlots("service_connection_status_abs_time", service_connection_status_abs_time)
321 ctx.fillSlots("service_connection_status_rel_time", service_connection_status_rel_time)
322 ctx.fillSlots("last_received_data_abs_time", last_received_data_abs_time)
323 ctx.fillSlots("last_received_data_rel_time", last_received_data_rel_time)
324 ctx.fillSlots("version", version)
325 ctx.fillSlots("available_space", available_space)
329 def render_download_form(self, ctx, data):
330 # this is a form where users can download files by URI
331 form = T.form(action="uri", method="get",
332 enctype="multipart/form-data")[
334 T.legend(class_="freeform-form-label")["Download a file"],
335 T.div["Tahoe-URI to download:"+SPACE,
336 T.input(type="text", name="uri")],
337 T.div["Filename to download as:"+SPACE,
338 T.input(type="text", name="filename")],
339 T.input(type="submit", value="Download!"),
343 def render_view_form(self, ctx, data):
344 # this is a form where users can download files by URI, or jump to a
346 form = T.form(action="uri", method="get",
347 enctype="multipart/form-data")[
349 T.legend(class_="freeform-form-label")["View a file or directory"],
350 "Tahoe-URI to view:"+SPACE,
351 T.input(type="text", name="uri"), SPACE*2,
352 T.input(type="submit", value="View!"),
356 def render_upload_form(self, ctx, data):
357 # This is a form where users can upload unlinked files.
358 # Users can choose immutable, SDMF, or MDMF from a radio button.
360 upload_chk = T.input(type='radio', name='format',
361 value='chk', id='upload-chk',
363 upload_sdmf = T.input(type='radio', name='format',
364 value='sdmf', id='upload-sdmf')
365 upload_mdmf = T.input(type='radio', name='format',
366 value='mdmf', id='upload-mdmf')
368 form = T.form(action="uri", method="post",
369 enctype="multipart/form-data")[
371 T.legend(class_="freeform-form-label")["Upload a file"],
372 T.div["Choose a file:"+SPACE,
373 T.input(type="file", name="file", class_="freeform-input-file")],
374 T.input(type="hidden", name="t", value="upload"),
375 T.div[upload_chk, T.label(for_="upload-chk") [" Immutable"], SPACE,
376 upload_sdmf, T.label(for_="upload-sdmf")[" SDMF"], SPACE,
377 upload_mdmf, T.label(for_="upload-mdmf")[" MDMF (experimental)"], SPACE*2,
378 T.input(type="submit", value="Upload!")],
382 def render_mkdir_form(self, ctx, data):
383 # This is a form where users can create new directories.
384 # Users can choose SDMF or MDMF from a radio button.
386 mkdir_sdmf = T.input(type='radio', name='format',
387 value='sdmf', id='mkdir-sdmf',
389 mkdir_mdmf = T.input(type='radio', name='format',
390 value='mdmf', id='mkdir-mdmf')
392 form = T.form(action="uri", method="post",
393 enctype="multipart/form-data")[
395 T.legend(class_="freeform-form-label")["Create a directory"],
396 mkdir_sdmf, T.label(for_='mkdir-sdmf')[" SDMF"], SPACE,
397 mkdir_mdmf, T.label(for_='mkdir-mdmf')[" MDMF (experimental)"], SPACE*2,
398 T.input(type="hidden", name="t", value="mkdir"),
399 T.input(type="hidden", name="redirect_to_result", value="true"),
400 T.input(type="submit", value="Create a directory"),
404 def render_incident_button(self, ctx, data):
405 # this button triggers a foolscap-logging "incident"
406 form = T.form(action="report_incident", method="post",
407 enctype="multipart/form-data")[
409 T.input(type="hidden", name="t", value="report-incident"),
410 "What went wrong?"+SPACE,
411 T.input(type="text", name="details"), SPACE,
412 T.input(type="submit", value=u"Save \u00BB"),