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
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, MDMF_VERSION, SDMF_VERSION
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, parse_mutable_type_arg
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.
26 def __init__(self, client):
27 rend.Page.__init__(self, client)
30 def render_GET(self, ctx):
32 uri = get_arg(req, "uri", 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)
42 def render_PUT(self, 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()
48 mutable = boolean_of_arg(get_arg(req, "mutable", "false").strip())
50 arg = get_arg(req, "mutable-type", None)
51 version = parse_mutable_type_arg(arg)
52 if version == "invalid":
53 errmsg = "Unknown type: %s" % arg
54 raise WebError(errmsg, http.BAD_REQUEST)
56 return unlinked.PUTUnlinkedSSK(req, self.client, version)
58 return unlinked.PUTUnlinkedCHK(req, self.client)
60 return unlinked.PUTUnlinkedCreateDirectory(req, self.client)
61 errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
63 raise WebError(errmsg, http.BAD_REQUEST)
65 def render_POST(self, ctx):
66 # "POST /uri?t=upload&file=newfile" to upload an
67 # unlinked file or "POST /uri?t=mkdir" to create a
70 t = get_arg(req, "t", "").strip()
71 if t in ("", "upload"):
72 mutable = bool(get_arg(req, "mutable", "").strip())
74 arg = get_arg(req, "mutable-type", None)
75 version = parse_mutable_type_arg(arg)
76 if version is "invalid":
77 raise WebError("Unknown type: %s" % arg, http.BAD_REQUEST)
78 return unlinked.POSTUnlinkedSSK(req, self.client, version)
80 return unlinked.POSTUnlinkedCHK(req, self.client)
82 return unlinked.POSTUnlinkedCreateDirectory(req, self.client)
83 elif t == "mkdir-with-children":
84 return unlinked.POSTUnlinkedCreateDirectoryWithChildren(req,
86 elif t == "mkdir-immutable":
87 return unlinked.POSTUnlinkedCreateImmutableDirectory(req,
89 errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
91 raise WebError(errmsg, http.BAD_REQUEST)
93 def childFactory(self, ctx, name):
94 # 'name' is expected to be a URI
96 node = self.client.create_node_from_uri(name)
97 return directory.make_handler_for(node, self.client)
98 except (TypeError, AssertionError):
99 raise WebError("'%s' is not a valid file- or directory- cap"
102 class FileHandler(rend.Page):
103 # I handle /file/$FILECAP[/IGNORED] , which provides a URL from which a
104 # file can be downloaded correctly by tools like "wget".
106 def __init__(self, client):
107 rend.Page.__init__(self, client)
110 def childFactory(self, ctx, name):
112 if req.method not in ("GET", "HEAD"):
113 raise WebError("/file can only be used with GET or HEAD")
114 # 'name' must be a file URI
116 node = self.client.create_node_from_uri(name)
117 except (TypeError, AssertionError):
118 # I think this can no longer be reached
119 raise WebError("'%s' is not a valid file- or directory- cap"
121 if not IFileNode.providedBy(node):
122 raise WebError("'%s' is not a file-cap" % name)
123 return filenode.FileNodeDownloadHandler(self.client, node)
125 def renderHTTP(self, ctx):
126 raise WebError("/file must be followed by a file-cap and a name",
129 class IncidentReporter(RenderMixin, rend.Page):
130 def render_POST(self, ctx):
132 log.msg(format="User reports incident through web page: %(details)s",
133 details=get_arg(req, "details", ""),
134 level=log.WEIRD, umid="LkD9Pw")
135 req.setHeader("content-type", "text/plain")
136 return "Thank you for your report!"
138 class NoReliability(rend.Page):
139 docFactory = loaders.xmlstr('''\
140 <html xmlns:n="http://nevow.com/ns/nevow/0.1">
142 <title>AllMyData - Tahoe</title>
143 <link href="/webform_css" rel="stylesheet" type="text/css"/>
144 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
147 <h2>"Reliability" page not available</h2>
148 <p>Please install the python "NumPy" module to enable this page.</p>
153 class Root(rend.Page):
156 docFactory = getxmlfile("welcome.xhtml")
158 def __init__(self, client, clock=None):
159 rend.Page.__init__(self, client)
161 # If set, clock is a twisted.internet.task.Clock that the tests
162 # use to test ophandle expiration.
163 self.child_operations = operations.OphandleTable(clock)
165 s = client.getServiceNamed("storage")
168 self.child_storage = storage.StorageStatus(s)
170 self.child_uri = URIHandler(client)
171 self.child_cap = URIHandler(client)
173 self.child_file = FileHandler(client)
174 self.child_named = FileHandler(client)
175 self.child_status = status.Status(client.get_history())
176 self.child_statistics = status.Statistics(client.stats_provider)
178 return nevow_File(resource_filename('allmydata.web', name))
179 self.putChild("download_status_timeline.js", f("download_status_timeline.js"))
180 self.putChild("jquery-1.6.1.min.js", f("jquery-1.6.1.min.js"))
181 self.putChild("protovis-3.3.1.min.js", f("protovis-3.3.1.min.js"))
183 def child_helper_status(self, ctx):
184 # the Helper isn't attached until after the Tub starts, so this child
185 # needs to created on each request
186 return status.HelperStatus(self.client.helper)
188 child_webform_css = webform.defaultCSS
189 child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
191 child_provisioning = provisioning.ProvisioningTool()
192 if reliability.is_available():
193 child_reliability = reliability.ReliabilityTool()
195 child_reliability = NoReliability()
197 child_report_incident = IncidentReporter()
198 #child_server # let's reserve this for storage-server-over-HTTP
200 # FIXME: This code is duplicated in root.py and introweb.py.
201 def data_version(self, ctx, data):
202 return get_package_versions_string()
203 def data_import_path(self, ctx, data):
204 return str(allmydata)
205 def data_my_nodeid(self, ctx, data):
206 return idlib.nodeid_b2a(self.client.nodeid)
207 def data_my_nickname(self, ctx, data):
208 return self.client.nickname
210 def render_services(self, ctx, data):
213 ss = self.client.getServiceNamed("storage")
214 stats = ss.get_stats()
215 if stats["storage_server.accepting_immutable_shares"]:
216 msg = "accepting new shares"
218 msg = "not accepting new shares (read-only)"
219 available = stats.get("storage_server.disk_avail")
220 if available is not None:
221 msg += ", %s available" % abbreviate_size(available)
222 ul[T.li[T.a(href="storage")["Storage Server"], ": ", msg]]
224 ul[T.li["Not running storage server"]]
226 if self.client.helper:
227 stats = self.client.helper.get_stats()
228 active_uploads = stats["chk_upload_helper.active_uploads"]
229 ul[T.li["Helper: %d active uploads" % (active_uploads,)]]
231 ul[T.li["Not running helper"]]
235 def data_introducer_furl(self, ctx, data):
236 return self.client.introducer_furl
237 def data_connected_to_introducer(self, ctx, data):
238 if self.client.connected_to_introducer():
242 def data_helper_furl(self, ctx, data):
244 uploader = self.client.getServiceNamed("uploader")
247 furl, connected = uploader.get_helper_info()
249 def data_connected_to_helper(self, ctx, data):
251 uploader = self.client.getServiceNamed("uploader")
253 return "no" # we don't even have an Uploader
254 furl, connected = uploader.get_helper_info()
259 def data_known_storage_servers(self, ctx, data):
260 sb = self.client.get_storage_broker()
261 return len(sb.get_all_serverids())
263 def data_connected_storage_servers(self, ctx, data):
264 sb = self.client.get_storage_broker()
265 return len(sb.get_connected_servers())
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())
271 def render_service_row(self, ctx, server):
272 nodeid = server.get_serverid()
274 ctx.fillSlots("peerid", server.get_longname())
275 ctx.fillSlots("nickname", server.get_nickname())
276 rhost = server.get_remote_host()
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)
284 connected = "Yes: to " + rhost_s
285 since = server.get_last_connect_time()
288 since = server.get_last_loss_time()
289 announced = server.get_announcement_time()
290 announcement = server.get_announcement()
291 version = announcement["my-version"]
292 service_name = announcement["service-name"]
294 TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
295 ctx.fillSlots("connected", connected)
296 ctx.fillSlots("connected-bool", bool(rhost))
297 ctx.fillSlots("since", time.strftime(TIME_FORMAT,
298 time.localtime(since)))
299 ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
300 time.localtime(announced)))
301 ctx.fillSlots("version", version)
302 ctx.fillSlots("service_name", service_name)
306 def render_download_form(self, ctx, data):
307 # this is a form where users can download files by URI
308 form = T.form(action="uri", method="get",
309 enctype="multipart/form-data")[
311 T.legend(class_="freeform-form-label")["Download a file"],
312 T.div["Tahoe-URI to download: ",
313 T.input(type="text", name="uri")],
314 T.div["Filename to download as: ",
315 T.input(type="text", name="filename")],
316 T.input(type="submit", value="Download!"),
320 def render_view_form(self, ctx, data):
321 # this is a form where users can download files by URI, or jump to a
323 form = T.form(action="uri", method="get",
324 enctype="multipart/form-data")[
326 T.legend(class_="freeform-form-label")["View a file or directory"],
327 "Tahoe-URI to view: ",
328 T.input(type="text", name="uri"), " ",
329 T.input(type="submit", value="View!"),
333 def render_upload_form(self, ctx, data):
334 # this is a form where users can upload unlinked files
336 # for mutable files, users can choose the format by selecting
337 # MDMF or SDMF from a radio button. They can also configure a
338 # default format in tahoe.cfg, which they rightly expect us to
339 # obey. we convey to them that we are obeying their choice by
340 # ensuring that the one that they've chosen is selected in the
342 if self.client.mutable_file_default == MDMF_VERSION:
343 mdmf_input = T.input(type='radio', name='mutable-type',
344 value='mdmf', id='mutable-type-mdmf',
347 mdmf_input = T.input(type='radio', name='mutable-type',
348 value='mdmf', id='mutable-type-mdmf')
350 if self.client.mutable_file_default == SDMF_VERSION:
351 sdmf_input = T.input(type='radio', name='mutable-type',
352 value='sdmf', id='mutable-type-sdmf',
355 sdmf_input = T.input(type='radio', name='mutable-type',
356 value='sdmf', id='mutable-type-sdmf')
359 form = T.form(action="uri", method="post",
360 enctype="multipart/form-data")[
362 T.legend(class_="freeform-form-label")["Upload a file"],
363 T.div["Choose a file: ",
364 T.input(type="file", name="file", class_="freeform-input-file")],
365 T.input(type="hidden", name="t", value="upload"),
366 T.div[T.input(type="checkbox", name="mutable"), T.label(for_="mutable")["Create mutable file"],
367 sdmf_input, T.label(for_="mutable-type-sdmf")["SDMF"],
369 T.label(for_='mutable-type-mdmf')['MDMF (experimental)'],
370 " ", T.input(type="submit", value="Upload!")],
374 def render_mkdir_form(self, ctx, data):
375 # this is a form where users can create new directories
376 mdmf_input = T.input(type='radio', name='mutable-type',
377 value='mdmf', id='mutable-directory-mdmf')
378 sdmf_input = T.input(type='radio', name='mutable-type',
379 value='sdmf', id='mutable-directory-sdmf',
381 form = T.form(action="uri", method="post",
382 enctype="multipart/form-data")[
384 T.legend(class_="freeform-form-label")["Create a directory"],
385 T.label(for_='mutable-directory-sdmf')["SDMF"],
387 T.label(for_='mutable-directory-mdmf')["MDMF"],
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"),
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")[
400 T.legend(class_="freeform-form-label")["Report an Incident"],
401 T.input(type="hidden", name="t", value="report-incident"),
402 "What went wrong?: ",
403 T.input(type="text", name="details"), " ",
404 T.input(type="submit", value="Report!"),