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
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 return unlinked.PUTUnlinkedSSK(req, self.client)
52 return unlinked.PUTUnlinkedCHK(req, self.client)
54 return unlinked.PUTUnlinkedCreateDirectory(req, self.client)
55 errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
57 raise WebError(errmsg, http.BAD_REQUEST)
59 def render_POST(self, ctx):
60 # "POST /uri?t=upload&file=newfile" to upload an
61 # unlinked file or "POST /uri?t=mkdir" to create a
64 t = get_arg(req, "t", "").strip()
65 if t in ("", "upload"):
66 mutable = bool(get_arg(req, "mutable", "").strip())
68 return unlinked.POSTUnlinkedSSK(req, self.client)
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 "Thank you for your report!"
128 class NoReliability(rend.Page):
129 docFactory = loaders.xmlstr('''\
130 <html xmlns:n="http://nevow.com/ns/nevow/0.1">
132 <title>AllMyData - Tahoe</title>
133 <link href="/webform_css" rel="stylesheet" type="text/css"/>
134 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
137 <h2>"Reliability" page not available</h2>
138 <p>Please install the python "NumPy" module to enable this page.</p>
143 class Root(rend.Page):
146 docFactory = getxmlfile("welcome.xhtml")
148 def __init__(self, client, clock=None):
149 rend.Page.__init__(self, client)
151 # If set, clock is a twisted.internet.task.Clock that the tests
152 # use to test ophandle expiration.
153 self.child_operations = operations.OphandleTable(clock)
155 s = client.getServiceNamed("storage")
158 self.child_storage = storage.StorageStatus(s)
160 self.child_uri = URIHandler(client)
161 self.child_cap = URIHandler(client)
163 self.child_file = FileHandler(client)
164 self.child_named = FileHandler(client)
165 self.child_status = status.Status(client.get_history())
166 self.child_statistics = status.Statistics(client.stats_provider)
168 return nevow_File(resource_filename('allmydata.web', name))
169 self.putChild("download_status_timeline.js", f("download_status_timeline.js"))
170 self.putChild("jquery-1.6.1.min.js", f("jquery-1.6.1.min.js"))
171 self.putChild("protovis-3.3.1.min.js", f("protovis-3.3.1.min.js"))
173 def child_helper_status(self, ctx):
174 # the Helper isn't attached until after the Tub starts, so this child
175 # needs to created on each request
176 return status.HelperStatus(self.client.helper)
178 child_webform_css = webform.defaultCSS
179 child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
181 child_provisioning = provisioning.ProvisioningTool()
182 if reliability.is_available():
183 child_reliability = reliability.ReliabilityTool()
185 child_reliability = NoReliability()
187 child_report_incident = IncidentReporter()
188 #child_server # let's reserve this for storage-server-over-HTTP
190 # FIXME: This code is duplicated in root.py and introweb.py.
191 def data_version(self, ctx, data):
192 return get_package_versions_string()
193 def data_import_path(self, ctx, data):
194 return str(allmydata)
195 def data_my_nodeid(self, ctx, data):
196 return idlib.nodeid_b2a(self.client.nodeid)
197 def data_my_nickname(self, ctx, data):
198 return self.client.nickname
200 def render_services(self, ctx, data):
203 ss = self.client.getServiceNamed("storage")
204 stats = ss.get_stats()
205 if stats["storage_server.accepting_immutable_shares"]:
206 msg = "accepting new shares"
208 msg = "not accepting new shares (read-only)"
209 available = stats.get("storage_server.disk_avail")
210 if available is not None:
211 msg += ", %s available" % abbreviate_size(available)
212 ul[T.li[T.a(href="storage")["Storage Server"], ": ", msg]]
214 ul[T.li["Not running storage server"]]
216 if self.client.helper:
217 stats = self.client.helper.get_stats()
218 active_uploads = stats["chk_upload_helper.active_uploads"]
219 ul[T.li["Helper: %d active uploads" % (active_uploads,)]]
221 ul[T.li["Not running helper"]]
225 def data_introducer_furl(self, ctx, data):
226 return self.client.introducer_furl
227 def data_connected_to_introducer(self, ctx, data):
228 if self.client.connected_to_introducer():
232 def data_helper_furl(self, ctx, data):
234 uploader = self.client.getServiceNamed("uploader")
237 furl, connected = uploader.get_helper_info()
239 def data_connected_to_helper(self, ctx, data):
241 uploader = self.client.getServiceNamed("uploader")
243 return "no" # we don't even have an Uploader
244 furl, connected = uploader.get_helper_info()
249 def data_known_storage_servers(self, ctx, data):
250 sb = self.client.get_storage_broker()
251 return len(sb.get_all_serverids())
253 def data_connected_storage_servers(self, ctx, data):
254 sb = self.client.get_storage_broker()
255 return len(sb.get_connected_servers())
257 def data_services(self, ctx, data):
258 sb = self.client.get_storage_broker()
259 return sorted(sb.get_known_servers(), key=lambda s: s.get_serverid())
261 def render_service_row(self, ctx, server):
262 nodeid = server.get_serverid()
264 ctx.fillSlots("peerid", server.get_longname())
265 ctx.fillSlots("nickname", server.get_nickname())
266 rhost = server.get_remote_host()
268 if nodeid == self.client.nodeid:
269 rhost_s = "(loopback)"
270 elif isinstance(rhost, address.IPv4Address):
271 rhost_s = "%s:%d" % (rhost.host, rhost.port)
274 connected = "Yes: to " + rhost_s
275 since = server.get_last_connect_time()
278 since = server.get_last_loss_time()
279 announced = server.get_announcement_time()
280 announcement = server.get_announcement()
281 version = announcement["my-version"]
282 service_name = announcement["service-name"]
284 TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
285 ctx.fillSlots("connected", connected)
286 ctx.fillSlots("connected-bool", bool(rhost))
287 ctx.fillSlots("since", time.strftime(TIME_FORMAT,
288 time.localtime(since)))
289 ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
290 time.localtime(announced)))
291 ctx.fillSlots("version", version)
292 ctx.fillSlots("service_name", service_name)
296 def render_download_form(self, ctx, data):
297 # this is a form where users can download files by URI
298 form = T.form(action="uri", method="get",
299 enctype="multipart/form-data")[
301 T.legend(class_="freeform-form-label")["Download a file"],
302 T.div["Tahoe-URI to download: ",
303 T.input(type="text", name="uri")],
304 T.div["Filename to download as: ",
305 T.input(type="text", name="filename")],
306 T.input(type="submit", value="Download!"),
310 def render_view_form(self, ctx, data):
311 # this is a form where users can download files by URI, or jump to a
313 form = T.form(action="uri", method="get",
314 enctype="multipart/form-data")[
316 T.legend(class_="freeform-form-label")["View a file or directory"],
317 "Tahoe-URI to view: ",
318 T.input(type="text", name="uri"), " ",
319 T.input(type="submit", value="View!"),
323 def render_upload_form(self, ctx, data):
324 # this is a form where users can upload unlinked files
325 form = T.form(action="uri", method="post",
326 enctype="multipart/form-data")[
328 T.legend(class_="freeform-form-label")["Upload a file"],
329 T.div["Choose a file: ",
330 T.input(type="file", name="file", class_="freeform-input-file")],
331 T.input(type="hidden", name="t", value="upload"),
332 T.div[T.input(type="checkbox", name="mutable"), T.label(for_="mutable")["Create mutable file"],
333 " ", T.input(type="submit", value="Upload!")],
337 def render_mkdir_form(self, ctx, data):
338 # this is a form where users can create new directories
339 form = T.form(action="uri", method="post",
340 enctype="multipart/form-data")[
342 T.legend(class_="freeform-form-label")["Create a directory"],
343 T.input(type="hidden", name="t", value="mkdir"),
344 T.input(type="hidden", name="redirect_to_result", value="true"),
345 T.input(type="submit", value="Create a directory"),
349 def render_incident_button(self, ctx, data):
350 # this button triggers a foolscap-logging "incident"
351 form = T.form(action="report_incident", method="post",
352 enctype="multipart/form-data")[
354 T.legend(class_="freeform-form-label")["Report an Incident"],
355 T.input(type="hidden", name="t", value="report-incident"),
356 "What went wrong?: ",
357 T.input(type="text", name="details"), " ",
358 T.input(type="submit", value="Report!"),