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 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
23 from nevow.util import resource_filename
26 return loaders.xmlfile(resource_filename('allmydata.web', '%s' % name))
28 class IClient(Interface):
30 class ILocalAccess(Interface):
31 def local_access_is_allowed():
32 """Return True if t=upload&localdir= is allowed, giving anyone who
33 can talk to the webserver control over the local (disk) filesystem."""
35 def boolean_of_arg(arg):
36 assert arg.lower() in ("true", "t", "1", "false", "f", "0")
37 return arg.lower() in ("true", "t", "1")
39 def get_arg(req, argname, default=None, multiple=False):
40 """Extract an argument from either the query args (req.args) or the form
41 body fields (req.fields). If multiple=False, this returns a single value
42 (or the default, which defaults to None), and the query args take
43 precedence. If multiple=True, this returns a tuple of arguments (possibly
44 empty), starting with all those in the query args.
47 if argname in req.args:
48 results.extend(req.args[argname])
49 if req.fields and argname in req.fields:
50 results.append(req.fields[argname].value)
57 # we must override twisted.web.http.Request.requestReceived with a version
58 # that doesn't use cgi.parse_multipart() . Since we actually use Nevow, we
59 # override the nevow-specific subclass, nevow.appserver.NevowRequest . This
60 # is an exact copy of twisted.web.http.Request (from SVN HEAD on 10-Aug-2007)
61 # that modifies the way form arguments are parsed. Note that this sort of
62 # surgery may induce a dependency upon a particular version of twisted.web
64 parse_qs = http.parse_qs
65 class MyRequest(appserver.NevowRequest):
67 def requestReceived(self, command, path, version):
68 """Called by channel when all data has been received.
70 This method is not intended for users.
72 self.content.seek(0,0)
76 self.method, self.uri = command, path
77 self.clientproto = version
78 x = self.uri.split('?', 1)
83 self.path, argstring = x
84 self.args = parse_qs(argstring, 1)
86 # cache the client and server information, we'll need this later to be
87 # serialized and sent with the request so CGIs will work remotely
88 self.client = self.channel.transport.getPeer()
89 self.host = self.channel.transport.getHost()
91 # Argument processing.
93 ## The original twisted.web.http.Request.requestReceived code parsed the
94 ## content and added the form fields it found there to self.args . It
95 ## did this with cgi.parse_multipart, which holds the arguments in RAM
96 ## and is thus unsuitable for large file uploads. The Nevow subclass
97 ## (nevow.appserver.NevowRequest) uses cgi.FieldStorage instead (putting
98 ## the results in self.fields), which is much more memory-efficient.
99 ## Since we know we're using Nevow, we can anticipate these arguments
100 ## appearing in self.fields instead of self.args, and thus skip the
101 ## parse-content-into-self.args step.
104 ## ctype = self.getHeader('content-type')
105 ## if self.method == "POST" and ctype:
106 ## mfd = 'multipart/form-data'
107 ## key, pdict = cgi.parse_header(ctype)
108 ## if key == 'application/x-www-form-urlencoded':
109 ## args.update(parse_qs(self.content.read(), 1))
112 ## args.update(cgi.parse_multipart(self.content, pdict))
113 ## except KeyError, e:
114 ## if e.args[0] == 'content-disposition':
115 ## # Parse_multipart can't cope with missing
116 ## # content-dispostion headers in multipart/form-data
117 ## # parts, so we catch the exception and tell the client
118 ## # it was a bad request.
119 ## self.channel.transport.write(
120 ## "HTTP/1.1 400 Bad Request\r\n\r\n")
121 ## self.channel.transport.loseConnection()
127 class Directory(rend.Page):
129 docFactory = getxmlfile("directory.xhtml")
131 def __init__(self, rootname, dirnode, dirpath):
132 self._rootname = rootname
133 self._dirnode = dirnode
134 self._dirpath = dirpath
136 def dirpath_as_string(self):
137 return "/" + "/".join(self._dirpath)
139 def render_title(self, ctx, data):
140 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
142 def render_header(self, ctx, data):
143 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
144 num_dirs = len(parent_directories)
146 header = ["Directory '"]
147 for i,d in enumerate(parent_directories):
148 upness = num_dirs - i - 1
150 link = "/".join( ("..",) * upness )
153 header.append(T.a(href=link)[d])
158 if self._dirnode.is_readonly():
159 header.append(" (readonly)")
161 return ctx.tag[header]
163 def render_welcome(self, ctx, data):
164 depth = len(self._dirpath) + 2
165 link = "/".join([".."] * depth)
166 return T.div[T.a(href=link)["Return to Welcome page"]]
168 def data_children(self, ctx, data):
169 d = self._dirnode.list()
170 d.addCallback(lambda dict: sorted(dict.items()))
173 def render_row(self, ctx, data):
174 name, (target, metadata) = data
176 if self._dirnode.is_readonly():
180 # this creates a button which will cause our child__delete method
181 # to be invoked, which deletes the file and then redirects the
182 # browser back to this directory
183 delete = T.form(action=url.here, method="post")[
184 T.input(type='hidden', name='t', value='delete'),
185 T.input(type='hidden', name='name', value=name),
186 T.input(type='hidden', name='when_done', value=url.here),
187 T.input(type='submit', value='del', name="del"),
190 rename = T.form(action=url.here, method="get")[
191 T.input(type='hidden', name='t', value='rename-form'),
192 T.input(type='hidden', name='name', value=name),
193 T.input(type='hidden', name='when_done', value=url.here),
194 T.input(type='submit', value='rename', name="rename"),
197 ctx.fillSlots("delete", delete)
198 ctx.fillSlots("rename", rename)
199 check = T.form(action=url.here, method="post")[
200 T.input(type='hidden', name='t', value='check'),
201 T.input(type='hidden', name='name', value=name),
202 T.input(type='hidden', name='when_done', value=url.here),
203 T.input(type='submit', value='check', name="check"),
205 ctx.fillSlots("overwrite", self.build_overwrite(ctx, (name, target)))
206 ctx.fillSlots("check", check)
208 # build the base of the uri_link link url
209 uri_link = "/uri/" + urllib.quote(target.get_uri())
211 assert (IFileNode.providedBy(target)
212 or IDirectoryNode.providedBy(target)
213 or IMutableFileNode.providedBy(target)), target
215 if IMutableFileNode.providedBy(target):
218 # add the filename to the uri_link url
219 uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
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)
227 ctx.fillSlots("filename",
228 T.a(href=dlurl)[html.escape(name)])
229 ctx.fillSlots("type", "SSK")
231 ctx.fillSlots("size", "?")
233 text_plain_link = uri_link + "?filename=foo.txt"
234 text_plain_tag = T.a(href=text_plain_link)["text/plain"]
236 elif IFileNode.providedBy(target):
239 # add the filename to the uri_link url
240 uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
242 # to prevent javascript in displayed .html files from stealing a
243 # secret directory URI from the URL, send the browser to a URI-based
244 # page that doesn't know about the directory at all
245 #dlurl = urllib.quote(name)
248 ctx.fillSlots("filename",
249 T.a(href=dlurl)[html.escape(name)])
250 ctx.fillSlots("type", "FILE")
252 ctx.fillSlots("size", target.get_size())
254 text_plain_link = uri_link + "?filename=foo.txt"
255 text_plain_tag = T.a(href=text_plain_link)["text/plain"]
257 elif IDirectoryNode.providedBy(target):
259 subdir_url = urllib.quote(name)
260 ctx.fillSlots("filename",
261 T.a(href=subdir_url)[html.escape(name)])
262 if target.is_readonly():
266 ctx.fillSlots("type", dirtype)
267 ctx.fillSlots("size", "-")
268 text_plain_tag = None
270 childdata = [T.a(href="%s?t=json" % name)["JSON"], ", ",
271 T.a(href="%s?t=uri" % name)["URI"], ", ",
272 T.a(href="%s?t=readonly-uri" % name)["readonly-URI"], ", ",
273 T.a(href=uri_link)["URI-link"],
276 childdata.extend([", ", text_plain_tag])
278 ctx.fillSlots("data", childdata)
281 checker = IClient(ctx).getServiceNamed("checker")
285 d = defer.maybeDeferred(checker.checker_results_for,
286 target.get_verifier())
287 def _got(checker_results):
288 recent_results = reversed(checker_results[-5:])
289 if IFileNode.providedBy(target):
291 ", ".join(["%d/%d" % (found, needed)
293 (needed, total, found, sharemap))
294 in recent_results]) +
296 elif IDirectoryNode.providedBy(target):
298 "".join([{True:"+",False:"-"}[res]
299 for (when, res) in recent_results]) +
302 results = "%d results" % len(checker_results)
308 # TODO: include a link to see more results, including timestamps
309 # TODO: use a sparkline
310 ctx.fillSlots("checker_results", results)
314 def render_forms(self, ctx, data):
315 if self._dirnode.is_readonly():
316 return T.div["No upload forms: directory is read-only"]
317 mkdir = T.form(action=".", method="post",
318 enctype="multipart/form-data")[
320 T.input(type="hidden", name="t", value="mkdir"),
321 T.input(type="hidden", name="when_done", value=url.here),
322 T.legend(class_="freeform-form-label")["Create a new directory"],
323 "New directory name: ",
324 T.input(type="text", name="name"), " ",
325 T.input(type="submit", value="Create"),
328 upload = T.form(action=".", method="post",
329 enctype="multipart/form-data")[
331 T.input(type="hidden", name="t", value="upload"),
332 T.input(type="hidden", name="when_done", value=url.here),
333 T.legend(class_="freeform-form-label")["Upload a file to this directory"],
334 "Choose a file to upload: ",
335 T.input(type="file", name="file", class_="freeform-input-file"),
337 T.input(type="submit", value="Upload"),
339 T.input(type="checkbox", name="mutable"),
342 mount = T.form(action=".", method="post",
343 enctype="multipart/form-data")[
345 T.input(type="hidden", name="t", value="uri"),
346 T.input(type="hidden", name="when_done", value=url.here),
347 T.legend(class_="freeform-form-label")["Attach a file or directory"
351 T.input(type="text", name="name"), " ",
352 "URI of new child: ",
353 T.input(type="text", name="uri"), " ",
354 T.input(type="submit", value="Attach"),
356 return [T.div(class_="freeform-form")[mkdir],
357 T.div(class_="freeform-form")[upload],
358 T.div(class_="freeform-form")[mount],
361 def build_overwrite(self, ctx, data):
363 if IMutableFileNode.providedBy(target) and not target.is_readonly():
364 action="/uri/" + urllib.quote(target.get_uri())
365 overwrite = T.form(action=action, method="post",
366 enctype="multipart/form-data")[
368 T.input(type="hidden", name="t", value="overwrite"),
369 T.input(type='hidden', name='name', value=name),
370 T.input(type='hidden', name='when_done', value=url.here),
371 T.legend(class_="freeform-form-label")["Overwrite"],
373 T.input(type="file", name="file", class_="freeform-input-file"),
375 T.input(type="submit", value="Overwrite")
377 return [T.div(class_="freeform-form")[overwrite],]
381 def render_results(self, ctx, data):
382 req = inevow.IRequest(ctx)
383 return get_arg(req, "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 get_arg(req, "save", False):
461 # TODO: make the API specification clear: should "save=" or
462 # "save=false" count?
463 save_to_file = self._name
464 wdt = WebDownloadTarget(req, type, encoding, save_to_file)
465 d = self._filenode.download(wdt)
466 # exceptions during download are handled by the WebDownloadTarget
467 d.addErrback(lambda why: None)
468 return server.NOT_DONE_YET
470 class BlockingFileError(Exception):
471 """We cannot auto-create a parent directory, because there is a file in
473 class NoReplacementError(Exception):
474 """There was already a child by that name, and you asked me to not replace it"""
475 class NoLocalDirectoryError(Exception):
476 """The localdir= directory didn't exist"""
478 LOCALHOST = "127.0.0.1"
480 class NeedLocalhostError:
481 implements(inevow.IResource)
483 def renderHTTP(self, ctx):
484 req = inevow.IRequest(ctx)
485 req.setResponseCode(http.FORBIDDEN)
486 req.setHeader("content-type", "text/plain")
487 return "localfile= or localdir= requires a local connection"
489 class NeedAbsolutePathError:
490 implements(inevow.IResource)
492 def renderHTTP(self, ctx):
493 req = inevow.IRequest(ctx)
494 req.setResponseCode(http.FORBIDDEN)
495 req.setHeader("content-type", "text/plain")
496 return "localfile= or localdir= requires an absolute path"
498 class LocalAccessDisabledError:
499 implements(inevow.IResource)
501 def renderHTTP(self, ctx):
502 req = inevow.IRequest(ctx)
503 req.setResponseCode(http.FORBIDDEN)
504 req.setHeader("content-type", "text/plain")
505 return "local file access is disabled"
508 class LocalFileDownloader(resource.Resource):
509 def __init__(self, filenode, local_filename):
510 self._local_filename = local_filename
512 self._filenode = filenode
514 def render(self, req):
515 target = download.FileName(self._local_filename)
516 d = self._filenode.download(target)
518 req.write(self._filenode.get_uri())
521 return server.NOT_DONE_YET
524 class FileJSONMetadata(rend.Page):
525 def __init__(self, filenode):
526 self._filenode = filenode
528 def renderHTTP(self, ctx):
529 req = inevow.IRequest(ctx)
530 req.setHeader("content-type", "text/plain")
531 return self.renderNode(self._filenode)
533 def renderNode(self, filenode):
534 file_uri = filenode.get_uri()
537 'size': filenode.get_size(),
539 return simplejson.dumps(data, indent=1)
541 class FileURI(FileJSONMetadata):
542 def renderNode(self, filenode):
543 file_uri = filenode.get_uri()
546 class FileReadOnlyURI(FileJSONMetadata):
547 def renderNode(self, filenode):
548 if filenode.is_readonly():
549 return filenode.get_uri()
551 return filenode.get_readonly().get_uri()
553 class DirnodeWalkerMixin:
554 """Visit all nodes underneath (and including) the rootnode, one at a
555 time. For each one, call the visitor. The visitor will see the
556 IDirectoryNode before it sees any of the IFileNodes inside. If the
557 visitor returns a Deferred, I do not call the visitor again until it has
561 ## def _walk_if_we_could_use_generators(self, rootnode, rootpath=()):
562 ## # this is what we'd be doing if we didn't have the Deferreds and
563 ## # thus could use generators
564 ## yield rootpath, rootnode
565 ## for childname, childnode in rootnode.list().items():
566 ## childpath = rootpath + (childname,)
567 ## if IFileNode.providedBy(childnode):
568 ## yield childpath, childnode
569 ## elif IDirectoryNode.providedBy(childnode):
570 ## for res in self._walk_if_we_could_use_generators(childnode,
574 def walk(self, rootnode, visitor, rootpath=()):
576 def _listed(listing):
577 return listing.items()
578 d.addCallback(_listed)
579 d.addCallback(self._handle_items, visitor, rootpath)
582 def _handle_items(self, items, visitor, rootpath):
585 childname, (childnode, metadata) = items[0]
586 childpath = rootpath + (childname,)
587 d = defer.maybeDeferred(visitor, childpath, childnode, metadata)
588 if IDirectoryNode.providedBy(childnode):
589 d.addCallback(lambda res: self.walk(childnode, visitor, childpath))
590 d.addCallback(lambda res:
591 self._handle_items(items[1:], visitor, rootpath))
594 class LocalDirectoryDownloader(resource.Resource, DirnodeWalkerMixin):
595 def __init__(self, dirnode, localdir):
596 self._dirnode = dirnode
597 self._localdir = localdir
599 def _handle(self, path, node, metadata):
600 localfile = os.path.join(self._localdir, os.sep.join(path))
601 if IDirectoryNode.providedBy(node):
602 fileutil.make_dirs(localfile)
603 elif IFileNode.providedBy(node):
604 target = download.FileName(localfile)
605 return node.download(target)
607 def render(self, req):
608 d = self.walk(self._dirnode, self._handle)
610 req.setHeader("content-type", "text/plain")
611 return "operation complete"
615 class DirectoryJSONMetadata(rend.Page):
616 def __init__(self, dirnode):
617 self._dirnode = dirnode
619 def renderHTTP(self, ctx):
620 req = inevow.IRequest(ctx)
621 req.setHeader("content-type", "text/plain")
622 return self.renderNode(self._dirnode)
624 def renderNode(self, node):
628 for name, (childnode, metadata) in children.iteritems():
629 if IFileNode.providedBy(childnode):
630 kiduri = childnode.get_uri()
631 kiddata = ("filenode",
633 'size': childnode.get_size(),
636 assert IDirectoryNode.providedBy(childnode), (childnode, children,)
637 kiddata = ("dirnode",
638 {'ro_uri': childnode.get_readonly_uri(),
640 if not childnode.is_readonly():
641 kiddata[1]['rw_uri'] = childnode.get_uri()
643 contents = { 'children': kids,
644 'ro_uri': node.get_readonly_uri(),
646 if not node.is_readonly():
647 contents['rw_uri'] = node.get_uri()
648 data = ("dirnode", contents)
649 return simplejson.dumps(data, indent=1)
653 class DirectoryURI(DirectoryJSONMetadata):
654 def renderNode(self, node):
655 return node.get_uri()
657 class DirectoryReadonlyURI(DirectoryJSONMetadata):
658 def renderNode(self, node):
659 return node.get_readonly_uri()
661 class RenameForm(rend.Page):
663 docFactory = getxmlfile("rename-form.xhtml")
665 def __init__(self, rootname, dirnode, dirpath):
666 self._rootname = rootname
667 self._dirnode = dirnode
668 self._dirpath = dirpath
670 def dirpath_as_string(self):
671 return "/" + "/".join(self._dirpath)
673 def render_title(self, ctx, data):
674 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
676 def render_header(self, ctx, data):
677 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
678 num_dirs = len(parent_directories)
680 header = [ "Rename in directory '",
681 "<%s>/" % self._rootname,
682 "/".join(self._dirpath),
685 if self._dirnode.is_readonly():
686 header.append(" (readonly)")
687 return ctx.tag[header]
689 def render_when_done(self, ctx, data):
690 return T.input(type="hidden", name="when_done", value=url.here)
692 def render_get_name(self, ctx, data):
693 req = inevow.IRequest(ctx)
694 name = get_arg(req, "name", "")
695 ctx.tag.attributes['value'] = name
698 class POSTHandler(rend.Page):
699 def __init__(self, node, replace):
701 self._replace = replace
703 def _check_replacement(self, name):
705 return defer.succeed(None)
706 d = self._node.has_child(name)
709 raise NoReplacementError("There was already a child by that "
710 "name, and you asked me to not "
716 def renderHTTP(self, ctx):
717 req = inevow.IRequest(ctx)
719 t = get_arg(req, "t")
722 name = get_arg(req, "name", None)
723 if name and "/" in name:
724 req.setResponseCode(http.BAD_REQUEST)
725 req.setHeader("content-type", "text/plain")
726 return "name= may not contain a slash"
729 # we allow the user to delete an empty-named file, but not to create
730 # them, since that's an easy and confusing mistake to make
732 when_done = get_arg(req, "when_done", None)
733 if not boolean_of_arg(get_arg(req, "replace", "true")):
734 self._replace = False
738 raise RuntimeError("mkdir requires a name")
739 d = self._check_replacement(name)
740 d.addCallback(lambda res: self._node.create_empty_directory(name))
741 d.addCallback(lambda res: "directory created")
744 raise RuntimeError("set-uri requires a name")
745 newuri = get_arg(req, "uri")
746 assert newuri is not None
747 d = self._check_replacement(name)
748 d.addCallback(lambda res: self._node.set_uri(name, newuri))
749 d.addCallback(lambda res: newuri)
752 # apparently an <input type="hidden" name="name" value="">
753 # won't show up in the resulting encoded form.. the 'name'
754 # field is completely missing. So to allow deletion of an
755 # empty file, we have to pretend that None means ''. The only
756 # downide of this is a slightly confusing error message if
757 # someone does a POST without a name= field. For our own HTML
758 # thisn't a big deal, because we create the 'delete' POST
761 d = self._node.delete(name)
762 d.addCallback(lambda res: "thing deleted")
764 from_name = 'from_name' in req.fields and req.fields["from_name"].value
765 if from_name is not None:
766 from_name = from_name.strip()
767 to_name = 'to_name' in req.fields and req.fields["to_name"].value
768 if to_name is not None:
769 to_name = to_name.strip()
770 if not from_name or not to_name:
771 raise RuntimeError("rename requires from_name and to_name")
772 if not IDirectoryNode.providedBy(self._node):
773 raise RuntimeError("rename must only be called on directories")
774 for k,v in [ ('from_name', from_name), ('to_name', to_name) ]:
776 req.setResponseCode(http.BAD_REQUEST)
777 req.setHeader("content-type", "text/plain")
778 return "%s= may not contain a slash" % (k,)
779 d = self._check_replacement(to_name)
780 d.addCallback(lambda res: self._node.get(from_name))
782 uri = child.get_uri()
783 # now actually do the rename
784 return self._node.set_uri(to_name, uri)
785 d.addCallback(add_dest)
787 return self._node.delete(from_name)
788 d.addCallback(rm_src)
789 d.addCallback(lambda res: "thing renamed")
792 if "mutable" in req.fields:
793 contents = req.fields["file"]
794 name = name or contents.filename
798 raise RuntimeError("upload-mutable requires a name")
799 # SDMF: files are small, and we can only upload data.
800 contents.file.seek(0)
801 data = contents.file.read()
802 uploadable = FileHandle(contents.file)
803 d = self._check_replacement(name)
804 d.addCallback(lambda res: self._node.has_child(name))
805 def _checked(present):
807 # modify the existing one instead of creating a new
809 d2 = self._node.get(name)
810 def _got_newnode(newnode):
811 d3 = newnode.replace(data)
812 d3.addCallback(lambda res: newnode.get_uri())
814 d2.addCallback(_got_newnode)
816 d2 = IClient(ctx).create_mutable_file(data)
817 def _uploaded(newnode):
818 d1 = self._node.set_node(name, newnode)
819 d1.addCallback(lambda res: newnode.get_uri())
821 d2.addCallback(_uploaded)
823 d.addCallback(_checked)
825 contents = req.fields["file"]
826 name = name or contents.filename
830 raise RuntimeError("upload requires a name")
831 uploadable = FileHandle(contents.file)
832 d = self._check_replacement(name)
833 d.addCallback(lambda res: self._node.add_file(name, uploadable))
835 return newnode.get_uri()
838 elif t == "overwrite":
839 contents = req.fields["file"]
840 # SDMF: files are small, and we can only upload data.
841 contents.file.seek(0)
842 data = contents.file.read()
843 # TODO: 'name' handling needs review
844 d = defer.succeed(self._node)
845 def _got_child_overwrite(child_node):
846 child_node.replace(data)
847 return child_node.get_uri()
848 d.addCallback(_got_child_overwrite)
851 d = self._node.get(name)
852 def _got_child_check(child_node):
853 d2 = child_node.check()
855 log.msg("checked %s, results %s" % (child_node, res))
857 d2.addCallback(_done)
859 d.addCallback(_got_child_check)
862 return "BAD t=%s" % t
864 d.addCallback(lambda res: url.URL.fromString(when_done))
865 def _check_replacement(f):
866 # TODO: make this more human-friendly: maybe send them to the
867 # when_done page but with an extra query-arg that will display
868 # the error message in a big box at the top of the page. The
869 # directory page that when_done= usually points to accepts a
870 # result= argument.. use that.
871 f.trap(NoReplacementError)
872 req.setResponseCode(http.CONFLICT)
873 req.setHeader("content-type", "text/plain")
875 d.addErrback(_check_replacement)
878 class DELETEHandler(rend.Page):
879 def __init__(self, node, name):
883 def renderHTTP(self, ctx):
884 req = inevow.IRequest(ctx)
885 d = self._node.delete(self._name)
887 # what should this return??
888 return "%s deleted" % self._name
890 def _trap_missing(f):
892 req.setResponseCode(http.NOT_FOUND)
893 req.setHeader("content-type", "text/plain")
894 return "no such child %s" % self._name
895 d.addErrback(_trap_missing)
898 class PUTHandler(rend.Page):
899 def __init__(self, node, path, t, localfile, localdir, replace):
903 self._localfile = localfile
904 self._localdir = localdir
905 self._replace = replace
907 def renderHTTP(self, ctx):
908 req = inevow.IRequest(ctx)
910 localfile = self._localfile
911 localdir = self._localdir
913 if t == "upload" and not (localfile or localdir):
914 req.setResponseCode(http.BAD_REQUEST)
915 req.setHeader("content-type", "text/plain")
916 return "t=upload requires localfile= or localdir="
918 # we must traverse the path, creating new directories as necessary
919 d = self._get_or_create_directories(self._node, self._path[:-1])
920 name = self._path[-1]
921 d.addCallback(self._check_replacement, name, self._replace)
924 d.addCallback(self._upload_localfile, localfile, name)
928 d.addCallback(self._get_or_create_directories, self._path[-1:])
929 d.addCallback(self._upload_localdir, localdir)
931 d.addCallback(self._attach_uri, req.content, name)
933 d.addCallback(self._mkdir, name)
935 d.addCallback(self._upload_file, req.content, name)
937 def _transform_error(f):
938 errors = {BlockingFileError: http.BAD_REQUEST,
939 NoReplacementError: http.CONFLICT,
940 NoLocalDirectoryError: http.BAD_REQUEST,
942 for k,v in errors.items():
944 req.setResponseCode(v)
945 req.setHeader("content-type", "text/plain")
948 d.addErrback(_transform_error)
951 def _get_or_create_directories(self, node, path):
952 if not IDirectoryNode.providedBy(node):
953 # unfortunately it is too late to provide the name of the
954 # blocking directory in the error message.
955 raise BlockingFileError("cannot create directory because there "
956 "is a file in the way")
958 return defer.succeed(node)
959 d = node.get(path[0])
960 def _maybe_create(f):
962 return node.create_empty_directory(path[0])
963 d.addErrback(_maybe_create)
964 d.addCallback(self._get_or_create_directories, path[1:])
967 def _check_replacement(self, node, name, replace):
970 d = node.has_child(name)
973 raise NoReplacementError("There was already a child by that "
974 "name, and you asked me to not "
980 def _mkdir(self, node, name):
981 d = node.create_empty_directory(name)
983 return newnode.get_uri()
987 def _upload_file(self, node, contents, name):
988 uploadable = FileHandle(contents)
989 d = node.add_file(name, uploadable)
991 log.msg("webish upload complete")
992 return filenode.get_uri()
996 def _upload_localfile(self, node, localfile, name):
997 uploadable = FileName(localfile)
998 d = node.add_file(name, uploadable)
999 d.addCallback(lambda filenode: filenode.get_uri())
1002 def _attach_uri(self, parentnode, contents, name):
1003 newuri = contents.read().strip()
1004 d = parentnode.set_uri(name, newuri)
1007 d.addCallback(_done)
1010 def _upload_localdir(self, node, localdir):
1011 # build up a list of files to upload
1014 msg = "No files to upload! %s is empty" % localdir
1015 if not os.path.exists(localdir):
1016 msg = "%s doesn't exist!" % localdir
1017 raise NoLocalDirectoryError(msg)
1018 for root, dirs, files in os.walk(localdir):
1019 if root == localdir:
1022 relative_root = root[len(localdir)+1:]
1023 path = tuple(relative_root.split(os.sep))
1025 all_dirs.append(path + (d,))
1027 all_files.append(path + (f,))
1028 d = defer.succeed(msg)
1029 for dir in all_dirs:
1031 d.addCallback(self._makedir, node, dir)
1033 d.addCallback(self._upload_one_file, node, localdir, f)
1036 def _makedir(self, res, node, dir):
1037 d = defer.succeed(None)
1038 # get the parent. As long as os.walk gives us parents before
1039 # children, this ought to work
1040 d.addCallback(lambda res: node.get_child_at_path(dir[:-1]))
1041 # then create the child directory
1042 d.addCallback(lambda parent: parent.create_empty_directory(dir[-1]))
1045 def _upload_one_file(self, res, node, localdir, f):
1046 # get the parent. We can be sure this exists because we already
1047 # went through and created all the directories we require.
1048 localfile = os.path.join(localdir, *f)
1049 d = node.get_child_at_path(f[:-1])
1050 d.addCallback(self._upload_localfile, localfile, f[-1])
1054 class Manifest(rend.Page):
1055 docFactory = getxmlfile("manifest.xhtml")
1056 def __init__(self, dirnode, dirpath):
1057 self._dirnode = dirnode
1058 self._dirpath = dirpath
1060 def dirpath_as_string(self):
1061 return "/" + "/".join(self._dirpath)
1063 def render_title(self, ctx):
1064 return T.title["Manifest of %s" % self.dirpath_as_string()]
1066 def render_header(self, ctx):
1067 return T.p["Manifest of %s" % self.dirpath_as_string()]
1069 def data_items(self, ctx, data):
1070 return self._dirnode.build_manifest()
1072 def render_row(self, ctx, refresh_cap):
1073 ctx.fillSlots("refresh_capability", refresh_cap)
1077 implements(inevow.IResource)
1078 def renderHTTP(self, ctx):
1079 req = inevow.IRequest(ctx)
1080 req.setResponseCode(http.BAD_REQUEST)
1081 req.setHeader("content-type", "text/plain")
1084 def child_error(text):
1089 class VDrive(rend.Page):
1091 def __init__(self, node, name):
1095 def get_child_at_path(self, path):
1097 return self.node.get_child_at_path(path)
1098 return defer.succeed(self.node)
1100 def locateChild(self, ctx, segments):
1101 req = inevow.IRequest(ctx)
1105 t = get_arg(req, "t", "")
1106 localfile = get_arg(req, "localfile", None)
1107 if localfile is not None:
1108 if localfile != os.path.abspath(localfile):
1109 return NeedAbsolutePathError(), ()
1110 localdir = get_arg(req, "localdir", None)
1111 if localdir is not None:
1112 if localdir != os.path.abspath(localdir):
1113 return NeedAbsolutePathError(), ()
1114 if localfile or localdir:
1115 if not ILocalAccess(ctx).local_access_is_allowed():
1116 return LocalAccessDisabledError(), ()
1117 if req.getHost().host != LOCALHOST:
1118 return NeedLocalhostError(), ()
1119 # TODO: think about clobbering/revealing config files and node secrets
1121 replace = boolean_of_arg(get_arg(req, "replace", "true"))
1124 # the node must exist, and our operation will be performed on the
1126 d = self.get_child_at_path(path)
1127 def file_or_dir(node):
1128 if (IFileNode.providedBy(node)
1129 or IMutableFileNode.providedBy(node)):
1130 filename = "unknown"
1133 filename = get_arg(req, "filename", filename)
1136 # write contents to a local file
1137 return LocalFileDownloader(node, localfile), ()
1138 # send contents as the result
1139 return FileDownloader(node, filename), ()
1141 # send contents as the result
1142 return FileDownloader(node, filename), ()
1144 return FileJSONMetadata(node), ()
1146 return FileURI(node), ()
1147 elif t == "readonly-uri":
1148 return FileReadOnlyURI(node), ()
1150 return child_error("bad t=%s" % t)
1151 elif IDirectoryNode.providedBy(node):
1154 # recursive download to a local directory
1155 return LocalDirectoryDownloader(node, localdir), ()
1156 return child_error("t=download requires localdir=")
1158 # send an HTML representation of the directory
1159 return Directory(self.name, node, path), ()
1161 return DirectoryJSONMetadata(node), ()
1163 return DirectoryURI(node), ()
1164 elif t == "readonly-uri":
1165 return DirectoryReadonlyURI(node), ()
1166 elif t == "manifest":
1167 return Manifest(node, path), ()
1168 elif t == 'rename-form':
1169 return RenameForm(self.name, node, path), ()
1171 return child_error("bad t=%s" % t)
1173 return child_error("unknown node type")
1174 d.addCallback(file_or_dir)
1175 elif method == "POST":
1176 # the node must exist, and our operation will be performed on the
1178 d = self.get_child_at_path(path)
1179 def _got_POST(node):
1180 return POSTHandler(node, replace), ()
1181 d.addCallback(_got_POST)
1182 elif method == "DELETE":
1183 # the node must exist, and our operation will be performed on its
1185 assert path # you can't delete the root
1186 d = self.get_child_at_path(path[:-1])
1187 def _got_DELETE(node):
1188 return DELETEHandler(node, path[-1]), ()
1189 d.addCallback(_got_DELETE)
1190 elif method in ("PUT",):
1191 # the node may or may not exist, and our operation may involve
1192 # all the ancestors of the node.
1193 return PUTHandler(self.node, path, t, localfile, localdir, replace), ()
1195 return rend.NotFound
1198 class URIPUTHandler(rend.Page):
1199 def renderHTTP(self, ctx):
1200 req = inevow.IRequest(ctx)
1201 assert req.method == "PUT"
1203 t = get_arg(req, "t", "")
1206 # "PUT /uri", to create an unlinked file. This is like PUT but
1207 # without the associated set_uri.
1208 uploadable = FileHandle(req.content)
1209 d = IClient(ctx).upload(uploadable)
1210 # that fires with the URI of the new file
1214 # "PUT /uri?t=mkdir", to create an unlinked directory.
1215 d = IClient(ctx).create_empty_dirnode()
1216 d.addCallback(lambda dirnode: dirnode.get_uri())
1217 # XXX add redirect_to_result
1220 req.setResponseCode(http.BAD_REQUEST)
1221 req.setHeader("content-type", "text/plain")
1222 return "/uri only accepts PUT and PUT?t=mkdir"
1224 class URIPOSTHandler(rend.Page):
1225 def renderHTTP(self, ctx):
1226 req = inevow.IRequest(ctx)
1227 assert req.method == "POST"
1229 t = get_arg(req, "t", "").strip()
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
1240 # "POST /uri?t=mkdir", to create an unlinked directory.
1241 d = IClient(ctx).create_empty_dirnode()
1242 redirect = get_arg(req, "redirect_to_result", "false")
1243 if boolean_of_arg(redirect):
1244 def _then_redir(res):
1245 new_url = "uri/" + urllib.quote(res.get_uri())
1246 req.setResponseCode(http.SEE_OTHER) # 303
1247 req.setHeader('location', new_url)
1250 d.addCallback(_then_redir)
1252 d.addCallback(lambda dirnode: dirnode.get_uri())
1255 req.setResponseCode(http.BAD_REQUEST)
1256 req.setHeader("content-type", "text/plain")
1257 err = "/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, and POST?t=mkdir"
1261 class Root(rend.Page):
1264 docFactory = getxmlfile("welcome.xhtml")
1266 def locateChild(self, ctx, segments):
1267 client = IClient(ctx)
1268 req = inevow.IRequest(ctx)
1270 segments = list(segments) # XXX HELP I AM YUCKY!
1271 while segments and not segments[-1]:
1275 segments = tuple(segments)
1277 if segments[0] == "uri":
1278 if len(segments) == 1 or segments[1] == '':
1279 uri = get_arg(req, "uri", None)
1281 there = url.URL.fromContext(ctx)
1282 there = there.clear("uri")
1283 there = there.child("uri").child(uri)
1285 if len(segments) == 1:
1287 if req.method == "PUT":
1288 # either "PUT /uri" to create an unlinked file, or
1289 # "PUT /uri?t=mkdir" to create an unlinked directory
1290 return URIPUTHandler(), ()
1291 elif req.method == "POST":
1292 # "POST /uri?t=upload&file=newfile" to upload an unlinked
1293 # file or "POST /uri?t=mkdir" to create a new directory
1294 return URIPOSTHandler(), ()
1295 if len(segments) < 2:
1296 return rend.NotFound
1298 d = defer.maybeDeferred(client.create_node_from_uri, uri)
1299 d.addCallback(lambda node: VDrive(node, "from-uri"))
1300 d.addCallback(lambda vd: vd.locateChild(ctx, segments[2:]))
1301 def _trap_KeyError(f):
1303 return rend.FourOhFour(), ()
1304 d.addErrback(_trap_KeyError)
1306 elif segments[0] == "xmlrpc":
1307 raise NotImplementedError()
1308 return rend.Page.locateChild(self, ctx, segments)
1310 child_webform_css = webform.defaultCSS
1311 child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
1313 child_provisioning = provisioning.ProvisioningTool()
1315 def data_version(self, ctx, data):
1316 return get_package_versions_string()
1318 def data_my_nodeid(self, ctx, data):
1319 return b32encode(IClient(ctx).nodeid).lower()
1320 def data_introducer_furl(self, ctx, data):
1321 return IClient(ctx).introducer_furl
1322 def data_connected_to_introducer(self, ctx, data):
1323 if IClient(ctx).connected_to_introducer():
1327 def data_helper_furl(self, ctx, data):
1329 uploader = IClient(ctx).getServiceNamed("uploader")
1332 furl, connected = uploader.get_helper_info()
1334 def data_connected_to_helper(self, ctx, data):
1336 uploader = IClient(ctx).getServiceNamed("uploader")
1338 return "no" # we don't even have an Uploader
1339 furl, connected = uploader.get_helper_info()
1344 def data_num_peers(self, ctx, data):
1345 #client = inevow.ISite(ctx)._client
1346 client = IClient(ctx)
1347 return len(list(client.get_all_peerids()))
1349 def data_peers(self, ctx, data):
1351 client = IClient(ctx)
1352 for nodeid in sorted(client.get_all_peerids()):
1353 row = (b32encode(nodeid).lower(),)
1357 def render_row(self, ctx, data):
1359 ctx.fillSlots("peerid", nodeid_a)
1362 # this is a form where users can download files by URI
1363 def render_download_form(self, ctx, data):
1364 form = T.form(action="uri", method="get",
1365 enctype="multipart/form-data")[
1367 T.legend(class_="freeform-form-label")["download a file"],
1368 "URI of file to download: ",
1369 T.input(type="text", name="uri"), " ",
1370 "Filename to download as: ",
1371 T.input(type="text", name="filename"), " ",
1372 T.input(type="submit", value="download"),
1376 # this is a form where users can create new directories
1377 def render_mkdir_form(self, ctx, data):
1378 form = T.form(action="uri", method="post",
1379 enctype="multipart/form-data")[
1381 T.legend(class_="freeform-form-label")["create a directory"],
1382 T.input(type="hidden", name="t", value="mkdir"),
1383 T.input(type="hidden", name="redirect_to_result", value="true"),
1384 T.input(type="submit", value="create"),
1390 implements(ILocalAccess)
1392 self.local_access = False
1393 def local_access_is_allowed(self):
1394 return self.local_access
1396 class WebishServer(service.MultiService):
1399 def __init__(self, webport, nodeurl_path=None):
1400 service.MultiService.__init__(self)
1401 self.webport = webport
1403 self.site = site = appserver.NevowSite(self.root)
1404 self.site.requestFactory = MyRequest
1405 self.allow_local = LocalAccess()
1406 self.site.remember(self.allow_local, ILocalAccess)
1407 s = strports.service(webport, site)
1408 s.setServiceParent(self)
1409 self.listener = s # stash it so the tests can query for the portnum
1410 self._started = defer.Deferred()
1412 self._started.addCallback(self._write_nodeurl_file, nodeurl_path)
1414 def allow_local_access(self, enable=True):
1415 self.allow_local.local_access = enable
1417 def startService(self):
1418 service.MultiService.startService(self)
1419 # to make various services available to render_* methods, we stash a
1420 # reference to the client on the NevowSite. This will be available by
1421 # adapting the 'context' argument to a special marker interface named
1423 self.site.remember(self.parent, IClient)
1424 # I thought you could do the same with an existing interface, but
1425 # apparently 'ISite' does not exist
1426 #self.site._client = self.parent
1427 self._started.callback(None)
1429 def _write_nodeurl_file(self, junk, nodeurl_path):
1430 # what is our webport?
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
1439 f = open(nodeurl_path, 'wb')
1440 # this file is world-readable
1441 f.write(base_url + "\n")