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_introducer_description(self, ctx, data):
205 if self.data_connected_to_introducer(ctx, data) == "no":
206 return "Introducer not connected"
209 def data_connected_to_introducer(self, ctx, data):
210 if self.client.connected_to_introducer():
214 def data_helper_furl(self, ctx, data):
216 uploader = self.client.getServiceNamed("uploader")
219 furl, connected = uploader.get_helper_info()
222 def data_helper_description(self, ctx, data):
223 if self.data_connected_to_helper(ctx, data) == "no":
224 return "Helper not connected"
227 def data_connected_to_helper(self, ctx, data):
229 uploader = self.client.getServiceNamed("uploader")
231 return "no" # we don't even have an Uploader
232 furl, connected = uploader.get_helper_info()
235 return "not-configured"
240 def data_known_storage_servers(self, ctx, data):
241 sb = self.client.get_storage_broker()
242 return len(sb.get_all_serverids())
244 def data_connected_storage_servers(self, ctx, data):
245 sb = self.client.get_storage_broker()
246 return len(sb.get_connected_servers())
248 def data_services(self, ctx, data):
249 sb = self.client.get_storage_broker()
250 return sorted(sb.get_known_servers(), key=lambda s: s.get_serverid())
252 def render_service_row(self, ctx, server):
253 nodeid = server.get_serverid()
255 ctx.fillSlots("peerid", server.get_longname())
256 ctx.fillSlots("nickname", server.get_nickname())
257 rhost = server.get_remote_host()
259 if nodeid == self.client.nodeid:
260 rhost_s = "(loopback)"
261 elif isinstance(rhost, address.IPv4Address):
262 rhost_s = "%s:%d" % (rhost.host, rhost.port)
267 since = server.get_last_connect_time()
271 since = server.get_last_loss_time()
272 announced = server.get_announcement_time()
273 announcement = server.get_announcement()
274 version = announcement["my-version"]
275 service_name = announcement["service-name"]
277 TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
278 ctx.fillSlots("address", addr)
279 ctx.fillSlots("connected", connected)
280 ctx.fillSlots("connected-bool", bool(rhost))
281 ctx.fillSlots("since", time.strftime(TIME_FORMAT,
282 time.localtime(since)))
283 ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
284 time.localtime(announced)))
285 ctx.fillSlots("version", version)
286 ctx.fillSlots("service_name", service_name)
290 def render_download_form(self, ctx, data):
291 # this is a form where users can download files by URI
292 form = T.form(action="uri", method="get",
293 enctype="multipart/form-data")[
295 T.legend(class_="freeform-form-label")["Download a file"],
296 T.div["Tahoe-URI to download:"+SPACE,
297 T.input(type="text", name="uri")],
298 T.div["Filename to download as:"+SPACE,
299 T.input(type="text", name="filename")],
300 T.input(type="submit", value="Download!"),
304 def render_view_form(self, ctx, data):
305 # this is a form where users can download files by URI, or jump to a
307 form = T.form(action="uri", method="get",
308 enctype="multipart/form-data")[
310 T.legend(class_="freeform-form-label")["View a file or directory"],
311 "Tahoe-URI to view:"+SPACE,
312 T.input(type="text", name="uri"), SPACE*2,
313 T.input(type="submit", value="View!"),
317 def render_upload_form(self, ctx, data):
318 # This is a form where users can upload unlinked files.
319 # Users can choose immutable, SDMF, or MDMF from a radio button.
321 upload_chk = T.input(type='radio', name='format',
322 value='chk', id='upload-chk',
324 upload_sdmf = T.input(type='radio', name='format',
325 value='sdmf', id='upload-sdmf')
326 upload_mdmf = T.input(type='radio', name='format',
327 value='mdmf', id='upload-mdmf')
329 form = T.form(action="uri", method="post",
330 enctype="multipart/form-data")[
332 T.legend(class_="freeform-form-label")["Upload a file"],
333 T.div["Choose a file:"+SPACE,
334 T.input(type="file", name="file", class_="freeform-input-file")],
335 T.input(type="hidden", name="t", value="upload"),
336 T.div[upload_chk, T.label(for_="upload-chk") [" Immutable"], SPACE,
337 upload_sdmf, T.label(for_="upload-sdmf")[" SDMF"], SPACE,
338 upload_mdmf, T.label(for_="upload-mdmf")[" MDMF (experimental)"], SPACE*2,
339 T.input(type="submit", value="Upload!")],
343 def render_mkdir_form(self, ctx, data):
344 # This is a form where users can create new directories.
345 # Users can choose SDMF or MDMF from a radio button.
347 mkdir_sdmf = T.input(type='radio', name='format',
348 value='sdmf', id='mkdir-sdmf',
350 mkdir_mdmf = T.input(type='radio', name='format',
351 value='mdmf', id='mkdir-mdmf')
353 form = T.form(action="uri", method="post",
354 enctype="multipart/form-data")[
356 T.legend(class_="freeform-form-label")["Create a directory"],
357 mkdir_sdmf, T.label(for_='mkdir-sdmf')[" SDMF"], SPACE,
358 mkdir_mdmf, T.label(for_='mkdir-mdmf')[" MDMF (experimental)"], SPACE*2,
359 T.input(type="hidden", name="t", value="mkdir"),
360 T.input(type="hidden", name="redirect_to_result", value="true"),
361 T.input(type="submit", value="Create a directory"),
365 def render_incident_button(self, ctx, data):
366 # this button triggers a foolscap-logging "incident"
367 form = T.form(action="report_incident", method="post",
368 enctype="multipart/form-data")[
370 T.input(type="hidden", name="t", value="report-incident"),
371 "What went wrong?"+SPACE,
372 T.input(type="text", name="details"), SPACE,
373 T.input(type="submit", value=u"Report \u00BB"),