2 from base64 import b32encode
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
13 from allmydata.interfaces import IDownloadTarget, IDirectoryNode, IFileNode, \
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
21 from formless import webform
24 return loaders.xmlfile(util.sibpath(__file__, "web/%s" % name))
26 class IClient(Interface):
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."""
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")
37 def get_arg(req, argname, default=None, multiple=False):
38 """Extract an argument from either the query args (req.args) or the form
39 body fields (req.fields). If multiple=False, this returns a single value
40 (or the default, which defaults to None), and the query args take
41 precedence. If multiple=True, this returns a tuple of arguments (possibly
42 empty), starting with all those in the query args.
45 if argname in req.args:
46 results.extend(req.args[argname])
47 if req.fields and argname in req.fields:
48 results.append(req.fields[argname].value)
55 # we must override twisted.web.http.Request.requestReceived with a version
56 # that doesn't use cgi.parse_multipart() . Since we actually use Nevow, we
57 # override the nevow-specific subclass, nevow.appserver.NevowRequest . This
58 # is an exact copy of twisted.web.http.Request (from SVN HEAD on 10-Aug-2007)
59 # that modifies the way form arguments are parsed. Note that this sort of
60 # surgery may induce a dependency upon a particular version of twisted.web
62 parse_qs = http.parse_qs
63 class MyRequest(appserver.NevowRequest):
64 def requestReceived(self, command, path, version):
65 """Called by channel when all data has been received.
67 This method is not intended for users.
69 self.content.seek(0,0)
73 self.method, self.uri = command, path
74 self.clientproto = version
75 x = self.uri.split('?', 1)
80 self.path, argstring = x
81 self.args = parse_qs(argstring, 1)
83 # cache the client and server information, we'll need this later to be
84 # serialized and sent with the request so CGIs will work remotely
85 self.client = self.channel.transport.getPeer()
86 self.host = self.channel.transport.getHost()
88 # Argument processing.
90 ## The original twisted.web.http.Request.requestReceived code parsed the
91 ## content and added the form fields it found there to self.args . It
92 ## did this with cgi.parse_multipart, which holds the arguments in RAM
93 ## and is thus unsuitable for large file uploads. The Nevow subclass
94 ## (nevow.appserver.NevowRequest) uses cgi.FieldStorage instead (putting
95 ## the results in self.fields), which is much more memory-efficient.
96 ## Since we know we're using Nevow, we can anticipate these arguments
97 ## appearing in self.fields instead of self.args, and thus skip the
98 ## parse-content-into-self.args step.
101 ## ctype = self.getHeader('content-type')
102 ## if self.method == "POST" and ctype:
103 ## mfd = 'multipart/form-data'
104 ## key, pdict = cgi.parse_header(ctype)
105 ## if key == 'application/x-www-form-urlencoded':
106 ## args.update(parse_qs(self.content.read(), 1))
109 ## args.update(cgi.parse_multipart(self.content, pdict))
110 ## except KeyError, e:
111 ## if e.args[0] == 'content-disposition':
112 ## # Parse_multipart can't cope with missing
113 ## # content-dispostion headers in multipart/form-data
114 ## # parts, so we catch the exception and tell the client
115 ## # it was a bad request.
116 ## self.channel.transport.write(
117 ## "HTTP/1.1 400 Bad Request\r\n\r\n")
118 ## self.channel.transport.loseConnection()
124 class Directory(rend.Page):
126 docFactory = getxmlfile("directory.xhtml")
128 def __init__(self, rootname, dirnode, dirpath):
129 self._rootname = rootname
130 self._dirnode = dirnode
131 self._dirpath = dirpath
133 def dirpath_as_string(self):
134 return "/" + "/".join(self._dirpath)
136 def render_title(self, ctx, data):
137 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
139 def render_header(self, ctx, data):
140 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
141 num_dirs = len(parent_directories)
143 header = ["Directory '"]
144 for i,d in enumerate(parent_directories):
145 upness = num_dirs - i - 1
147 link = "/".join( ("..",) * upness )
150 header.append(T.a(href=link)[d])
155 if self._dirnode.is_readonly():
156 header.append(" (readonly)")
158 return ctx.tag[header]
160 def render_welcome(self, ctx, data):
161 depth = len(self._dirpath) + 2
162 link = "/".join([".."] * depth)
163 return T.div[T.a(href=link)["Return to Welcome page"]]
165 def data_children(self, ctx, data):
166 d = self._dirnode.list()
167 d.addCallback(lambda dict: sorted(dict.items()))
170 def render_row(self, ctx, data):
171 name, (target, metadata) = data
173 if self._dirnode.is_readonly():
177 # this creates a button which will cause our child__delete method
178 # to be invoked, which deletes the file and then redirects the
179 # browser back to this directory
180 delete = T.form(action=url.here, method="post")[
181 T.input(type='hidden', name='t', value='delete'),
182 T.input(type='hidden', name='name', value=name),
183 T.input(type='hidden', name='when_done', value=url.here),
184 T.input(type='submit', value='del', name="del"),
187 rename = T.form(action=url.here, method="get")[
188 T.input(type='hidden', name='t', value='rename-form'),
189 T.input(type='hidden', name='name', value=name),
190 T.input(type='hidden', name='when_done', value=url.here),
191 T.input(type='submit', value='rename', name="rename"),
194 ctx.fillSlots("delete", delete)
195 ctx.fillSlots("rename", rename)
196 check = T.form(action=url.here, method="post")[
197 T.input(type='hidden', name='t', value='check'),
198 T.input(type='hidden', name='name', value=name),
199 T.input(type='hidden', name='when_done', value=url.here),
200 T.input(type='submit', value='check', name="check"),
202 ctx.fillSlots("overwrite", self.build_overwrite(ctx, (name, target)))
203 ctx.fillSlots("check", check)
205 # build the base of the uri_link link url
206 uri_link = "/uri/" + urllib.quote(target.get_uri())
208 assert (IFileNode.providedBy(target)
209 or IDirectoryNode.providedBy(target)
210 or IMutableFileNode.providedBy(target)), target
212 if IMutableFileNode.providedBy(target):
215 # add the filename to the uri_link url
216 uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
218 # to prevent javascript in displayed .html files from stealing a
219 # secret directory URI from the URL, send the browser to a URI-based
220 # page that doesn't know about the directory at all
221 #dlurl = urllib.quote(name)
224 ctx.fillSlots("filename",
225 T.a(href=dlurl)[html.escape(name)])
226 ctx.fillSlots("type", "SSK")
228 ctx.fillSlots("size", "?")
230 text_plain_link = uri_link + "?filename=foo.txt"
231 text_plain_tag = T.a(href=text_plain_link)["text/plain"]
233 elif IFileNode.providedBy(target):
236 # add the filename to the uri_link url
237 uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
239 # to prevent javascript in displayed .html files from stealing a
240 # secret directory URI from the URL, send the browser to a URI-based
241 # page that doesn't know about the directory at all
242 #dlurl = urllib.quote(name)
245 ctx.fillSlots("filename",
246 T.a(href=dlurl)[html.escape(name)])
247 ctx.fillSlots("type", "FILE")
249 ctx.fillSlots("size", target.get_size())
251 text_plain_link = uri_link + "?filename=foo.txt"
252 text_plain_tag = T.a(href=text_plain_link)["text/plain"]
254 elif IDirectoryNode.providedBy(target):
256 subdir_url = urllib.quote(name)
257 ctx.fillSlots("filename",
258 T.a(href=subdir_url)[html.escape(name)])
259 if target.is_readonly():
263 ctx.fillSlots("type", dirtype)
264 ctx.fillSlots("size", "-")
265 text_plain_tag = None
267 childdata = [T.a(href="%s?t=json" % name)["JSON"], ", ",
268 T.a(href="%s?t=uri" % name)["URI"], ", ",
269 T.a(href="%s?t=readonly-uri" % name)["readonly-URI"], ", ",
270 T.a(href=uri_link)["URI-link"],
273 childdata.extend([", ", text_plain_tag])
275 ctx.fillSlots("data", childdata)
278 checker = IClient(ctx).getServiceNamed("checker")
282 d = defer.maybeDeferred(checker.checker_results_for,
283 target.get_verifier())
284 def _got(checker_results):
285 recent_results = reversed(checker_results[-5:])
286 if IFileNode.providedBy(target):
288 ", ".join(["%d/%d" % (found, needed)
290 (needed, total, found, sharemap))
291 in recent_results]) +
293 elif IDirectoryNode.providedBy(target):
295 "".join([{True:"+",False:"-"}[res]
296 for (when, res) in recent_results]) +
299 results = "%d results" % len(checker_results)
305 # TODO: include a link to see more results, including timestamps
306 # TODO: use a sparkline
307 ctx.fillSlots("checker_results", results)
311 def render_forms(self, ctx, data):
312 if self._dirnode.is_readonly():
313 return T.div["No upload forms: directory is read-only"]
314 mkdir = T.form(action=".", method="post",
315 enctype="multipart/form-data")[
317 T.input(type="hidden", name="t", value="mkdir"),
318 T.input(type="hidden", name="when_done", value=url.here),
319 T.legend(class_="freeform-form-label")["Create a new directory"],
320 "New directory name: ",
321 T.input(type="text", name="name"), " ",
322 T.input(type="submit", value="Create"),
325 upload = T.form(action=".", method="post",
326 enctype="multipart/form-data")[
328 T.input(type="hidden", name="t", value="upload"),
329 T.input(type="hidden", name="when_done", value=url.here),
330 T.legend(class_="freeform-form-label")["Upload a file to this directory"],
331 "Choose a file to upload: ",
332 T.input(type="file", name="file", class_="freeform-input-file"),
334 T.input(type="submit", value="Upload"),
336 T.input(type="checkbox", name="mutable"),
339 mount = T.form(action=".", method="post",
340 enctype="multipart/form-data")[
342 T.input(type="hidden", name="t", value="uri"),
343 T.input(type="hidden", name="when_done", value=url.here),
344 T.legend(class_="freeform-form-label")["Attach a file or directory"
348 T.input(type="text", name="name"), " ",
349 "URI of new child: ",
350 T.input(type="text", name="uri"), " ",
351 T.input(type="submit", value="Attach"),
353 return [T.div(class_="freeform-form")[mkdir],
354 T.div(class_="freeform-form")[upload],
355 T.div(class_="freeform-form")[mount],
358 def build_overwrite(self, ctx, data):
360 if IMutableFileNode.providedBy(target) and not target.is_readonly():
361 action="/uri/" + urllib.quote(target.get_uri())
362 overwrite = T.form(action=action, method="post",
363 enctype="multipart/form-data")[
365 T.input(type="hidden", name="t", value="overwrite"),
366 T.input(type='hidden', name='name', value=name),
367 T.input(type='hidden', name='when_done', value=url.here),
368 T.legend(class_="freeform-form-label")["Overwrite"],
370 T.input(type="file", name="file", class_="freeform-input-file"),
372 T.input(type="submit", value="Overwrite")
374 return [T.div(class_="freeform-form")[overwrite],]
378 def render_results(self, ctx, data):
379 req = inevow.IRequest(ctx)
380 if "results" in req.args:
381 return req.args["results"]
385 class WebDownloadTarget:
386 implements(IDownloadTarget, IConsumer)
387 def __init__(self, req, content_type, content_encoding, save_to_file):
389 self._content_type = content_type
390 self._content_encoding = content_encoding
392 self._producer = None
393 self._save_to_file = save_to_file
395 def registerProducer(self, producer, streaming):
396 self._req.registerProducer(producer, streaming)
397 def unregisterProducer(self):
398 self._req.unregisterProducer()
400 def open(self, size):
402 self._req.setHeader("content-type", self._content_type)
403 if self._content_encoding:
404 self._req.setHeader("content-encoding", self._content_encoding)
405 self._req.setHeader("content-length", str(size))
406 if self._save_to_file is not None:
407 # tell the browser to save the file rather display it
408 # TODO: quote save_to_file properly
409 self._req.setHeader("content-disposition",
410 'attachment; filename="%s"'
411 % self._save_to_file)
413 def write(self, data):
414 self._req.write(data)
420 # The content-type is already set, and the response code
421 # has already been sent, so we can't provide a clean error
422 # indication. We can emit text (which a browser might interpret
423 # as something else), and if we sent a Size header, they might
424 # notice that we've truncated the data. Keep the error message
425 # small to improve the chances of having our error response be
426 # shorter than the intended results.
428 # We don't have a lot of options, unfortunately.
429 self._req.write("problem during download\n")
431 # We haven't written anything yet, so we can provide a sensible
434 msg.replace("\n", "|")
435 self._req.setResponseCode(http.GONE, msg)
436 self._req.setHeader("content-type", "text/plain")
437 # TODO: HTML-formatted exception?
438 self._req.write(str(why))
441 def register_canceller(self, cb):
446 class FileDownloader(resource.Resource):
447 def __init__(self, filenode, name):
448 assert (IFileNode.providedBy(filenode)
449 or IMutableFileNode.providedBy(filenode))
450 self._filenode = filenode
453 def render(self, req):
454 gte = static.getTypeAndEncoding
455 type, encoding = gte(self._name,
456 static.File.contentTypes,
457 static.File.contentEncodings,
458 defaultType="text/plain")
460 if "save" in req.args:
461 save_to_file = self._name
462 wdt = WebDownloadTarget(req, type, encoding, save_to_file)
463 d = self._filenode.download(wdt)
464 # exceptions during download are handled by the WebDownloadTarget
465 d.addErrback(lambda why: None)
466 return server.NOT_DONE_YET
468 class BlockingFileError(Exception):
469 """We cannot auto-create a parent directory, because there is a file in
471 class NoReplacementError(Exception):
472 """There was already a child by that name, and you asked me to not replace it"""
474 LOCALHOST = "127.0.0.1"
476 class NeedLocalhostError:
477 implements(inevow.IResource)
479 def renderHTTP(self, ctx):
480 req = inevow.IRequest(ctx)
481 req.setResponseCode(http.FORBIDDEN)
482 req.setHeader("content-type", "text/plain")
483 return "localfile= or localdir= requires a local connection"
485 class NeedAbsolutePathError:
486 implements(inevow.IResource)
488 def renderHTTP(self, ctx):
489 req = inevow.IRequest(ctx)
490 req.setResponseCode(http.FORBIDDEN)
491 req.setHeader("content-type", "text/plain")
492 return "localfile= or localdir= requires an absolute path"
494 class LocalAccessDisabledError:
495 implements(inevow.IResource)
497 def renderHTTP(self, ctx):
498 req = inevow.IRequest(ctx)
499 req.setResponseCode(http.FORBIDDEN)
500 req.setHeader("content-type", "text/plain")
501 return "local file access is disabled"
504 class LocalFileDownloader(resource.Resource):
505 def __init__(self, filenode, local_filename):
506 self._local_filename = local_filename
508 self._filenode = filenode
510 def render(self, req):
511 target = download.FileName(self._local_filename)
512 d = self._filenode.download(target)
514 req.write(self._filenode.get_uri())
517 return server.NOT_DONE_YET
520 class FileJSONMetadata(rend.Page):
521 def __init__(self, filenode):
522 self._filenode = filenode
524 def renderHTTP(self, ctx):
525 req = inevow.IRequest(ctx)
526 req.setHeader("content-type", "text/plain")
527 return self.renderNode(self._filenode)
529 def renderNode(self, filenode):
530 file_uri = filenode.get_uri()
533 'size': filenode.get_size(),
535 return simplejson.dumps(data, indent=1)
537 class FileURI(FileJSONMetadata):
538 def renderNode(self, filenode):
539 file_uri = filenode.get_uri()
542 class FileReadOnlyURI(FileJSONMetadata):
543 def renderNode(self, filenode):
544 if filenode.is_readonly():
545 return filenode.get_uri()
547 return filenode.get_readonly().get_uri()
549 class DirnodeWalkerMixin:
550 """Visit all nodes underneath (and including) the rootnode, one at a
551 time. For each one, call the visitor. The visitor will see the
552 IDirectoryNode before it sees any of the IFileNodes inside. If the
553 visitor returns a Deferred, I do not call the visitor again until it has
557 ## def _walk_if_we_could_use_generators(self, rootnode, rootpath=()):
558 ## # this is what we'd be doing if we didn't have the Deferreds and
559 ## # thus could use generators
560 ## yield rootpath, rootnode
561 ## for childname, childnode in rootnode.list().items():
562 ## childpath = rootpath + (childname,)
563 ## if IFileNode.providedBy(childnode):
564 ## yield childpath, childnode
565 ## elif IDirectoryNode.providedBy(childnode):
566 ## for res in self._walk_if_we_could_use_generators(childnode,
570 def walk(self, rootnode, visitor, rootpath=()):
572 def _listed(listing):
573 return listing.items()
574 d.addCallback(_listed)
575 d.addCallback(self._handle_items, visitor, rootpath)
578 def _handle_items(self, items, visitor, rootpath):
581 childname, (childnode, metadata) = items[0]
582 childpath = rootpath + (childname,)
583 d = defer.maybeDeferred(visitor, childpath, childnode, metadata)
584 if IDirectoryNode.providedBy(childnode):
585 d.addCallback(lambda res: self.walk(childnode, visitor, childpath))
586 d.addCallback(lambda res:
587 self._handle_items(items[1:], visitor, rootpath))
590 class LocalDirectoryDownloader(resource.Resource, DirnodeWalkerMixin):
591 def __init__(self, dirnode, localdir):
592 self._dirnode = dirnode
593 self._localdir = localdir
595 def _handle(self, path, node, metadata):
596 localfile = os.path.join(self._localdir, os.sep.join(path))
597 if IDirectoryNode.providedBy(node):
598 fileutil.make_dirs(localfile)
599 elif IFileNode.providedBy(node):
600 target = download.FileName(localfile)
601 return node.download(target)
603 def render(self, req):
604 d = self.walk(self._dirnode, self._handle)
606 req.setHeader("content-type", "text/plain")
607 return "operation complete"
611 class DirectoryJSONMetadata(rend.Page):
612 def __init__(self, dirnode):
613 self._dirnode = dirnode
615 def renderHTTP(self, ctx):
616 req = inevow.IRequest(ctx)
617 req.setHeader("content-type", "text/plain")
618 return self.renderNode(self._dirnode)
620 def renderNode(self, node):
624 for name, (childnode, metadata) in children.iteritems():
625 if IFileNode.providedBy(childnode):
626 kiduri = childnode.get_uri()
627 kiddata = ("filenode",
629 'size': childnode.get_size(),
632 assert IDirectoryNode.providedBy(childnode), (childnode, children,)
633 kiddata = ("dirnode",
634 {'ro_uri': childnode.get_readonly_uri(),
636 if not childnode.is_readonly():
637 kiddata[1]['rw_uri'] = childnode.get_uri()
639 contents = { 'children': kids,
640 'ro_uri': node.get_readonly_uri(),
642 if not node.is_readonly():
643 contents['rw_uri'] = node.get_uri()
644 data = ("dirnode", contents)
645 return simplejson.dumps(data, indent=1)
649 class DirectoryURI(DirectoryJSONMetadata):
650 def renderNode(self, node):
651 return node.get_uri()
653 class DirectoryReadonlyURI(DirectoryJSONMetadata):
654 def renderNode(self, node):
655 return node.get_readonly_uri()
657 class RenameForm(rend.Page):
659 docFactory = getxmlfile("rename-form.xhtml")
661 def __init__(self, rootname, dirnode, dirpath):
662 self._rootname = rootname
663 self._dirnode = dirnode
664 self._dirpath = dirpath
666 def dirpath_as_string(self):
667 return "/" + "/".join(self._dirpath)
669 def render_title(self, ctx, data):
670 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
672 def render_header(self, ctx, data):
673 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
674 num_dirs = len(parent_directories)
676 header = [ "Rename in directory '",
677 "<%s>/" % self._rootname,
678 "/".join(self._dirpath),
681 if self._dirnode.is_readonly():
682 header.append(" (readonly)")
683 return ctx.tag[header]
685 def render_when_done(self, ctx, data):
686 return T.input(type="hidden", name="when_done", value=url.here)
688 def render_get_name(self, ctx, data):
689 req = inevow.IRequest(ctx)
690 if 'name' in req.args:
691 name = req.args['name'][0]
694 ctx.tag.attributes['value'] = name
697 class POSTHandler(rend.Page):
698 def __init__(self, node, replace):
700 self._replace = replace
702 def _check_replacement(self, name):
704 return defer.succeed(None)
705 d = self._node.has_child(name)
708 raise NoReplacementError("There was already a child by that "
709 "name, and you asked me to not "
715 def renderHTTP(self, ctx):
716 req = inevow.IRequest(ctx)
721 t = req.fields["t"].value
724 if "name" in req.args:
725 name = req.args["name"][0]
726 elif "name" in req.fields:
727 name = req.fields["name"].value
728 if name and "/" in name:
729 req.setResponseCode(http.BAD_REQUEST)
730 req.setHeader("content-type", "text/plain")
731 return "name= may not contain a slash"
734 # we allow the user to delete an empty-named file, but not to create
735 # them, since that's an easy and confusing mistake to make
738 if "when_done" in req.args:
739 when_done = req.args["when_done"][0]
740 if "when_done" in req.fields:
741 when_done = req.fields["when_done"].value
743 if "replace" in req.fields:
744 if not boolean_of_arg(req.fields["replace"].value):
745 self._replace = False
749 raise RuntimeError("mkdir requires a name")
750 d = self._check_replacement(name)
751 d.addCallback(lambda res: self._node.create_empty_directory(name))
752 d.addCallback(lambda res: "directory created")
755 raise RuntimeError("set-uri requires a name")
756 if "uri" in req.args:
757 newuri = req.args["uri"][0].strip()
759 newuri = req.fields["uri"].value.strip()
760 d = self._check_replacement(name)
761 d.addCallback(lambda res: self._node.set_uri(name, newuri))
762 d.addCallback(lambda res: newuri)
765 # apparently an <input type="hidden" name="name" value="">
766 # won't show up in the resulting encoded form.. the 'name'
767 # field is completely missing. So to allow deletion of an
768 # empty file, we have to pretend that None means ''. The only
769 # downide of this is a slightly confusing error message if
770 # someone does a POST without a name= field. For our own HTML
771 # thisn't a big deal, because we create the 'delete' POST
774 d = self._node.delete(name)
775 d.addCallback(lambda res: "thing deleted")
777 from_name = 'from_name' in req.fields and req.fields["from_name"].value
778 if from_name is not None:
779 from_name = from_name.strip()
780 to_name = 'to_name' in req.fields and req.fields["to_name"].value
781 if to_name is not None:
782 to_name = to_name.strip()
783 if not from_name or not to_name:
784 raise RuntimeError("rename requires from_name and to_name")
785 if not IDirectoryNode.providedBy(self._node):
786 raise RuntimeError("rename must only be called on directories")
787 for k,v in [ ('from_name', from_name), ('to_name', to_name) ]:
789 req.setResponseCode(http.BAD_REQUEST)
790 req.setHeader("content-type", "text/plain")
791 return "%s= may not contain a slash" % (k,)
792 d = self._check_replacement(to_name)
793 d.addCallback(lambda res: self._node.get(from_name))
795 uri = child.get_uri()
796 # now actually do the rename
797 return self._node.set_uri(to_name, uri)
798 d.addCallback(add_dest)
800 return self._node.delete(from_name)
801 d.addCallback(rm_src)
802 d.addCallback(lambda res: "thing renamed")
805 if "mutable" in req.fields:
806 contents = req.fields["file"]
807 name = name or contents.filename
811 raise RuntimeError("upload-mutable requires a name")
812 # SDMF: files are small, and we can only upload data.
813 contents.file.seek(0)
814 data = contents.file.read()
815 uploadable = FileHandle(contents.file)
816 d = self._check_replacement(name)
817 d.addCallback(lambda res: self._node.has_child(name))
818 def _checked(present):
820 # modify the existing one instead of creating a new
822 d2 = self._node.get(name)
823 def _got_newnode(newnode):
824 d3 = newnode.replace(data)
825 d3.addCallback(lambda res: newnode.get_uri())
827 d2.addCallback(_got_newnode)
829 d2 = IClient(ctx).create_mutable_file(data)
830 def _uploaded(newnode):
831 d1 = self._node.set_node(name, newnode)
832 d1.addCallback(lambda res: newnode.get_uri())
834 d2.addCallback(_uploaded)
836 d.addCallback(_checked)
838 contents = req.fields["file"]
839 name = name or contents.filename
843 raise RuntimeError("upload requires a name")
844 uploadable = FileHandle(contents.file)
845 d = self._check_replacement(name)
846 d.addCallback(lambda res: self._node.add_file(name, uploadable))
848 return newnode.get_uri()
851 elif t == "overwrite":
852 contents = req.fields["file"]
853 # SDMF: files are small, and we can only upload data.
854 contents.file.seek(0)
855 data = contents.file.read()
856 # TODO: 'name' handling needs review
857 d = defer.succeed(self._node)
858 def _got_child_overwrite(child_node):
859 child_node.replace(data)
860 return child_node.get_uri()
861 d.addCallback(_got_child_overwrite)
864 d = self._node.get(name)
865 def _got_child_check(child_node):
866 d2 = child_node.check()
868 log.msg("checked %s, results %s" % (child_node, res))
870 d2.addCallback(_done)
872 d.addCallback(_got_child_check)
875 return "BAD t=%s" % t
877 d.addCallback(lambda res: url.URL.fromString(when_done))
878 def _check_replacement(f):
879 # TODO: make this more human-friendly: maybe send them to the
880 # when_done page but with an extra query-arg that will display
881 # the error message in a big box at the top of the page. The
882 # directory page that when_done= usually points to accepts a
883 # result= argument.. use that.
884 f.trap(NoReplacementError)
885 req.setResponseCode(http.CONFLICT)
886 req.setHeader("content-type", "text/plain")
888 d.addErrback(_check_replacement)
891 class DELETEHandler(rend.Page):
892 def __init__(self, node, name):
896 def renderHTTP(self, ctx):
897 req = inevow.IRequest(ctx)
898 d = self._node.delete(self._name)
900 # what should this return??
901 return "%s deleted" % self._name
903 def _trap_missing(f):
905 req.setResponseCode(http.NOT_FOUND)
906 req.setHeader("content-type", "text/plain")
907 return "no such child %s" % self._name
908 d.addErrback(_trap_missing)
911 class PUTHandler(rend.Page):
912 def __init__(self, node, path, t, localfile, localdir, replace):
916 self._localfile = localfile
917 self._localdir = localdir
918 self._replace = replace
920 def renderHTTP(self, ctx):
921 req = inevow.IRequest(ctx)
923 localfile = self._localfile
924 localdir = self._localdir
926 # we must traverse the path, creating new directories as necessary
927 d = self._get_or_create_directories(self._node, self._path[:-1])
928 name = self._path[-1]
929 d.addCallback(self._check_replacement, name, self._replace)
932 d.addCallback(self._upload_localfile, localfile, name)
935 d.addCallback(self._get_or_create_directories, self._path[-1:])
936 d.addCallback(self._upload_localdir, localdir)
938 raise RuntimeError("t=upload requires localfile= or localdir=")
940 d.addCallback(self._attach_uri, req.content, name)
942 d.addCallback(self._mkdir, name)
944 d.addCallback(self._upload_file, req.content, name)
945 def _check_blocking(f):
946 f.trap(BlockingFileError)
947 req.setResponseCode(http.BAD_REQUEST)
948 req.setHeader("content-type", "text/plain")
950 d.addErrback(_check_blocking)
951 def _check_replacement(f):
952 f.trap(NoReplacementError)
953 req.setResponseCode(http.CONFLICT)
954 req.setHeader("content-type", "text/plain")
956 d.addErrback(_check_replacement)
959 def _get_or_create_directories(self, node, path):
960 if not IDirectoryNode.providedBy(node):
961 # unfortunately it is too late to provide the name of the
962 # blocking directory in the error message.
963 raise BlockingFileError("cannot create directory because there "
964 "is a file in the way")
966 return defer.succeed(node)
967 d = node.get(path[0])
968 def _maybe_create(f):
970 return node.create_empty_directory(path[0])
971 d.addErrback(_maybe_create)
972 d.addCallback(self._get_or_create_directories, path[1:])
975 def _check_replacement(self, node, name, replace):
978 d = node.has_child(name)
981 raise NoReplacementError("There was already a child by that "
982 "name, and you asked me to not "
988 def _mkdir(self, node, name):
989 d = node.create_empty_directory(name)
991 return newnode.get_uri()
995 def _upload_file(self, node, contents, name):
996 uploadable = FileHandle(contents)
997 d = node.add_file(name, uploadable)
999 log.msg("webish upload complete")
1000 return filenode.get_uri()
1001 d.addCallback(_done)
1004 def _upload_localfile(self, node, localfile, name):
1005 uploadable = FileName(localfile)
1006 d = node.add_file(name, uploadable)
1007 d.addCallback(lambda filenode: filenode.get_uri())
1010 def _attach_uri(self, parentnode, contents, name):
1011 newuri = contents.read().strip()
1012 d = parentnode.set_uri(name, newuri)
1015 d.addCallback(_done)
1018 def _upload_localdir(self, node, localdir):
1019 # build up a list of files to upload
1022 msg = "No files to upload! %s is empty" % localdir
1023 if not os.path.exists(localdir):
1024 msg = "%s doesn't exist!" % localdir
1025 for root, dirs, files in os.walk(localdir):
1026 if root == localdir:
1029 relative_root = root[len(localdir)+1:]
1030 path = tuple(relative_root.split(os.sep))
1032 all_dirs.append(path + (d,))
1034 all_files.append(path + (f,))
1035 d = defer.succeed(msg)
1036 for dir in all_dirs:
1038 d.addCallback(self._makedir, node, dir)
1040 d.addCallback(self._upload_one_file, node, localdir, f)
1043 def _makedir(self, res, node, dir):
1044 d = defer.succeed(None)
1045 # get the parent. As long as os.walk gives us parents before
1046 # children, this ought to work
1047 d.addCallback(lambda res: node.get_child_at_path(dir[:-1]))
1048 # then create the child directory
1049 d.addCallback(lambda parent: parent.create_empty_directory(dir[-1]))
1052 def _upload_one_file(self, res, node, localdir, f):
1053 # get the parent. We can be sure this exists because we already
1054 # went through and created all the directories we require.
1055 localfile = os.path.join(localdir, *f)
1056 d = node.get_child_at_path(f[:-1])
1057 d.addCallback(self._upload_localfile, localfile, f[-1])
1061 class Manifest(rend.Page):
1062 docFactory = getxmlfile("manifest.xhtml")
1063 def __init__(self, dirnode, dirpath):
1064 self._dirnode = dirnode
1065 self._dirpath = dirpath
1067 def dirpath_as_string(self):
1068 return "/" + "/".join(self._dirpath)
1070 def render_title(self, ctx):
1071 return T.title["Manifest of %s" % self.dirpath_as_string()]
1073 def render_header(self, ctx):
1074 return T.p["Manifest of %s" % self.dirpath_as_string()]
1076 def data_items(self, ctx, data):
1077 return self._dirnode.build_manifest()
1079 def render_row(self, ctx, refresh_cap):
1080 ctx.fillSlots("refresh_capability", refresh_cap)
1083 class VDrive(rend.Page):
1085 def __init__(self, node, name):
1089 def get_child_at_path(self, path):
1091 return self.node.get_child_at_path(path)
1092 return defer.succeed(self.node)
1094 def locateChild(self, ctx, segments):
1095 req = inevow.IRequest(ctx)
1099 # when we're pointing at a directory (like /uri/$DIR_URI/my_pix),
1100 # Directory.addSlash causes a redirect to /uri/$DIR_URI/my_pix/,
1101 # which appears here as ['my_pix', '']. This is supposed to hit the
1102 # same Directory as ['my_pix'].
1103 if path and path[-1] == '':
1108 t = req.args["t"][0]
1111 if "localfile" in req.args:
1112 localfile = req.args["localfile"][0]
1113 if localfile != os.path.abspath(localfile):
1114 return NeedAbsolutePathError(), ()
1116 if "localdir" in req.args:
1117 localdir = req.args["localdir"][0]
1118 if localdir != os.path.abspath(localdir):
1119 return NeedAbsolutePathError(), ()
1120 if localfile or localdir:
1121 if not ILocalAccess(ctx).local_access_is_allowed():
1122 return LocalAccessDisabledError(), ()
1123 if req.getHost().host != LOCALHOST:
1124 return NeedLocalhostError(), ()
1125 # TODO: think about clobbering/revealing config files and node secrets
1128 if "replace" in req.args:
1129 if not boolean_of_arg(req.args["replace"][0]):
1133 # the node must exist, and our operation will be performed on the
1135 d = self.get_child_at_path(path)
1136 def file_or_dir(node):
1137 if (IFileNode.providedBy(node)
1138 or IMutableFileNode.providedBy(node)):
1139 filename = "unknown"
1142 if "filename" in req.args:
1143 filename = req.args["filename"][0]
1146 # write contents to a local file
1147 return LocalFileDownloader(node, localfile), ()
1148 # send contents as the result
1149 return FileDownloader(node, filename), ()
1151 # send contents as the result
1152 return FileDownloader(node, filename), ()
1154 return FileJSONMetadata(node), ()
1156 return FileURI(node), ()
1157 elif t == "readonly-uri":
1158 return FileReadOnlyURI(node), ()
1160 raise RuntimeError("bad t=%s" % t)
1161 elif IDirectoryNode.providedBy(node):
1164 # recursive download to a local directory
1165 return LocalDirectoryDownloader(node, localdir), ()
1166 raise RuntimeError("t=download requires localdir=")
1168 # send an HTML representation of the directory
1169 return Directory(self.name, node, path), ()
1171 return DirectoryJSONMetadata(node), ()
1173 return DirectoryURI(node), ()
1174 elif t == "readonly-uri":
1175 return DirectoryReadonlyURI(node), ()
1176 elif t == "manifest":
1177 return Manifest(node, path), ()
1178 elif t == 'rename-form':
1179 return RenameForm(self.name, node, path), ()
1181 raise RuntimeError("bad t=%s" % t)
1183 raise RuntimeError("unknown node type")
1184 d.addCallback(file_or_dir)
1185 elif method == "POST":
1186 # the node must exist, and our operation will be performed on the
1188 d = self.get_child_at_path(path)
1189 def _got_POST(node):
1190 return POSTHandler(node, replace), ()
1191 d.addCallback(_got_POST)
1192 elif method == "DELETE":
1193 # the node must exist, and our operation will be performed on its
1195 assert path # you can't delete the root
1196 d = self.get_child_at_path(path[:-1])
1197 def _got_DELETE(node):
1198 return DELETEHandler(node, path[-1]), ()
1199 d.addCallback(_got_DELETE)
1200 elif method in ("PUT",):
1201 # the node may or may not exist, and our operation may involve
1202 # all the ancestors of the node.
1203 return PUTHandler(self.node, path, t, localfile, localdir, replace), ()
1205 return rend.NotFound
1206 def _trap_KeyError(f):
1208 return rend.FourOhFour(), ()
1209 d.addErrback(_trap_KeyError)
1212 class URIPUTHandler(rend.Page):
1213 def renderHTTP(self, ctx):
1214 req = inevow.IRequest(ctx)
1215 assert req.method == "PUT"
1219 t = req.args["t"][0]
1222 # "PUT /uri", to create an unlinked file. This is like PUT but
1223 # without the associated set_uri.
1224 uploadable = FileHandle(req.content)
1225 d = IClient(ctx).upload(uploadable)
1226 # that fires with the URI of the new file
1230 # "PUT /uri?t=mkdir", to create an unlinked directory.
1231 d = IClient(ctx).create_empty_dirnode()
1232 d.addCallback(lambda dirnode: dirnode.get_uri())
1233 # XXX add redirect_to_result
1236 req.setResponseCode(http.BAD_REQUEST)
1237 req.setHeader("content-type", "text/plain")
1238 return "/uri only accepts PUT and PUT?t=mkdir"
1240 class URIPOSTHandler(rend.Page):
1241 def renderHTTP(self, ctx):
1242 req = inevow.IRequest(ctx)
1243 assert req.method == "POST"
1245 t = get_arg(req, "t", "").strip()
1247 if t in ("", "upload"):
1248 # "POST /uri", to create an unlinked file.
1249 fileobj = req.fields["file"].file
1250 uploadable = FileHandle(fileobj)
1251 d = IClient(ctx).upload(uploadable)
1252 # that fires with the URI of the new file
1256 # "POST /uri?t=mkdir", to create an unlinked directory.
1257 d = IClient(ctx).create_empty_dirnode()
1258 redirect = get_arg(req, "redirect_to_result", "false")
1259 if boolean_of_arg(redirect):
1260 def _then_redir(res):
1261 new_url = "uri/" + urllib.quote(res.get_uri())
1262 req.setResponseCode(http.SEE_OTHER) # 303
1263 req.setHeader('location', new_url)
1266 d.addCallback(_then_redir)
1268 d.addCallback(lambda dirnode: dirnode.get_uri())
1271 req.setResponseCode(http.BAD_REQUEST)
1272 req.setHeader("content-type", "text/plain")
1273 err = "/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, and POST?t=mkdir"
1277 class Root(rend.Page):
1280 docFactory = getxmlfile("welcome.xhtml")
1282 def locateChild(self, ctx, segments):
1283 client = IClient(ctx)
1284 req = inevow.IRequest(ctx)
1286 segments = list(segments) # XXX HELP I AM YUCKY!
1287 while segments and not segments[-1]:
1291 segments = tuple(segments)
1293 if segments[0] == "uri":
1294 if len(segments) == 1 or segments[1] == '':
1295 if "uri" in req.args:
1296 uri = req.args["uri"][0]
1297 there = url.URL.fromContext(ctx)
1298 there = there.clear("uri")
1299 there = there.child("uri").child(uri)
1301 if len(segments) == 1:
1303 if req.method == "PUT":
1304 # either "PUT /uri" to create an unlinked file, or
1305 # "PUT /uri?t=mkdir" to create an unlinked directory
1306 return URIPUTHandler(), ()
1307 elif req.method == "POST":
1308 # "POST /uri?t=upload&file=newfile" to upload an unlinked
1309 # file or "POST /uri?t=mkdir" to create a new directory
1310 return URIPOSTHandler(), ()
1311 if len(segments) < 2:
1312 return rend.NotFound
1314 d = defer.maybeDeferred(client.create_node_from_uri, uri)
1315 d.addCallback(lambda node: VDrive(node, "from-uri"))
1316 d.addCallback(lambda vd: vd.locateChild(ctx, segments[2:]))
1317 def _trap_KeyError(f):
1319 return rend.FourOhFour(), ()
1320 d.addErrback(_trap_KeyError)
1322 elif segments[0] == "xmlrpc":
1323 raise NotImplementedError()
1324 return rend.Page.locateChild(self, ctx, segments)
1326 child_webform_css = webform.defaultCSS
1327 child_tahoe_css = nevow_File(util.sibpath(__file__, "web/tahoe.css"))
1329 child_provisioning = provisioning.ProvisioningTool()
1331 def data_version(self, ctx, data):
1332 return get_package_versions_string()
1334 def data_my_nodeid(self, ctx, data):
1335 return b32encode(IClient(ctx).nodeid).lower()
1336 def data_introducer_furl(self, ctx, data):
1337 return IClient(ctx).introducer_furl
1338 def data_connected_to_introducer(self, ctx, data):
1339 if IClient(ctx).connected_to_introducer():
1342 def data_num_peers(self, ctx, data):
1343 #client = inevow.ISite(ctx)._client
1344 client = IClient(ctx)
1345 return len(list(client.get_all_peerids()))
1347 def data_peers(self, ctx, data):
1349 client = IClient(ctx)
1350 for nodeid in sorted(client.get_all_peerids()):
1351 row = (b32encode(nodeid).lower(),)
1355 def render_row(self, ctx, data):
1357 ctx.fillSlots("peerid", nodeid_a)
1360 def render_private_vdrive(self, ctx, data):
1361 basedir = IClient(ctx).basedir
1362 start_html = os.path.abspath(os.path.join(basedir, "private", "start.html"))
1363 basedir = IClient(ctx).basedir
1364 if os.path.exists(start_html) and os.path.exists(os.path.join(basedir, "private", "my_private_dir.cap")):
1365 return T.p["To view your personal private non-shared filestore, ",
1366 "use this browser to open the following file from ",
1367 "your local filesystem:",
1370 return T.p["personal vdrive not available."]
1372 # this is a form where users can download files by URI
1373 def render_download_form(self, ctx, data):
1374 form = T.form(action="uri", method="get",
1375 enctype="multipart/form-data")[
1377 T.legend(class_="freeform-form-label")["download a file"],
1378 "URI of file to download: ",
1379 T.input(type="text", name="uri"), " ",
1380 "Filename to download as: ",
1381 T.input(type="text", name="filename"), " ",
1382 T.input(type="submit", value="download"),
1386 # this is a form where users can create new directories
1387 def render_mkdir_form(self, ctx, data):
1388 form = T.form(action="uri", method="post",
1389 enctype="multipart/form-data")[
1391 T.legend(class_="freeform-form-label")["create a directory"],
1392 T.input(type="hidden", name="t", value="mkdir"),
1393 T.input(type="hidden", name="redirect_to_result", value="true"),
1394 T.input(type="submit", value="create"),
1400 implements(ILocalAccess)
1402 self.local_access = False
1403 def local_access_is_allowed(self):
1404 return self.local_access
1406 class WebishServer(service.MultiService):
1409 def __init__(self, webport):
1410 service.MultiService.__init__(self)
1411 self.webport = webport
1413 self.site = site = appserver.NevowSite(self.root)
1414 self.site.requestFactory = MyRequest
1415 self.allow_local = LocalAccess()
1416 self.site.remember(self.allow_local, ILocalAccess)
1417 s = strports.service(webport, site)
1418 s.setServiceParent(self)
1419 self.listener = s # stash it so the tests can query for the portnum
1420 self._started = defer.Deferred()
1422 def allow_local_access(self, enable=True):
1423 self.allow_local.local_access = enable
1425 def startService(self):
1426 service.MultiService.startService(self)
1427 # to make various services available to render_* methods, we stash a
1428 # reference to the client on the NevowSite. This will be available by
1429 # adapting the 'context' argument to a special marker interface named
1431 self.site.remember(self.parent, IClient)
1432 # I thought you could do the same with an existing interface, but
1433 # apparently 'ISite' does not exist
1434 #self.site._client = self.parent
1435 self._started.callback(None)
1437 def create_start_html(self, private_uri, startfile, nodeurl_file):
1439 Returns a deferred that eventually fires once the start.html page has
1442 self._started.addCallback(self._create_start_html, private_uri, startfile, nodeurl_file)
1443 return self._started
1445 def _create_start_html(self, dummy, private_uri, startfile, nodeurl_file):
1446 f = open(startfile, "w")
1447 template = open(util.sibpath(__file__, "web/start.html"), "r").read()
1448 # what is our webport?
1450 if isinstance(s, internet.TCPServer):
1451 base_url = "http://localhost:%d" % s._port.getHost().port
1452 elif isinstance(s, internet.SSLServer):
1453 base_url = "https://localhost:%d" % s._port.getHost().port
1455 base_url = "UNKNOWN" # this will break the href
1456 # TODO: emit a start.html that explains that we don't know
1457 # how to create a suitable URL
1459 link_to_private_uri = "View <a href=\"%s/uri/%s\">your personal private non-shared filestore</a>." % (base_url, private_uri)
1460 fields = {"link_to_private_uri": link_to_private_uri,
1461 "base_url": base_url,
1464 fields = {"link_to_private_uri": "",
1465 "base_url": base_url,
1467 f.write(template % fields)
1470 f = open(nodeurl_file, "w")
1471 # this file is world-readable
1472 f.write(base_url + "\n")