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
203 def data_connected_to_introducer(self, ctx, data):
204 if self.client.connected_to_introducer():
208 def data_helper_furl(self, ctx, data):
210 uploader = self.client.getServiceNamed("uploader")
213 furl, connected = uploader.get_helper_info()
215 def data_connected_to_helper(self, ctx, data):
217 uploader = self.client.getServiceNamed("uploader")
219 return "no" # we don't even have an Uploader
220 furl, connected = uploader.get_helper_info()
225 def data_known_storage_servers(self, ctx, data):
226 sb = self.client.get_storage_broker()
227 return len(sb.get_all_serverids())
229 def data_connected_storage_servers(self, ctx, data):
230 sb = self.client.get_storage_broker()
231 return len(sb.get_connected_servers())
233 def data_services(self, ctx, data):
234 sb = self.client.get_storage_broker()
235 return sorted(sb.get_known_servers(), key=lambda s: s.get_serverid())
237 def render_service_row(self, ctx, server):
238 nodeid = server.get_serverid()
240 ctx.fillSlots("peerid", server.get_longname())
241 ctx.fillSlots("nickname", server.get_nickname())
242 rhost = server.get_remote_host()
244 if nodeid == self.client.nodeid:
245 rhost_s = "(loopback)"
246 elif isinstance(rhost, address.IPv4Address):
247 rhost_s = "%s:%d" % (rhost.host, rhost.port)
250 connected = "Yes: to " + rhost_s
251 since = server.get_last_connect_time()
254 since = server.get_last_loss_time()
255 announced = server.get_announcement_time()
256 announcement = server.get_announcement()
257 version = announcement["my-version"]
258 service_name = announcement["service-name"]
260 TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
261 ctx.fillSlots("connected", connected)
262 ctx.fillSlots("connected-bool", bool(rhost))
263 ctx.fillSlots("since", time.strftime(TIME_FORMAT,
264 time.localtime(since)))
265 ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
266 time.localtime(announced)))
267 ctx.fillSlots("version", version)
268 ctx.fillSlots("service_name", service_name)
272 def render_download_form(self, ctx, data):
273 # this is a form where users can download files by URI
274 form = T.form(action="uri", method="get",
275 enctype="multipart/form-data")[
277 T.legend(class_="freeform-form-label")["Download a file"],
278 T.div["Tahoe-URI to download:"+SPACE,
279 T.input(type="text", name="uri")],
280 T.div["Filename to download as:"+SPACE,
281 T.input(type="text", name="filename")],
282 T.input(type="submit", value="Download!"),
286 def render_view_form(self, ctx, data):
287 # this is a form where users can download files by URI, or jump to a
289 form = T.form(action="uri", method="get",
290 enctype="multipart/form-data")[
292 T.legend(class_="freeform-form-label")["View a file or directory"],
293 "Tahoe-URI to view:"+SPACE,
294 T.input(type="text", name="uri"), SPACE*2,
295 T.input(type="submit", value="View!"),
299 def render_upload_form(self, ctx, data):
300 # This is a form where users can upload unlinked files.
301 # Users can choose immutable, SDMF, or MDMF from a radio button.
303 upload_chk = T.input(type='radio', name='format',
304 value='chk', id='upload-chk',
306 upload_sdmf = T.input(type='radio', name='format',
307 value='sdmf', id='upload-sdmf')
308 upload_mdmf = T.input(type='radio', name='format',
309 value='mdmf', id='upload-mdmf')
311 form = T.form(action="uri", method="post",
312 enctype="multipart/form-data")[
314 T.legend(class_="freeform-form-label")["Upload a file"],
315 T.div["Choose a file:"+SPACE,
316 T.input(type="file", name="file", class_="freeform-input-file")],
317 T.input(type="hidden", name="t", value="upload"),
318 T.div[upload_chk, T.label(for_="upload-chk") [" Immutable"], SPACE,
319 upload_sdmf, T.label(for_="upload-sdmf")[" SDMF"], SPACE,
320 upload_mdmf, T.label(for_="upload-mdmf")[" MDMF (experimental)"], SPACE*2,
321 T.input(type="submit", value="Upload!")],
325 def render_mkdir_form(self, ctx, data):
326 # This is a form where users can create new directories.
327 # Users can choose SDMF or MDMF from a radio button.
329 mkdir_sdmf = T.input(type='radio', name='format',
330 value='sdmf', id='mkdir-sdmf',
332 mkdir_mdmf = T.input(type='radio', name='format',
333 value='mdmf', id='mkdir-mdmf')
335 form = T.form(action="uri", method="post",
336 enctype="multipart/form-data")[
338 T.legend(class_="freeform-form-label")["Create a directory"],
339 mkdir_sdmf, T.label(for_='mkdir-sdmf')[" SDMF"], SPACE,
340 mkdir_mdmf, T.label(for_='mkdir-mdmf')[" MDMF (experimental)"], SPACE*2,
341 T.input(type="hidden", name="t", value="mkdir"),
342 T.input(type="hidden", name="redirect_to_result", value="true"),
343 T.input(type="submit", value="Create a directory"),
347 def render_incident_button(self, ctx, data):
348 # this button triggers a foolscap-logging "incident"
349 form = T.form(action="report_incident", method="post",
350 enctype="multipart/form-data")[
352 T.legend(class_="freeform-form-label")["Report an Incident"],
353 T.input(type="hidden", name="t", value="report-incident"),
354 "What went wrong?:"+SPACE,
355 T.input(type="text", name="details"), SPACE,
356 T.input(type="submit", value="Report!"),