3 from twisted.application import service, strports, internet
4 from twisted.web import static, resource, server, html, http
5 from twisted.python import log
6 from twisted.internet import defer, address
7 from twisted.internet.interfaces import IConsumer
8 from nevow import inevow, rend, loaders, appserver, url, tags as T
9 from nevow.static import File as nevow_File # TODO: merge with static.File?
10 from allmydata.util import fileutil, idlib
12 from allmydata.interfaces import IDownloadTarget, IDirectoryNode, IFileNode, \
14 from allmydata import download
15 from allmydata.upload import FileHandle, FileName
16 from allmydata import provisioning
17 from allmydata import get_package_versions_string
18 from zope.interface import implements, Interface
20 from formless import webform
22 from nevow.util import resource_filename
25 return loaders.xmlfile(resource_filename('allmydata.web', '%s' % name))
27 class IClient(Interface):
29 class ILocalAccess(Interface):
30 def local_access_is_allowed():
31 """Return True if t=upload&localdir= is allowed, giving anyone who
32 can talk to the webserver control over the local (disk) filesystem."""
34 def boolean_of_arg(arg):
35 assert arg.lower() in ("true", "t", "1", "false", "f", "0")
36 return arg.lower() in ("true", "t", "1")
38 def get_arg(req, argname, default=None, multiple=False):
39 """Extract an argument from either the query args (req.args) or the form
40 body fields (req.fields). If multiple=False, this returns a single value
41 (or the default, which defaults to None), and the query args take
42 precedence. If multiple=True, this returns a tuple of arguments (possibly
43 empty), starting with all those in the query args.
46 if argname in req.args:
47 results.extend(req.args[argname])
48 if req.fields and argname in req.fields:
49 results.append(req.fields[argname].value)
56 # we must override twisted.web.http.Request.requestReceived with a version
57 # that doesn't use cgi.parse_multipart() . Since we actually use Nevow, we
58 # override the nevow-specific subclass, nevow.appserver.NevowRequest . This
59 # is an exact copy of twisted.web.http.Request (from SVN HEAD on 10-Aug-2007)
60 # that modifies the way form arguments are parsed. Note that this sort of
61 # surgery may induce a dependency upon a particular version of twisted.web
63 parse_qs = http.parse_qs
64 class MyRequest(appserver.NevowRequest):
66 def requestReceived(self, command, path, version):
67 """Called by channel when all data has been received.
69 This method is not intended for users.
71 self.content.seek(0,0)
75 self.method, self.uri = command, path
76 self.clientproto = version
77 x = self.uri.split('?', 1)
82 self.path, argstring = x
83 self.args = parse_qs(argstring, 1)
85 # cache the client and server information, we'll need this later to be
86 # serialized and sent with the request so CGIs will work remotely
87 self.client = self.channel.transport.getPeer()
88 self.host = self.channel.transport.getHost()
90 # Argument processing.
92 ## The original twisted.web.http.Request.requestReceived code parsed the
93 ## content and added the form fields it found there to self.args . It
94 ## did this with cgi.parse_multipart, which holds the arguments in RAM
95 ## and is thus unsuitable for large file uploads. The Nevow subclass
96 ## (nevow.appserver.NevowRequest) uses cgi.FieldStorage instead (putting
97 ## the results in self.fields), which is much more memory-efficient.
98 ## Since we know we're using Nevow, we can anticipate these arguments
99 ## appearing in self.fields instead of self.args, and thus skip the
100 ## parse-content-into-self.args step.
103 ## ctype = self.getHeader('content-type')
104 ## if self.method == "POST" and ctype:
105 ## mfd = 'multipart/form-data'
106 ## key, pdict = cgi.parse_header(ctype)
107 ## if key == 'application/x-www-form-urlencoded':
108 ## args.update(parse_qs(self.content.read(), 1))
111 ## args.update(cgi.parse_multipart(self.content, pdict))
112 ## except KeyError, e:
113 ## if e.args[0] == 'content-disposition':
114 ## # Parse_multipart can't cope with missing
115 ## # content-dispostion headers in multipart/form-data
116 ## # parts, so we catch the exception and tell the client
117 ## # it was a bad request.
118 ## self.channel.transport.write(
119 ## "HTTP/1.1 400 Bad Request\r\n\r\n")
120 ## self.channel.transport.loseConnection()
126 class Directory(rend.Page):
128 docFactory = getxmlfile("directory.xhtml")
130 def __init__(self, rootname, dirnode, dirpath):
131 self._rootname = rootname
132 self._dirnode = dirnode
133 self._dirpath = dirpath
135 def dirpath_as_string(self):
136 return "/" + "/".join(self._dirpath)
138 def render_title(self, ctx, data):
139 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
141 def render_header(self, ctx, data):
142 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
143 num_dirs = len(parent_directories)
145 header = ["Directory '"]
146 for i,d in enumerate(parent_directories):
147 upness = num_dirs - i - 1
149 link = "/".join( ("..",) * upness )
152 header.append(T.a(href=link)[d])
157 if self._dirnode.is_readonly():
158 header.append(" (readonly)")
160 return ctx.tag[header]
162 def render_welcome(self, ctx, data):
163 depth = len(self._dirpath) + 2
164 link = "/".join([".."] * depth)
165 return T.div[T.a(href=link)["Return to Welcome page"]]
167 def data_children(self, ctx, data):
168 d = self._dirnode.list()
169 d.addCallback(lambda dict: sorted(dict.items()))
172 def render_row(self, ctx, data):
173 name, (target, metadata) = data
175 if self._dirnode.is_readonly():
179 # this creates a button which will cause our child__delete method
180 # to be invoked, which deletes the file and then redirects the
181 # browser back to this directory
182 delete = T.form(action=url.here, method="post")[
183 T.input(type='hidden', name='t', value='delete'),
184 T.input(type='hidden', name='name', value=name),
185 T.input(type='hidden', name='when_done', value=url.here),
186 T.input(type='submit', value='del', name="del"),
189 rename = T.form(action=url.here, method="get")[
190 T.input(type='hidden', name='t', value='rename-form'),
191 T.input(type='hidden', name='name', value=name),
192 T.input(type='hidden', name='when_done', value=url.here),
193 T.input(type='submit', value='rename', name="rename"),
196 ctx.fillSlots("delete", delete)
197 ctx.fillSlots("rename", rename)
198 check = T.form(action=url.here, method="post")[
199 T.input(type='hidden', name='t', value='check'),
200 T.input(type='hidden', name='name', value=name),
201 T.input(type='hidden', name='when_done', value=url.here),
202 T.input(type='submit', value='check', name="check"),
204 ctx.fillSlots("overwrite", self.build_overwrite(ctx, (name, target)))
205 ctx.fillSlots("check", check)
207 # build the base of the uri_link link url
208 uri_link = "/uri/" + urllib.quote(target.get_uri())
210 assert (IFileNode.providedBy(target)
211 or IDirectoryNode.providedBy(target)
212 or IMutableFileNode.providedBy(target)), target
214 if IMutableFileNode.providedBy(target):
217 # add the filename to the uri_link url
218 uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
220 # to prevent javascript in displayed .html files from stealing a
221 # secret directory URI from the URL, send the browser to a URI-based
222 # page that doesn't know about the directory at all
223 #dlurl = urllib.quote(name)
226 ctx.fillSlots("filename",
227 T.a(href=dlurl)[html.escape(name)])
228 ctx.fillSlots("type", "SSK")
230 ctx.fillSlots("size", "?")
232 text_plain_link = uri_link + "?filename=foo.txt"
233 text_plain_tag = T.a(href=text_plain_link)["text/plain"]
235 elif IFileNode.providedBy(target):
238 # add the filename to the uri_link url
239 uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
241 # to prevent javascript in displayed .html files from stealing a
242 # secret directory URI from the URL, send the browser to a URI-based
243 # page that doesn't know about the directory at all
244 #dlurl = urllib.quote(name)
247 ctx.fillSlots("filename",
248 T.a(href=dlurl)[html.escape(name)])
249 ctx.fillSlots("type", "FILE")
251 ctx.fillSlots("size", target.get_size())
253 text_plain_link = uri_link + "?filename=foo.txt"
254 text_plain_tag = T.a(href=text_plain_link)["text/plain"]
256 elif IDirectoryNode.providedBy(target):
258 ctx.fillSlots("filename",
259 T.a(href=uri_link)[html.escape(name)])
260 if target.is_readonly():
264 ctx.fillSlots("type", dirtype)
265 ctx.fillSlots("size", "-")
266 text_plain_tag = None
268 childdata = [T.a(href="%s?t=json" % name)["JSON"], ", ",
269 T.a(href="%s?t=uri" % name)["URI"], ", ",
270 T.a(href="%s?t=readonly-uri" % name)["readonly-URI"],
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 return get_arg(req, "results", "")
382 class WebDownloadTarget:
383 implements(IDownloadTarget, IConsumer)
384 def __init__(self, req, content_type, content_encoding, save_to_file):
386 self._content_type = content_type
387 self._content_encoding = content_encoding
389 self._producer = None
390 self._save_to_file = save_to_file
392 def registerProducer(self, producer, streaming):
393 self._req.registerProducer(producer, streaming)
394 def unregisterProducer(self):
395 self._req.unregisterProducer()
397 def open(self, size):
399 self._req.setHeader("content-type", self._content_type)
400 if self._content_encoding:
401 self._req.setHeader("content-encoding", self._content_encoding)
402 self._req.setHeader("content-length", str(size))
403 if self._save_to_file is not None:
404 # tell the browser to save the file rather display it
405 # TODO: quote save_to_file properly
406 self._req.setHeader("content-disposition",
407 'attachment; filename="%s"'
408 % self._save_to_file)
410 def write(self, data):
411 self._req.write(data)
417 # The content-type is already set, and the response code
418 # has already been sent, so we can't provide a clean error
419 # indication. We can emit text (which a browser might interpret
420 # as something else), and if we sent a Size header, they might
421 # notice that we've truncated the data. Keep the error message
422 # small to improve the chances of having our error response be
423 # shorter than the intended results.
425 # We don't have a lot of options, unfortunately.
426 self._req.write("problem during download\n")
428 # We haven't written anything yet, so we can provide a sensible
431 msg.replace("\n", "|")
432 self._req.setResponseCode(http.GONE, msg)
433 self._req.setHeader("content-type", "text/plain")
434 # TODO: HTML-formatted exception?
435 self._req.write(str(why))
438 def register_canceller(self, cb):
443 class FileDownloader(resource.Resource):
444 def __init__(self, filenode, name):
445 assert (IFileNode.providedBy(filenode)
446 or IMutableFileNode.providedBy(filenode))
447 self._filenode = filenode
450 def render(self, req):
451 gte = static.getTypeAndEncoding
452 type, encoding = gte(self._name,
453 static.File.contentTypes,
454 static.File.contentEncodings,
455 defaultType="text/plain")
457 if get_arg(req, "save", False):
458 # TODO: make the API specification clear: should "save=" or
459 # "save=false" count?
460 save_to_file = self._name
461 wdt = WebDownloadTarget(req, type, encoding, save_to_file)
462 d = self._filenode.download(wdt)
463 # exceptions during download are handled by the WebDownloadTarget
464 d.addErrback(lambda why: None)
465 return server.NOT_DONE_YET
467 class BlockingFileError(Exception):
468 """We cannot auto-create a parent directory, because there is a file in
470 class NoReplacementError(Exception):
471 """There was already a child by that name, and you asked me to not replace it"""
472 class NoLocalDirectoryError(Exception):
473 """The localdir= directory didn't exist"""
475 LOCALHOST = "127.0.0.1"
477 class NeedLocalhostError:
478 implements(inevow.IResource)
480 def renderHTTP(self, ctx):
481 req = inevow.IRequest(ctx)
482 req.setResponseCode(http.FORBIDDEN)
483 req.setHeader("content-type", "text/plain")
484 return "localfile= or localdir= requires a local connection"
486 class NeedAbsolutePathError:
487 implements(inevow.IResource)
489 def renderHTTP(self, ctx):
490 req = inevow.IRequest(ctx)
491 req.setResponseCode(http.FORBIDDEN)
492 req.setHeader("content-type", "text/plain")
493 return "localfile= or localdir= requires an absolute path"
495 class LocalAccessDisabledError:
496 implements(inevow.IResource)
498 def renderHTTP(self, ctx):
499 req = inevow.IRequest(ctx)
500 req.setResponseCode(http.FORBIDDEN)
501 req.setHeader("content-type", "text/plain")
502 return "local file access is disabled"
505 class LocalFileDownloader(resource.Resource):
506 def __init__(self, filenode, local_filename):
507 self._local_filename = local_filename
509 self._filenode = filenode
511 def render(self, req):
512 target = download.FileName(self._local_filename)
513 d = self._filenode.download(target)
515 req.write(self._filenode.get_uri())
518 return server.NOT_DONE_YET
521 class FileJSONMetadata(rend.Page):
522 def __init__(self, filenode):
523 self._filenode = filenode
525 def renderHTTP(self, ctx):
526 req = inevow.IRequest(ctx)
527 req.setHeader("content-type", "text/plain")
528 return self.renderNode(self._filenode)
530 def renderNode(self, filenode):
531 file_uri = filenode.get_uri()
534 'size': filenode.get_size(),
536 return simplejson.dumps(data, indent=1)
538 class FileURI(FileJSONMetadata):
539 def renderNode(self, filenode):
540 file_uri = filenode.get_uri()
543 class FileReadOnlyURI(FileJSONMetadata):
544 def renderNode(self, filenode):
545 if filenode.is_readonly():
546 return filenode.get_uri()
548 return filenode.get_readonly().get_uri()
550 class DirnodeWalkerMixin:
551 """Visit all nodes underneath (and including) the rootnode, one at a
552 time. For each one, call the visitor. The visitor will see the
553 IDirectoryNode before it sees any of the IFileNodes inside. If the
554 visitor returns a Deferred, I do not call the visitor again until it has
558 ## def _walk_if_we_could_use_generators(self, rootnode, rootpath=()):
559 ## # this is what we'd be doing if we didn't have the Deferreds and
560 ## # thus could use generators
561 ## yield rootpath, rootnode
562 ## for childname, childnode in rootnode.list().items():
563 ## childpath = rootpath + (childname,)
564 ## if IFileNode.providedBy(childnode):
565 ## yield childpath, childnode
566 ## elif IDirectoryNode.providedBy(childnode):
567 ## for res in self._walk_if_we_could_use_generators(childnode,
571 def walk(self, rootnode, visitor, rootpath=()):
573 def _listed(listing):
574 return listing.items()
575 d.addCallback(_listed)
576 d.addCallback(self._handle_items, visitor, rootpath)
579 def _handle_items(self, items, visitor, rootpath):
582 childname, (childnode, metadata) = items[0]
583 childpath = rootpath + (childname,)
584 d = defer.maybeDeferred(visitor, childpath, childnode, metadata)
585 if IDirectoryNode.providedBy(childnode):
586 d.addCallback(lambda res: self.walk(childnode, visitor, childpath))
587 d.addCallback(lambda res:
588 self._handle_items(items[1:], visitor, rootpath))
591 class LocalDirectoryDownloader(resource.Resource, DirnodeWalkerMixin):
592 def __init__(self, dirnode, localdir):
593 self._dirnode = dirnode
594 self._localdir = localdir
596 def _handle(self, path, node, metadata):
597 localfile = os.path.join(self._localdir, os.sep.join(path))
598 if IDirectoryNode.providedBy(node):
599 fileutil.make_dirs(localfile)
600 elif IFileNode.providedBy(node):
601 target = download.FileName(localfile)
602 return node.download(target)
604 def render(self, req):
605 d = self.walk(self._dirnode, self._handle)
607 req.setHeader("content-type", "text/plain")
608 return "operation complete"
612 class DirectoryJSONMetadata(rend.Page):
613 def __init__(self, dirnode):
614 self._dirnode = dirnode
616 def renderHTTP(self, ctx):
617 req = inevow.IRequest(ctx)
618 req.setHeader("content-type", "text/plain")
619 return self.renderNode(self._dirnode)
621 def renderNode(self, node):
625 for name, (childnode, metadata) in children.iteritems():
626 if IFileNode.providedBy(childnode):
627 kiduri = childnode.get_uri()
628 kiddata = ("filenode",
630 'size': childnode.get_size(),
633 assert IDirectoryNode.providedBy(childnode), (childnode, children,)
634 kiddata = ("dirnode",
635 {'ro_uri': childnode.get_readonly_uri(),
637 if not childnode.is_readonly():
638 kiddata[1]['rw_uri'] = childnode.get_uri()
640 contents = { 'children': kids,
641 'ro_uri': node.get_readonly_uri(),
643 if not node.is_readonly():
644 contents['rw_uri'] = node.get_uri()
645 data = ("dirnode", contents)
646 return simplejson.dumps(data, indent=1)
650 class DirectoryURI(DirectoryJSONMetadata):
651 def renderNode(self, node):
652 return node.get_uri()
654 class DirectoryReadonlyURI(DirectoryJSONMetadata):
655 def renderNode(self, node):
656 return node.get_readonly_uri()
658 class RenameForm(rend.Page):
660 docFactory = getxmlfile("rename-form.xhtml")
662 def __init__(self, rootname, dirnode, dirpath):
663 self._rootname = rootname
664 self._dirnode = dirnode
665 self._dirpath = dirpath
667 def dirpath_as_string(self):
668 return "/" + "/".join(self._dirpath)
670 def render_title(self, ctx, data):
671 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
673 def render_header(self, ctx, data):
674 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
675 num_dirs = len(parent_directories)
677 header = [ "Rename in directory '",
678 "<%s>/" % self._rootname,
679 "/".join(self._dirpath),
682 if self._dirnode.is_readonly():
683 header.append(" (readonly)")
684 return ctx.tag[header]
686 def render_when_done(self, ctx, data):
687 return T.input(type="hidden", name="when_done", value=url.here)
689 def render_get_name(self, ctx, data):
690 req = inevow.IRequest(ctx)
691 name = get_arg(req, "name", "")
692 ctx.tag.attributes['value'] = name
695 class POSTHandler(rend.Page):
696 def __init__(self, node, replace):
698 self._replace = replace
700 def _check_replacement(self, name):
702 return defer.succeed(None)
703 d = self._node.has_child(name)
706 raise NoReplacementError("There was already a child by that "
707 "name, and you asked me to not "
713 def renderHTTP(self, ctx):
714 req = inevow.IRequest(ctx)
716 t = get_arg(req, "t")
719 name = get_arg(req, "name", None)
720 if name and "/" in name:
721 req.setResponseCode(http.BAD_REQUEST)
722 req.setHeader("content-type", "text/plain")
723 return "name= may not contain a slash"
726 # we allow the user to delete an empty-named file, but not to create
727 # them, since that's an easy and confusing mistake to make
729 when_done = get_arg(req, "when_done", None)
730 if not boolean_of_arg(get_arg(req, "replace", "true")):
731 self._replace = False
735 raise RuntimeError("mkdir requires a name")
736 d = self._check_replacement(name)
737 d.addCallback(lambda res: self._node.create_empty_directory(name))
738 d.addCallback(lambda res: "directory created")
741 raise RuntimeError("set-uri requires a name")
742 newuri = get_arg(req, "uri")
743 assert newuri is not None
744 d = self._check_replacement(name)
745 d.addCallback(lambda res: self._node.set_uri(name, newuri))
746 d.addCallback(lambda res: newuri)
749 # apparently an <input type="hidden" name="name" value="">
750 # won't show up in the resulting encoded form.. the 'name'
751 # field is completely missing. So to allow deletion of an
752 # empty file, we have to pretend that None means ''. The only
753 # downide of this is a slightly confusing error message if
754 # someone does a POST without a name= field. For our own HTML
755 # thisn't a big deal, because we create the 'delete' POST
758 d = self._node.delete(name)
759 d.addCallback(lambda res: "thing deleted")
761 from_name = 'from_name' in req.fields and req.fields["from_name"].value
762 if from_name is not None:
763 from_name = from_name.strip()
764 to_name = 'to_name' in req.fields and req.fields["to_name"].value
765 if to_name is not None:
766 to_name = to_name.strip()
767 if not from_name or not to_name:
768 raise RuntimeError("rename requires from_name and to_name")
769 if not IDirectoryNode.providedBy(self._node):
770 raise RuntimeError("rename must only be called on directories")
771 for k,v in [ ('from_name', from_name), ('to_name', to_name) ]:
773 req.setResponseCode(http.BAD_REQUEST)
774 req.setHeader("content-type", "text/plain")
775 return "%s= may not contain a slash" % (k,)
776 d = self._check_replacement(to_name)
777 d.addCallback(lambda res: self._node.get(from_name))
779 uri = child.get_uri()
780 # now actually do the rename
781 return self._node.set_uri(to_name, uri)
782 d.addCallback(add_dest)
784 return self._node.delete(from_name)
785 d.addCallback(rm_src)
786 d.addCallback(lambda res: "thing renamed")
789 if "mutable" in req.fields:
790 contents = req.fields["file"]
791 name = name or contents.filename
795 raise RuntimeError("upload-mutable requires a name")
796 # SDMF: files are small, and we can only upload data.
797 contents.file.seek(0)
798 data = contents.file.read()
799 uploadable = FileHandle(contents.file)
800 d = self._check_replacement(name)
801 d.addCallback(lambda res: self._node.has_child(name))
802 def _checked(present):
804 # modify the existing one instead of creating a new
806 d2 = self._node.get(name)
807 def _got_newnode(newnode):
808 d3 = newnode.replace(data)
809 d3.addCallback(lambda res: newnode.get_uri())
811 d2.addCallback(_got_newnode)
813 d2 = IClient(ctx).create_mutable_file(data)
814 def _uploaded(newnode):
815 d1 = self._node.set_node(name, newnode)
816 d1.addCallback(lambda res: newnode.get_uri())
818 d2.addCallback(_uploaded)
820 d.addCallback(_checked)
822 contents = req.fields["file"]
823 name = name or contents.filename
827 raise RuntimeError("upload requires a name")
828 uploadable = FileHandle(contents.file)
829 d = self._check_replacement(name)
830 d.addCallback(lambda res: self._node.add_file(name, uploadable))
832 return newnode.get_uri()
835 elif t == "overwrite":
836 contents = req.fields["file"]
837 # SDMF: files are small, and we can only upload data.
838 contents.file.seek(0)
839 data = contents.file.read()
840 # TODO: 'name' handling needs review
841 d = defer.succeed(self._node)
842 def _got_child_overwrite(child_node):
843 child_node.replace(data)
844 return child_node.get_uri()
845 d.addCallback(_got_child_overwrite)
848 d = self._node.get(name)
849 def _got_child_check(child_node):
850 d2 = child_node.check()
852 log.msg("checked %s, results %s" % (child_node, res))
854 d2.addCallback(_done)
856 d.addCallback(_got_child_check)
859 return "BAD t=%s" % t
861 d.addCallback(lambda res: url.URL.fromString(when_done))
862 def _check_replacement(f):
863 # TODO: make this more human-friendly: maybe send them to the
864 # when_done page but with an extra query-arg that will display
865 # the error message in a big box at the top of the page. The
866 # directory page that when_done= usually points to accepts a
867 # result= argument.. use that.
868 f.trap(NoReplacementError)
869 req.setResponseCode(http.CONFLICT)
870 req.setHeader("content-type", "text/plain")
872 d.addErrback(_check_replacement)
875 class DELETEHandler(rend.Page):
876 def __init__(self, node, name):
880 def renderHTTP(self, ctx):
881 req = inevow.IRequest(ctx)
882 d = self._node.delete(self._name)
884 # what should this return??
885 return "%s deleted" % self._name
887 def _trap_missing(f):
889 req.setResponseCode(http.NOT_FOUND)
890 req.setHeader("content-type", "text/plain")
891 return "no such child %s" % self._name
892 d.addErrback(_trap_missing)
895 class PUTHandler(rend.Page):
896 def __init__(self, node, path, t, localfile, localdir, replace):
900 self._localfile = localfile
901 self._localdir = localdir
902 self._replace = replace
904 def renderHTTP(self, ctx):
905 req = inevow.IRequest(ctx)
907 localfile = self._localfile
908 localdir = self._localdir
910 if t == "upload" and not (localfile or localdir):
911 req.setResponseCode(http.BAD_REQUEST)
912 req.setHeader("content-type", "text/plain")
913 return "t=upload requires localfile= or localdir="
915 # we must traverse the path, creating new directories as necessary
916 d = self._get_or_create_directories(self._node, self._path[:-1])
917 name = self._path[-1]
918 d.addCallback(self._check_replacement, name, self._replace)
921 d.addCallback(self._upload_localfile, localfile, name)
925 d.addCallback(self._get_or_create_directories, self._path[-1:])
926 d.addCallback(self._upload_localdir, localdir)
928 d.addCallback(self._attach_uri, req.content, name)
930 d.addCallback(self._mkdir, name)
932 d.addCallback(self._upload_file, req.content, name)
934 def _transform_error(f):
935 errors = {BlockingFileError: http.BAD_REQUEST,
936 NoReplacementError: http.CONFLICT,
937 NoLocalDirectoryError: http.BAD_REQUEST,
939 for k,v in errors.items():
941 req.setResponseCode(v)
942 req.setHeader("content-type", "text/plain")
945 d.addErrback(_transform_error)
948 def _get_or_create_directories(self, node, path):
949 if not IDirectoryNode.providedBy(node):
950 # unfortunately it is too late to provide the name of the
951 # blocking directory in the error message.
952 raise BlockingFileError("cannot create directory because there "
953 "is a file in the way")
955 return defer.succeed(node)
956 d = node.get(path[0])
957 def _maybe_create(f):
959 return node.create_empty_directory(path[0])
960 d.addErrback(_maybe_create)
961 d.addCallback(self._get_or_create_directories, path[1:])
964 def _check_replacement(self, node, name, replace):
967 d = node.has_child(name)
970 raise NoReplacementError("There was already a child by that "
971 "name, and you asked me to not "
977 def _mkdir(self, node, name):
978 d = node.create_empty_directory(name)
980 return newnode.get_uri()
984 def _upload_file(self, node, contents, name):
985 uploadable = FileHandle(contents)
986 d = node.add_file(name, uploadable)
988 log.msg("webish upload complete")
989 return filenode.get_uri()
993 def _upload_localfile(self, node, localfile, name):
994 uploadable = FileName(localfile)
995 d = node.add_file(name, uploadable)
996 d.addCallback(lambda filenode: filenode.get_uri())
999 def _attach_uri(self, parentnode, contents, name):
1000 newuri = contents.read().strip()
1001 d = parentnode.set_uri(name, newuri)
1004 d.addCallback(_done)
1007 def _upload_localdir(self, node, localdir):
1008 # build up a list of files to upload
1011 msg = "No files to upload! %s is empty" % localdir
1012 if not os.path.exists(localdir):
1013 msg = "%s doesn't exist!" % localdir
1014 raise NoLocalDirectoryError(msg)
1015 for root, dirs, files in os.walk(localdir):
1016 if root == localdir:
1019 relative_root = root[len(localdir)+1:]
1020 path = tuple(relative_root.split(os.sep))
1022 all_dirs.append(path + (d,))
1024 all_files.append(path + (f,))
1025 d = defer.succeed(msg)
1026 for dir in all_dirs:
1028 d.addCallback(self._makedir, node, dir)
1030 d.addCallback(self._upload_one_file, node, localdir, f)
1033 def _makedir(self, res, node, dir):
1034 d = defer.succeed(None)
1035 # get the parent. As long as os.walk gives us parents before
1036 # children, this ought to work
1037 d.addCallback(lambda res: node.get_child_at_path(dir[:-1]))
1038 # then create the child directory
1039 d.addCallback(lambda parent: parent.create_empty_directory(dir[-1]))
1042 def _upload_one_file(self, res, node, localdir, f):
1043 # get the parent. We can be sure this exists because we already
1044 # went through and created all the directories we require.
1045 localfile = os.path.join(localdir, *f)
1046 d = node.get_child_at_path(f[:-1])
1047 d.addCallback(self._upload_localfile, localfile, f[-1])
1051 class Manifest(rend.Page):
1052 docFactory = getxmlfile("manifest.xhtml")
1053 def __init__(self, dirnode, dirpath):
1054 self._dirnode = dirnode
1055 self._dirpath = dirpath
1057 def dirpath_as_string(self):
1058 return "/" + "/".join(self._dirpath)
1060 def render_title(self, ctx):
1061 return T.title["Manifest of %s" % self.dirpath_as_string()]
1063 def render_header(self, ctx):
1064 return T.p["Manifest of %s" % self.dirpath_as_string()]
1066 def data_items(self, ctx, data):
1067 return self._dirnode.build_manifest()
1069 def render_row(self, ctx, refresh_cap):
1070 ctx.fillSlots("refresh_capability", refresh_cap)
1074 implements(inevow.IResource)
1075 def renderHTTP(self, ctx):
1076 req = inevow.IRequest(ctx)
1077 req.setResponseCode(http.BAD_REQUEST)
1078 req.setHeader("content-type", "text/plain")
1081 def child_error(text):
1086 class VDrive(rend.Page):
1088 def __init__(self, node, name):
1092 def get_child_at_path(self, path):
1094 return self.node.get_child_at_path(path)
1095 return defer.succeed(self.node)
1097 def locateChild(self, ctx, segments):
1098 req = inevow.IRequest(ctx)
1102 t = get_arg(req, "t", "")
1103 localfile = get_arg(req, "localfile", None)
1104 if localfile is not None:
1105 if localfile != os.path.abspath(localfile):
1106 return NeedAbsolutePathError(), ()
1107 localdir = get_arg(req, "localdir", None)
1108 if localdir is not None:
1109 if localdir != os.path.abspath(localdir):
1110 return NeedAbsolutePathError(), ()
1111 if localfile or localdir:
1112 if not ILocalAccess(ctx).local_access_is_allowed():
1113 return LocalAccessDisabledError(), ()
1114 if req.getHost().host != LOCALHOST:
1115 return NeedLocalhostError(), ()
1116 # TODO: think about clobbering/revealing config files and node secrets
1118 replace = boolean_of_arg(get_arg(req, "replace", "true"))
1121 # the node must exist, and our operation will be performed on the
1123 d = self.get_child_at_path(path)
1124 def file_or_dir(node):
1125 if (IFileNode.providedBy(node)
1126 or IMutableFileNode.providedBy(node)):
1127 filename = "unknown"
1130 filename = get_arg(req, "filename", filename)
1133 # write contents to a local file
1134 return LocalFileDownloader(node, localfile), ()
1135 # send contents as the result
1136 return FileDownloader(node, filename), ()
1138 # send contents as the result
1139 return FileDownloader(node, filename), ()
1141 return FileJSONMetadata(node), ()
1143 return FileURI(node), ()
1144 elif t == "readonly-uri":
1145 return FileReadOnlyURI(node), ()
1147 return child_error("bad t=%s" % t)
1148 elif IDirectoryNode.providedBy(node):
1151 # recursive download to a local directory
1152 return LocalDirectoryDownloader(node, localdir), ()
1153 return child_error("t=download requires localdir=")
1155 # send an HTML representation of the directory
1156 return Directory(self.name, node, path), ()
1158 return DirectoryJSONMetadata(node), ()
1160 return DirectoryURI(node), ()
1161 elif t == "readonly-uri":
1162 return DirectoryReadonlyURI(node), ()
1163 elif t == "manifest":
1164 return Manifest(node, path), ()
1165 elif t == 'rename-form':
1166 return RenameForm(self.name, node, path), ()
1168 return child_error("bad t=%s" % t)
1170 return child_error("unknown node type")
1171 d.addCallback(file_or_dir)
1172 elif method == "POST":
1173 # the node must exist, and our operation will be performed on the
1175 d = self.get_child_at_path(path)
1176 def _got_POST(node):
1177 return POSTHandler(node, replace), ()
1178 d.addCallback(_got_POST)
1179 elif method == "DELETE":
1180 # the node must exist, and our operation will be performed on its
1182 assert path # you can't delete the root
1183 d = self.get_child_at_path(path[:-1])
1184 def _got_DELETE(node):
1185 return DELETEHandler(node, path[-1]), ()
1186 d.addCallback(_got_DELETE)
1187 elif method in ("PUT",):
1188 # the node may or may not exist, and our operation may involve
1189 # all the ancestors of the node.
1190 return PUTHandler(self.node, path, t, localfile, localdir, replace), ()
1192 return rend.NotFound
1195 class URIPUTHandler(rend.Page):
1196 def renderHTTP(self, ctx):
1197 req = inevow.IRequest(ctx)
1198 assert req.method == "PUT"
1200 t = get_arg(req, "t", "")
1203 # "PUT /uri", to create an unlinked file. This is like PUT but
1204 # without the associated set_uri.
1205 uploadable = FileHandle(req.content)
1206 d = IClient(ctx).upload(uploadable)
1207 # that fires with the URI of the new file
1211 # "PUT /uri?t=mkdir", to create an unlinked directory.
1212 d = IClient(ctx).create_empty_dirnode()
1213 d.addCallback(lambda dirnode: dirnode.get_uri())
1214 # XXX add redirect_to_result
1217 req.setResponseCode(http.BAD_REQUEST)
1218 req.setHeader("content-type", "text/plain")
1219 return "/uri only accepts PUT and PUT?t=mkdir"
1221 class URIPOSTHandler(rend.Page):
1222 def renderHTTP(self, ctx):
1223 req = inevow.IRequest(ctx)
1224 assert req.method == "POST"
1226 t = get_arg(req, "t", "").strip()
1228 if t in ("", "upload"):
1229 # "POST /uri", to create an unlinked file.
1230 fileobj = req.fields["file"].file
1231 uploadable = FileHandle(fileobj)
1232 d = IClient(ctx).upload(uploadable)
1233 # that fires with the URI of the new file
1237 # "POST /uri?t=mkdir", to create an unlinked directory.
1238 d = IClient(ctx).create_empty_dirnode()
1239 redirect = get_arg(req, "redirect_to_result", "false")
1240 if boolean_of_arg(redirect):
1241 def _then_redir(res):
1242 new_url = "uri/" + urllib.quote(res.get_uri())
1243 req.setResponseCode(http.SEE_OTHER) # 303
1244 req.setHeader('location', new_url)
1247 d.addCallback(_then_redir)
1249 d.addCallback(lambda dirnode: dirnode.get_uri())
1252 req.setResponseCode(http.BAD_REQUEST)
1253 req.setHeader("content-type", "text/plain")
1254 err = "/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, and POST?t=mkdir"
1258 class Root(rend.Page):
1261 docFactory = getxmlfile("welcome.xhtml")
1263 def locateChild(self, ctx, segments):
1264 client = IClient(ctx)
1265 req = inevow.IRequest(ctx)
1267 segments = list(segments) # XXX HELP I AM YUCKY!
1268 while segments and not segments[-1]:
1272 segments = tuple(segments)
1274 if segments[0] == "uri":
1275 if len(segments) == 1 or segments[1] == '':
1276 uri = get_arg(req, "uri", None)
1278 there = url.URL.fromContext(ctx)
1279 there = there.clear("uri")
1280 there = there.child("uri").child(uri)
1282 if len(segments) == 1:
1284 if req.method == "PUT":
1285 # either "PUT /uri" to create an unlinked file, or
1286 # "PUT /uri?t=mkdir" to create an unlinked directory
1287 return URIPUTHandler(), ()
1288 elif req.method == "POST":
1289 # "POST /uri?t=upload&file=newfile" to upload an unlinked
1290 # file or "POST /uri?t=mkdir" to create a new directory
1291 return URIPOSTHandler(), ()
1292 if len(segments) < 2:
1293 return rend.NotFound
1295 d = defer.maybeDeferred(client.create_node_from_uri, uri)
1296 d.addCallback(lambda node: VDrive(node, uri))
1297 d.addCallback(lambda vd: vd.locateChild(ctx, segments[2:]))
1298 def _trap_KeyError(f):
1300 return rend.FourOhFour(), ()
1301 d.addErrback(_trap_KeyError)
1303 elif segments[0] == "xmlrpc":
1304 raise NotImplementedError()
1305 return rend.Page.locateChild(self, ctx, segments)
1307 child_webform_css = webform.defaultCSS
1308 child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
1310 child_provisioning = provisioning.ProvisioningTool()
1312 def data_version(self, ctx, data):
1313 return get_package_versions_string()
1315 def data_my_nodeid(self, ctx, data):
1316 return idlib.nodeid_b2a(IClient(ctx).nodeid)
1317 def data_introducer_furl(self, ctx, data):
1318 return IClient(ctx).introducer_furl
1319 def data_connected_to_introducer(self, ctx, data):
1320 if IClient(ctx).connected_to_introducer():
1324 def data_helper_furl(self, ctx, data):
1326 uploader = IClient(ctx).getServiceNamed("uploader")
1329 furl, connected = uploader.get_helper_info()
1331 def data_connected_to_helper(self, ctx, data):
1333 uploader = IClient(ctx).getServiceNamed("uploader")
1335 return "no" # we don't even have an Uploader
1336 furl, connected = uploader.get_helper_info()
1341 def data_known_storage_servers(self, ctx, data):
1342 ic = IClient(ctx).introducer_client
1344 for c in ic.get_all_connectors().values()
1345 if c.service_name == "storage"]
1348 def data_connected_storage_servers(self, ctx, data):
1349 ic = IClient(ctx).introducer_client
1350 return len(ic.get_all_connections_for("storage"))
1352 def data_services(self, ctx, data):
1353 ic = IClient(ctx).introducer_client
1354 c = [ (service_name, nodeid, rsc)
1355 for (nodeid, service_name), rsc
1356 in ic.get_all_connectors().items() ]
1360 def render_service_row(self, ctx, data):
1361 (service_name, nodeid, rsc) = data
1362 ctx.fillSlots("peerid", "%s %s" % (idlib.nodeid_b2a(nodeid),
1365 rhost = rsc.remote_host
1366 if nodeid == IClient(ctx).nodeid:
1367 rhost_s = "(loopback)"
1368 elif isinstance(rhost, address.IPv4Address):
1369 rhost_s = "%s:%d" % (rhost.host, rhost.port)
1371 rhost_s = str(rhost)
1372 connected = "Yes: to " + rhost_s
1373 since = rsc.last_connect_time
1376 since = rsc.last_loss_time
1378 TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
1379 ctx.fillSlots("connected", connected)
1380 ctx.fillSlots("since", time.strftime(TIME_FORMAT, time.localtime(since)))
1381 ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
1382 time.localtime(rsc.announcement_time)))
1383 ctx.fillSlots("version", rsc.version)
1384 ctx.fillSlots("service_name", rsc.service_name)
1388 # this is a form where users can download files by URI
1389 def render_download_form(self, ctx, data):
1390 form = T.form(action="uri", method="get",
1391 enctype="multipart/form-data")[
1393 T.legend(class_="freeform-form-label")["download a file"],
1394 "URI of file to download: ",
1395 T.input(type="text", name="uri"), " ",
1396 "Filename to download as: ",
1397 T.input(type="text", name="filename"), " ",
1398 T.input(type="submit", value="download"),
1402 # this is a form where users can create new directories
1403 def render_mkdir_form(self, ctx, data):
1404 form = T.form(action="uri", method="post",
1405 enctype="multipart/form-data")[
1407 T.legend(class_="freeform-form-label")["create a directory"],
1408 T.input(type="hidden", name="t", value="mkdir"),
1409 T.input(type="hidden", name="redirect_to_result", value="true"),
1410 T.input(type="submit", value="create"),
1416 implements(ILocalAccess)
1418 self.local_access = False
1419 def local_access_is_allowed(self):
1420 return self.local_access
1422 class WebishServer(service.MultiService):
1425 def __init__(self, webport, nodeurl_path=None):
1426 service.MultiService.__init__(self)
1427 self.webport = webport
1429 self.site = site = appserver.NevowSite(self.root)
1430 self.site.requestFactory = MyRequest
1431 self.allow_local = LocalAccess()
1432 self.site.remember(self.allow_local, ILocalAccess)
1433 s = strports.service(webport, site)
1434 s.setServiceParent(self)
1435 self.listener = s # stash it so the tests can query for the portnum
1436 self._started = defer.Deferred()
1438 self._started.addCallback(self._write_nodeurl_file, nodeurl_path)
1440 def allow_local_access(self, enable=True):
1441 self.allow_local.local_access = enable
1443 def startService(self):
1444 service.MultiService.startService(self)
1445 # to make various services available to render_* methods, we stash a
1446 # reference to the client on the NevowSite. This will be available by
1447 # adapting the 'context' argument to a special marker interface named
1449 self.site.remember(self.parent, IClient)
1450 # I thought you could do the same with an existing interface, but
1451 # apparently 'ISite' does not exist
1452 #self.site._client = self.parent
1453 self._started.callback(None)
1455 def _write_nodeurl_file(self, junk, nodeurl_path):
1456 # what is our webport?
1458 if isinstance(s, internet.TCPServer):
1459 base_url = "http://localhost:%d" % s._port.getHost().port
1460 elif isinstance(s, internet.SSLServer):
1461 base_url = "https://localhost:%d" % s._port.getHost().port
1465 f = open(nodeurl_path, 'wb')
1466 # this file is world-readable
1467 f.write(base_url + "\n")