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 upload, download
16 from allmydata import provisioning
17 from zope.interface import implements, Interface
19 from formless import webform
22 return loaders.xmlfile(util.sibpath(__file__, "web/%s" % name))
24 class IClient(Interface):
26 class ILocalAccess(Interface):
27 def local_access_is_allowed():
28 """Return True if t=upload&localdir= is allowed, giving anyone who
29 can talk to the webserver control over the local (disk) filesystem."""
32 # we must override twisted.web.http.Request.requestReceived with a version
33 # that doesn't use cgi.parse_multipart() . Since we actually use Nevow, we
34 # override the nevow-specific subclass, nevow.appserver.NevowRequest . This
35 # is an exact copy of twisted.web.http.Request (from SVN HEAD on 10-Aug-2007)
36 # that modifies the way form arguments are parsed. Note that this sort of
37 # surgery may induce a dependency upon a particular version of twisted.web
39 parse_qs = http.parse_qs
40 class MyRequest(appserver.NevowRequest):
41 def requestReceived(self, command, path, version):
42 """Called by channel when all data has been received.
44 This method is not intended for users.
46 self.content.seek(0,0)
50 self.method, self.uri = command, path
51 self.clientproto = version
52 x = self.uri.split('?', 1)
57 self.path, argstring = x
58 self.args = parse_qs(argstring, 1)
60 # cache the client and server information, we'll need this later to be
61 # serialized and sent with the request so CGIs will work remotely
62 self.client = self.channel.transport.getPeer()
63 self.host = self.channel.transport.getHost()
65 # Argument processing.
67 ## The original twisted.web.http.Request.requestReceived code parsed the
68 ## content and added the form fields it found there to self.args . It
69 ## did this with cgi.parse_multipart, which holds the arguments in RAM
70 ## and is thus unsuitable for large file uploads. The Nevow subclass
71 ## (nevow.appserver.NevowRequest) uses cgi.FieldStorage instead (putting
72 ## the results in self.fields), which is much more memory-efficient.
73 ## Since we know we're using Nevow, we can anticipate these arguments
74 ## appearing in self.fields instead of self.args, and thus skip the
75 ## parse-content-into-self.args step.
78 ## ctype = self.getHeader('content-type')
79 ## if self.method == "POST" and ctype:
80 ## mfd = 'multipart/form-data'
81 ## key, pdict = cgi.parse_header(ctype)
82 ## if key == 'application/x-www-form-urlencoded':
83 ## args.update(parse_qs(self.content.read(), 1))
86 ## args.update(cgi.parse_multipart(self.content, pdict))
87 ## except KeyError, e:
88 ## if e.args[0] == 'content-disposition':
89 ## # Parse_multipart can't cope with missing
90 ## # content-dispostion headers in multipart/form-data
91 ## # parts, so we catch the exception and tell the client
92 ## # it was a bad request.
93 ## self.channel.transport.write(
94 ## "HTTP/1.1 400 Bad Request\r\n\r\n")
95 ## self.channel.transport.loseConnection()
101 class Directory(rend.Page):
103 docFactory = getxmlfile("directory.xhtml")
105 def __init__(self, rootname, dirnode, dirpath):
106 self._rootname = rootname
107 self._dirnode = dirnode
108 self._dirpath = dirpath
110 def dirpath_as_string(self):
111 return "/" + "/".join(self._dirpath)
113 def render_title(self, ctx, data):
114 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
116 def render_header(self, ctx, data):
117 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
118 num_dirs = len(parent_directories)
120 header = ["Directory '"]
121 for i,d in enumerate(parent_directories):
122 upness = num_dirs - i - 1
124 link = "/".join( ("..",) * upness )
127 header.append(T.a(href=link)[d])
132 if self._dirnode.is_readonly():
133 header.append(" (readonly)")
135 return ctx.tag[header]
137 def render_welcome(self, ctx, data):
138 depth = len(self._dirpath) + 2
139 link = "/".join([".."] * depth)
140 return T.div[T.a(href=link)["Return to Welcome page"]]
142 def data_children(self, ctx, data):
143 d = self._dirnode.list()
144 d.addCallback(lambda dict: sorted(dict.items()))
147 def render_row(self, ctx, data):
148 name, (target, metadata) = data
150 if self._dirnode.is_readonly():
154 # this creates a button which will cause our child__delete method
155 # to be invoked, which deletes the file and then redirects the
156 # browser back to this directory
157 delete = T.form(action=url.here, method="post")[
158 T.input(type='hidden', name='t', value='delete'),
159 T.input(type='hidden', name='name', value=name),
160 T.input(type='hidden', name='when_done', value=url.here),
161 T.input(type='submit', value='del', name="del"),
164 rename = T.form(action=url.here, method="get")[
165 T.input(type='hidden', name='t', value='rename-form'),
166 T.input(type='hidden', name='name', value=name),
167 T.input(type='hidden', name='when_done', value=url.here),
168 T.input(type='submit', value='rename', name="rename"),
171 ctx.fillSlots("delete", delete)
172 ctx.fillSlots("rename", rename)
173 check = T.form(action=url.here, method="post")[
174 T.input(type='hidden', name='t', value='check'),
175 T.input(type='hidden', name='name', value=name),
176 T.input(type='hidden', name='when_done', value=url.here),
177 T.input(type='submit', value='check', name="check"),
179 ctx.fillSlots("overwrite", self.build_overwrite(ctx, (name, target)))
180 ctx.fillSlots("check", check)
182 # build the base of the uri_link link url
183 uri_link = "/uri/" + urllib.quote(target.get_uri().replace("/", "!"))
185 assert (IFileNode.providedBy(target)
186 or IDirectoryNode.providedBy(target)
187 or IMutableFileNode.providedBy(target)), target
189 if IMutableFileNode.providedBy(target):
192 # add the filename to the uri_link url
193 uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
195 # to prevent javascript in displayed .html files from stealing a
196 # secret directory URI from the URL, send the browser to a URI-based
197 # page that doesn't know about the directory at all
198 #dlurl = urllib.quote(name)
201 ctx.fillSlots("filename",
202 T.a(href=dlurl)[html.escape(name)])
203 ctx.fillSlots("type", "SSK")
205 ctx.fillSlots("size", "?")
207 text_plain_link = uri_link + "?filename=foo.txt"
208 text_plain_tag = T.a(href=text_plain_link)["text/plain"]
210 elif IFileNode.providedBy(target):
213 # add the filename to the uri_link url
214 uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
216 # to prevent javascript in displayed .html files from stealing a
217 # secret directory URI from the URL, send the browser to a URI-based
218 # page that doesn't know about the directory at all
219 #dlurl = urllib.quote(name)
222 ctx.fillSlots("filename",
223 T.a(href=dlurl)[html.escape(name)])
224 ctx.fillSlots("type", "FILE")
226 ctx.fillSlots("size", target.get_size())
228 text_plain_link = uri_link + "?filename=foo.txt"
229 text_plain_tag = T.a(href=text_plain_link)["text/plain"]
231 elif IDirectoryNode.providedBy(target):
233 subdir_url = urllib.quote(name)
234 ctx.fillSlots("filename",
235 T.a(href=subdir_url)[html.escape(name)])
236 if target.is_readonly():
240 ctx.fillSlots("type", dirtype)
241 ctx.fillSlots("size", "-")
242 text_plain_tag = None
244 childdata = [T.a(href="%s?t=json" % name)["JSON"], ", ",
245 T.a(href="%s?t=uri" % name)["URI"], ", ",
246 T.a(href="%s?t=readonly-uri" % name)["readonly-URI"], ", ",
247 T.a(href=uri_link)["URI-link"],
250 childdata.extend([", ", text_plain_tag])
252 ctx.fillSlots("data", childdata)
255 checker = IClient(ctx).getServiceNamed("checker")
259 d = defer.maybeDeferred(checker.checker_results_for,
260 target.get_verifier())
261 def _got(checker_results):
262 recent_results = reversed(checker_results[-5:])
263 if IFileNode.providedBy(target):
265 ", ".join(["%d/%d" % (found, needed)
267 (needed, total, found, sharemap))
268 in recent_results]) +
270 elif IDirectoryNode.providedBy(target):
272 "".join([{True:"+",False:"-"}[res]
273 for (when, res) in recent_results]) +
276 results = "%d results" % len(checker_results)
282 # TODO: include a link to see more results, including timestamps
283 # TODO: use a sparkline
284 ctx.fillSlots("checker_results", results)
288 def render_forms(self, ctx, data):
289 if self._dirnode.is_readonly():
290 return T.div["No upload forms: directory is read-only"]
291 mkdir = T.form(action=".", method="post",
292 enctype="multipart/form-data")[
294 T.input(type="hidden", name="t", value="mkdir"),
295 T.input(type="hidden", name="when_done", value=url.here),
296 T.legend(class_="freeform-form-label")["Create a new directory"],
297 "New directory name: ",
298 T.input(type="text", name="name"), " ",
299 T.input(type="submit", value="Create"),
302 upload = T.form(action=".", method="post",
303 enctype="multipart/form-data")[
305 T.input(type="hidden", name="t", value="upload"),
306 T.input(type="hidden", name="when_done", value=url.here),
307 T.legend(class_="freeform-form-label")["Upload a file to this directory"],
308 "Choose a file to upload: ",
309 T.input(type="file", name="file", class_="freeform-input-file"),
311 T.input(type="submit", value="Upload"),
313 T.input(type="checkbox", name="mutable"),
316 mount = T.form(action=".", method="post",
317 enctype="multipart/form-data")[
319 T.input(type="hidden", name="t", value="uri"),
320 T.input(type="hidden", name="when_done", value=url.here),
321 T.legend(class_="freeform-form-label")["Attach a file or directory"
325 T.input(type="text", name="name"), " ",
326 "URI of new child: ",
327 T.input(type="text", name="uri"), " ",
328 T.input(type="submit", value="Attach"),
330 return [T.div(class_="freeform-form")[mkdir],
331 T.div(class_="freeform-form")[upload],
332 T.div(class_="freeform-form")[mount],
335 def build_overwrite(self, ctx, data):
337 if IMutableFileNode.providedBy(target) and not target.is_readonly():
338 overwrite = T.form(action=".", method="post",
339 enctype="multipart/form-data")[
341 T.input(type="hidden", name="t", value="overwrite"),
342 T.input(type='hidden', name='name', value=name),
343 T.input(type='hidden', name='when_done', value=url.here),
344 T.legend(class_="freeform-form-label")["Overwrite"],
346 T.input(type="file", name="file", class_="freeform-input-file"),
348 T.input(type="submit", value="Overwrite")
350 return [T.div(class_="freeform-form")[overwrite],]
354 def render_results(self, ctx, data):
355 req = inevow.IRequest(ctx)
356 if "results" in req.args:
357 return req.args["results"]
361 class WebDownloadTarget:
362 implements(IDownloadTarget, IConsumer)
363 def __init__(self, req, content_type, content_encoding):
365 self._content_type = content_type
366 self._content_encoding = content_encoding
368 self._producer = None
370 def registerProducer(self, producer, streaming):
371 self._req.registerProducer(producer, streaming)
372 def unregisterProducer(self):
373 self._req.unregisterProducer()
375 def open(self, size):
377 self._req.setHeader("content-type", self._content_type)
378 if self._content_encoding:
379 self._req.setHeader("content-encoding", self._content_encoding)
380 self._req.setHeader("content-length", str(size))
382 def write(self, data):
383 self._req.write(data)
389 # The content-type is already set, and the response code
390 # has already been sent, so we can't provide a clean error
391 # indication. We can emit text (which a browser might interpret
392 # as something else), and if we sent a Size header, they might
393 # notice that we've truncated the data. Keep the error message
394 # small to improve the chances of having our error response be
395 # shorter than the intended results.
397 # We don't have a lot of options, unfortunately.
398 self._req.write("problem during download\n")
400 # We haven't written anything yet, so we can provide a sensible
403 msg.replace("\n", "|")
404 self._req.setResponseCode(http.GONE, msg)
405 self._req.setHeader("content-type", "text/plain")
406 # TODO: HTML-formatted exception?
407 self._req.write(str(why))
410 def register_canceller(self, cb):
415 class FileDownloader(resource.Resource):
416 def __init__(self, filenode, name):
417 assert (IFileNode.providedBy(filenode)
418 or IMutableFileNode.providedBy(filenode))
419 self._filenode = filenode
422 def render(self, req):
423 gte = static.getTypeAndEncoding
424 type, encoding = gte(self._name,
425 static.File.contentTypes,
426 static.File.contentEncodings,
427 defaultType="text/plain")
429 d = self._filenode.download(WebDownloadTarget(req, type, encoding))
430 # exceptions during download are handled by the WebDownloadTarget
431 d.addErrback(lambda why: None)
432 return server.NOT_DONE_YET
434 class BlockingFileError(Exception):
435 """We cannot auto-create a parent directory, because there is a file in
437 class NoReplacementError(Exception):
438 """There was already a child by that name, and you asked me to not replace it"""
440 LOCALHOST = "127.0.0.1"
442 class NeedLocalhostError:
443 implements(inevow.IResource)
445 def renderHTTP(self, ctx):
446 req = inevow.IRequest(ctx)
447 req.setResponseCode(http.FORBIDDEN)
448 req.setHeader("content-type", "text/plain")
449 return "localfile= or localdir= requires a local connection"
451 class NeedAbsolutePathError:
452 implements(inevow.IResource)
454 def renderHTTP(self, ctx):
455 req = inevow.IRequest(ctx)
456 req.setResponseCode(http.FORBIDDEN)
457 req.setHeader("content-type", "text/plain")
458 return "localfile= or localdir= requires an absolute path"
460 class LocalAccessDisabledError:
461 implements(inevow.IResource)
463 def renderHTTP(self, ctx):
464 req = inevow.IRequest(ctx)
465 req.setResponseCode(http.FORBIDDEN)
466 req.setHeader("content-type", "text/plain")
467 return "local file access is disabled"
470 class LocalFileDownloader(resource.Resource):
471 def __init__(self, filenode, local_filename):
472 self._local_filename = local_filename
474 self._filenode = filenode
476 def render(self, req):
477 target = download.FileName(self._local_filename)
478 d = self._filenode.download(target)
480 req.write(self._filenode.get_uri())
483 return server.NOT_DONE_YET
486 class FileJSONMetadata(rend.Page):
487 def __init__(self, filenode):
488 self._filenode = filenode
490 def renderHTTP(self, ctx):
491 req = inevow.IRequest(ctx)
492 req.setHeader("content-type", "text/plain")
493 return self.renderNode(self._filenode)
495 def renderNode(self, filenode):
496 file_uri = filenode.get_uri()
499 'size': filenode.get_size(),
501 return simplejson.dumps(data, indent=1)
503 class FileURI(FileJSONMetadata):
504 def renderNode(self, filenode):
505 file_uri = filenode.get_uri()
508 class FileReadOnlyURI(FileJSONMetadata):
509 def renderNode(self, filenode):
510 if filenode.is_readonly():
511 return filenode.get_uri()
513 return filenode.get_readonly().get_uri()
515 class DirnodeWalkerMixin:
516 """Visit all nodes underneath (and including) the rootnode, one at a
517 time. For each one, call the visitor. The visitor will see the
518 IDirectoryNode before it sees any of the IFileNodes inside. If the
519 visitor returns a Deferred, I do not call the visitor again until it has
523 ## def _walk_if_we_could_use_generators(self, rootnode, rootpath=()):
524 ## # this is what we'd be doing if we didn't have the Deferreds and
525 ## # thus could use generators
526 ## yield rootpath, rootnode
527 ## for childname, childnode in rootnode.list().items():
528 ## childpath = rootpath + (childname,)
529 ## if IFileNode.providedBy(childnode):
530 ## yield childpath, childnode
531 ## elif IDirectoryNode.providedBy(childnode):
532 ## for res in self._walk_if_we_could_use_generators(childnode,
536 def walk(self, rootnode, visitor, rootpath=()):
538 def _listed(listing):
539 return listing.items()
540 d.addCallback(_listed)
541 d.addCallback(self._handle_items, visitor, rootpath)
544 def _handle_items(self, items, visitor, rootpath):
547 childname, (childnode, metadata) = items[0]
548 childpath = rootpath + (childname,)
549 d = defer.maybeDeferred(visitor, childpath, childnode, metadata)
550 if IDirectoryNode.providedBy(childnode):
551 d.addCallback(lambda res: self.walk(childnode, visitor, childpath))
552 d.addCallback(lambda res:
553 self._handle_items(items[1:], visitor, rootpath))
556 class LocalDirectoryDownloader(resource.Resource, DirnodeWalkerMixin):
557 def __init__(self, dirnode, localdir):
558 self._dirnode = dirnode
559 self._localdir = localdir
561 def _handle(self, path, node, metadata):
562 localfile = os.path.join(self._localdir, os.sep.join(path))
563 if IDirectoryNode.providedBy(node):
564 fileutil.make_dirs(localfile)
565 elif IFileNode.providedBy(node):
566 target = download.FileName(localfile)
567 return node.download(target)
569 def render(self, req):
570 d = self.walk(self._dirnode, self._handle)
572 req.setHeader("content-type", "text/plain")
573 return "operation complete"
577 class DirectoryJSONMetadata(rend.Page):
578 def __init__(self, dirnode):
579 self._dirnode = dirnode
581 def renderHTTP(self, ctx):
582 req = inevow.IRequest(ctx)
583 req.setHeader("content-type", "text/plain")
584 return self.renderNode(self._dirnode)
586 def renderNode(self, node):
590 for name, (childnode, metadata) in children.iteritems():
591 if IFileNode.providedBy(childnode):
592 kiduri = childnode.get_uri()
593 kiddata = ("filenode",
595 'size': childnode.get_size(),
598 assert IDirectoryNode.providedBy(childnode), (childnode, children,)
599 kiddata = ("dirnode",
600 {'ro_uri': childnode.get_readonly_uri(),
602 if not childnode.is_readonly():
603 kiddata[1]['rw_uri'] = childnode.get_uri()
605 contents = { 'children': kids,
606 'ro_uri': node.get_readonly_uri(),
608 if not node.is_readonly():
609 contents['rw_uri'] = node.get_uri()
610 data = ("dirnode", contents)
611 return simplejson.dumps(data, indent=1)
615 class DirectoryURI(DirectoryJSONMetadata):
616 def renderNode(self, node):
617 return node.get_uri()
619 class DirectoryReadonlyURI(DirectoryJSONMetadata):
620 def renderNode(self, node):
621 return node.get_readonly_uri()
623 class RenameForm(rend.Page):
625 docFactory = getxmlfile("rename-form.xhtml")
627 def __init__(self, rootname, dirnode, dirpath):
628 self._rootname = rootname
629 self._dirnode = dirnode
630 self._dirpath = dirpath
632 def dirpath_as_string(self):
633 return "/" + "/".join(self._dirpath)
635 def render_title(self, ctx, data):
636 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
638 def render_header(self, ctx, data):
639 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
640 num_dirs = len(parent_directories)
642 header = [ "Rename in directory '",
643 "<%s>/" % self._rootname,
644 "/".join(self._dirpath),
647 if self._dirnode.is_readonly():
648 header.append(" (readonly)")
649 return ctx.tag[header]
651 def render_when_done(self, ctx, data):
652 return T.input(type="hidden", name="when_done", value=url.here)
654 def render_get_name(self, ctx, data):
655 req = inevow.IRequest(ctx)
656 if 'name' in req.args:
657 name = req.args['name'][0]
660 ctx.tag.attributes['value'] = name
663 class POSTHandler(rend.Page):
664 def __init__(self, node, replace):
666 self._replace = replace
668 def _check_replacement(self, name):
670 return defer.succeed(None)
671 d = self._node.has_child(name)
674 raise NoReplacementError("There was already a child by that "
675 "name, and you asked me to not "
681 def renderHTTP(self, ctx):
682 req = inevow.IRequest(ctx)
687 t = req.fields["t"].value
690 if "name" in req.args:
691 name = req.args["name"][0]
692 elif "name" in req.fields:
693 name = req.fields["name"].value
694 if name and "/" in name:
695 req.setResponseCode(http.BAD_REQUEST)
696 req.setHeader("content-type", "text/plain")
697 return "name= may not contain a slash"
700 # we allow the user to delete an empty-named file, but not to create
701 # them, since that's an easy and confusing mistake to make
704 if "when_done" in req.args:
705 when_done = req.args["when_done"][0]
706 if "when_done" in req.fields:
707 when_done = req.fields["when_done"].value
709 if "replace" in req.fields:
710 if req.fields["replace"].value.lower() in ("false", "0"):
711 self._replace = False
715 raise RuntimeError("mkdir requires a name")
716 d = self._check_replacement(name)
717 d.addCallback(lambda res: self._node.create_empty_directory(name))
719 return "directory created"
723 raise RuntimeError("set-uri requires a name")
724 if "uri" in req.args:
725 newuri = req.args["uri"][0].strip()
727 newuri = req.fields["uri"].value.strip()
728 d = self._check_replacement(name)
729 d.addCallback(lambda res: self._node.set_uri(name, newuri))
735 # apparently an <input type="hidden" name="name" value="">
736 # won't show up in the resulting encoded form.. the 'name'
737 # field is completely missing. So to allow deletion of an
738 # empty file, we have to pretend that None means ''. The only
739 # downide of this is a slightly confusing error message if
740 # someone does a POST without a name= field. For our own HTML
741 # thisn't a big deal, because we create the 'delete' POST
744 d = self._node.delete(name)
746 return "thing deleted"
749 from_name = 'from_name' in req.fields and req.fields["from_name"].value
750 if from_name is not None:
751 from_name = from_name.strip()
752 to_name = 'to_name' in req.fields and req.fields["to_name"].value
753 if to_name is not None:
754 to_name = to_name.strip()
755 if not from_name or not to_name:
756 raise RuntimeError("rename requires from_name and to_name")
757 if not IDirectoryNode.providedBy(self._node):
758 raise RuntimeError("rename must only be called on directories")
759 for k,v in [ ('from_name', from_name), ('to_name', to_name) ]:
761 req.setResponseCode(http.BAD_REQUEST)
762 req.setHeader("content-type", "text/plain")
763 return "%s= may not contain a slash" % (k,)
764 d = self._check_replacement(to_name)
765 d.addCallback(lambda res: self._node.get(from_name))
767 uri = child.get_uri()
768 # now actually do the rename
769 return self._node.set_uri(to_name, uri)
770 d.addCallback(add_dest)
772 return self._node.delete(from_name)
773 d.addCallback(rm_src)
775 return "thing renamed"
779 if "mutable" in req.fields:
780 contents = req.fields["file"]
781 name = name or contents.filename
785 raise RuntimeError("upload-mutable requires a name")
786 # SDMF: files are small, and we can only upload data.
787 contents.file.seek(0)
788 data = contents.file.read()
789 uploadable = upload.FileHandle(contents.file)
790 d = self._check_replacement(name)
791 d.addCallback(lambda res: self._node.has_child(name))
792 def _checked(present):
794 # modify the existing one instead of creating a new
796 d2 = self._node.get(name)
797 def _got_newnode(newnode):
798 d3 = newnode.replace(data)
799 d3.addCallback(lambda res: newnode.get_uri())
801 d2.addCallback(_got_newnode)
803 d2 = IClient(ctx).create_mutable_file(data)
804 def _uploaded(newnode):
805 d1 = self._node.set_node(name, newnode)
806 d1.addCallback(lambda res: newnode.get_uri())
808 d2.addCallback(_uploaded)
810 d.addCallback(_checked)
812 contents = req.fields["file"]
813 name = name or contents.filename
817 raise RuntimeError("upload requires a name")
818 uploadable = upload.FileHandle(contents.file)
819 d = self._check_replacement(name)
820 d.addCallback(lambda res: self._node.add_file(name, uploadable))
822 return newnode.get_uri()
825 elif t == "overwrite":
826 contents = req.fields["file"]
827 # SDMF: files are small, and we can only upload data.
828 contents.file.seek(0)
829 data = contents.file.read()
830 # TODO: 'name' handling needs review
831 d = defer.succeed(self._node)
832 def _got_child(child_node):
833 child_node.replace(data)
834 return child_node.get_uri()
835 d.addCallback(_got_child)
838 d = self._node.get(name)
839 def _got_child(child_node):
840 d2 = child_node.check()
842 log.msg("checked %s, results %s" % (child_node, res))
844 d2.addCallback(_done)
846 d.addCallback(_got_child)
849 return "BAD t=%s" % t
851 d.addCallback(lambda res: url.URL.fromString(when_done))
852 def _check_replacement(f):
853 # TODO: make this more human-friendly: maybe send them to the
854 # when_done page but with an extra query-arg that will display
855 # the error message in a big box at the top of the page. The
856 # directory page that when_done= usually points to accepts a
857 # result= argument.. use that.
858 f.trap(NoReplacementError)
859 req.setResponseCode(http.CONFLICT)
860 req.setHeader("content-type", "text/plain")
862 d.addErrback(_check_replacement)
865 class DELETEHandler(rend.Page):
866 def __init__(self, node, name):
870 def renderHTTP(self, ctx):
871 req = inevow.IRequest(ctx)
872 d = self._node.delete(self._name)
874 # what should this return??
875 return "%s deleted" % self._name
877 def _trap_missing(f):
879 req.setResponseCode(http.NOT_FOUND)
880 req.setHeader("content-type", "text/plain")
881 return "no such child %s" % self._name
882 d.addErrback(_trap_missing)
885 class PUTHandler(rend.Page):
886 def __init__(self, node, path, t, localfile, localdir, replace):
890 self._localfile = localfile
891 self._localdir = localdir
892 self._replace = replace
894 def renderHTTP(self, ctx):
895 req = inevow.IRequest(ctx)
897 localfile = self._localfile
898 localdir = self._localdir
900 # we must traverse the path, creating new directories as necessary
901 d = self._get_or_create_directories(self._node, self._path[:-1])
902 name = self._path[-1]
903 d.addCallback(self._check_replacement, name, self._replace)
906 d.addCallback(self._upload_localfile, localfile, name)
909 d.addCallback(self._get_or_create_directories, self._path[-1:])
910 d.addCallback(self._upload_localdir, localdir)
912 raise RuntimeError("t=upload requires localfile= or localdir=")
914 d.addCallback(self._attach_uri, req.content, name)
916 d.addCallback(self._mkdir, name)
918 d.addCallback(self._upload_file, req.content, name)
919 def _check_blocking(f):
920 f.trap(BlockingFileError)
921 req.setResponseCode(http.BAD_REQUEST)
922 req.setHeader("content-type", "text/plain")
924 d.addErrback(_check_blocking)
925 def _check_replacement(f):
926 f.trap(NoReplacementError)
927 req.setResponseCode(http.CONFLICT)
928 req.setHeader("content-type", "text/plain")
930 d.addErrback(_check_replacement)
933 def _get_or_create_directories(self, node, path):
934 if not IDirectoryNode.providedBy(node):
935 # unfortunately it is too late to provide the name of the
936 # blocking directory in the error message.
937 raise BlockingFileError("cannot create directory because there "
938 "is a file in the way")
940 return defer.succeed(node)
941 d = node.get(path[0])
942 def _maybe_create(f):
944 return node.create_empty_directory(path[0])
945 d.addErrback(_maybe_create)
946 d.addCallback(self._get_or_create_directories, path[1:])
949 def _check_replacement(self, node, name, replace):
952 d = node.has_child(name)
955 raise NoReplacementError("There was already a child by that "
956 "name, and you asked me to not "
962 def _mkdir(self, node, name):
963 d = node.create_empty_directory(name)
965 return newnode.get_uri()
969 def _upload_file(self, node, contents, name):
970 uploadable = upload.FileHandle(contents)
971 d = node.add_file(name, uploadable)
973 log.msg("webish upload complete")
974 return filenode.get_uri()
978 def _upload_localfile(self, node, localfile, name):
979 uploadable = upload.FileName(localfile)
980 d = node.add_file(name, uploadable)
981 d.addCallback(lambda filenode: filenode.get_uri())
984 def _attach_uri(self, parentnode, contents, name):
985 newuri = contents.read().strip()
986 d = parentnode.set_uri(name, newuri)
992 def _upload_localdir(self, node, localdir):
993 # build up a list of files to upload
996 msg = "No files to upload! %s is empty" % localdir
997 if not os.path.exists(localdir):
998 msg = "%s doesn't exist!" % localdir
999 for root, dirs, files in os.walk(localdir):
1000 if root == localdir:
1003 relative_root = root[len(localdir)+1:]
1004 path = tuple(relative_root.split(os.sep))
1006 all_dirs.append(path + (d,))
1008 all_files.append(path + (f,))
1009 d = defer.succeed(msg)
1010 for dir in all_dirs:
1012 d.addCallback(self._makedir, node, dir)
1014 d.addCallback(self._upload_one_file, node, localdir, f)
1017 def _makedir(self, res, node, dir):
1018 d = defer.succeed(None)
1019 # get the parent. As long as os.walk gives us parents before
1020 # children, this ought to work
1021 d.addCallback(lambda res: node.get_child_at_path(dir[:-1]))
1022 # then create the child directory
1023 d.addCallback(lambda parent: parent.create_empty_directory(dir[-1]))
1026 def _upload_one_file(self, res, node, localdir, f):
1027 # get the parent. We can be sure this exists because we already
1028 # went through and created all the directories we require.
1029 localfile = os.path.join(localdir, *f)
1030 d = node.get_child_at_path(f[:-1])
1031 d.addCallback(self._upload_localfile, localfile, f[-1])
1035 class Manifest(rend.Page):
1036 docFactory = getxmlfile("manifest.xhtml")
1037 def __init__(self, dirnode, dirpath):
1038 self._dirnode = dirnode
1039 self._dirpath = dirpath
1041 def dirpath_as_string(self):
1042 return "/" + "/".join(self._dirpath)
1044 def render_title(self, ctx):
1045 return T.title["Manifest of %s" % self.dirpath_as_string()]
1047 def render_header(self, ctx):
1048 return T.p["Manifest of %s" % self.dirpath_as_string()]
1050 def data_items(self, ctx, data):
1051 return self._dirnode.build_manifest()
1053 def render_row(self, ctx, refresh_cap):
1054 ctx.fillSlots("refresh_capability", refresh_cap)
1057 class VDrive(rend.Page):
1059 def __init__(self, node, name):
1063 def get_child_at_path(self, path):
1065 return self.node.get_child_at_path(path)
1066 return defer.succeed(self.node)
1068 def locateChild(self, ctx, segments):
1069 req = inevow.IRequest(ctx)
1073 # when we're pointing at a directory (like /uri/$DIR_URI/my_pix),
1074 # Directory.addSlash causes a redirect to /uri/$DIR_URI/my_pix/,
1075 # which appears here as ['my_pix', '']. This is supposed to hit the
1076 # same Directory as ['my_pix'].
1077 if path and path[-1] == '':
1082 t = req.args["t"][0]
1085 if "localfile" in req.args:
1086 localfile = req.args["localfile"][0]
1087 if localfile != os.path.abspath(localfile):
1088 return NeedAbsolutePathError(), ()
1090 if "localdir" in req.args:
1091 localdir = req.args["localdir"][0]
1092 if localdir != os.path.abspath(localdir):
1093 return NeedAbsolutePathError(), ()
1094 if localfile or localdir:
1095 if not ILocalAccess(ctx).local_access_is_allowed():
1096 return LocalAccessDisabledError(), ()
1097 if req.getHost().host != LOCALHOST:
1098 return NeedLocalhostError(), ()
1099 # TODO: think about clobbering/revealing config files and node secrets
1102 if "replace" in req.args:
1103 if req.args["replace"][0].lower() in ("false", "0"):
1107 # the node must exist, and our operation will be performed on the
1109 d = self.get_child_at_path(path)
1110 def file_or_dir(node):
1111 if (IFileNode.providedBy(node)
1112 or IMutableFileNode.providedBy(node)):
1113 filename = "unknown"
1116 if "filename" in req.args:
1117 filename = req.args["filename"][0]
1120 # write contents to a local file
1121 return LocalFileDownloader(node, localfile), ()
1122 # send contents as the result
1123 return FileDownloader(node, filename), ()
1125 # send contents as the result
1126 return FileDownloader(node, filename), ()
1128 return FileJSONMetadata(node), ()
1130 return FileURI(node), ()
1131 elif t == "readonly-uri":
1132 return FileReadOnlyURI(node), ()
1134 raise RuntimeError("bad t=%s" % t)
1135 elif IDirectoryNode.providedBy(node):
1138 # recursive download to a local directory
1139 return LocalDirectoryDownloader(node, localdir), ()
1140 raise RuntimeError("t=download requires localdir=")
1142 # send an HTML representation of the directory
1143 return Directory(self.name, node, path), ()
1145 return DirectoryJSONMetadata(node), ()
1147 return DirectoryURI(node), ()
1148 elif t == "readonly-uri":
1149 return DirectoryReadonlyURI(node), ()
1150 elif t == "manifest":
1151 return Manifest(node, path), ()
1152 elif t == 'rename-form':
1153 return RenameForm(self.name, node, path), ()
1155 raise RuntimeError("bad t=%s" % t)
1157 raise RuntimeError("unknown node type")
1158 d.addCallback(file_or_dir)
1159 elif method == "POST":
1160 # the node must exist, and our operation will be performed on the
1162 d = self.get_child_at_path(path)
1164 return POSTHandler(node, replace), ()
1166 elif method == "DELETE":
1167 # the node must exist, and our operation will be performed on its
1169 assert path # you can't delete the root
1170 d = self.get_child_at_path(path[:-1])
1172 return DELETEHandler(node, path[-1]), ()
1174 elif method in ("PUT",):
1175 # the node may or may not exist, and our operation may involve
1176 # all the ancestors of the node.
1177 return PUTHandler(self.node, path, t, localfile, localdir, replace), ()
1179 return rend.NotFound
1180 def _trap_KeyError(f):
1182 return rend.FourOhFour(), ()
1183 d.addErrback(_trap_KeyError)
1186 class URIPUTHandler(rend.Page):
1187 def renderHTTP(self, ctx):
1188 req = inevow.IRequest(ctx)
1189 assert req.method == "PUT"
1193 t = req.args["t"][0]
1196 # "PUT /uri", to create an unlinked file. This is like PUT but
1197 # without the associated set_uri.
1198 uploadable = upload.FileHandle(req.content)
1199 d = IClient(ctx).upload(uploadable)
1200 # that fires with the URI of the new file
1204 # "PUT /uri?t=mkdir", to create an unlinked directory.
1205 d = IClient(ctx).create_empty_dirnode()
1206 d.addCallback(lambda dirnode: dirnode.get_uri())
1209 req.setResponseCode(http.BAD_REQUEST)
1210 req.setHeader("content-type", "text/plain")
1211 return "/uri only accepts PUT and PUT?t=mkdir"
1214 class Root(rend.Page):
1217 docFactory = getxmlfile("welcome.xhtml")
1219 def locateChild(self, ctx, segments):
1220 client = IClient(ctx)
1221 req = inevow.IRequest(ctx)
1223 if segments[0] == "uri":
1224 if len(segments) == 1 or segments[1] == '':
1225 if "uri" in req.args:
1226 uri = req.args["uri"][0].replace("/", "!")
1227 there = url.URL.fromContext(ctx)
1228 there = there.clear("uri")
1229 there = there.child("uri").child(uri)
1231 if len(segments) == 1 and req.method == "PUT":
1233 # either "PUT /uri" to create an unlinked file, or
1234 # "PUT /uri?t=mkdir" to create an unlinked directory
1235 return URIPUTHandler(), ()
1236 if len(segments) < 2:
1237 return rend.NotFound
1238 uri = segments[1].replace("!", "/")
1239 d = defer.maybeDeferred(client.create_node_from_uri, uri)
1240 d.addCallback(lambda node: VDrive(node, "from-uri"))
1241 d.addCallback(lambda vd: vd.locateChild(ctx, segments[2:]))
1242 def _trap_KeyError(f):
1244 return rend.FourOhFour(), ()
1245 d.addErrback(_trap_KeyError)
1247 elif segments[0] == "xmlrpc":
1248 raise NotImplementedError()
1249 return rend.Page.locateChild(self, ctx, segments)
1251 child_webform_css = webform.defaultCSS
1252 child_tahoe_css = nevow_File(util.sibpath(__file__, "web/tahoe.css"))
1254 child_provisioning = provisioning.ProvisioningTool()
1256 def data_version(self, ctx, data):
1257 v = IClient(ctx).get_versions()
1258 return "tahoe: %s, zfec: %s, foolscap: %s, twisted: %s" % \
1259 (v['allmydata'], v['zfec'], v['foolscap'], v['twisted'])
1261 def data_my_nodeid(self, ctx, data):
1262 return b32encode(IClient(ctx).nodeid).lower()
1263 def data_introducer_furl(self, ctx, data):
1264 return IClient(ctx).introducer_furl
1265 def data_connected_to_introducer(self, ctx, data):
1266 if IClient(ctx).connected_to_introducer():
1269 def data_num_peers(self, ctx, data):
1270 #client = inevow.ISite(ctx)._client
1271 client = IClient(ctx)
1272 return len(list(client.get_all_peerids()))
1274 def data_peers(self, ctx, data):
1276 client = IClient(ctx)
1277 for nodeid in sorted(client.get_all_peerids()):
1278 row = (b32encode(nodeid).lower(),)
1282 def render_row(self, ctx, data):
1284 ctx.fillSlots("peerid", nodeid_a)
1287 def render_private_vdrive(self, ctx, data):
1288 basedir = IClient(ctx).basedir
1289 start_html = os.path.abspath(os.path.join(basedir, "start.html"))
1290 if os.path.exists(start_html):
1291 return T.p["To view your personal private non-shared filestore, ",
1292 "use this browser to open the following file from ",
1293 "your local filesystem:",
1296 return T.p["personal vdrive not available."]
1298 # this is a form where users can download files by URI
1300 def render_download_form(self, ctx, data):
1301 form = T.form(action="uri", method="get",
1302 enctype="multipart/form-data")[
1304 T.legend(class_="freeform-form-label")["Download a file"],
1305 "URI of file to download: ",
1306 T.input(type="text", name="uri"), " ",
1307 "Filename to download as: ",
1308 T.input(type="text", name="filename"), " ",
1309 T.input(type="submit", value="Download"),
1315 implements(ILocalAccess)
1317 self.local_access = False
1318 def local_access_is_allowed(self):
1319 return self.local_access
1321 class WebishServer(service.MultiService):
1324 def __init__(self, webport):
1325 service.MultiService.__init__(self)
1326 self.webport = webport
1328 self.site = site = appserver.NevowSite(self.root)
1329 self.site.requestFactory = MyRequest
1330 self.allow_local = LocalAccess()
1331 self.site.remember(self.allow_local, ILocalAccess)
1332 s = strports.service(webport, site)
1333 s.setServiceParent(self)
1334 self.listener = s # stash it so the tests can query for the portnum
1335 self._started = defer.Deferred()
1337 def allow_local_access(self, enable=True):
1338 self.allow_local.local_access = enable
1340 def startService(self):
1341 service.MultiService.startService(self)
1342 # to make various services available to render_* methods, we stash a
1343 # reference to the client on the NevowSite. This will be available by
1344 # adapting the 'context' argument to a special marker interface named
1346 self.site.remember(self.parent, IClient)
1347 # I thought you could do the same with an existing interface, but
1348 # apparently 'ISite' does not exist
1349 #self.site._client = self.parent
1350 self._started.callback(None)
1352 def create_start_html(self, private_uri, startfile, nodeurl_file):
1354 Returns a deferred that eventually fires once the start.html page has
1357 self._started.addCallback(self._create_start_html, private_uri, startfile, nodeurl_file)
1358 return self._started
1360 def _create_start_html(self, dummy, private_uri, startfile, nodeurl_file):
1361 f = open(startfile, "w")
1362 os.chmod(startfile, 0600)
1363 template = open(util.sibpath(__file__, "web/start.html"), "r").read()
1364 # what is our webport?
1366 if isinstance(s, internet.TCPServer):
1367 base_url = "http://localhost:%d" % s._port.getHost().port
1368 elif isinstance(s, internet.SSLServer):
1369 base_url = "https://localhost:%d" % s._port.getHost().port
1371 base_url = "UNKNOWN" # this will break the href
1372 # TODO: emit a start.html that explains that we don't know
1373 # how to create a suitable URL
1375 private_uri = private_uri.replace("/","!")
1376 link_to_private_uri = "View <a href=\"%s/uri/%s\">your personal private non-shared filestore</a>." % (base_url, private_uri)
1377 fields = {"link_to_private_uri": link_to_private_uri,
1378 "base_url": base_url,
1381 fields = {"link_to_private_uri": "",
1382 "base_url": base_url,
1384 f.write(template % fields)
1387 f = open(nodeurl_file, "w")
1388 # this file is world-readable
1389 f.write(base_url + "\n")