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