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
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, get_format, get_mutable_type
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 file_format = get_format(req, "CHK")
49 mutable_type = get_mutable_type(file_format)
50 if mutable_type is not None:
51 return unlinked.PUTUnlinkedSSK(req, self.client, mutable_type)
53 return unlinked.PUTUnlinkedCHK(req, self.client)
55 return unlinked.PUTUnlinkedCreateDirectory(req, self.client)
56 errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
58 raise WebError(errmsg, http.BAD_REQUEST)
60 def render_POST(self, ctx):
61 # "POST /uri?t=upload&file=newfile" to upload an
62 # unlinked file or "POST /uri?t=mkdir" to create a
65 t = get_arg(req, "t", "").strip()
66 if t in ("", "upload"):
67 file_format = get_format(req)
68 mutable_type = get_mutable_type(file_format)
69 if mutable_type is not None:
70 return unlinked.POSTUnlinkedSSK(req, self.client, mutable_type)
72 return unlinked.POSTUnlinkedCHK(req, self.client)
74 return unlinked.POSTUnlinkedCreateDirectory(req, self.client)
75 elif t == "mkdir-with-children":
76 return unlinked.POSTUnlinkedCreateDirectoryWithChildren(req,
78 elif t == "mkdir-immutable":
79 return unlinked.POSTUnlinkedCreateImmutableDirectory(req,
81 errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
83 raise WebError(errmsg, http.BAD_REQUEST)
85 def childFactory(self, ctx, name):
86 # 'name' is expected to be a URI
88 node = self.client.create_node_from_uri(name)
89 return directory.make_handler_for(node, self.client)
90 except (TypeError, AssertionError):
91 raise WebError("'%s' is not a valid file- or directory- cap"
94 class FileHandler(rend.Page):
95 # I handle /file/$FILECAP[/IGNORED] , which provides a URL from which a
96 # file can be downloaded correctly by tools like "wget".
98 def __init__(self, client):
99 rend.Page.__init__(self, client)
102 def childFactory(self, ctx, name):
104 if req.method not in ("GET", "HEAD"):
105 raise WebError("/file can only be used with GET or HEAD")
106 # 'name' must be a file URI
108 node = self.client.create_node_from_uri(name)
109 except (TypeError, AssertionError):
110 # I think this can no longer be reached
111 raise WebError("'%s' is not a valid file- or directory- cap"
113 if not IFileNode.providedBy(node):
114 raise WebError("'%s' is not a file-cap" % name)
115 return filenode.FileNodeDownloadHandler(self.client, node)
117 def renderHTTP(self, ctx):
118 raise WebError("/file must be followed by a file-cap and a name",
121 class IncidentReporter(RenderMixin, rend.Page):
122 def render_POST(self, ctx):
124 log.msg(format="User reports incident through web page: %(details)s",
125 details=get_arg(req, "details", ""),
126 level=log.WEIRD, umid="LkD9Pw")
127 req.setHeader("content-type", "text/plain")
128 return "Thank you for your report!"
130 class NoReliability(rend.Page):
131 docFactory = loaders.xmlstr('''\
132 <html xmlns:n="http://nevow.com/ns/nevow/0.1">
134 <title>AllMyData - Tahoe</title>
135 <link href="/webform_css" rel="stylesheet" type="text/css"/>
136 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
139 <h2>"Reliability" page not available</h2>
140 <p>Please install the python "NumPy" module to enable this page.</p>
147 class Root(rend.Page):
150 docFactory = getxmlfile("welcome.xhtml")
152 def __init__(self, client, clock=None):
153 rend.Page.__init__(self, client)
155 # If set, clock is a twisted.internet.task.Clock that the tests
156 # use to test ophandle expiration.
157 self.child_operations = operations.OphandleTable(clock)
159 s = client.getServiceNamed("storage")
162 self.child_storage = storage.StorageStatus(s)
164 self.child_uri = URIHandler(client)
165 self.child_cap = URIHandler(client)
167 self.child_file = FileHandler(client)
168 self.child_named = FileHandler(client)
169 self.child_status = status.Status(client.get_history())
170 self.child_statistics = status.Statistics(client.stats_provider)
172 return nevow_File(resource_filename('allmydata.web', name))
173 self.putChild("download_status_timeline.js", f("download_status_timeline.js"))
174 self.putChild("jquery-1.6.1.min.js", f("jquery-1.6.1.min.js"))
175 self.putChild("d3-2.4.6.min.js", f("d3-2.4.6.min.js"))
176 self.putChild("d3-2.4.6.time.min.js", f("d3-2.4.6.time.min.js"))
178 def child_helper_status(self, ctx):
179 # the Helper isn't attached until after the Tub starts, so this child
180 # needs to created on each request
181 return status.HelperStatus(self.client.helper)
183 child_webform_css = webform.defaultCSS
184 child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
186 child_provisioning = provisioning.ProvisioningTool()
187 if reliability.is_available():
188 child_reliability = reliability.ReliabilityTool()
190 child_reliability = NoReliability()
192 child_report_incident = IncidentReporter()
193 #child_server # let's reserve this for storage-server-over-HTTP
195 # FIXME: This code is duplicated in root.py and introweb.py.
196 def data_version(self, ctx, data):
197 return get_package_versions_string()
198 def data_import_path(self, ctx, data):
199 return str(allmydata)
200 def data_my_nodeid(self, ctx, data):
201 return idlib.nodeid_b2a(self.client.nodeid)
202 def data_my_nickname(self, ctx, data):
203 return self.client.nickname
205 def render_services(self, ctx, data):
208 ss = self.client.getServiceNamed("storage")
209 stats = ss.get_stats()
210 if stats["storage_server.accepting_immutable_shares"]:
211 msg = "accepting new shares"
213 msg = "not accepting new shares (read-only)"
214 available = stats.get("storage_server.disk_avail")
215 if available is not None:
216 msg += ", %s available" % abbreviate_size(available)
217 ul[T.li[T.a(href="storage")["Storage Server"], ": ", msg]]
219 ul[T.li["Not running storage server"]]
221 if self.client.helper:
222 stats = self.client.helper.get_stats()
223 active_uploads = stats["chk_upload_helper.active_uploads"]
224 ul[T.li["Helper: %d active uploads" % (active_uploads,)]]
226 ul[T.li["Not running helper"]]
230 def data_introducer_furl(self, ctx, data):
231 return self.client.introducer_furl
232 def data_connected_to_introducer(self, ctx, data):
233 if self.client.connected_to_introducer():
237 def data_helper_furl(self, ctx, data):
239 uploader = self.client.getServiceNamed("uploader")
242 furl, connected = uploader.get_helper_info()
244 def data_connected_to_helper(self, ctx, data):
246 uploader = self.client.getServiceNamed("uploader")
248 return "no" # we don't even have an Uploader
249 furl, connected = uploader.get_helper_info()
254 def data_known_storage_servers(self, ctx, data):
255 sb = self.client.get_storage_broker()
256 return len(sb.get_all_serverids())
258 def data_connected_storage_servers(self, ctx, data):
259 sb = self.client.get_storage_broker()
260 return len(sb.get_connected_servers())
262 def data_services(self, ctx, data):
263 sb = self.client.get_storage_broker()
264 return sorted(sb.get_known_servers(), key=lambda s: s.get_serverid())
266 def render_service_row(self, ctx, server):
267 nodeid = server.get_serverid()
269 ctx.fillSlots("peerid", server.get_longname())
270 ctx.fillSlots("nickname", server.get_nickname())
271 rhost = server.get_remote_host()
273 if nodeid == self.client.nodeid:
274 rhost_s = "(loopback)"
275 elif isinstance(rhost, address.IPv4Address):
276 rhost_s = "%s:%d" % (rhost.host, rhost.port)
279 connected = "Yes: to " + rhost_s
280 since = server.get_last_connect_time()
283 since = server.get_last_loss_time()
284 announced = server.get_announcement_time()
285 announcement = server.get_announcement()
286 version = announcement["my-version"]
287 service_name = announcement["service-name"]
289 TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
290 ctx.fillSlots("connected", connected)
291 ctx.fillSlots("connected-bool", bool(rhost))
292 ctx.fillSlots("since", time.strftime(TIME_FORMAT,
293 time.localtime(since)))
294 ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
295 time.localtime(announced)))
296 ctx.fillSlots("version", version)
297 ctx.fillSlots("service_name", service_name)
301 def render_download_form(self, ctx, data):
302 # this is a form where users can download files by URI
303 form = T.form(action="uri", method="get",
304 enctype="multipart/form-data")[
306 T.legend(class_="freeform-form-label")["Download a file"],
307 T.div["Tahoe-URI to download:"+SPACE,
308 T.input(type="text", name="uri")],
309 T.div["Filename to download as:"+SPACE,
310 T.input(type="text", name="filename")],
311 T.input(type="submit", value="Download!"),
315 def render_view_form(self, ctx, data):
316 # this is a form where users can download files by URI, or jump to a
318 form = T.form(action="uri", method="get",
319 enctype="multipart/form-data")[
321 T.legend(class_="freeform-form-label")["View a file or directory"],
322 "Tahoe-URI to view:"+SPACE,
323 T.input(type="text", name="uri"), SPACE*2,
324 T.input(type="submit", value="View!"),
328 def render_upload_form(self, ctx, data):
329 # This is a form where users can upload unlinked files.
330 # Users can choose immutable, SDMF, or MDMF from a radio button.
332 upload_chk = T.input(type='radio', name='format',
333 value='chk', id='upload-chk',
335 upload_sdmf = T.input(type='radio', name='format',
336 value='sdmf', id='upload-sdmf')
337 upload_mdmf = T.input(type='radio', name='format',
338 value='mdmf', id='upload-mdmf')
340 form = T.form(action="uri", method="post",
341 enctype="multipart/form-data")[
343 T.legend(class_="freeform-form-label")["Upload a file"],
344 T.div["Choose a file:"+SPACE,
345 T.input(type="file", name="file", class_="freeform-input-file")],
346 T.input(type="hidden", name="t", value="upload"),
347 T.div[upload_chk, T.label(for_="upload-chk") [" Immutable"], SPACE,
348 upload_sdmf, T.label(for_="upload-sdmf")[" SDMF"], SPACE,
349 upload_mdmf, T.label(for_="upload-mdmf")[" MDMF (experimental)"], SPACE*2,
350 T.input(type="submit", value="Upload!")],
354 def render_mkdir_form(self, ctx, data):
355 # This is a form where users can create new directories.
356 # Users can choose SDMF or MDMF from a radio button.
358 mkdir_sdmf = T.input(type='radio', name='format',
359 value='sdmf', id='mkdir-sdmf',
361 mkdir_mdmf = T.input(type='radio', name='format',
362 value='mdmf', id='mkdir-mdmf')
364 form = T.form(action="uri", method="post",
365 enctype="multipart/form-data")[
367 T.legend(class_="freeform-form-label")["Create a directory"],
368 mkdir_sdmf, T.label(for_='mkdir-sdmf')[" SDMF"], SPACE,
369 mkdir_mdmf, T.label(for_='mkdir-mdmf')[" MDMF (experimental)"], SPACE*2,
370 T.input(type="hidden", name="t", value="mkdir"),
371 T.input(type="hidden", name="redirect_to_result", value="true"),
372 T.input(type="submit", value="Create a directory"),
376 def render_incident_button(self, ctx, data):
377 # this button triggers a foolscap-logging "incident"
378 form = T.form(action="report_incident", method="post",
379 enctype="multipart/form-data")[
381 T.legend(class_="freeform-form-label")["Report an Incident"],
382 T.input(type="hidden", name="t", value="report-incident"),
383 "What went wrong?:"+SPACE,
384 T.input(type="text", name="details"), SPACE,
385 T.input(type="submit", value="Report!"),