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, 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>
155 class Root(rend.Page):
158 docFactory = getxmlfile("welcome.xhtml")
160 def __init__(self, client, clock=None):
161 rend.Page.__init__(self, client)
163 # If set, clock is a twisted.internet.task.Clock that the tests
164 # use to test ophandle expiration.
165 self.child_operations = operations.OphandleTable(clock)
167 s = client.getServiceNamed("storage")
170 self.child_storage = storage.StorageStatus(s)
172 self.child_uri = URIHandler(client)
173 self.child_cap = URIHandler(client)
175 self.child_file = FileHandler(client)
176 self.child_named = FileHandler(client)
177 self.child_status = status.Status(client.get_history())
178 self.child_statistics = status.Statistics(client.stats_provider)
180 return nevow_File(resource_filename('allmydata.web', name))
181 self.putChild("download_status_timeline.js", f("download_status_timeline.js"))
182 self.putChild("jquery-1.6.1.min.js", f("jquery-1.6.1.min.js"))
183 self.putChild("protovis-3.3.1.min.js", f("protovis-3.3.1.min.js"))
185 def child_helper_status(self, ctx):
186 # the Helper isn't attached until after the Tub starts, so this child
187 # needs to created on each request
188 return status.HelperStatus(self.client.helper)
190 child_webform_css = webform.defaultCSS
191 child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
193 child_provisioning = provisioning.ProvisioningTool()
194 if reliability.is_available():
195 child_reliability = reliability.ReliabilityTool()
197 child_reliability = NoReliability()
199 child_report_incident = IncidentReporter()
200 #child_server # let's reserve this for storage-server-over-HTTP
202 # FIXME: This code is duplicated in root.py and introweb.py.
203 def data_version(self, ctx, data):
204 return get_package_versions_string()
205 def data_import_path(self, ctx, data):
206 return str(allmydata)
207 def data_my_nodeid(self, ctx, data):
208 return idlib.nodeid_b2a(self.client.nodeid)
209 def data_my_nickname(self, ctx, data):
210 return self.client.nickname
212 def render_services(self, ctx, data):
215 ss = self.client.getServiceNamed("storage")
216 stats = ss.get_stats()
217 if stats["storage_server.accepting_immutable_shares"]:
218 msg = "accepting new shares"
220 msg = "not accepting new shares (read-only)"
221 available = stats.get("storage_server.disk_avail")
222 if available is not None:
223 msg += ", %s available" % abbreviate_size(available)
224 ul[T.li[T.a(href="storage")["Storage Server"], ": ", msg]]
226 ul[T.li["Not running storage server"]]
228 if self.client.helper:
229 stats = self.client.helper.get_stats()
230 active_uploads = stats["chk_upload_helper.active_uploads"]
231 ul[T.li["Helper: %d active uploads" % (active_uploads,)]]
233 ul[T.li["Not running helper"]]
237 def data_introducer_furl(self, ctx, data):
238 return self.client.introducer_furl
239 def data_connected_to_introducer(self, ctx, data):
240 if self.client.connected_to_introducer():
244 def data_helper_furl(self, ctx, data):
246 uploader = self.client.getServiceNamed("uploader")
249 furl, connected = uploader.get_helper_info()
251 def data_connected_to_helper(self, ctx, data):
253 uploader = self.client.getServiceNamed("uploader")
255 return "no" # we don't even have an Uploader
256 furl, connected = uploader.get_helper_info()
261 def data_known_storage_servers(self, ctx, data):
262 sb = self.client.get_storage_broker()
263 return len(sb.get_all_serverids())
265 def data_connected_storage_servers(self, ctx, data):
266 sb = self.client.get_storage_broker()
267 return len(sb.get_connected_servers())
269 def data_services(self, ctx, data):
270 sb = self.client.get_storage_broker()
271 return sorted(sb.get_known_servers(), key=lambda s: s.get_serverid())
273 def render_service_row(self, ctx, server):
274 nodeid = server.get_serverid()
276 ctx.fillSlots("peerid", server.get_longname())
277 ctx.fillSlots("nickname", server.get_nickname())
278 rhost = server.get_remote_host()
280 if nodeid == self.client.nodeid:
281 rhost_s = "(loopback)"
282 elif isinstance(rhost, address.IPv4Address):
283 rhost_s = "%s:%d" % (rhost.host, rhost.port)
286 connected = "Yes: to " + rhost_s
287 since = server.get_last_connect_time()
290 since = server.get_last_loss_time()
291 announced = server.get_announcement_time()
292 announcement = server.get_announcement()
293 version = announcement["my-version"]
294 service_name = announcement["service-name"]
296 TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
297 ctx.fillSlots("connected", connected)
298 ctx.fillSlots("connected-bool", bool(rhost))
299 ctx.fillSlots("since", time.strftime(TIME_FORMAT,
300 time.localtime(since)))
301 ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
302 time.localtime(announced)))
303 ctx.fillSlots("version", version)
304 ctx.fillSlots("service_name", service_name)
308 def render_download_form(self, ctx, data):
309 # this is a form where users can download files by URI
310 form = T.form(action="uri", method="get",
311 enctype="multipart/form-data")[
313 T.legend(class_="freeform-form-label")["Download a file"],
314 T.div["Tahoe-URI to download:"+SPACE,
315 T.input(type="text", name="uri")],
316 T.div["Filename to download as:"+SPACE,
317 T.input(type="text", name="filename")],
318 T.input(type="submit", value="Download!"),
322 def render_view_form(self, ctx, data):
323 # this is a form where users can download files by URI, or jump to a
325 form = T.form(action="uri", method="get",
326 enctype="multipart/form-data")[
328 T.legend(class_="freeform-form-label")["View a file or directory"],
329 "Tahoe-URI to view:"+SPACE,
330 T.input(type="text", name="uri"), SPACE*2,
331 T.input(type="submit", value="View!"),
335 def render_upload_form(self, ctx, data):
336 # This is a form where users can upload unlinked files.
337 # Users can choose immutable, SDMF, or MDMF from a radio button.
339 upload_chk = T.input(type='radio', name='format',
340 value='chk', id='upload-chk',
342 upload_sdmf = T.input(type='radio', name='format',
343 value='sdmf', id='upload-sdmf')
344 upload_mdmf = T.input(type='radio', name='format',
345 value='mdmf', id='upload-mdmf')
347 form = T.form(action="uri", method="post",
348 enctype="multipart/form-data")[
350 T.legend(class_="freeform-form-label")["Upload a file"],
351 T.div["Choose a file:"+SPACE,
352 T.input(type="file", name="file", class_="freeform-input-file")],
353 T.input(type="hidden", name="t", value="upload"),
354 T.div[upload_chk, T.label(for_="upload-chk") [" Immutable"], SPACE,
355 upload_sdmf, T.label(for_="upload-sdmf")[" SDMF"], SPACE,
356 upload_mdmf, T.label(for_="upload-mdmf")[" MDMF (experimental)"], SPACE*2,
357 T.input(type="submit", value="Upload!")],
361 def render_mkdir_form(self, ctx, data):
362 # This is a form where users can create new directories.
363 # Users can choose SDMF or MDMF from a radio button.
365 mkdir_sdmf = T.input(type='radio', name='format',
366 value='sdmf', id='mkdir-sdmf',
368 mkdir_mdmf = T.input(type='radio', name='format',
369 value='mdmf', id='mkdir-mdmf')
371 form = T.form(action="uri", method="post",
372 enctype="multipart/form-data")[
374 T.legend(class_="freeform-form-label")["Create a directory"],
375 mkdir_sdmf, T.label(for_='mkdir-sdmf')[" SDMF"], SPACE,
376 mkdir_mdmf, T.label(for_='mkdir-mdmf')[" MDMF (experimental)"], SPACE*2,
377 T.input(type="hidden", name="t", value="mkdir"),
378 T.input(type="hidden", name="redirect_to_result", value="true"),
379 T.input(type="submit", value="Create a directory"),
383 def render_incident_button(self, ctx, data):
384 # this button triggers a foolscap-logging "incident"
385 form = T.form(action="report_incident", method="post",
386 enctype="multipart/form-data")[
388 T.legend(class_="freeform-form-label")["Report an Incident"],
389 T.input(type="hidden", name="t", value="report-incident"),
390 "What went wrong?:"+SPACE,
391 T.input(type="text", name="details"), SPACE,
392 T.input(type="submit", value="Report!"),