]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/webish.py
e8a9a6f0da5adc5da413083e0e46a79464e432be
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / webish.py
1
2 import time, os.path
3 from twisted.application import service, strports, internet
4 from twisted.web import static, resource, server, html, http
5 from twisted.internet import defer, address
6 from twisted.internet.interfaces import IConsumer
7 from nevow import inevow, rend, appserver, url, tags as T
8 from nevow.static import File as nevow_File # TODO: merge with static.File?
9 from allmydata.util import fileutil, idlib, log
10 import simplejson
11 from allmydata.interfaces import IDownloadTarget, IDirectoryNode, IFileNode, \
12      IMutableFileNode
13 import allmydata # to display import path
14 from allmydata import download
15 from allmydata.upload import FileHandle, FileName
16 from allmydata import provisioning
17 from allmydata import get_package_versions_string
18 from zope.interface import implements, Interface
19 import urllib
20 from formless import webform
21 from foolscap.eventual import fireEventually
22
23 from nevow.util import resource_filename
24
25 from allmydata.web import status, unlinked
26 from allmydata.web.common import IClient, getxmlfile, get_arg, boolean_of_arg
27
28 class ILocalAccess(Interface):
29     def local_access_is_allowed():
30         """Return True if t=upload&localdir= is allowed, giving anyone who
31         can talk to the webserver control over the local (disk) filesystem."""
32
33 # we must override twisted.web.http.Request.requestReceived with a version
34 # that doesn't use cgi.parse_multipart() . Since we actually use Nevow, we
35 # override the nevow-specific subclass, nevow.appserver.NevowRequest . This
36 # is an exact copy of twisted.web.http.Request (from SVN HEAD on 10-Aug-2007)
37 # that modifies the way form arguments are parsed. Note that this sort of
38 # surgery may induce a dependency upon a particular version of twisted.web
39
40 parse_qs = http.parse_qs
41 class MyRequest(appserver.NevowRequest):
42     fields = None
43     def requestReceived(self, command, path, version):
44         """Called by channel when all data has been received.
45
46         This method is not intended for users.
47         """
48         self.content.seek(0,0)
49         self.args = {}
50         self.stack = []
51
52         self.method, self.uri = command, path
53         self.clientproto = version
54         x = self.uri.split('?', 1)
55
56         if len(x) == 1:
57             self.path = self.uri
58         else:
59             self.path, argstring = x
60             self.args = parse_qs(argstring, 1)
61
62         # cache the client and server information, we'll need this later to be
63         # serialized and sent with the request so CGIs will work remotely
64         self.client = self.channel.transport.getPeer()
65         self.host = self.channel.transport.getHost()
66
67         # Argument processing.
68
69 ##      The original twisted.web.http.Request.requestReceived code parsed the
70 ##      content and added the form fields it found there to self.args . It
71 ##      did this with cgi.parse_multipart, which holds the arguments in RAM
72 ##      and is thus unsuitable for large file uploads. The Nevow subclass
73 ##      (nevow.appserver.NevowRequest) uses cgi.FieldStorage instead (putting
74 ##      the results in self.fields), which is much more memory-efficient.
75 ##      Since we know we're using Nevow, we can anticipate these arguments
76 ##      appearing in self.fields instead of self.args, and thus skip the
77 ##      parse-content-into-self.args step.
78
79 ##      args = self.args
80 ##      ctype = self.getHeader('content-type')
81 ##      if self.method == "POST" and ctype:
82 ##          mfd = 'multipart/form-data'
83 ##          key, pdict = cgi.parse_header(ctype)
84 ##          if key == 'application/x-www-form-urlencoded':
85 ##              args.update(parse_qs(self.content.read(), 1))
86 ##          elif key == mfd:
87 ##              try:
88 ##                  args.update(cgi.parse_multipart(self.content, pdict))
89 ##              except KeyError, e:
90 ##                  if e.args[0] == 'content-disposition':
91 ##                      # Parse_multipart can't cope with missing
92 ##                      # content-dispostion headers in multipart/form-data
93 ##                      # parts, so we catch the exception and tell the client
94 ##                      # it was a bad request.
95 ##                      self.channel.transport.write(
96 ##                              "HTTP/1.1 400 Bad Request\r\n\r\n")
97 ##                      self.channel.transport.loseConnection()
98 ##                      return
99 ##                  raise
100
101         self.process()
102
103     def _logger(self):
104         # we build up a log string that hides most of the cap, to preserve
105         # user privacy. We retain the query args so we can identify things
106         # like t=json. Then we send it to the flog. We make no attempt to
107         # match apache formatting. TODO: when we move to DSA dirnodes and
108         # shorter caps, consider exposing a few characters of the cap, or
109         # maybe a few characters of its hash.
110         x = self.uri.split("?", 1)
111         if len(x) == 1:
112             # no query args
113             path = self.uri
114             queryargs = ""
115         else:
116             path, queryargs = x
117             # there is a form handler which redirects POST /uri?uri=FOO into
118             # GET /uri/FOO so folks can paste in non-HTTP-prefixed uris. Make
119             # sure we censor these too.
120             if queryargs.startswith("uri="):
121                 queryargs = "[uri=CENSORED]"
122             queryargs = "?" + queryargs
123         if path.startswith("/uri"):
124             path = "/uri/[CENSORED].."
125         uri = path + queryargs
126
127         log.msg(format="web: %(clientip)s %(method)s %(uri)s %(code)s %(length)s",
128                 clientip=self.getClientIP(),
129                 method=self.method,
130                 uri=uri,
131                 code=self.code,
132                 length=(self.sentLength or "-"),
133                 facility="tahoe.webish",
134                 level=log.OPERATIONAL,
135                 )
136
137 class Directory(rend.Page):
138     addSlash = True
139     docFactory = getxmlfile("directory.xhtml")
140
141     def __init__(self, rootname, dirnode, dirpath):
142         self._rootname = rootname
143         self._dirnode = dirnode
144         self._dirpath = dirpath
145
146     def dirpath_as_string(self):
147         return "/" + "/".join(self._dirpath)
148
149     def render_title(self, ctx, data):
150         return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
151
152     def render_header(self, ctx, data):
153         parent_directories = ("<%s>" % self._rootname,) + self._dirpath
154         num_dirs = len(parent_directories)
155
156         header = ["Directory '"]
157         for i,d in enumerate(parent_directories):
158             upness = num_dirs - i - 1
159             if upness:
160                 link = "/".join( ("..",) * upness )
161             else:
162                 link = "."
163             header.append(T.a(href=link)[d])
164             if upness != 0:
165                 header.append("/")
166         header.append("'")
167
168         if self._dirnode.is_readonly():
169             header.append(" (readonly)")
170         header.append(":")
171         return ctx.tag[header]
172
173     def render_welcome(self, ctx, data):
174         depth = len(self._dirpath) + 2
175         link = "/".join([".."] * depth)
176         return T.div[T.a(href=link)["Return to Welcome page"]]
177
178     def data_children(self, ctx, data):
179         d = self._dirnode.list()
180         d.addCallback(lambda dict: sorted(dict.items()))
181         def _stall_some(items):
182             # Deferreds don't optimize out tail recursion, and the way
183             # Nevow's flattener handles Deferreds doesn't take this into
184             # account. As a result, large lists of Deferreds that fire in the
185             # same turn (i.e. the output of defer.succeed) will cause a stack
186             # overflow. To work around this, we insert a turn break after
187             # every 100 items, using foolscap's fireEventually(). This gives
188             # the stack a chance to be popped. It would also work to put
189             # every item in its own turn, but that'd be a lot more
190             # inefficient. This addresses ticket #237, for which I was never
191             # able to create a failing unit test.
192             output = []
193             for i,item in enumerate(items):
194                 if i % 100 == 0:
195                     output.append(fireEventually(item))
196                 else:
197                     output.append(item)
198             return output
199         d.addCallback(_stall_some)
200         return d
201
202     def render_row(self, ctx, data):
203         name, (target, metadata) = data
204         name = name.encode("utf-8")
205         assert not isinstance(name, unicode)
206
207         if self._dirnode.is_readonly():
208             delete = "-"
209             rename = "-"
210         else:
211             # this creates a button which will cause our child__delete method
212             # to be invoked, which deletes the file and then redirects the
213             # browser back to this directory
214             delete = T.form(action=url.here, method="post")[
215                 T.input(type='hidden', name='t', value='delete'),
216                 T.input(type='hidden', name='name', value=name),
217                 T.input(type='hidden', name='when_done', value=url.here),
218                 T.input(type='submit', value='del', name="del"),
219                 ]
220
221             rename = T.form(action=url.here, method="get")[
222                 T.input(type='hidden', name='t', value='rename-form'),
223                 T.input(type='hidden', name='name', value=name),
224                 T.input(type='hidden', name='when_done', value=url.here),
225                 T.input(type='submit', value='rename', name="rename"),
226                 ]
227
228         ctx.fillSlots("delete", delete)
229         ctx.fillSlots("rename", rename)
230         check = T.form(action=url.here, method="post")[
231             T.input(type='hidden', name='t', value='check'),
232             T.input(type='hidden', name='name', value=name),
233             T.input(type='hidden', name='when_done', value=url.here),
234             T.input(type='submit', value='check', name="check"),
235             ]
236         ctx.fillSlots("overwrite", self.build_overwrite(ctx, (name, target)))
237         ctx.fillSlots("check", check)
238
239         times = []
240         TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
241         if "ctime" in metadata:
242             ctime = time.strftime(TIME_FORMAT,
243                                   time.localtime(metadata["ctime"]))
244             times.append("c: " + ctime)
245         if "mtime" in metadata:
246             mtime = time.strftime(TIME_FORMAT,
247                                   time.localtime(metadata["mtime"]))
248             if times:
249                 times.append(T.br())
250                 times.append("m: " + mtime)
251         ctx.fillSlots("times", times)
252
253
254         # build the base of the uri_link link url
255         uri_link = "/uri/" + urllib.quote(target.get_uri())
256
257         assert (IFileNode.providedBy(target)
258                 or IDirectoryNode.providedBy(target)
259                 or IMutableFileNode.providedBy(target)), target
260
261         if IMutableFileNode.providedBy(target):
262             # file
263
264             # add the filename to the uri_link url
265             uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
266
267             # to prevent javascript in displayed .html files from stealing a
268             # secret directory URI from the URL, send the browser to a URI-based
269             # page that doesn't know about the directory at all
270             #dlurl = urllib.quote(name)
271             dlurl = uri_link
272
273             ctx.fillSlots("filename",
274                           T.a(href=dlurl)[html.escape(name)])
275             ctx.fillSlots("type", "SSK")
276
277             ctx.fillSlots("size", "?")
278
279             text_plain_link = uri_link + "?filename=foo.txt"
280             text_plain_tag = T.a(href=text_plain_link)["text/plain"]
281
282         elif IFileNode.providedBy(target):
283             # file
284
285             # add the filename to the uri_link url
286             uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
287
288             # to prevent javascript in displayed .html files from stealing a
289             # secret directory URI from the URL, send the browser to a URI-based
290             # page that doesn't know about the directory at all
291             #dlurl = urllib.quote(name)
292             dlurl = uri_link
293
294             ctx.fillSlots("filename",
295                           T.a(href=dlurl)[html.escape(name)])
296             ctx.fillSlots("type", "FILE")
297
298             ctx.fillSlots("size", target.get_size())
299
300             text_plain_link = uri_link + "?filename=foo.txt"
301             text_plain_tag = T.a(href=text_plain_link)["text/plain"]
302
303         elif IDirectoryNode.providedBy(target):
304             # directory
305             ctx.fillSlots("filename",
306                           T.a(href=uri_link)[html.escape(name)])
307             if target.is_readonly():
308                 dirtype = "DIR-RO"
309             else:
310                 dirtype = "DIR"
311             ctx.fillSlots("type", dirtype)
312             ctx.fillSlots("size", "-")
313             text_plain_tag = None
314
315         childdata = [T.a(href="%s?t=json" % name)["JSON"], ", ",
316                      T.a(href="%s?t=uri" % name)["URI"], ", ",
317                      T.a(href="%s?t=readonly-uri" % name)["readonly-URI"],
318                      ]
319         if text_plain_tag:
320             childdata.extend([", ", text_plain_tag])
321
322         ctx.fillSlots("data", childdata)
323
324         try:
325             checker = IClient(ctx).getServiceNamed("checker")
326         except KeyError:
327             checker = None
328         if checker:
329             d = defer.maybeDeferred(checker.checker_results_for,
330                                     target.get_verifier())
331             def _got(checker_results):
332                 recent_results = reversed(checker_results[-5:])
333                 if IFileNode.providedBy(target):
334                     results = ("[" +
335                                ", ".join(["%d/%d" % (found, needed)
336                                           for (when,
337                                                (needed, total, found, sharemap))
338                                           in recent_results]) +
339                                "]")
340                 elif IDirectoryNode.providedBy(target):
341                     results = ("[" +
342                                "".join([{True:"+",False:"-"}[res]
343                                         for (when, res) in recent_results]) +
344                                "]")
345                 else:
346                     results = "%d results" % len(checker_results)
347                 return results
348             d.addCallback(_got)
349             results = d
350         else:
351             results = "--"
352         # TODO: include a link to see more results, including timestamps
353         # TODO: use a sparkline
354         ctx.fillSlots("checker_results", results)
355
356         return ctx.tag
357
358     def render_forms(self, ctx, data):
359         if self._dirnode.is_readonly():
360             return T.div["No upload forms: directory is read-only"]
361         mkdir = T.form(action=".", method="post",
362                        enctype="multipart/form-data")[
363             T.fieldset[
364             T.input(type="hidden", name="t", value="mkdir"),
365             T.input(type="hidden", name="when_done", value=url.here),
366             T.legend(class_="freeform-form-label")["Create a new directory"],
367             "New directory name: ",
368             T.input(type="text", name="name"), " ",
369             T.input(type="submit", value="Create"),
370             ]]
371
372         upload = T.form(action=".", method="post",
373                         enctype="multipart/form-data")[
374             T.fieldset[
375             T.input(type="hidden", name="t", value="upload"),
376             T.input(type="hidden", name="when_done", value=url.here),
377             T.legend(class_="freeform-form-label")["Upload a file to this directory"],
378             "Choose a file to upload: ",
379             T.input(type="file", name="file", class_="freeform-input-file"),
380             " ",
381             T.input(type="submit", value="Upload"),
382             " Mutable?:",
383             T.input(type="checkbox", name="mutable"),
384             ]]
385
386         mount = T.form(action=".", method="post",
387                         enctype="multipart/form-data")[
388             T.fieldset[
389             T.input(type="hidden", name="t", value="uri"),
390             T.input(type="hidden", name="when_done", value=url.here),
391             T.legend(class_="freeform-form-label")["Attach a file or directory"
392                                                    " (by URI) to this"
393                                                    " directory"],
394             "New child name: ",
395             T.input(type="text", name="name"), " ",
396             "URI of new child: ",
397             T.input(type="text", name="uri"), " ",
398             T.input(type="submit", value="Attach"),
399             ]]
400         return [T.div(class_="freeform-form")[mkdir],
401                 T.div(class_="freeform-form")[upload],
402                 T.div(class_="freeform-form")[mount],
403                 ]
404
405     def build_overwrite(self, ctx, data):
406         name, target = data
407         if IMutableFileNode.providedBy(target) and not target.is_readonly():
408             action="/uri/" + urllib.quote(target.get_uri())
409             overwrite = T.form(action=action, method="post",
410                                enctype="multipart/form-data")[
411                 T.fieldset[
412                 T.input(type="hidden", name="t", value="overwrite"),
413                 T.input(type='hidden', name='name', value=name),
414                 T.input(type='hidden', name='when_done', value=url.here),
415                 T.legend(class_="freeform-form-label")["Overwrite"],
416                 "Choose new file: ",
417                 T.input(type="file", name="file", class_="freeform-input-file"),
418                 " ",
419                 T.input(type="submit", value="Overwrite")
420                 ]]
421             return [T.div(class_="freeform-form")[overwrite],]
422         else:
423             return []
424
425     def render_results(self, ctx, data):
426         req = inevow.IRequest(ctx)
427         return get_arg(req, "results", "")
428
429 class WebDownloadTarget:
430     implements(IDownloadTarget, IConsumer)
431     def __init__(self, req, content_type, content_encoding, save_to_file):
432         self._req = req
433         self._content_type = content_type
434         self._content_encoding = content_encoding
435         self._opened = False
436         self._producer = None
437         self._save_to_file = save_to_file
438
439     def registerProducer(self, producer, streaming):
440         self._req.registerProducer(producer, streaming)
441     def unregisterProducer(self):
442         self._req.unregisterProducer()
443
444     def open(self, size):
445         self._opened = True
446         self._req.setHeader("content-type", self._content_type)
447         if self._content_encoding:
448             self._req.setHeader("content-encoding", self._content_encoding)
449         self._req.setHeader("content-length", str(size))
450         if self._save_to_file is not None:
451             # tell the browser to save the file rather display it
452             # TODO: quote save_to_file properly
453             filename = self._save_to_file.encode("utf-8")
454             self._req.setHeader("content-disposition",
455                                 'attachment; filename="%s"'
456                                 % filename)
457
458     def write(self, data):
459         self._req.write(data)
460     def close(self):
461         self._req.finish()
462
463     def fail(self, why):
464         if self._opened:
465             # The content-type is already set, and the response code
466             # has already been sent, so we can't provide a clean error
467             # indication. We can emit text (which a browser might interpret
468             # as something else), and if we sent a Size header, they might
469             # notice that we've truncated the data. Keep the error message
470             # small to improve the chances of having our error response be
471             # shorter than the intended results.
472             #
473             # We don't have a lot of options, unfortunately.
474             self._req.write("problem during download\n")
475         else:
476             # We haven't written anything yet, so we can provide a sensible
477             # error message.
478             msg = str(why.type)
479             msg.replace("\n", "|")
480             self._req.setResponseCode(http.GONE, msg)
481             self._req.setHeader("content-type", "text/plain")
482             # TODO: HTML-formatted exception?
483             self._req.write(str(why))
484         self._req.finish()
485
486     def register_canceller(self, cb):
487         pass
488     def finish(self):
489         pass
490
491 class FileDownloader(resource.Resource):
492     def __init__(self, filenode, name):
493         assert (IFileNode.providedBy(filenode)
494                 or IMutableFileNode.providedBy(filenode))
495         self._filenode = filenode
496         self._name = name
497
498     def render(self, req):
499         gte = static.getTypeAndEncoding
500         ctype, encoding = gte(self._name,
501                               static.File.contentTypes,
502                               static.File.contentEncodings,
503                               defaultType="text/plain")
504         save_to_file = None
505         if get_arg(req, "save", False):
506             # TODO: make the API specification clear: should "save=" or
507             # "save=false" count?
508             save_to_file = self._name
509         wdt = WebDownloadTarget(req, ctype, encoding, save_to_file)
510         d = self._filenode.download(wdt)
511         # exceptions during download are handled by the WebDownloadTarget
512         d.addErrback(lambda why: None)
513         return server.NOT_DONE_YET
514
515 class BlockingFileError(Exception):
516     """We cannot auto-create a parent directory, because there is a file in
517     the way"""
518 class NoReplacementError(Exception):
519     """There was already a child by that name, and you asked me to not replace it"""
520 class NoLocalDirectoryError(Exception):
521     """The localdir= directory didn't exist"""
522
523 LOCALHOST = "127.0.0.1"
524
525 class NeedLocalhostError:
526     implements(inevow.IResource)
527
528     def renderHTTP(self, ctx):
529         req = inevow.IRequest(ctx)
530         req.setResponseCode(http.FORBIDDEN)
531         req.setHeader("content-type", "text/plain")
532         return "localfile= or localdir= requires a local connection"
533
534 class NeedAbsolutePathError:
535     implements(inevow.IResource)
536
537     def renderHTTP(self, ctx):
538         req = inevow.IRequest(ctx)
539         req.setResponseCode(http.FORBIDDEN)
540         req.setHeader("content-type", "text/plain")
541         return "localfile= or localdir= requires an absolute path"
542
543 class LocalAccessDisabledError:
544     implements(inevow.IResource)
545
546     def renderHTTP(self, ctx):
547         req = inevow.IRequest(ctx)
548         req.setResponseCode(http.FORBIDDEN)
549         req.setHeader("content-type", "text/plain")
550         return "local file access is disabled"
551
552 class WebError:
553     implements(inevow.IResource)
554     def __init__(self, response_code, errmsg):
555         self._response_code = response_code
556         self._errmsg = errmsg
557
558     def renderHTTP(self, ctx):
559         req = inevow.IRequest(ctx)
560         req.setResponseCode(self._response_code)
561         req.setHeader("content-type", "text/plain")
562         return self._errmsg
563
564
565 class LocalFileDownloader(resource.Resource):
566     def __init__(self, filenode, local_filename):
567         self._local_filename = local_filename
568         IFileNode(filenode)
569         self._filenode = filenode
570
571     def render(self, req):
572         target = download.FileName(self._local_filename)
573         d = self._filenode.download(target)
574         def _done(res):
575             req.write(self._filenode.get_uri())
576             req.finish()
577         d.addCallback(_done)
578         return server.NOT_DONE_YET
579
580
581 class FileJSONMetadata(rend.Page):
582     def __init__(self, filenode):
583         self._filenode = filenode
584
585     def renderHTTP(self, ctx):
586         req = inevow.IRequest(ctx)
587         req.setHeader("content-type", "text/plain")
588         return self.renderNode(self._filenode)
589
590     def renderNode(self, filenode):
591         file_uri = filenode.get_uri()
592         data = ("filenode",
593                 {'ro_uri': file_uri,
594                  'size': filenode.get_size(),
595                  })
596         return simplejson.dumps(data, indent=1)
597
598 class FileURI(FileJSONMetadata):
599     def renderNode(self, filenode):
600         file_uri = filenode.get_uri()
601         return file_uri
602
603 class FileReadOnlyURI(FileJSONMetadata):
604     def renderNode(self, filenode):
605         if filenode.is_readonly():
606             return filenode.get_uri()
607         else:
608             return filenode.get_readonly().get_uri()
609
610 class DirnodeWalkerMixin:
611     """Visit all nodes underneath (and including) the rootnode, one at a
612     time. For each one, call the visitor. The visitor will see the
613     IDirectoryNode before it sees any of the IFileNodes inside. If the
614     visitor returns a Deferred, I do not call the visitor again until it has
615     fired.
616     """
617
618 ##    def _walk_if_we_could_use_generators(self, rootnode, rootpath=()):
619 ##        # this is what we'd be doing if we didn't have the Deferreds and
620 ##        # thus could use generators
621 ##        yield rootpath, rootnode
622 ##        for childname, childnode in rootnode.list().items():
623 ##            childpath = rootpath + (childname,)
624 ##            if IFileNode.providedBy(childnode):
625 ##                yield childpath, childnode
626 ##            elif IDirectoryNode.providedBy(childnode):
627 ##                for res in self._walk_if_we_could_use_generators(childnode,
628 ##                                                                 childpath):
629 ##                    yield res
630
631     def walk(self, rootnode, visitor, rootpath=()):
632         d = rootnode.list()
633         def _listed(listing):
634             return listing.items()
635         d.addCallback(_listed)
636         d.addCallback(self._handle_items, visitor, rootpath)
637         return d
638
639     def _handle_items(self, items, visitor, rootpath):
640         if not items:
641             return
642         childname, (childnode, metadata) = items[0]
643         childpath = rootpath + (childname,)
644         d = defer.maybeDeferred(visitor, childpath, childnode, metadata)
645         if IDirectoryNode.providedBy(childnode):
646             d.addCallback(lambda res: self.walk(childnode, visitor, childpath))
647         d.addCallback(lambda res:
648                       self._handle_items(items[1:], visitor, rootpath))
649         return d
650
651 class LocalDirectoryDownloader(resource.Resource, DirnodeWalkerMixin):
652     def __init__(self, dirnode, localdir):
653         self._dirnode = dirnode
654         self._localdir = localdir
655
656     def _handle(self, path, node, metadata):
657         path = tuple([p.encode("utf-8") for p in path])
658         localfile = os.path.join(self._localdir, os.sep.join(path))
659         if IDirectoryNode.providedBy(node):
660             fileutil.make_dirs(localfile)
661         elif IFileNode.providedBy(node):
662             target = download.FileName(localfile)
663             return node.download(target)
664
665     def render(self, req):
666         d = self.walk(self._dirnode, self._handle)
667         def _done(res):
668             req.setHeader("content-type", "text/plain")
669             return "operation complete"
670         d.addCallback(_done)
671         return d
672
673 class DirectoryJSONMetadata(rend.Page):
674     def __init__(self, dirnode):
675         self._dirnode = dirnode
676
677     def renderHTTP(self, ctx):
678         req = inevow.IRequest(ctx)
679         req.setHeader("content-type", "text/plain")
680         return self.renderNode(self._dirnode)
681
682     def renderNode(self, node):
683         d = node.list()
684         def _got(children):
685             kids = {}
686             for name, (childnode, metadata) in children.iteritems():
687                 if IFileNode.providedBy(childnode):
688                     kiduri = childnode.get_uri()
689                     kiddata = ("filenode",
690                                {'ro_uri': kiduri,
691                                 'size': childnode.get_size(),
692                                 'metadata': metadata,
693                                 })
694                 else:
695                     assert IDirectoryNode.providedBy(childnode), (childnode, children,)
696                     kiddata = ("dirnode",
697                                {'ro_uri': childnode.get_readonly_uri(),
698                                 'metadata': metadata,
699                                 })
700                     if not childnode.is_readonly():
701                         kiddata[1]['rw_uri'] = childnode.get_uri()
702                 kids[name] = kiddata
703             contents = { 'children': kids,
704                          'ro_uri': node.get_readonly_uri(),
705                          }
706             if not node.is_readonly():
707                 contents['rw_uri'] = node.get_uri()
708             data = ("dirnode", contents)
709             return simplejson.dumps(data, indent=1)
710         d.addCallback(_got)
711         return d
712
713 class DirectoryURI(DirectoryJSONMetadata):
714     def renderNode(self, node):
715         return node.get_uri()
716
717 class DirectoryReadonlyURI(DirectoryJSONMetadata):
718     def renderNode(self, node):
719         return node.get_readonly_uri()
720
721 class RenameForm(rend.Page):
722     addSlash = True
723     docFactory = getxmlfile("rename-form.xhtml")
724
725     def __init__(self, rootname, dirnode, dirpath):
726         self._rootname = rootname
727         self._dirnode = dirnode
728         self._dirpath = dirpath
729
730     def dirpath_as_string(self):
731         return "/" + "/".join(self._dirpath)
732
733     def render_title(self, ctx, data):
734         return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
735
736     def render_header(self, ctx, data):
737         parent_directories = ("<%s>" % self._rootname,) + self._dirpath
738         num_dirs = len(parent_directories)
739
740         header = [ "Rename in directory '",
741                    "<%s>/" % self._rootname,
742                    "/".join(self._dirpath),
743                    "':", ]
744
745         if self._dirnode.is_readonly():
746             header.append(" (readonly)")
747         return ctx.tag[header]
748
749     def render_when_done(self, ctx, data):
750         return T.input(type="hidden", name="when_done", value=url.here)
751
752     def render_get_name(self, ctx, data):
753         req = inevow.IRequest(ctx)
754         name = get_arg(req, "name", "")
755         ctx.tag.attributes['value'] = name
756         return ctx.tag
757
758 class POSTHandler(rend.Page):
759     def __init__(self, node, replace):
760         self._node = node
761         self._replace = replace
762
763     def _check_replacement(self, name):
764         if self._replace:
765             return defer.succeed(None)
766         d = self._node.has_child(name)
767         def _got(present):
768             if present:
769                 raise NoReplacementError("There was already a child by that "
770                                          "name, and you asked me to not "
771                                          "replace it.")
772             return None
773         d.addCallback(_got)
774         return d
775
776     def _POST_mkdir(self, name):
777         d = self._check_replacement(name)
778         d.addCallback(lambda res: self._node.create_empty_directory(name))
779         d.addCallback(lambda res: "directory created")
780         return d
781
782     def _POST_uri(self, name, newuri):
783         d = self._check_replacement(name)
784         d.addCallback(lambda res: self._node.set_uri(name, newuri))
785         d.addCallback(lambda res: newuri)
786         return d
787
788     def _POST_delete(self, name):
789         if name is None:
790             # apparently an <input type="hidden" name="name" value="">
791             # won't show up in the resulting encoded form.. the 'name'
792             # field is completely missing. So to allow deletion of an
793             # empty file, we have to pretend that None means ''. The only
794             # downide of this is a slightly confusing error message if
795             # someone does a POST without a name= field. For our own HTML
796             # thisn't a big deal, because we create the 'delete' POST
797             # buttons ourselves.
798             name = ''
799         d = self._node.delete(name)
800         d.addCallback(lambda res: "thing deleted")
801         return d
802
803     def _POST_rename(self, name, from_name, to_name):
804         d = self._check_replacement(to_name)
805         d.addCallback(lambda res: self._node.get(from_name))
806         def add_dest(child):
807             uri = child.get_uri()
808             # now actually do the rename
809             return self._node.set_uri(to_name, uri)
810         d.addCallback(add_dest)
811         def rm_src(junk):
812             return self._node.delete(from_name)
813         d.addCallback(rm_src)
814         d.addCallback(lambda res: "thing renamed")
815         return d
816
817     def _POST_upload(self, contents, name, mutable, client):
818         if mutable:
819             # SDMF: files are small, and we can only upload data.
820             contents.file.seek(0)
821             data = contents.file.read()
822             #uploadable = FileHandle(contents.file)
823             d = self._check_replacement(name)
824             d.addCallback(lambda res: self._node.has_child(name))
825             def _checked(present):
826                 if present:
827                     # modify the existing one instead of creating a new
828                     # one
829                     d2 = self._node.get(name)
830                     def _got_newnode(newnode):
831                         d3 = newnode.replace(data)
832                         d3.addCallback(lambda res: newnode.get_uri())
833                         return d3
834                     d2.addCallback(_got_newnode)
835                 else:
836                     d2 = client.create_mutable_file(data)
837                     def _uploaded(newnode):
838                         d1 = self._node.set_node(name, newnode)
839                         d1.addCallback(lambda res: newnode.get_uri())
840                         return d1
841                     d2.addCallback(_uploaded)
842                 return d2
843             d.addCallback(_checked)
844         else:
845             uploadable = FileHandle(contents.file)
846             d = self._check_replacement(name)
847             d.addCallback(lambda res: self._node.add_file(name, uploadable))
848             def _done(newnode):
849                 return newnode.get_uri()
850             d.addCallback(_done)
851         return d
852
853     def _POST_overwrite(self, contents):
854         # SDMF: files are small, and we can only upload data.
855         contents.file.seek(0)
856         data = contents.file.read()
857         # TODO: 'name' handling needs review
858         d = defer.succeed(self._node)
859         def _got_child_overwrite(child_node):
860             child_node.replace(data)
861             return child_node.get_uri()
862         d.addCallback(_got_child_overwrite)
863         return d
864
865     def _POST_check(self, name):
866         d = self._node.get(name)
867         def _got_child_check(child_node):
868             d2 = child_node.check()
869             def _done(res):
870                 log.msg("checked %s, results %s" % (child_node, res),
871                         facility="tahoe.webish", level=log.NOISY)
872                 return str(res)
873             d2.addCallback(_done)
874             return d2
875         d.addCallback(_got_child_check)
876         return d
877
878     def _POST_set_children(self, children):
879         cs = []
880         for name, (file_or_dir, mddict) in children.iteritems():
881             cap = str(mddict.get('rw_uri') or mddict.get('ro_uri'))
882             cs.append((name, cap, mddict.get('metadata')))
883
884         d = self._node.set_children(cs)
885         d.addCallback(lambda res: "Okay so I did it.")
886         return d
887
888     def renderHTTP(self, ctx):
889         req = inevow.IRequest(ctx)
890
891         t = get_arg(req, "t")
892         assert t is not None
893
894         charset = get_arg(req, "_charset", "utf-8")
895
896         name = get_arg(req, "name", None)
897         if name and "/" in name:
898             req.setResponseCode(http.BAD_REQUEST)
899             req.setHeader("content-type", "text/plain")
900             return "name= may not contain a slash"
901         if name is not None:
902             name = name.strip()
903             name = name.decode(charset)
904             assert isinstance(name, unicode)
905         # we allow the user to delete an empty-named file, but not to create
906         # them, since that's an easy and confusing mistake to make
907
908         when_done = get_arg(req, "when_done", None)
909         if not boolean_of_arg(get_arg(req, "replace", "true")):
910             self._replace = False
911
912         if t == "mkdir":
913             if not name:
914                 raise RuntimeError("mkdir requires a name")
915             d = self._POST_mkdir(name)
916         elif t == "uri":
917             if not name:
918                 raise RuntimeError("set-uri requires a name")
919             newuri = get_arg(req, "uri")
920             assert newuri is not None
921             d = self._POST_uri(name, newuri)
922         elif t == "delete":
923             d = self._POST_delete(name)
924         elif t == "rename":
925             from_name = get_arg(req, "from_name")
926             if from_name is not None:
927                 from_name = from_name.strip()
928                 from_name = from_name.decode(charset)
929                 assert isinstance(from_name, unicode)
930             to_name = get_arg(req, "to_name")
931             if to_name is not None:
932                 to_name = to_name.strip()
933                 to_name = to_name.decode(charset)
934                 assert isinstance(to_name, unicode)
935             if not from_name or not to_name:
936                 raise RuntimeError("rename requires from_name and to_name")
937             if not IDirectoryNode.providedBy(self._node):
938                 raise RuntimeError("rename must only be called on directories")
939             for k,v in [ ('from_name', from_name), ('to_name', to_name) ]:
940                 if v and "/" in v:
941                     req.setResponseCode(http.BAD_REQUEST)
942                     req.setHeader("content-type", "text/plain")
943                     return "%s= may not contain a slash" % (k,)
944             d = self._POST_rename(name, from_name, to_name)
945         elif t == "upload":
946             contents = req.fields["file"]
947             name = name or contents.filename
948             if name is not None:
949                 name = name.strip()
950             if not name:
951                 # this prohibts empty, missing, and all-whitespace filenames
952                 raise RuntimeError("upload requires a name")
953             name = name.decode(charset)
954             assert isinstance(name, unicode)
955             mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
956             d = self._POST_upload(contents, name, mutable, IClient(ctx))
957         elif t == "overwrite":
958             contents = req.fields["file"]
959             d = self._POST_overwrite(contents)
960         elif t == "check":
961             d = self._POST_check(name)
962         elif t == "set_children":
963             req.content.seek(0)
964             body = req.content.read()
965             try:
966                 children = simplejson.loads(body)
967             except ValueError, le:
968                 le.args = tuple(le.args + (body,))
969                 # TODO test handling of bad JSON
970                 raise
971             d = self._POST_set_children(children)
972         else:
973             print "BAD t=%s" % t
974             return "BAD t=%s" % t
975         if when_done:
976             d.addCallback(lambda res: url.URL.fromString(when_done))
977         def _check_replacement(f):
978             # TODO: make this more human-friendly: maybe send them to the
979             # when_done page but with an extra query-arg that will display
980             # the error message in a big box at the top of the page. The
981             # directory page that when_done= usually points to accepts a
982             # result= argument.. use that.
983             f.trap(NoReplacementError)
984             req.setResponseCode(http.CONFLICT)
985             req.setHeader("content-type", "text/plain")
986             return str(f.value)
987         d.addErrback(_check_replacement)
988         return d
989
990 class DELETEHandler(rend.Page):
991     def __init__(self, node, name):
992         self._node = node
993         self._name = name
994
995     def renderHTTP(self, ctx):
996         req = inevow.IRequest(ctx)
997         d = self._node.delete(self._name)
998         def _done(res):
999             # what should this return??
1000             return "%s deleted" % self._name.encode("utf-8")
1001         d.addCallback(_done)
1002         def _trap_missing(f):
1003             f.trap(KeyError)
1004             req.setResponseCode(http.NOT_FOUND)
1005             req.setHeader("content-type", "text/plain")
1006             return "no such child %s" % self._name.encode("utf-8")
1007         d.addErrback(_trap_missing)
1008         return d
1009
1010 class PUTHandler(rend.Page):
1011     def __init__(self, node, path, t, localfile, localdir, replace):
1012         self._node = node
1013         self._path = path
1014         self._t = t
1015         self._localfile = localfile
1016         self._localdir = localdir
1017         self._replace = replace
1018
1019     def renderHTTP(self, ctx):
1020         req = inevow.IRequest(ctx)
1021         t = self._t
1022         localfile = self._localfile
1023         localdir = self._localdir
1024
1025         if t == "upload" and not (localfile or localdir):
1026             req.setResponseCode(http.BAD_REQUEST)
1027             req.setHeader("content-type", "text/plain")
1028             return "t=upload requires localfile= or localdir="
1029
1030         # we must traverse the path, creating new directories as necessary
1031         d = self._get_or_create_directories(self._node, self._path[:-1])
1032         name = self._path[-1]
1033         d.addCallback(self._check_replacement, name, self._replace)
1034         if t == "upload":
1035             if localfile:
1036                 d.addCallback(self._upload_localfile, localfile, name)
1037             else:
1038                 # localdir
1039                 # take the last step
1040                 d.addCallback(self._get_or_create_directories, self._path[-1:])
1041                 d.addCallback(self._upload_localdir, localdir)
1042         elif t == "uri":
1043             d.addCallback(self._attach_uri, req.content, name)
1044         elif t == "mkdir":
1045             d.addCallback(self._mkdir, name)
1046         else:
1047             d.addCallback(self._upload_file, req.content, name)
1048
1049         def _transform_error(f):
1050             errors = {BlockingFileError: http.BAD_REQUEST,
1051                       NoReplacementError: http.CONFLICT,
1052                       NoLocalDirectoryError: http.BAD_REQUEST,
1053                       }
1054             for k,v in errors.items():
1055                 if f.check(k):
1056                     req.setResponseCode(v)
1057                     req.setHeader("content-type", "text/plain")
1058                     return str(f.value)
1059             return f
1060         d.addErrback(_transform_error)
1061         return d
1062
1063     def _get_or_create_directories(self, node, path):
1064         if not IDirectoryNode.providedBy(node):
1065             # unfortunately it is too late to provide the name of the
1066             # blocking directory in the error message.
1067             raise BlockingFileError("cannot create directory because there "
1068                                     "is a file in the way")
1069         if not path:
1070             return defer.succeed(node)
1071         d = node.get(path[0])
1072         def _maybe_create(f):
1073             f.trap(KeyError)
1074             return node.create_empty_directory(path[0])
1075         d.addErrback(_maybe_create)
1076         d.addCallback(self._get_or_create_directories, path[1:])
1077         return d
1078
1079     def _check_replacement(self, node, name, replace):
1080         if replace:
1081             return node
1082         d = node.has_child(name)
1083         def _got(present):
1084             if present:
1085                 raise NoReplacementError("There was already a child by that "
1086                                          "name, and you asked me to not "
1087                                          "replace it.")
1088             return node
1089         d.addCallback(_got)
1090         return d
1091
1092     def _mkdir(self, node, name):
1093         d = node.create_empty_directory(name)
1094         def _done(newnode):
1095             return newnode.get_uri()
1096         d.addCallback(_done)
1097         return d
1098
1099     def _upload_file(self, node, contents, name):
1100         uploadable = FileHandle(contents)
1101         d = node.add_file(name, uploadable)
1102         def _done(filenode):
1103             log.msg("webish upload complete",
1104                     facility="tahoe.webish", level=log.NOISY)
1105             return filenode.get_uri()
1106         d.addCallback(_done)
1107         return d
1108
1109     def _upload_localfile(self, node, localfile, name):
1110         uploadable = FileName(localfile)
1111         d = node.add_file(name, uploadable)
1112         d.addCallback(lambda filenode: filenode.get_uri())
1113         return d
1114
1115     def _attach_uri(self, parentnode, contents, name):
1116         newuri = contents.read().strip()
1117         d = parentnode.set_uri(name, newuri)
1118         def _done(res):
1119             return newuri
1120         d.addCallback(_done)
1121         return d
1122
1123     def _upload_localdir(self, node, localdir):
1124         # build up a list of files to upload. TODO: for now, these files and
1125         # directories must have UTF-8 encoded filenames: anything else will
1126         # cause the upload to break.
1127         all_files = []
1128         all_dirs = []
1129         msg = "No files to upload! %s is empty" % localdir
1130         if not os.path.exists(localdir):
1131             msg = "%s doesn't exist!" % localdir
1132             raise NoLocalDirectoryError(msg)
1133         for root, dirs, files in os.walk(localdir):
1134             if root == localdir:
1135                 path = ()
1136             else:
1137                 relative_root = root[len(localdir)+1:]
1138                 path = tuple(relative_root.split(os.sep))
1139             for d in dirs:
1140                 this_dir = path + (d,)
1141                 this_dir = tuple([p.decode("utf-8") for p in this_dir])
1142                 all_dirs.append(this_dir)
1143             for f in files:
1144                 this_file = path + (f,)
1145                 this_file = tuple([p.decode("utf-8") for p in this_file])
1146                 all_files.append(this_file)
1147         d = defer.succeed(msg)
1148         for dir in all_dirs:
1149             if dir:
1150                 d.addCallback(self._makedir, node, dir)
1151         for f in all_files:
1152             d.addCallback(self._upload_one_file, node, localdir, f)
1153         return d
1154
1155     def _makedir(self, res, node, dir):
1156         d = defer.succeed(None)
1157         # get the parent. As long as os.walk gives us parents before
1158         # children, this ought to work
1159         d.addCallback(lambda res: node.get_child_at_path(dir[:-1]))
1160         # then create the child directory
1161         d.addCallback(lambda parent: parent.create_empty_directory(dir[-1]))
1162         return d
1163
1164     def _upload_one_file(self, res, node, localdir, f):
1165         # get the parent. We can be sure this exists because we already
1166         # went through and created all the directories we require.
1167         localfile = os.path.join(localdir, *f)
1168         d = node.get_child_at_path(f[:-1])
1169         d.addCallback(self._upload_localfile, localfile, f[-1])
1170         return d
1171
1172
1173 class Manifest(rend.Page):
1174     docFactory = getxmlfile("manifest.xhtml")
1175     def __init__(self, dirnode, dirpath):
1176         self._dirnode = dirnode
1177         self._dirpath = dirpath
1178
1179     def dirpath_as_string(self):
1180         return "/" + "/".join(self._dirpath)
1181
1182     def render_title(self, ctx):
1183         return T.title["Manifest of %s" % self.dirpath_as_string()]
1184
1185     def render_header(self, ctx):
1186         return T.p["Manifest of %s" % self.dirpath_as_string()]
1187
1188     def data_items(self, ctx, data):
1189         return self._dirnode.build_manifest()
1190
1191     def render_row(self, ctx, refresh_cap):
1192         ctx.fillSlots("refresh_capability", refresh_cap)
1193         return ctx.tag
1194
1195 class ChildError:
1196     implements(inevow.IResource)
1197     def renderHTTP(self, ctx):
1198         req = inevow.IRequest(ctx)
1199         req.setResponseCode(http.BAD_REQUEST)
1200         req.setHeader("content-type", "text/plain")
1201         return self.text
1202
1203 def child_error(text):
1204     ce = ChildError()
1205     ce.text = text
1206     return ce, ()
1207
1208 class VDrive(rend.Page):
1209
1210     def __init__(self, node, name):
1211         self.node = node
1212         self.name = name
1213
1214     def get_child_at_path(self, path):
1215         if path:
1216             return self.node.get_child_at_path(path)
1217         return defer.succeed(self.node)
1218
1219     def locateChild(self, ctx, segments):
1220         req = inevow.IRequest(ctx)
1221         method = req.method
1222         path = tuple([seg.decode("utf-8") for seg in segments])
1223
1224         t = get_arg(req, "t", "")
1225         localfile = get_arg(req, "localfile", None)
1226         if localfile is not None:
1227             if localfile != os.path.abspath(localfile):
1228                 return NeedAbsolutePathError(), ()
1229         localdir = get_arg(req, "localdir", None)
1230         if localdir is not None:
1231             if localdir != os.path.abspath(localdir):
1232                 return NeedAbsolutePathError(), ()
1233         if localfile or localdir:
1234             if not ILocalAccess(ctx).local_access_is_allowed():
1235                 return LocalAccessDisabledError(), ()
1236             if req.getHost().host != LOCALHOST:
1237                 return NeedLocalhostError(), ()
1238         # TODO: think about clobbering/revealing config files and node secrets
1239
1240         replace = boolean_of_arg(get_arg(req, "replace", "true"))
1241
1242         if method == "GET":
1243             # the node must exist, and our operation will be performed on the
1244             # node itself.
1245             d = self.get_child_at_path(path)
1246             def file_or_dir(node):
1247                 if (IFileNode.providedBy(node)
1248                     or IMutableFileNode.providedBy(node)):
1249                     filename = "unknown"
1250                     if path:
1251                         filename = path[-1]
1252                     filename = get_arg(req, "filename", filename)
1253                     if t == "download":
1254                         if localfile:
1255                             # write contents to a local file
1256                             return LocalFileDownloader(node, localfile), ()
1257                         # send contents as the result
1258                         return FileDownloader(node, filename), ()
1259                     elif t == "":
1260                         # send contents as the result
1261                         return FileDownloader(node, filename), ()
1262                     elif t == "json":
1263                         return FileJSONMetadata(node), ()
1264                     elif t == "uri":
1265                         return FileURI(node), ()
1266                     elif t == "readonly-uri":
1267                         return FileReadOnlyURI(node), ()
1268                     else:
1269                         return child_error("bad t=%s" % t)
1270                 elif IDirectoryNode.providedBy(node):
1271                     if t == "download":
1272                         if localdir:
1273                             # recursive download to a local directory
1274                             return LocalDirectoryDownloader(node, localdir), ()
1275                         return child_error("t=download requires localdir=")
1276                     elif t == "":
1277                         # send an HTML representation of the directory
1278                         return Directory(self.name, node, path), ()
1279                     elif t == "json":
1280                         return DirectoryJSONMetadata(node), ()
1281                     elif t == "uri":
1282                         return DirectoryURI(node), ()
1283                     elif t == "readonly-uri":
1284                         return DirectoryReadonlyURI(node), ()
1285                     elif t == "manifest":
1286                         return Manifest(node, path), ()
1287                     elif t == 'rename-form':
1288                         return RenameForm(self.name, node, path), ()
1289                     else:
1290                         return child_error("bad t=%s" % t)
1291                 else:
1292                     return child_error("unknown node type")
1293             d.addCallback(file_or_dir)
1294         elif method == "POST":
1295             # the node must exist, and our operation will be performed on the
1296             # node itself.
1297             d = self.get_child_at_path(path)
1298             def _got_POST(node):
1299                 return POSTHandler(node, replace), ()
1300             d.addCallback(_got_POST)
1301         elif method == "DELETE":
1302             # the node must exist, and our operation will be performed on its
1303             # parent node.
1304             assert path # you can't delete the root
1305             d = self.get_child_at_path(path[:-1])
1306             def _got_DELETE(node):
1307                 return DELETEHandler(node, path[-1]), ()
1308             d.addCallback(_got_DELETE)
1309         elif method in ("PUT",):
1310             # the node may or may not exist, and our operation may involve
1311             # all the ancestors of the node.
1312             return PUTHandler(self.node, path, t, localfile, localdir, replace), ()
1313         else:
1314             return rend.NotFound
1315         return d
1316
1317 class Root(rend.Page):
1318
1319     addSlash = True
1320     docFactory = getxmlfile("welcome.xhtml")
1321
1322     def locateChild(self, ctx, segments):
1323         client = IClient(ctx)
1324         req = inevow.IRequest(ctx)
1325
1326         segments = list(segments) # XXX HELP I AM YUCKY!
1327         while segments and not segments[-1]:
1328             segments.pop()
1329         if not segments:
1330             segments.append('')
1331         segments = tuple(segments)
1332         if segments:
1333             if segments[0] == "uri":
1334                 if len(segments) == 1 or segments[1] == '':
1335                     uri = get_arg(req, "uri", None)
1336                     if uri is not None:
1337                         there = url.URL.fromContext(ctx)
1338                         there = there.clear("uri")
1339                         there = there.child("uri").child(uri)
1340                         return there, ()
1341                 if len(segments) == 1:
1342                     # /uri
1343                     if req.method == "PUT":
1344                         # either "PUT /uri" to create an unlinked file, or
1345                         # "PUT /uri?t=mkdir" to create an unlinked directory
1346                         t = get_arg(req, "t", "").strip()
1347                         if t == "":
1348                             mutable = bool(get_arg(req, "mutable", "").strip())
1349                             if mutable:
1350                                 return unlinked.UnlinkedPUTSSKUploader(), ()
1351                             else:
1352                                 return unlinked.UnlinkedPUTCHKUploader(), ()
1353                         if t == "mkdir":
1354                             return unlinked.UnlinkedPUTCreateDirectory(), ()
1355                         errmsg = "/uri only accepts PUT and PUT?t=mkdir"
1356                         return WebError(http.BAD_REQUEST, errmsg), ()
1357
1358                     elif req.method == "POST":
1359                         # "POST /uri?t=upload&file=newfile" to upload an
1360                         # unlinked file or "POST /uri?t=mkdir" to create a
1361                         # new directory
1362                         t = get_arg(req, "t", "").strip()
1363                         if t in ("", "upload"):
1364                             mutable = bool(get_arg(req, "mutable", "").strip())
1365                             if mutable:
1366                                 return unlinked.UnlinkedPOSTSSKUploader(), ()
1367                             else:
1368                                 return unlinked.UnlinkedPOSTCHKUploader(client, req), ()
1369                         if t == "mkdir":
1370                             return unlinked.UnlinkedPOSTCreateDirectory(), ()
1371                         errmsg = "/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, and POST?t=mkdir"
1372                         return WebError(http.BAD_REQUEST, errmsg), ()
1373                 if len(segments) < 2:
1374                     return rend.NotFound
1375                 uri = segments[1]
1376                 d = defer.maybeDeferred(client.create_node_from_uri, uri)
1377                 d.addCallback(lambda node: VDrive(node, uri))
1378                 d.addCallback(lambda vd: vd.locateChild(ctx, segments[2:]))
1379                 def _trap_KeyError(f):
1380                     f.trap(KeyError)
1381                     return rend.FourOhFour(), ()
1382                 d.addErrback(_trap_KeyError)
1383                 return d
1384             elif segments[0] == "xmlrpc":
1385                 raise NotImplementedError()
1386         return rend.Page.locateChild(self, ctx, segments)
1387
1388     child_webform_css = webform.defaultCSS
1389     child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
1390
1391     child_provisioning = provisioning.ProvisioningTool()
1392     child_status = status.Status()
1393
1394     def data_version(self, ctx, data):
1395         return get_package_versions_string()
1396     def data_import_path(self, ctx, data):
1397         return str(allmydata)
1398     def data_my_nodeid(self, ctx, data):
1399         return idlib.nodeid_b2a(IClient(ctx).nodeid)
1400     def data_storage(self, ctx, data):
1401         client = IClient(ctx)
1402         try:
1403             ss = client.getServiceNamed("storage")
1404         except KeyError:
1405             return "Not running"
1406         allocated = ss.allocated_size()
1407         return "about %d bytes allocated" % allocated
1408
1409     def data_introducer_furl(self, ctx, data):
1410         return IClient(ctx).introducer_furl
1411     def data_connected_to_introducer(self, ctx, data):
1412         if IClient(ctx).connected_to_introducer():
1413             return "yes"
1414         return "no"
1415
1416     def data_helper_furl(self, ctx, data):
1417         try:
1418             uploader = IClient(ctx).getServiceNamed("uploader")
1419         except KeyError:
1420             return None
1421         furl, connected = uploader.get_helper_info()
1422         return furl
1423     def data_connected_to_helper(self, ctx, data):
1424         try:
1425             uploader = IClient(ctx).getServiceNamed("uploader")
1426         except KeyError:
1427             return "no" # we don't even have an Uploader
1428         furl, connected = uploader.get_helper_info()
1429         if connected:
1430             return "yes"
1431         return "no"
1432
1433     def data_known_storage_servers(self, ctx, data):
1434         ic = IClient(ctx).introducer_client
1435         servers = [c
1436                    for c in ic.get_all_connectors().values()
1437                    if c.service_name == "storage"]
1438         return len(servers)
1439
1440     def data_connected_storage_servers(self, ctx, data):
1441         ic = IClient(ctx).introducer_client
1442         return len(ic.get_all_connections_for("storage"))
1443
1444     def data_services(self, ctx, data):
1445         ic = IClient(ctx).introducer_client
1446         c = [ (service_name, nodeid, rsc)
1447               for (nodeid, service_name), rsc
1448               in ic.get_all_connectors().items() ]
1449         c.sort()
1450         return c
1451
1452     def render_service_row(self, ctx, data):
1453         (service_name, nodeid, rsc) = data
1454         ctx.fillSlots("peerid", "%s %s" % (idlib.nodeid_b2a(nodeid),
1455                                            rsc.nickname))
1456         if rsc.rref:
1457             rhost = rsc.remote_host
1458             if nodeid == IClient(ctx).nodeid:
1459                 rhost_s = "(loopback)"
1460             elif isinstance(rhost, address.IPv4Address):
1461                 rhost_s = "%s:%d" % (rhost.host, rhost.port)
1462             else:
1463                 rhost_s = str(rhost)
1464             connected = "Yes: to " + rhost_s
1465             since = rsc.last_connect_time
1466         else:
1467             connected = "No"
1468             since = rsc.last_loss_time
1469
1470         TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
1471         ctx.fillSlots("connected", connected)
1472         ctx.fillSlots("since", time.strftime(TIME_FORMAT, time.localtime(since)))
1473         ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
1474                                                  time.localtime(rsc.announcement_time)))
1475         ctx.fillSlots("version", rsc.version)
1476         ctx.fillSlots("service_name", rsc.service_name)
1477
1478         return ctx.tag
1479
1480     def render_download_form(self, ctx, data):
1481         # this is a form where users can download files by URI
1482         form = T.form(action="uri", method="get",
1483                       enctype="multipart/form-data")[
1484             T.fieldset[
1485             T.legend(class_="freeform-form-label")["Download a file"],
1486             "URI to download: ",
1487             T.input(type="text", name="uri"), " ",
1488             "Filename to download as: ",
1489             T.input(type="text", name="filename"), " ",
1490             T.input(type="submit", value="Download!"),
1491             ]]
1492         return T.div[form]
1493
1494     def render_view_form(self, ctx, data):
1495         # this is a form where users can download files by URI, or jump to a
1496         # named directory
1497         form = T.form(action="uri", method="get",
1498                       enctype="multipart/form-data")[
1499             T.fieldset[
1500             T.legend(class_="freeform-form-label")["View a file or directory"],
1501             "URI to view: ",
1502             T.input(type="text", name="uri"), " ",
1503             T.input(type="submit", value="View!"),
1504             ]]
1505         return T.div[form]
1506
1507     def render_upload_form(self, ctx, data):
1508         # this is a form where users can upload unlinked files
1509         form = T.form(action="uri", method="post",
1510                       enctype="multipart/form-data")[
1511             T.fieldset[
1512             T.legend(class_="freeform-form-label")["Upload a file"],
1513             "Choose a file: ",
1514             T.input(type="file", name="file", class_="freeform-input-file"),
1515             T.input(type="hidden", name="t", value="upload"),
1516             " Mutable?:", T.input(type="checkbox", name="mutable"),
1517             T.input(type="submit", value="Upload!"),
1518             ]]
1519         return T.div[form]
1520
1521     def render_mkdir_form(self, ctx, data):
1522         # this is a form where users can create new directories
1523         form = T.form(action="uri", method="post",
1524                       enctype="multipart/form-data")[
1525             T.fieldset[
1526             T.legend(class_="freeform-form-label")["Create a directory"],
1527             T.input(type="hidden", name="t", value="mkdir"),
1528             T.input(type="hidden", name="redirect_to_result", value="true"),
1529             T.input(type="submit", value="Create Directory!"),
1530             ]]
1531         return T.div[form]
1532
1533
1534 class LocalAccess:
1535     implements(ILocalAccess)
1536     def __init__(self):
1537         self.local_access = False
1538     def local_access_is_allowed(self):
1539         return self.local_access
1540
1541 class WebishServer(service.MultiService):
1542     name = "webish"
1543
1544     def __init__(self, webport, nodeurl_path=None):
1545         service.MultiService.__init__(self)
1546         self.webport = webport
1547         self.root = Root()
1548         self.site = site = appserver.NevowSite(self.root)
1549         self.site.requestFactory = MyRequest
1550         self.allow_local = LocalAccess()
1551         self.site.remember(self.allow_local, ILocalAccess)
1552         s = strports.service(webport, site)
1553         s.setServiceParent(self)
1554         self.listener = s # stash it so the tests can query for the portnum
1555         self._started = defer.Deferred()
1556         if nodeurl_path:
1557             self._started.addCallback(self._write_nodeurl_file, nodeurl_path)
1558
1559     def allow_local_access(self, enable=True):
1560         self.allow_local.local_access = enable
1561
1562     def startService(self):
1563         service.MultiService.startService(self)
1564         # to make various services available to render_* methods, we stash a
1565         # reference to the client on the NevowSite. This will be available by
1566         # adapting the 'context' argument to a special marker interface named
1567         # IClient.
1568         self.site.remember(self.parent, IClient)
1569         # I thought you could do the same with an existing interface, but
1570         # apparently 'ISite' does not exist
1571         #self.site._client = self.parent
1572         self._started.callback(None)
1573
1574     def _write_nodeurl_file(self, junk, nodeurl_path):
1575         # what is our webport?
1576         s = self.listener
1577         if isinstance(s, internet.TCPServer):
1578             base_url = "http://localhost:%d" % s._port.getHost().port
1579         elif isinstance(s, internet.SSLServer):
1580             base_url = "https://localhost:%d" % s._port.getHost().port
1581         else:
1582             base_url = None
1583         if base_url:
1584             f = open(nodeurl_path, 'wb')
1585             # this file is world-readable
1586             f.write(base_url + "\n")
1587             f.close()
1588