3 from twisted.internet import address
4 from twisted.web import http
5 from nevow import rend, url, 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.util import idlib, log
13 from allmydata.interfaces import IFileNode
14 from allmydata.web import filenode, directory, unlinked, status, operations
15 from allmydata.web import storage
16 from allmydata.web.common import abbreviate_size, getxmlfile, WebError, \
17 get_arg, RenderMixin, get_format, get_mutable_type
20 class URIHandler(RenderMixin, rend.Page):
21 # I live at /uri . There are several operations defined on /uri itself,
22 # mostly involved with creation of unlinked files and directories.
24 def __init__(self, client):
25 rend.Page.__init__(self, client)
28 def render_GET(self, ctx):
30 uri = get_arg(req, "uri", None)
32 raise WebError("GET /uri requires uri=")
33 there = url.URL.fromContext(ctx)
34 there = there.clear("uri")
35 # I thought about escaping the childcap that we attach to the URL
36 # here, but it seems that nevow does that for us.
37 there = there.child(uri)
40 def render_PUT(self, ctx):
42 # either "PUT /uri" to create an unlinked file, or
43 # "PUT /uri?t=mkdir" to create an unlinked directory
44 t = get_arg(req, "t", "").strip()
46 file_format = get_format(req, "CHK")
47 mutable_type = get_mutable_type(file_format)
48 if mutable_type is not None:
49 return unlinked.PUTUnlinkedSSK(req, self.client, mutable_type)
51 return unlinked.PUTUnlinkedCHK(req, self.client)
53 return unlinked.PUTUnlinkedCreateDirectory(req, self.client)
54 errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
56 raise WebError(errmsg, http.BAD_REQUEST)
58 def render_POST(self, ctx):
59 # "POST /uri?t=upload&file=newfile" to upload an
60 # unlinked file or "POST /uri?t=mkdir" to create a
63 t = get_arg(req, "t", "").strip()
64 if t in ("", "upload"):
65 file_format = get_format(req)
66 mutable_type = get_mutable_type(file_format)
67 if mutable_type is not None:
68 return unlinked.POSTUnlinkedSSK(req, self.client, mutable_type)
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!"
130 class Root(rend.Page):
133 docFactory = getxmlfile("welcome.xhtml")
135 def __init__(self, client, clock=None):
136 rend.Page.__init__(self, client)
138 # If set, clock is a twisted.internet.task.Clock that the tests
139 # use to test ophandle expiration.
140 self.child_operations = operations.OphandleTable(clock)
142 s = client.getServiceNamed("storage")
145 self.child_storage = storage.StorageStatus(s, self.client.nickname)
147 self.child_uri = URIHandler(client)
148 self.child_cap = URIHandler(client)
150 self.child_file = FileHandler(client)
151 self.child_named = FileHandler(client)
152 self.child_status = status.Status(client.get_history())
153 self.child_statistics = status.Statistics(client.stats_provider)
154 static_dir = resource_filename("allmydata.web", "static")
155 for filen in os.listdir(static_dir):
156 self.putChild(filen, nevow_File(os.path.join(static_dir, filen)))
158 def child_helper_status(self, ctx):
159 # the Helper isn't attached until after the Tub starts, so this child
160 # needs to created on each request
161 return status.HelperStatus(self.client.helper)
163 child_report_incident = IncidentReporter()
164 #child_server # let's reserve this for storage-server-over-HTTP
166 # FIXME: This code is duplicated in root.py and introweb.py.
167 def data_version(self, ctx, data):
168 return get_package_versions_string()
169 def data_import_path(self, ctx, data):
170 return str(allmydata)
171 def data_my_nodeid(self, ctx, data):
172 return idlib.nodeid_b2a(self.client.nodeid)
173 def data_my_nickname(self, ctx, data):
174 return self.client.nickname
176 def render_services(self, ctx, data):
179 ss = self.client.getServiceNamed("storage")
180 stats = ss.get_stats()
181 if stats["storage_server.accepting_immutable_shares"]:
182 msg = "accepting new shares"
184 msg = "not accepting new shares (read-only)"
185 available = stats.get("storage_server.disk_avail")
186 if available is not None:
187 msg += ", %s available" % abbreviate_size(available)
188 ul[T.li[T.a(href="storage")["Storage Server"], ": ", msg]]
190 ul[T.li["Not running storage server"]]
192 if self.client.helper:
193 stats = self.client.helper.get_stats()
194 active_uploads = stats["chk_upload_helper.active_uploads"]
195 ul[T.li["Helper: %d active uploads" % (active_uploads,)]]
197 ul[T.li["Not running helper"]]
201 def data_introducer_furl_prefix(self, ctx, data):
202 ifurl = self.client.introducer_furl
203 # trim off the secret swissnum
204 (prefix, _, swissnum) = ifurl.rpartition("/")
207 if swissnum == "introducer":
210 return "%s/[censored]" % (prefix,)
212 def data_introducer_description(self, ctx, data):
213 if self.data_connected_to_introducer(ctx, data) == "no":
214 return "Introducer not connected"
217 def data_connected_to_introducer(self, ctx, data):
218 if self.client.connected_to_introducer():
222 def data_helper_furl_prefix(self, ctx, data):
224 uploader = self.client.getServiceNamed("uploader")
227 furl, connected = uploader.get_helper_info()
230 # trim off the secret swissnum
231 (prefix, _, swissnum) = furl.rpartition("/")
232 return "%s/[censored]" % (prefix,)
234 def data_helper_description(self, ctx, data):
235 if self.data_connected_to_helper(ctx, data) == "no":
236 return "Helper not connected"
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()
247 return "not-configured"
252 def data_known_storage_servers(self, ctx, data):
253 sb = self.client.get_storage_broker()
254 return len(sb.get_all_serverids())
256 def data_connected_storage_servers(self, ctx, data):
257 sb = self.client.get_storage_broker()
258 return len(sb.get_connected_servers())
260 def data_services(self, ctx, data):
261 sb = self.client.get_storage_broker()
262 return sorted(sb.get_known_servers(), key=lambda s: s.get_serverid())
264 def render_service_row(self, ctx, server):
265 nodeid = server.get_serverid()
267 ctx.fillSlots("peerid", server.get_longname())
268 ctx.fillSlots("nickname", server.get_nickname())
269 rhost = server.get_remote_host()
271 if nodeid == self.client.nodeid:
272 rhost_s = "(loopback)"
273 elif isinstance(rhost, address.IPv4Address):
274 rhost_s = "%s:%d" % (rhost.host, rhost.port)
279 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("address", addr)
291 ctx.fillSlots("connected", connected)
292 ctx.fillSlots("connected-bool", bool(rhost))
293 ctx.fillSlots("since", time.strftime(TIME_FORMAT,
294 time.localtime(since)))
295 ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
296 time.localtime(announced)))
297 ctx.fillSlots("version", version)
298 ctx.fillSlots("service_name", service_name)
302 def render_download_form(self, ctx, data):
303 # this is a form where users can download files by URI
304 form = T.form(action="uri", method="get",
305 enctype="multipart/form-data")[
307 T.legend(class_="freeform-form-label")["Download a file"],
308 T.div["Tahoe-URI to download:"+SPACE,
309 T.input(type="text", name="uri")],
310 T.div["Filename to download as:"+SPACE,
311 T.input(type="text", name="filename")],
312 T.input(type="submit", value="Download!"),
316 def render_view_form(self, ctx, data):
317 # this is a form where users can download files by URI, or jump to a
319 form = T.form(action="uri", method="get",
320 enctype="multipart/form-data")[
322 T.legend(class_="freeform-form-label")["View a file or directory"],
323 "Tahoe-URI to view:"+SPACE,
324 T.input(type="text", name="uri"), SPACE*2,
325 T.input(type="submit", value="View!"),
329 def render_upload_form(self, ctx, data):
330 # This is a form where users can upload unlinked files.
331 # Users can choose immutable, SDMF, or MDMF from a radio button.
333 upload_chk = T.input(type='radio', name='format',
334 value='chk', id='upload-chk',
336 upload_sdmf = T.input(type='radio', name='format',
337 value='sdmf', id='upload-sdmf')
338 upload_mdmf = T.input(type='radio', name='format',
339 value='mdmf', id='upload-mdmf')
341 form = T.form(action="uri", method="post",
342 enctype="multipart/form-data")[
344 T.legend(class_="freeform-form-label")["Upload a file"],
345 T.div["Choose a file:"+SPACE,
346 T.input(type="file", name="file", class_="freeform-input-file")],
347 T.input(type="hidden", name="t", value="upload"),
348 T.div[upload_chk, T.label(for_="upload-chk") [" Immutable"], SPACE,
349 upload_sdmf, T.label(for_="upload-sdmf")[" SDMF"], SPACE,
350 upload_mdmf, T.label(for_="upload-mdmf")[" MDMF (experimental)"], SPACE*2,
351 T.input(type="submit", value="Upload!")],
355 def render_mkdir_form(self, ctx, data):
356 # This is a form where users can create new directories.
357 # Users can choose SDMF or MDMF from a radio button.
359 mkdir_sdmf = T.input(type='radio', name='format',
360 value='sdmf', id='mkdir-sdmf',
362 mkdir_mdmf = T.input(type='radio', name='format',
363 value='mdmf', id='mkdir-mdmf')
365 form = T.form(action="uri", method="post",
366 enctype="multipart/form-data")[
368 T.legend(class_="freeform-form-label")["Create a directory"],
369 mkdir_sdmf, T.label(for_='mkdir-sdmf')[" SDMF"], SPACE,
370 mkdir_mdmf, T.label(for_='mkdir-mdmf')[" MDMF (experimental)"], SPACE*2,
371 T.input(type="hidden", name="t", value="mkdir"),
372 T.input(type="hidden", name="redirect_to_result", value="true"),
373 T.input(type="submit", value="Create a directory"),
377 def render_incident_button(self, ctx, data):
378 # this button triggers a foolscap-logging "incident"
379 form = T.form(action="report_incident", method="post",
380 enctype="multipart/form-data")[
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=u"Report \u00BB"),