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(self, ctx, data):
202 return self.client.introducer_furl
204 def data_connected_to_introducer(self, ctx, data):
205 if self.client.connected_to_introducer():
209 def data_helper_furl(self, ctx, data):
211 uploader = self.client.getServiceNamed("uploader")
214 furl, connected = uploader.get_helper_info()
217 def data_connected_to_helper_description(self, ctx, data):
218 return self.data_connected_to_helper(ctx, data).replace('-', ' ')
220 def data_connected_to_helper(self, ctx, data):
222 uploader = self.client.getServiceNamed("uploader")
224 return "no" # we don't even have an Uploader
225 furl, connected = uploader.get_helper_info()
228 return "not-configured"
233 def data_known_storage_servers(self, ctx, data):
234 sb = self.client.get_storage_broker()
235 return len(sb.get_all_serverids())
237 def data_connected_storage_servers(self, ctx, data):
238 sb = self.client.get_storage_broker()
239 return len(sb.get_connected_servers())
241 def data_services(self, ctx, data):
242 sb = self.client.get_storage_broker()
243 return sorted(sb.get_known_servers(), key=lambda s: s.get_serverid())
245 def render_service_row(self, ctx, server):
246 nodeid = server.get_serverid()
248 ctx.fillSlots("peerid", server.get_longname())
249 ctx.fillSlots("nickname", server.get_nickname())
250 rhost = server.get_remote_host()
252 if nodeid == self.client.nodeid:
253 rhost_s = "(loopback)"
254 elif isinstance(rhost, address.IPv4Address):
255 rhost_s = "%s:%d" % (rhost.host, rhost.port)
260 since = server.get_last_connect_time()
264 since = server.get_last_loss_time()
265 announced = server.get_announcement_time()
266 announcement = server.get_announcement()
267 version = announcement["my-version"]
268 service_name = announcement["service-name"]
270 TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
271 ctx.fillSlots("address", addr)
272 ctx.fillSlots("connected", connected)
273 ctx.fillSlots("connected-bool", bool(rhost))
274 ctx.fillSlots("since", time.strftime(TIME_FORMAT,
275 time.localtime(since)))
276 ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
277 time.localtime(announced)))
278 ctx.fillSlots("version", version)
279 ctx.fillSlots("service_name", service_name)
283 def render_download_form(self, ctx, data):
284 # this is a form where users can download files by URI
285 form = T.form(action="uri", method="get",
286 enctype="multipart/form-data")[
288 T.legend(class_="freeform-form-label")["Download a file"],
289 T.div["Tahoe-URI to download:"+SPACE,
290 T.input(type="text", name="uri")],
291 T.div["Filename to download as:"+SPACE,
292 T.input(type="text", name="filename")],
293 T.input(type="submit", value="Download!"),
297 def render_view_form(self, ctx, data):
298 # this is a form where users can download files by URI, or jump to a
300 form = T.form(action="uri", method="get",
301 enctype="multipart/form-data")[
303 T.legend(class_="freeform-form-label")["View a file or directory"],
304 "Tahoe-URI to view:"+SPACE,
305 T.input(type="text", name="uri"), SPACE*2,
306 T.input(type="submit", value="View!"),
310 def render_upload_form(self, ctx, data):
311 # This is a form where users can upload unlinked files.
312 # Users can choose immutable, SDMF, or MDMF from a radio button.
314 upload_chk = T.input(type='radio', name='format',
315 value='chk', id='upload-chk',
317 upload_sdmf = T.input(type='radio', name='format',
318 value='sdmf', id='upload-sdmf')
319 upload_mdmf = T.input(type='radio', name='format',
320 value='mdmf', id='upload-mdmf')
322 form = T.form(action="uri", method="post",
323 enctype="multipart/form-data")[
325 T.legend(class_="freeform-form-label")["Upload a file"],
326 T.div["Choose a file:"+SPACE,
327 T.input(type="file", name="file", class_="freeform-input-file")],
328 T.input(type="hidden", name="t", value="upload"),
329 T.div[upload_chk, T.label(for_="upload-chk") [" Immutable"], SPACE,
330 upload_sdmf, T.label(for_="upload-sdmf")[" SDMF"], SPACE,
331 upload_mdmf, T.label(for_="upload-mdmf")[" MDMF (experimental)"], SPACE*2,
332 T.input(type="submit", value="Upload!")],
336 def render_mkdir_form(self, ctx, data):
337 # This is a form where users can create new directories.
338 # Users can choose SDMF or MDMF from a radio button.
340 mkdir_sdmf = T.input(type='radio', name='format',
341 value='sdmf', id='mkdir-sdmf',
343 mkdir_mdmf = T.input(type='radio', name='format',
344 value='mdmf', id='mkdir-mdmf')
346 form = T.form(action="uri", method="post",
347 enctype="multipart/form-data")[
349 T.legend(class_="freeform-form-label")["Create a directory"],
350 mkdir_sdmf, T.label(for_='mkdir-sdmf')[" SDMF"], SPACE,
351 mkdir_mdmf, T.label(for_='mkdir-mdmf')[" MDMF (experimental)"], SPACE*2,
352 T.input(type="hidden", name="t", value="mkdir"),
353 T.input(type="hidden", name="redirect_to_result", value="true"),
354 T.input(type="submit", value="Create a directory"),
358 def render_incident_button(self, ctx, data):
359 # this button triggers a foolscap-logging "incident"
360 form = T.form(action="report_incident", method="post",
361 enctype="multipart/form-data")[
363 T.input(type="hidden", name="t", value="report-incident"),
364 "What went wrong?"+SPACE,
365 T.input(type="text", name="details"), SPACE,
366 T.input(type="submit", value=u"Report \u00BB"),