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
10 import allmydata # to display import path
11 from allmydata import get_package_versions_string
12 from allmydata import provisioning
13 from allmydata.util import idlib, log
14 from allmydata.interfaces import IFileNode
15 from allmydata.web import filenode, directory, unlinked, status, operations
16 from allmydata.web import reliability, storage
17 from allmydata.web.common import abbreviate_size, getxmlfile, WebError, \
18 get_arg, RenderMixin, get_format, get_mutable_type
21 class URIHandler(RenderMixin, rend.Page):
22 # I live at /uri . There are several operations defined on /uri itself,
23 # mostly involved with creation of unlinked files and directories.
25 def __init__(self, client):
26 rend.Page.__init__(self, client)
29 def render_GET(self, ctx):
31 uri = get_arg(req, "uri", None)
33 raise WebError("GET /uri requires uri=")
34 there = url.URL.fromContext(ctx)
35 there = there.clear("uri")
36 # I thought about escaping the childcap that we attach to the URL
37 # here, but it seems that nevow does that for us.
38 there = there.child(uri)
41 def render_PUT(self, ctx):
43 # either "PUT /uri" to create an unlinked file, or
44 # "PUT /uri?t=mkdir" to create an unlinked directory
45 t = get_arg(req, "t", "").strip()
47 file_format = get_format(req, "CHK")
48 mutable_type = get_mutable_type(file_format)
49 if mutable_type is not None:
50 return unlinked.PUTUnlinkedSSK(req, self.client, mutable_type)
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 file_format = get_format(req)
67 mutable_type = get_mutable_type(file_format)
68 if mutable_type is not None:
69 return unlinked.POSTUnlinkedSSK(req, self.client, mutable_type)
71 return unlinked.POSTUnlinkedCHK(req, self.client)
73 return unlinked.POSTUnlinkedCreateDirectory(req, self.client)
74 elif t == "mkdir-with-children":
75 return unlinked.POSTUnlinkedCreateDirectoryWithChildren(req,
77 elif t == "mkdir-immutable":
78 return unlinked.POSTUnlinkedCreateImmutableDirectory(req,
80 errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
82 raise WebError(errmsg, http.BAD_REQUEST)
84 def childFactory(self, ctx, name):
85 # 'name' is expected to be a URI
87 node = self.client.create_node_from_uri(name)
88 return directory.make_handler_for(node, self.client)
89 except (TypeError, AssertionError):
90 raise WebError("'%s' is not a valid file- or directory- cap"
93 class FileHandler(rend.Page):
94 # I handle /file/$FILECAP[/IGNORED] , which provides a URL from which a
95 # file can be downloaded correctly by tools like "wget".
97 def __init__(self, client):
98 rend.Page.__init__(self, client)
101 def childFactory(self, ctx, name):
103 if req.method not in ("GET", "HEAD"):
104 raise WebError("/file can only be used with GET or HEAD")
105 # 'name' must be a file URI
107 node = self.client.create_node_from_uri(name)
108 except (TypeError, AssertionError):
109 # I think this can no longer be reached
110 raise WebError("'%s' is not a valid file- or directory- cap"
112 if not IFileNode.providedBy(node):
113 raise WebError("'%s' is not a file-cap" % name)
114 return filenode.FileNodeDownloadHandler(self.client, node)
116 def renderHTTP(self, ctx):
117 raise WebError("/file must be followed by a file-cap and a name",
120 class IncidentReporter(RenderMixin, rend.Page):
121 def render_POST(self, ctx):
123 log.msg(format="User reports incident through web page: %(details)s",
124 details=get_arg(req, "details", ""),
125 level=log.WEIRD, umid="LkD9Pw")
126 req.setHeader("content-type", "text/plain")
127 return "Thank you for your report!"
129 class NoReliability(rend.Page):
130 docFactory = loaders.xmlstr('''\
131 <html xmlns:n="http://nevow.com/ns/nevow/0.1">
133 <title>AllMyData - Tahoe</title>
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>
145 class Root(rend.Page):
148 docFactory = getxmlfile("welcome.xhtml")
150 def __init__(self, client, clock=None):
151 rend.Page.__init__(self, client)
153 # If set, clock is a twisted.internet.task.Clock that the tests
154 # use to test ophandle expiration.
155 self.child_operations = operations.OphandleTable(clock)
157 s = client.getServiceNamed("storage")
160 self.child_storage = storage.StorageStatus(s)
162 self.child_uri = URIHandler(client)
163 self.child_cap = URIHandler(client)
165 self.child_file = FileHandler(client)
166 self.child_named = FileHandler(client)
167 self.child_status = status.Status(client.get_history())
168 self.child_statistics = status.Statistics(client.stats_provider)
169 static_dir = resource_filename("allmydata.web", "static")
170 for filen in os.listdir(static_dir):
171 self.putChild(filen, nevow_File(os.path.join(static_dir, filen)))
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_provisioning = provisioning.ProvisioningTool()
179 if reliability.is_available():
180 child_reliability = reliability.ReliabilityTool()
182 child_reliability = NoReliability()
184 child_report_incident = IncidentReporter()
185 #child_server # let's reserve this for storage-server-over-HTTP
187 # FIXME: This code is duplicated in root.py and introweb.py.
188 def data_version(self, ctx, data):
189 return get_package_versions_string()
190 def data_import_path(self, ctx, data):
191 return str(allmydata)
192 def data_my_nodeid(self, ctx, data):
193 return idlib.nodeid_b2a(self.client.nodeid)
194 def data_my_nickname(self, ctx, data):
195 return self.client.nickname
197 def render_services(self, ctx, data):
200 ss = self.client.getServiceNamed("storage")
201 stats = ss.get_stats()
202 if stats["storage_server.accepting_immutable_shares"]:
203 msg = "accepting new shares"
205 msg = "not accepting new shares (read-only)"
206 available = stats.get("storage_server.disk_avail")
207 if available is not None:
208 msg += ", %s available" % abbreviate_size(available)
209 ul[T.li[T.a(href="storage")["Storage Server"], ": ", msg]]
211 ul[T.li["Not running storage server"]]
213 if self.client.helper:
214 stats = self.client.helper.get_stats()
215 active_uploads = stats["chk_upload_helper.active_uploads"]
216 ul[T.li["Helper: %d active uploads" % (active_uploads,)]]
218 ul[T.li["Not running helper"]]
222 def data_introducer_furl(self, ctx, data):
223 return self.client.introducer_furl
224 def data_connected_to_introducer(self, ctx, data):
225 if self.client.connected_to_introducer():
229 def data_helper_furl(self, ctx, data):
231 uploader = self.client.getServiceNamed("uploader")
234 furl, connected = uploader.get_helper_info()
236 def data_connected_to_helper(self, ctx, data):
238 uploader = self.client.getServiceNamed("uploader")
240 return "no" # we don't even have an Uploader
241 furl, connected = uploader.get_helper_info()
246 def data_known_storage_servers(self, ctx, data):
247 sb = self.client.get_storage_broker()
248 return len(sb.get_all_serverids())
250 def data_connected_storage_servers(self, ctx, data):
251 sb = self.client.get_storage_broker()
252 return len(sb.get_connected_servers())
254 def data_services(self, ctx, data):
255 sb = self.client.get_storage_broker()
256 return sorted(sb.get_known_servers(), key=lambda s: s.get_serverid())
258 def render_service_row(self, ctx, server):
259 nodeid = server.get_serverid()
261 ctx.fillSlots("peerid", server.get_longname())
262 ctx.fillSlots("nickname", server.get_nickname())
263 rhost = server.get_remote_host()
265 if nodeid == self.client.nodeid:
266 rhost_s = "(loopback)"
267 elif isinstance(rhost, address.IPv4Address):
268 rhost_s = "%s:%d" % (rhost.host, rhost.port)
271 connected = "Yes: to " + rhost_s
272 since = server.get_last_connect_time()
275 since = server.get_last_loss_time()
276 announced = server.get_announcement_time()
277 announcement = server.get_announcement()
278 version = announcement["my-version"]
279 service_name = announcement["service-name"]
281 TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
282 ctx.fillSlots("connected", connected)
283 ctx.fillSlots("connected-bool", bool(rhost))
284 ctx.fillSlots("since", time.strftime(TIME_FORMAT,
285 time.localtime(since)))
286 ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
287 time.localtime(announced)))
288 ctx.fillSlots("version", version)
289 ctx.fillSlots("service_name", service_name)
293 def render_download_form(self, ctx, data):
294 # this is a form where users can download files by URI
295 form = T.form(action="uri", method="get",
296 enctype="multipart/form-data")[
298 T.legend(class_="freeform-form-label")["Download a file"],
299 T.div["Tahoe-URI to download:"+SPACE,
300 T.input(type="text", name="uri")],
301 T.div["Filename to download as:"+SPACE,
302 T.input(type="text", name="filename")],
303 T.input(type="submit", value="Download!"),
307 def render_view_form(self, ctx, data):
308 # this is a form where users can download files by URI, or jump to a
310 form = T.form(action="uri", method="get",
311 enctype="multipart/form-data")[
313 T.legend(class_="freeform-form-label")["View a file or directory"],
314 "Tahoe-URI to view:"+SPACE,
315 T.input(type="text", name="uri"), SPACE*2,
316 T.input(type="submit", value="View!"),
320 def render_upload_form(self, ctx, data):
321 # This is a form where users can upload unlinked files.
322 # Users can choose immutable, SDMF, or MDMF from a radio button.
324 upload_chk = T.input(type='radio', name='format',
325 value='chk', id='upload-chk',
327 upload_sdmf = T.input(type='radio', name='format',
328 value='sdmf', id='upload-sdmf')
329 upload_mdmf = T.input(type='radio', name='format',
330 value='mdmf', id='upload-mdmf')
332 form = T.form(action="uri", method="post",
333 enctype="multipart/form-data")[
335 T.legend(class_="freeform-form-label")["Upload a file"],
336 T.div["Choose a file:"+SPACE,
337 T.input(type="file", name="file", class_="freeform-input-file")],
338 T.input(type="hidden", name="t", value="upload"),
339 T.div[upload_chk, T.label(for_="upload-chk") [" Immutable"], SPACE,
340 upload_sdmf, T.label(for_="upload-sdmf")[" SDMF"], SPACE,
341 upload_mdmf, T.label(for_="upload-mdmf")[" MDMF (experimental)"], SPACE*2,
342 T.input(type="submit", value="Upload!")],
346 def render_mkdir_form(self, ctx, data):
347 # This is a form where users can create new directories.
348 # Users can choose SDMF or MDMF from a radio button.
350 mkdir_sdmf = T.input(type='radio', name='format',
351 value='sdmf', id='mkdir-sdmf',
353 mkdir_mdmf = T.input(type='radio', name='format',
354 value='mdmf', id='mkdir-mdmf')
356 form = T.form(action="uri", method="post",
357 enctype="multipart/form-data")[
359 T.legend(class_="freeform-form-label")["Create a directory"],
360 mkdir_sdmf, T.label(for_='mkdir-sdmf')[" SDMF"], SPACE,
361 mkdir_mdmf, T.label(for_='mkdir-mdmf')[" MDMF (experimental)"], SPACE*2,
362 T.input(type="hidden", name="t", value="mkdir"),
363 T.input(type="hidden", name="redirect_to_result", value="true"),
364 T.input(type="submit", value="Create a directory"),
368 def render_incident_button(self, ctx, data):
369 # this button triggers a foolscap-logging "incident"
370 form = T.form(action="report_incident", method="post",
371 enctype="multipart/form-data")[
373 T.legend(class_="freeform-form-label")["Report an Incident"],
374 T.input(type="hidden", name="t", value="report-incident"),
375 "What went wrong?:"+SPACE,
376 T.input(type="text", name="details"), SPACE,
377 T.input(type="submit", value="Report!"),