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 # 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
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.
49 This method is not intended for users.
51 self.content.seek(0,0)
55 self.method, self.uri = command, path
56 self.clientproto = version
57 x = self.uri.split('?', 1)
62 self.path, argstring = x
63 self.args = parse_qs(argstring, 1)
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()
70 # Argument processing.
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.
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))
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()
106 class Directory(rend.Page):
108 docFactory = getxmlfile("directory.xhtml")
110 def __init__(self, rootname, dirnode, dirpath):
111 self._rootname = rootname
112 self._dirnode = dirnode
113 self._dirpath = dirpath
115 def dirpath_as_string(self):
116 return "/" + "/".join(self._dirpath)
118 def render_title(self, ctx, data):
119 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
121 def render_header(self, ctx, data):
122 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
123 num_dirs = len(parent_directories)
125 header = ["Directory '"]
126 for i,d in enumerate(parent_directories):
127 upness = num_dirs - i - 1
129 link = "/".join( ("..",) * upness )
132 header.append(T.a(href=link)[d])
137 if self._dirnode.is_readonly():
138 header.append(" (readonly)")
140 return ctx.tag[header]
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"]]
147 def data_children(self, ctx, data):
148 d = self._dirnode.list()
149 d.addCallback(lambda dict: sorted(dict.items()))
152 def render_row(self, ctx, data):
153 name, (target, metadata) = data
155 if self._dirnode.is_readonly():
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"),
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"),
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"),
184 ctx.fillSlots("overwrite", self.build_overwrite(ctx, (name, target)))
185 ctx.fillSlots("check", check)
187 # build the base of the uri_link link url
188 uri_link = "/uri/" + urllib.quote(target.get_uri())
190 assert (IFileNode.providedBy(target)
191 or IDirectoryNode.providedBy(target)
192 or IMutableFileNode.providedBy(target)), target
194 if IMutableFileNode.providedBy(target):
197 # add the filename to the uri_link url
198 uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
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)
206 ctx.fillSlots("filename",
207 T.a(href=dlurl)[html.escape(name)])
208 ctx.fillSlots("type", "SSK")
210 ctx.fillSlots("size", "?")
212 text_plain_link = uri_link + "?filename=foo.txt"
213 text_plain_tag = T.a(href=text_plain_link)["text/plain"]
215 elif IFileNode.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", "FILE")
231 ctx.fillSlots("size", target.get_size())
233 text_plain_link = uri_link + "?filename=foo.txt"
234 text_plain_tag = T.a(href=text_plain_link)["text/plain"]
236 elif IDirectoryNode.providedBy(target):
238 subdir_url = urllib.quote(name)
239 ctx.fillSlots("filename",
240 T.a(href=subdir_url)[html.escape(name)])
241 if target.is_readonly():
245 ctx.fillSlots("type", dirtype)
246 ctx.fillSlots("size", "-")
247 text_plain_tag = None
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"],
255 childdata.extend([", ", text_plain_tag])
257 ctx.fillSlots("data", childdata)
260 checker = IClient(ctx).getServiceNamed("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):
270 ", ".join(["%d/%d" % (found, needed)
272 (needed, total, found, sharemap))
273 in recent_results]) +
275 elif IDirectoryNode.providedBy(target):
277 "".join([{True:"+",False:"-"}[res]
278 for (when, res) in recent_results]) +
281 results = "%d results" % len(checker_results)
287 # TODO: include a link to see more results, including timestamps
288 # TODO: use a sparkline
289 ctx.fillSlots("checker_results", results)
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")[
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"),
307 upload = T.form(action=".", method="post",
308 enctype="multipart/form-data")[
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"),
316 T.input(type="submit", value="Upload"),
318 T.input(type="checkbox", name="mutable"),
321 mount = T.form(action=".", method="post",
322 enctype="multipart/form-data")[
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"
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"),
335 return [T.div(class_="freeform-form")[mkdir],
336 T.div(class_="freeform-form")[upload],
337 T.div(class_="freeform-form")[mount],
340 def build_overwrite(self, ctx, 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")[
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"],
352 T.input(type="file", name="file", class_="freeform-input-file"),
354 T.input(type="submit", value="Overwrite")
356 return [T.div(class_="freeform-form")[overwrite],]
360 def render_results(self, ctx, data):
361 req = inevow.IRequest(ctx)
362 if "results" in req.args:
363 return req.args["results"]
367 class WebDownloadTarget:
368 implements(IDownloadTarget, IConsumer)
369 def __init__(self, req, content_type, content_encoding, save_to_file):
371 self._content_type = content_type
372 self._content_encoding = content_encoding
374 self._producer = None
375 self._save_to_file = save_to_file
377 def registerProducer(self, producer, streaming):
378 self._req.registerProducer(producer, streaming)
379 def unregisterProducer(self):
380 self._req.unregisterProducer()
382 def open(self, size):
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)
395 def write(self, data):
396 self._req.write(data)
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.
410 # We don't have a lot of options, unfortunately.
411 self._req.write("problem during download\n")
413 # We haven't written anything yet, so we can provide a sensible
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))
423 def register_canceller(self, cb):
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
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")
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
450 class BlockingFileError(Exception):
451 """We cannot auto-create a parent directory, because there is a file in
453 class NoReplacementError(Exception):
454 """There was already a child by that name, and you asked me to not replace it"""
456 LOCALHOST = "127.0.0.1"
458 class NeedLocalhostError:
459 implements(inevow.IResource)
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"
467 class NeedAbsolutePathError:
468 implements(inevow.IResource)
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"
476 class LocalAccessDisabledError:
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 "local file access is disabled"
486 class LocalFileDownloader(resource.Resource):
487 def __init__(self, filenode, local_filename):
488 self._local_filename = local_filename
490 self._filenode = filenode
492 def render(self, req):
493 target = download.FileName(self._local_filename)
494 d = self._filenode.download(target)
496 req.write(self._filenode.get_uri())
499 return server.NOT_DONE_YET
502 class FileJSONMetadata(rend.Page):
503 def __init__(self, filenode):
504 self._filenode = filenode
506 def renderHTTP(self, ctx):
507 req = inevow.IRequest(ctx)
508 req.setHeader("content-type", "text/plain")
509 return self.renderNode(self._filenode)
511 def renderNode(self, filenode):
512 file_uri = filenode.get_uri()
515 'size': filenode.get_size(),
517 return simplejson.dumps(data, indent=1)
519 class FileURI(FileJSONMetadata):
520 def renderNode(self, filenode):
521 file_uri = filenode.get_uri()
524 class FileReadOnlyURI(FileJSONMetadata):
525 def renderNode(self, filenode):
526 if filenode.is_readonly():
527 return filenode.get_uri()
529 return filenode.get_readonly().get_uri()
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
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,
552 def walk(self, rootnode, visitor, rootpath=()):
554 def _listed(listing):
555 return listing.items()
556 d.addCallback(_listed)
557 d.addCallback(self._handle_items, visitor, rootpath)
560 def _handle_items(self, items, visitor, rootpath):
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))
572 class LocalDirectoryDownloader(resource.Resource, DirnodeWalkerMixin):
573 def __init__(self, dirnode, localdir):
574 self._dirnode = dirnode
575 self._localdir = localdir
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)
585 def render(self, req):
586 d = self.walk(self._dirnode, self._handle)
588 req.setHeader("content-type", "text/plain")
589 return "operation complete"
593 class DirectoryJSONMetadata(rend.Page):
594 def __init__(self, dirnode):
595 self._dirnode = dirnode
597 def renderHTTP(self, ctx):
598 req = inevow.IRequest(ctx)
599 req.setHeader("content-type", "text/plain")
600 return self.renderNode(self._dirnode)
602 def renderNode(self, node):
606 for name, (childnode, metadata) in children.iteritems():
607 if IFileNode.providedBy(childnode):
608 kiduri = childnode.get_uri()
609 kiddata = ("filenode",
611 'size': childnode.get_size(),
614 assert IDirectoryNode.providedBy(childnode), (childnode, children,)
615 kiddata = ("dirnode",
616 {'ro_uri': childnode.get_readonly_uri(),
618 if not childnode.is_readonly():
619 kiddata[1]['rw_uri'] = childnode.get_uri()
621 contents = { 'children': kids,
622 'ro_uri': node.get_readonly_uri(),
624 if not node.is_readonly():
625 contents['rw_uri'] = node.get_uri()
626 data = ("dirnode", contents)
627 return simplejson.dumps(data, indent=1)
631 class DirectoryURI(DirectoryJSONMetadata):
632 def renderNode(self, node):
633 return node.get_uri()
635 class DirectoryReadonlyURI(DirectoryJSONMetadata):
636 def renderNode(self, node):
637 return node.get_readonly_uri()
639 class RenameForm(rend.Page):
641 docFactory = getxmlfile("rename-form.xhtml")
643 def __init__(self, rootname, dirnode, dirpath):
644 self._rootname = rootname
645 self._dirnode = dirnode
646 self._dirpath = dirpath
648 def dirpath_as_string(self):
649 return "/" + "/".join(self._dirpath)
651 def render_title(self, ctx, data):
652 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
654 def render_header(self, ctx, data):
655 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
656 num_dirs = len(parent_directories)
658 header = [ "Rename in directory '",
659 "<%s>/" % self._rootname,
660 "/".join(self._dirpath),
663 if self._dirnode.is_readonly():
664 header.append(" (readonly)")
665 return ctx.tag[header]
667 def render_when_done(self, ctx, data):
668 return T.input(type="hidden", name="when_done", value=url.here)
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]
676 ctx.tag.attributes['value'] = name
679 class POSTHandler(rend.Page):
680 def __init__(self, node, replace):
682 self._replace = replace
684 def _check_replacement(self, name):
686 return defer.succeed(None)
687 d = self._node.has_child(name)
690 raise NoReplacementError("There was already a child by that "
691 "name, and you asked me to not "
697 def renderHTTP(self, ctx):
698 req = inevow.IRequest(ctx)
703 t = req.fields["t"].value
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"
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
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
725 if "replace" in req.fields:
726 if not boolean_of_arg(req.fields["replace"].value):
727 self._replace = False
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")
737 raise RuntimeError("set-uri requires a name")
738 if "uri" in req.args:
739 newuri = req.args["uri"][0].strip()
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)
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
756 d = self._node.delete(name)
757 d.addCallback(lambda res: "thing deleted")
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) ]:
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))
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)
782 return self._node.delete(from_name)
783 d.addCallback(rm_src)
784 d.addCallback(lambda res: "thing renamed")
787 if "mutable" in req.fields:
788 contents = req.fields["file"]
789 name = name or contents.filename
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):
802 # modify the existing one instead of creating a new
804 d2 = self._node.get(name)
805 def _got_newnode(newnode):
806 d3 = newnode.replace(data)
807 d3.addCallback(lambda res: newnode.get_uri())
809 d2.addCallback(_got_newnode)
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())
816 d2.addCallback(_uploaded)
818 d.addCallback(_checked)
820 contents = req.fields["file"]
821 name = name or contents.filename
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))
830 return newnode.get_uri()
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)
846 d = self._node.get(name)
847 def _got_child_check(child_node):
848 d2 = child_node.check()
850 log.msg("checked %s, results %s" % (child_node, res))
852 d2.addCallback(_done)
854 d.addCallback(_got_child_check)
857 return "BAD t=%s" % t
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")
870 d.addErrback(_check_replacement)
873 class DELETEHandler(rend.Page):
874 def __init__(self, node, name):
878 def renderHTTP(self, ctx):
879 req = inevow.IRequest(ctx)
880 d = self._node.delete(self._name)
882 # what should this return??
883 return "%s deleted" % self._name
885 def _trap_missing(f):
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)
893 class PUTHandler(rend.Page):
894 def __init__(self, node, path, t, localfile, localdir, replace):
898 self._localfile = localfile
899 self._localdir = localdir
900 self._replace = replace
902 def renderHTTP(self, ctx):
903 req = inevow.IRequest(ctx)
905 localfile = self._localfile
906 localdir = self._localdir
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)
914 d.addCallback(self._upload_localfile, localfile, name)
917 d.addCallback(self._get_or_create_directories, self._path[-1:])
918 d.addCallback(self._upload_localdir, localdir)
920 raise RuntimeError("t=upload requires localfile= or localdir=")
922 d.addCallback(self._attach_uri, req.content, name)
924 d.addCallback(self._mkdir, name)
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")
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")
938 d.addErrback(_check_replacement)
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")
948 return defer.succeed(node)
949 d = node.get(path[0])
950 def _maybe_create(f):
952 return node.create_empty_directory(path[0])
953 d.addErrback(_maybe_create)
954 d.addCallback(self._get_or_create_directories, path[1:])
957 def _check_replacement(self, node, name, replace):
960 d = node.has_child(name)
963 raise NoReplacementError("There was already a child by that "
964 "name, and you asked me to not "
970 def _mkdir(self, node, name):
971 d = node.create_empty_directory(name)
973 return newnode.get_uri()
977 def _upload_file(self, node, contents, name):
978 uploadable = FileHandle(contents)
979 d = node.add_file(name, uploadable)
981 log.msg("webish upload complete")
982 return filenode.get_uri()
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())
992 def _attach_uri(self, parentnode, contents, name):
993 newuri = contents.read().strip()
994 d = parentnode.set_uri(name, newuri)
1000 def _upload_localdir(self, node, localdir):
1001 # build up a list of files to upload
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:
1011 relative_root = root[len(localdir)+1:]
1012 path = tuple(relative_root.split(os.sep))
1014 all_dirs.append(path + (d,))
1016 all_files.append(path + (f,))
1017 d = defer.succeed(msg)
1018 for dir in all_dirs:
1020 d.addCallback(self._makedir, node, dir)
1022 d.addCallback(self._upload_one_file, node, localdir, f)
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]))
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])
1043 class Manifest(rend.Page):
1044 docFactory = getxmlfile("manifest.xhtml")
1045 def __init__(self, dirnode, dirpath):
1046 self._dirnode = dirnode
1047 self._dirpath = dirpath
1049 def dirpath_as_string(self):
1050 return "/" + "/".join(self._dirpath)
1052 def render_title(self, ctx):
1053 return T.title["Manifest of %s" % self.dirpath_as_string()]
1055 def render_header(self, ctx):
1056 return T.p["Manifest of %s" % self.dirpath_as_string()]
1058 def data_items(self, ctx, data):
1059 return self._dirnode.build_manifest()
1061 def render_row(self, ctx, refresh_cap):
1062 ctx.fillSlots("refresh_capability", refresh_cap)
1065 class VDrive(rend.Page):
1067 def __init__(self, node, name):
1071 def get_child_at_path(self, path):
1073 return self.node.get_child_at_path(path)
1074 return defer.succeed(self.node)
1076 def locateChild(self, ctx, segments):
1077 req = inevow.IRequest(ctx)
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] == '':
1090 t = req.args["t"][0]
1093 if "localfile" in req.args:
1094 localfile = req.args["localfile"][0]
1095 if localfile != os.path.abspath(localfile):
1096 return NeedAbsolutePathError(), ()
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
1110 if "replace" in req.args:
1111 if not boolean_of_arg(req.args["replace"][0]):
1115 # the node must exist, and our operation will be performed on the
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"
1124 if "filename" in req.args:
1125 filename = req.args["filename"][0]
1128 # write contents to a local file
1129 return LocalFileDownloader(node, localfile), ()
1130 # send contents as the result
1131 return FileDownloader(node, filename), ()
1133 # send contents as the result
1134 return FileDownloader(node, filename), ()
1136 return FileJSONMetadata(node), ()
1138 return FileURI(node), ()
1139 elif t == "readonly-uri":
1140 return FileReadOnlyURI(node), ()
1142 raise RuntimeError("bad t=%s" % t)
1143 elif IDirectoryNode.providedBy(node):
1146 # recursive download to a local directory
1147 return LocalDirectoryDownloader(node, localdir), ()
1148 raise RuntimeError("t=download requires localdir=")
1150 # send an HTML representation of the directory
1151 return Directory(self.name, node, path), ()
1153 return DirectoryJSONMetadata(node), ()
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), ()
1163 raise RuntimeError("bad t=%s" % t)
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
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
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), ()
1187 return rend.NotFound
1188 def _trap_KeyError(f):
1190 return rend.FourOhFour(), ()
1191 d.addErrback(_trap_KeyError)
1194 class URIPUTHandler(rend.Page):
1195 def renderHTTP(self, ctx):
1196 req = inevow.IRequest(ctx)
1197 assert req.method == "PUT"
1201 t = req.args["t"][0]
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
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
1218 req.setResponseCode(http.BAD_REQUEST)
1219 req.setHeader("content-type", "text/plain")
1220 return "/uri only accepts PUT and PUT?t=mkdir"
1222 class URIPOSTHandler(rend.Page):
1223 def renderHTTP(self, ctx):
1224 req = inevow.IRequest(ctx)
1225 assert req.method == "POST"
1229 t = req.args["t"][0]
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 # "PUT /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])
1244 def _then_redir(res):
1245 req.setResponseCode(303)
1246 req.setHeader('location', res.get_uri())
1249 d.addCallback(_then_redir)
1251 d.addCallback(lambda dirnode: dirnode.get_uri())
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?
1259 class Root(rend.Page):
1262 docFactory = getxmlfile("welcome.xhtml")
1264 def locateChild(self, ctx, segments):
1265 client = IClient(ctx)
1266 req = inevow.IRequest(ctx)
1268 segments = list(segments) # XXX HELP I AM YUCKY!
1269 while segments and not segments[-1]:
1273 segments = tuple(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)
1283 if len(segments) == 1:
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
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):
1301 return rend.FourOhFour(), ()
1302 d.addErrback(_trap_KeyError)
1304 elif segments[0] == "xmlrpc":
1305 raise NotImplementedError()
1306 return rend.Page.locateChild(self, ctx, segments)
1308 child_webform_css = webform.defaultCSS
1309 child_tahoe_css = nevow_File(util.sibpath(__file__, "web/tahoe.css"))
1311 child_provisioning = provisioning.ProvisioningTool()
1313 def data_version(self, ctx, data):
1314 return get_package_versions_string()
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():
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()))
1329 def data_peers(self, ctx, data):
1331 client = IClient(ctx)
1332 for nodeid in sorted(client.get_all_peerids()):
1333 row = (b32encode(nodeid).lower(),)
1337 def render_row(self, ctx, data):
1339 ctx.fillSlots("peerid", nodeid_a)
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:",
1352 return T.p["personal vdrive not available."]
1354 # this is a form where users can download files by URI
1356 def render_download_form(self, ctx, data):
1357 form = T.form(action="uri", method="get",
1358 enctype="multipart/form-data")[
1360 T.legend(class_="freeform-form-label")["Download a file"],
1361 "URI of file to download: ",
1362 T.input(type="text", name="uri"), " ",
1363 "Filename to download as: ",
1364 T.input(type="text", name="filename"), " ",
1365 T.input(type="submit", value="Download"),
1371 implements(ILocalAccess)
1373 self.local_access = False
1374 def local_access_is_allowed(self):
1375 return self.local_access
1377 class WebishServer(service.MultiService):
1380 def __init__(self, webport):
1381 service.MultiService.__init__(self)
1382 self.webport = webport
1384 self.site = site = appserver.NevowSite(self.root)
1385 self.site.requestFactory = MyRequest
1386 self.allow_local = LocalAccess()
1387 self.site.remember(self.allow_local, ILocalAccess)
1388 s = strports.service(webport, site)
1389 s.setServiceParent(self)
1390 self.listener = s # stash it so the tests can query for the portnum
1391 self._started = defer.Deferred()
1393 def allow_local_access(self, enable=True):
1394 self.allow_local.local_access = enable
1396 def startService(self):
1397 service.MultiService.startService(self)
1398 # to make various services available to render_* methods, we stash a
1399 # reference to the client on the NevowSite. This will be available by
1400 # adapting the 'context' argument to a special marker interface named
1402 self.site.remember(self.parent, IClient)
1403 # I thought you could do the same with an existing interface, but
1404 # apparently 'ISite' does not exist
1405 #self.site._client = self.parent
1406 self._started.callback(None)
1408 def create_start_html(self, private_uri, startfile, nodeurl_file):
1410 Returns a deferred that eventually fires once the start.html page has
1413 self._started.addCallback(self._create_start_html, private_uri, startfile, nodeurl_file)
1414 return self._started
1416 def _create_start_html(self, dummy, private_uri, startfile, nodeurl_file):
1417 f = open(startfile, "w")
1418 template = open(util.sibpath(__file__, "web/start.html"), "r").read()
1419 # what is our webport?
1421 if isinstance(s, internet.TCPServer):
1422 base_url = "http://localhost:%d" % s._port.getHost().port
1423 elif isinstance(s, internet.SSLServer):
1424 base_url = "https://localhost:%d" % s._port.getHost().port
1426 base_url = "UNKNOWN" # this will break the href
1427 # TODO: emit a start.html that explains that we don't know
1428 # how to create a suitable URL
1430 link_to_private_uri = "View <a href=\"%s/uri/%s\">your personal private non-shared filestore</a>." % (base_url, private_uri)
1431 fields = {"link_to_private_uri": link_to_private_uri,
1432 "base_url": base_url,
1435 fields = {"link_to_private_uri": "",
1436 "base_url": base_url,
1438 f.write(template % fields)
1441 f = open(nodeurl_file, "w")
1442 # this file is world-readable
1443 f.write(base_url + "\n")