]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/web/root.py
Add t=mkdir-immutable to the webapi. Closes #607.
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / web / root.py
1 import time
2
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
9 from formless import webform
10
11 import allmydata # to display import path
12 from allmydata import get_package_versions_string
13 from allmydata import provisioning
14 from allmydata.util import idlib, log
15 from allmydata.interfaces import IFileNode, UnhandledCapTypeError
16 from allmydata.web import filenode, directory, unlinked, status, operations
17 from allmydata.web import reliability, storage
18 from allmydata.web.common import abbreviate_size, getxmlfile, WebError, \
19      get_arg, RenderMixin, boolean_of_arg
20
21
22 class URIHandler(RenderMixin, rend.Page):
23     # I live at /uri . There are several operations defined on /uri itself,
24     # mostly involved with creation of unlinked files and directories.
25
26     def __init__(self, client):
27         rend.Page.__init__(self, client)
28         self.client = client
29
30     def render_GET(self, ctx):
31         req = IRequest(ctx)
32         uri = get_arg(req, "uri", None)
33         if uri is None:
34             raise WebError("GET /uri requires uri=")
35         there = url.URL.fromContext(ctx)
36         there = there.clear("uri")
37         # I thought about escaping the childcap that we attach to the URL
38         # here, but it seems that nevow does that for us.
39         there = there.child(uri)
40         return there
41
42     def render_PUT(self, ctx):
43         req = IRequest(ctx)
44         # either "PUT /uri" to create an unlinked file, or
45         # "PUT /uri?t=mkdir" to create an unlinked directory
46         t = get_arg(req, "t", "").strip()
47         if t == "":
48             mutable = boolean_of_arg(get_arg(req, "mutable", "false").strip())
49             if mutable:
50                 return unlinked.PUTUnlinkedSSK(req, self.client)
51             else:
52                 return unlinked.PUTUnlinkedCHK(req, self.client)
53         if t == "mkdir":
54             return unlinked.PUTUnlinkedCreateDirectory(req, self.client)
55         errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
56                   "and POST?t=mkdir")
57         raise WebError(errmsg, http.BAD_REQUEST)
58
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
62         # new directory
63         req = IRequest(ctx)
64         t = get_arg(req, "t", "").strip()
65         if t in ("", "upload"):
66             mutable = bool(get_arg(req, "mutable", "").strip())
67             if mutable:
68                 return unlinked.POSTUnlinkedSSK(req, self.client)
69             else:
70                 return unlinked.POSTUnlinkedCHK(req, self.client)
71         if t == "mkdir":
72             return unlinked.POSTUnlinkedCreateDirectory(req, self.client)
73         elif t == "mkdir-with-children":
74             return unlinked.POSTUnlinkedCreateDirectoryWithChildren(req,
75                                                                     self.client)
76         elif t == "mkdir-immutable":
77             return unlinked.POSTUnlinkedCreateImmutableDirectory(req,
78                                                                  self.client)
79         errmsg = ("/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, "
80                   "and POST?t=mkdir")
81         raise WebError(errmsg, http.BAD_REQUEST)
82
83     def childFactory(self, ctx, name):
84         # 'name' is expected to be a URI
85         try:
86             node = self.client.create_node_from_uri(name)
87             return directory.make_handler_for(node, self.client)
88         except (TypeError, UnhandledCapTypeError, AssertionError):
89             raise WebError("'%s' is not a valid file- or directory- cap"
90                            % name)
91
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".
95
96     def __init__(self, client):
97         rend.Page.__init__(self, client)
98         self.client = client
99
100     def childFactory(self, ctx, name):
101         req = IRequest(ctx)
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
105         try:
106             node = self.client.create_node_from_uri(name)
107         except (TypeError, UnhandledCapTypeError, AssertionError):
108             # I think this can no longer be reached
109             raise WebError("'%s' is not a valid file- or directory- cap"
110                            % name)
111         if not IFileNode.providedBy(node):
112             raise WebError("'%s' is not a file-cap" % name)
113         return filenode.FileNodeDownloadHandler(self.client, node)
114
115     def renderHTTP(self, ctx):
116         raise WebError("/file must be followed by a file-cap and a name",
117                        http.NOT_FOUND)
118
119 class IncidentReporter(RenderMixin, rend.Page):
120     def render_POST(self, ctx):
121         req = IRequest(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!"
127
128 class NoReliability(rend.Page):
129     docFactory = loaders.xmlstr('''\
130 <html xmlns:n="http://nevow.com/ns/nevow/0.1">
131   <head>
132     <title>AllMyData - Tahoe</title>
133     <link href="/webform_css" rel="stylesheet" type="text/css"/>
134     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
135   </head>
136   <body>
137   <h2>"Reliability" page not available</h2>
138   <p>Please install the python "NumPy" module to enable this page.</p>
139   </body>
140 </html>
141 ''')
142
143 class Root(rend.Page):
144
145     addSlash = True
146     docFactory = getxmlfile("welcome.xhtml")
147
148     def __init__(self, client):
149         rend.Page.__init__(self, client)
150         self.client = client
151         self.child_operations = operations.OphandleTable()
152         try:
153             s = client.getServiceNamed("storage")
154         except KeyError:
155             s = None
156         self.child_storage = storage.StorageStatus(s)
157
158         self.child_uri = URIHandler(client)
159         self.child_cap = URIHandler(client)
160
161         self.child_file = FileHandler(client)
162         self.child_named = FileHandler(client)
163         self.child_status = status.Status(client.get_history())
164         self.child_statistics = status.Statistics(client.stats_provider)
165
166     def child_helper_status(self, ctx):
167         # the Helper isn't attached until after the Tub starts, so this child
168         # needs to created on each request
169         return status.HelperStatus(self.client.helper)
170
171     child_webform_css = webform.defaultCSS
172     child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
173
174     child_provisioning = provisioning.ProvisioningTool()
175     if reliability.is_available():
176         child_reliability = reliability.ReliabilityTool()
177     else:
178         child_reliability = NoReliability()
179
180     child_report_incident = IncidentReporter()
181     #child_server # let's reserve this for storage-server-over-HTTP
182
183     # FIXME: This code is duplicated in root.py and introweb.py.
184     def data_version(self, ctx, data):
185         return get_package_versions_string()
186     def data_import_path(self, ctx, data):
187         return str(allmydata)
188     def data_my_nodeid(self, ctx, data):
189         return idlib.nodeid_b2a(self.client.nodeid)
190     def data_my_nickname(self, ctx, data):
191         return self.client.nickname
192
193     def render_services(self, ctx, data):
194         ul = T.ul()
195         try:
196             ss = self.client.getServiceNamed("storage")
197             stats = ss.get_stats()
198             if stats["storage_server.accepting_immutable_shares"]:
199                 msg = "accepting new shares"
200             else:
201                 msg = "not accepting new shares (read-only)"
202             available = stats.get("storage_server.disk_avail")
203             if available is not None:
204                 msg += ", %s available" % abbreviate_size(available)
205             ul[T.li[T.a(href="storage")["Storage Server"], ": ", msg]]
206         except KeyError:
207             ul[T.li["Not running storage server"]]
208
209         if self.client.helper:
210             stats = self.client.helper.get_stats()
211             active_uploads = stats["chk_upload_helper.active_uploads"]
212             ul[T.li["Helper: %d active uploads" % (active_uploads,)]]
213         else:
214             ul[T.li["Not running helper"]]
215
216         return ctx.tag[ul]
217
218     def data_introducer_furl(self, ctx, data):
219         return self.client.introducer_furl
220     def data_connected_to_introducer(self, ctx, data):
221         if self.client.connected_to_introducer():
222             return "yes"
223         return "no"
224
225     def data_helper_furl(self, ctx, data):
226         try:
227             uploader = self.client.getServiceNamed("uploader")
228         except KeyError:
229             return None
230         furl, connected = uploader.get_helper_info()
231         return furl
232     def data_connected_to_helper(self, ctx, data):
233         try:
234             uploader = self.client.getServiceNamed("uploader")
235         except KeyError:
236             return "no" # we don't even have an Uploader
237         furl, connected = uploader.get_helper_info()
238         if connected:
239             return "yes"
240         return "no"
241
242     def data_known_storage_servers(self, ctx, data):
243         sb = self.client.get_storage_broker()
244         return len(sb.get_all_serverids())
245
246     def data_connected_storage_servers(self, ctx, data):
247         sb = self.client.get_storage_broker()
248         return len(sb.get_all_servers())
249
250     def data_services(self, ctx, data):
251         sb = self.client.get_storage_broker()
252         return sb.get_all_descriptors()
253
254     def render_service_row(self, ctx, descriptor):
255         nodeid = descriptor.get_serverid()
256
257         ctx.fillSlots("peerid", idlib.nodeid_b2a(nodeid))
258         ctx.fillSlots("nickname", descriptor.get_nickname())
259         rhost = descriptor.get_remote_host()
260         if rhost:
261             if nodeid == self.client.nodeid:
262                 rhost_s = "(loopback)"
263             elif isinstance(rhost, address.IPv4Address):
264                 rhost_s = "%s:%d" % (rhost.host, rhost.port)
265             else:
266                 rhost_s = str(rhost)
267             connected = "Yes: to " + rhost_s
268             since = descriptor.get_last_connect_time()
269         else:
270             connected = "No"
271             since = descriptor.get_last_loss_time()
272         announced = descriptor.get_announcement_time()
273         announcement = descriptor.get_announcement()
274         version = announcement["version"]
275         service_name = announcement["service-name"]
276
277         TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
278         ctx.fillSlots("connected", connected)
279         ctx.fillSlots("connected-bool", bool(rhost))
280         ctx.fillSlots("since", time.strftime(TIME_FORMAT,
281                                              time.localtime(since)))
282         ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
283                                                  time.localtime(announced)))
284         ctx.fillSlots("version", version)
285         ctx.fillSlots("service_name", service_name)
286
287         return ctx.tag
288
289     def render_download_form(self, ctx, data):
290         # this is a form where users can download files by URI
291         form = T.form(action="uri", method="get",
292                       enctype="multipart/form-data")[
293             T.fieldset[
294             T.legend(class_="freeform-form-label")["Download a file"],
295             T.div["Tahoe-URI to download: ",
296                   T.input(type="text", name="uri")],
297             T.div["Filename to download as: ",
298                   T.input(type="text", name="filename")],
299             T.input(type="submit", value="Download!"),
300             ]]
301         return T.div[form]
302
303     def render_view_form(self, ctx, data):
304         # this is a form where users can download files by URI, or jump to a
305         # named directory
306         form = T.form(action="uri", method="get",
307                       enctype="multipart/form-data")[
308             T.fieldset[
309             T.legend(class_="freeform-form-label")["View a file or directory"],
310             "Tahoe-URI to view: ",
311             T.input(type="text", name="uri"), " ",
312             T.input(type="submit", value="View!"),
313             ]]
314         return T.div[form]
315
316     def render_upload_form(self, ctx, data):
317         # this is a form where users can upload unlinked files
318         form = T.form(action="uri", method="post",
319                       enctype="multipart/form-data")[
320             T.fieldset[
321             T.legend(class_="freeform-form-label")["Upload a file"],
322             T.div["Choose a file: ",
323                   T.input(type="file", name="file", class_="freeform-input-file")],
324             T.input(type="hidden", name="t", value="upload"),
325             T.div[T.input(type="checkbox", name="mutable"), T.label(for_="mutable")["Create mutable file"],
326                   " ", T.input(type="submit", value="Upload!")],
327             ]]
328         return T.div[form]
329
330     def render_mkdir_form(self, ctx, data):
331         # this is a form where users can create new directories
332         form = T.form(action="uri", method="post",
333                       enctype="multipart/form-data")[
334             T.fieldset[
335             T.legend(class_="freeform-form-label")["Create a directory"],
336             T.input(type="hidden", name="t", value="mkdir"),
337             T.input(type="hidden", name="redirect_to_result", value="true"),
338             T.input(type="submit", value="Create a directory"),
339             ]]
340         return T.div[form]
341
342     def render_incident_button(self, ctx, data):
343         # this button triggers a foolscap-logging "incident"
344         form = T.form(action="report_incident", method="post",
345                       enctype="multipart/form-data")[
346             T.fieldset[
347             T.legend(class_="freeform-form-label")["Report an Incident"],
348             T.input(type="hidden", name="t", value="report-incident"),
349             "What went wrong?: ",
350             T.input(type="text", name="details"), " ",
351             T.input(type="submit", value="Report!"),
352             ]]
353         return T.div[form]