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("check", check)
181 # build the base of the uri_link link url
182 uri_link = "/uri/" + urllib.quote(target.get_uri().replace("/", "!"))
184 assert (IFileNode.providedBy(target)
185 or IDirectoryNode.providedBy(target)
186 or IMutableFileNode.providedBy(target)), target
188 if IMutableFileNode.providedBy(target):
191 # add the filename to the uri_link url
192 uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
194 # to prevent javascript in displayed .html files from stealing a
195 # secret directory URI from the URL, send the browser to a URI-based
196 # page that doesn't know about the directory at all
197 #dlurl = urllib.quote(name)
200 ctx.fillSlots("filename",
201 T.a(href=dlurl)[html.escape(name)])
202 ctx.fillSlots("type", "SSK")
204 ctx.fillSlots("size", "?")
206 text_plain_link = uri_link + "?filename=foo.txt"
207 text_plain_tag = T.a(href=text_plain_link)["text/plain"]
209 elif IFileNode.providedBy(target):
212 # add the filename to the uri_link url
213 uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
215 # to prevent javascript in displayed .html files from stealing a
216 # secret directory URI from the URL, send the browser to a URI-based
217 # page that doesn't know about the directory at all
218 #dlurl = urllib.quote(name)
221 ctx.fillSlots("filename",
222 T.a(href=dlurl)[html.escape(name)])
223 ctx.fillSlots("type", "FILE")
225 ctx.fillSlots("size", target.get_size())
227 text_plain_link = uri_link + "?filename=foo.txt"
228 text_plain_tag = T.a(href=text_plain_link)["text/plain"]
230 elif IDirectoryNode.providedBy(target):
232 subdir_url = urllib.quote(name)
233 ctx.fillSlots("filename",
234 T.a(href=subdir_url)[html.escape(name)])
235 if target.is_readonly():
239 ctx.fillSlots("type", dirtype)
240 ctx.fillSlots("size", "-")
241 text_plain_tag = None
243 childdata = [T.a(href="%s?t=json" % name)["JSON"], ", ",
244 T.a(href="%s?t=uri" % name)["URI"], ", ",
245 T.a(href="%s?t=readonly-uri" % name)["readonly-URI"], ", ",
246 T.a(href=uri_link)["URI-link"],
249 childdata.extend([", ", text_plain_tag])
251 ctx.fillSlots("data", childdata)
254 checker = IClient(ctx).getServiceNamed("checker")
258 d = defer.maybeDeferred(checker.checker_results_for,
259 target.get_verifier())
260 def _got(checker_results):
261 recent_results = reversed(checker_results[-5:])
262 if IFileNode.providedBy(target):
264 ", ".join(["%d/%d" % (found, needed)
266 (needed, total, found, sharemap))
267 in recent_results]) +
269 elif IDirectoryNode.providedBy(target):
271 "".join([{True:"+",False:"-"}[res]
272 for (when, res) in recent_results]) +
275 results = "%d results" % len(checker_results)
281 # TODO: include a link to see more results, including timestamps
282 # TODO: use a sparkline
283 ctx.fillSlots("checker_results", results)
287 def render_forms(self, ctx, data):
288 if self._dirnode.is_readonly():
289 return T.div["No upload forms: directory is read-only"]
290 mkdir = T.form(action=".", method="post",
291 enctype="multipart/form-data")[
293 T.input(type="hidden", name="t", value="mkdir"),
294 T.input(type="hidden", name="when_done", value=url.here),
295 T.legend(class_="freeform-form-label")["Create a new directory"],
296 "New directory name: ",
297 T.input(type="text", name="name"), " ",
298 T.input(type="submit", value="Create"),
301 upload = T.form(action=".", method="post",
302 enctype="multipart/form-data")[
304 T.input(type="hidden", name="t", value="upload"),
305 T.input(type="hidden", name="when_done", value=url.here),
306 T.legend(class_="freeform-form-label")["Upload a file to this directory"],
307 "Choose a file to upload: ",
308 T.input(type="file", name="file", class_="freeform-input-file"),
310 T.input(type="submit", value="Upload"),
312 T.input(type="checkbox", name="mutable"),
315 mount = T.form(action=".", method="post",
316 enctype="multipart/form-data")[
318 T.input(type="hidden", name="t", value="uri"),
319 T.input(type="hidden", name="when_done", value=url.here),
320 T.legend(class_="freeform-form-label")["Attach a file or directory"
324 T.input(type="text", name="name"), " ",
325 "URI of new child: ",
326 T.input(type="text", name="uri"), " ",
327 T.input(type="submit", value="Attach"),
329 return [T.div(class_="freeform-form")[mkdir],
330 T.div(class_="freeform-form")[upload],
331 T.div(class_="freeform-form")[mount],
334 def render_overwrite(self, ctx, data):
336 if IMutableFileNode.providedBy(target) and not target.is_readonly():
337 overwrite = T.form(action=".", method="post",
338 enctype="multipart/form-data")[
340 T.input(type="hidden", name="t", value="overwrite"),
341 T.input(type='hidden', name='name', value=name),
342 T.input(type='hidden', name='when_done', value=url.here),
343 T.legend(class_="freeform-form-label")["Overwrite"],
345 T.input(type="file", name="file", class_="freeform-input-file"),
347 T.input(type="submit", value="Overwrite")
349 return [T.div(class_="freeform-form")[overwrite],]
353 def render_results(self, ctx, data):
354 req = inevow.IRequest(ctx)
355 if "results" in req.args:
356 return req.args["results"]
360 class WebDownloadTarget:
361 implements(IDownloadTarget, IConsumer)
362 def __init__(self, req, content_type, content_encoding):
364 self._content_type = content_type
365 self._content_encoding = content_encoding
367 self._producer = None
369 def registerProducer(self, producer, streaming):
370 self._req.registerProducer(producer, streaming)
371 def unregisterProducer(self):
372 self._req.unregisterProducer()
374 def open(self, size):
376 self._req.setHeader("content-type", self._content_type)
377 if self._content_encoding:
378 self._req.setHeader("content-encoding", self._content_encoding)
379 self._req.setHeader("content-length", str(size))
381 def write(self, data):
382 self._req.write(data)
388 # The content-type is already set, and the response code
389 # has already been sent, so we can't provide a clean error
390 # indication. We can emit text (which a browser might interpret
391 # as something else), and if we sent a Size header, they might
392 # notice that we've truncated the data. Keep the error message
393 # small to improve the chances of having our error response be
394 # shorter than the intended results.
396 # We don't have a lot of options, unfortunately.
397 self._req.write("problem during download\n")
399 # We haven't written anything yet, so we can provide a sensible
402 msg.replace("\n", "|")
403 self._req.setResponseCode(http.GONE, msg)
404 self._req.setHeader("content-type", "text/plain")
405 # TODO: HTML-formatted exception?
406 self._req.write(str(why))
409 def register_canceller(self, cb):
414 class FileDownloader(resource.Resource):
415 def __init__(self, filenode, name):
416 assert (IFileNode.providedBy(filenode)
417 or IMutableFileNode.providedBy(filenode))
418 self._filenode = filenode
421 def render(self, req):
422 gte = static.getTypeAndEncoding
423 type, encoding = gte(self._name,
424 static.File.contentTypes,
425 static.File.contentEncodings,
426 defaultType="text/plain")
428 d = self._filenode.download(WebDownloadTarget(req, type, encoding))
429 # exceptions during download are handled by the WebDownloadTarget
430 d.addErrback(lambda why: None)
431 return server.NOT_DONE_YET
433 class BlockingFileError(Exception):
434 """We cannot auto-create a parent directory, because there is a file in
436 class NoReplacementError(Exception):
437 """There was already a child by that name, and you asked me to not replace it"""
439 LOCALHOST = "127.0.0.1"
441 class NeedLocalhostError:
442 implements(inevow.IResource)
444 def renderHTTP(self, ctx):
445 req = inevow.IRequest(ctx)
446 req.setResponseCode(http.FORBIDDEN)
447 req.setHeader("content-type", "text/plain")
448 return "localfile= or localdir= requires a local connection"
450 class NeedAbsolutePathError:
451 implements(inevow.IResource)
453 def renderHTTP(self, ctx):
454 req = inevow.IRequest(ctx)
455 req.setResponseCode(http.FORBIDDEN)
456 req.setHeader("content-type", "text/plain")
457 return "localfile= or localdir= requires an absolute path"
459 class LocalAccessDisabledError:
460 implements(inevow.IResource)
462 def renderHTTP(self, ctx):
463 req = inevow.IRequest(ctx)
464 req.setResponseCode(http.FORBIDDEN)
465 req.setHeader("content-type", "text/plain")
466 return "local file access is disabled"
469 class LocalFileDownloader(resource.Resource):
470 def __init__(self, filenode, local_filename):
471 self._local_filename = local_filename
473 self._filenode = filenode
475 def render(self, req):
476 target = download.FileName(self._local_filename)
477 d = self._filenode.download(target)
479 req.write(self._filenode.get_uri())
482 return server.NOT_DONE_YET
485 class FileJSONMetadata(rend.Page):
486 def __init__(self, filenode):
487 self._filenode = filenode
489 def renderHTTP(self, ctx):
490 req = inevow.IRequest(ctx)
491 req.setHeader("content-type", "text/plain")
492 return self.renderNode(self._filenode)
494 def renderNode(self, filenode):
495 file_uri = filenode.get_uri()
498 'size': filenode.get_size(),
500 return simplejson.dumps(data, indent=1)
502 class FileURI(FileJSONMetadata):
503 def renderNode(self, filenode):
504 file_uri = filenode.get_uri()
507 class FileReadOnlyURI(FileJSONMetadata):
508 def renderNode(self, filenode):
509 if filenode.is_readonly():
510 return filenode.get_uri()
512 return filenode.get_readonly().get_uri()
514 class DirnodeWalkerMixin:
515 """Visit all nodes underneath (and including) the rootnode, one at a
516 time. For each one, call the visitor. The visitor will see the
517 IDirectoryNode before it sees any of the IFileNodes inside. If the
518 visitor returns a Deferred, I do not call the visitor again until it has
522 ## def _walk_if_we_could_use_generators(self, rootnode, rootpath=()):
523 ## # this is what we'd be doing if we didn't have the Deferreds and
524 ## # thus could use generators
525 ## yield rootpath, rootnode
526 ## for childname, childnode in rootnode.list().items():
527 ## childpath = rootpath + (childname,)
528 ## if IFileNode.providedBy(childnode):
529 ## yield childpath, childnode
530 ## elif IDirectoryNode.providedBy(childnode):
531 ## for res in self._walk_if_we_could_use_generators(childnode,
535 def walk(self, rootnode, visitor, rootpath=()):
537 def _listed(listing):
538 return listing.items()
539 d.addCallback(_listed)
540 d.addCallback(self._handle_items, visitor, rootpath)
543 def _handle_items(self, items, visitor, rootpath):
546 childname, (childnode, metadata) = items[0]
547 childpath = rootpath + (childname,)
548 d = defer.maybeDeferred(visitor, childpath, childnode, metadata)
549 if IDirectoryNode.providedBy(childnode):
550 d.addCallback(lambda res: self.walk(childnode, visitor, childpath))
551 d.addCallback(lambda res:
552 self._handle_items(items[1:], visitor, rootpath))
555 class LocalDirectoryDownloader(resource.Resource, DirnodeWalkerMixin):
556 def __init__(self, dirnode, localdir):
557 self._dirnode = dirnode
558 self._localdir = localdir
560 def _handle(self, path, node, metadata):
561 localfile = os.path.join(self._localdir, os.sep.join(path))
562 if IDirectoryNode.providedBy(node):
563 fileutil.make_dirs(localfile)
564 elif IFileNode.providedBy(node):
565 target = download.FileName(localfile)
566 return node.download(target)
568 def render(self, req):
569 d = self.walk(self._dirnode, self._handle)
571 req.setHeader("content-type", "text/plain")
572 return "operation complete"
576 class DirectoryJSONMetadata(rend.Page):
577 def __init__(self, dirnode):
578 self._dirnode = dirnode
580 def renderHTTP(self, ctx):
581 req = inevow.IRequest(ctx)
582 req.setHeader("content-type", "text/plain")
583 return self.renderNode(self._dirnode)
585 def renderNode(self, node):
589 for name, (childnode, metadata) in children.iteritems():
590 if IFileNode.providedBy(childnode):
591 kiduri = childnode.get_uri()
592 kiddata = ("filenode",
594 'size': childnode.get_size(),
597 assert IDirectoryNode.providedBy(childnode), (childnode, children,)
598 kiddata = ("dirnode",
599 {'ro_uri': childnode.get_readonly_uri(),
601 if not childnode.is_readonly():
602 kiddata[1]['rw_uri'] = childnode.get_uri()
604 contents = { 'children': kids,
605 'ro_uri': node.get_readonly_uri(),
607 if not node.is_readonly():
608 contents['rw_uri'] = node.get_uri()
609 data = ("dirnode", contents)
610 return simplejson.dumps(data, indent=1)
614 class DirectoryURI(DirectoryJSONMetadata):
615 def renderNode(self, node):
616 return node.get_uri()
618 class DirectoryReadonlyURI(DirectoryJSONMetadata):
619 def renderNode(self, node):
620 return node.get_readonly_uri()
622 class RenameForm(rend.Page):
624 docFactory = getxmlfile("rename-form.xhtml")
626 def __init__(self, rootname, dirnode, dirpath):
627 self._rootname = rootname
628 self._dirnode = dirnode
629 self._dirpath = dirpath
631 def dirpath_as_string(self):
632 return "/" + "/".join(self._dirpath)
634 def render_title(self, ctx, data):
635 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
637 def render_header(self, ctx, data):
638 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
639 num_dirs = len(parent_directories)
641 header = [ "Rename in directory '",
642 "<%s>/" % self._rootname,
643 "/".join(self._dirpath),
646 if self._dirnode.is_readonly():
647 header.append(" (readonly)")
648 return ctx.tag[header]
650 def render_when_done(self, ctx, data):
651 return T.input(type="hidden", name="when_done", value=url.here)
653 def render_get_name(self, ctx, data):
654 req = inevow.IRequest(ctx)
655 if 'name' in req.args:
656 name = req.args['name'][0]
659 ctx.tag.attributes['value'] = name
662 class POSTHandler(rend.Page):
663 def __init__(self, node, replace):
665 self._replace = replace
667 def _check_replacement(self, name):
669 return defer.succeed(None)
670 d = self._node.has_child(name)
673 raise NoReplacementError("There was already a child by that "
674 "name, and you asked me to not "
680 def renderHTTP(self, ctx):
681 req = inevow.IRequest(ctx)
686 t = req.fields["t"].value
689 if "name" in req.args:
690 name = req.args["name"][0]
691 elif "name" in req.fields:
692 name = req.fields["name"].value
693 if name and "/" in name:
694 req.setResponseCode(http.BAD_REQUEST)
695 req.setHeader("content-type", "text/plain")
696 return "name= may not contain a slash"
699 # we allow the user to delete an empty-named file, but not to create
700 # them, since that's an easy and confusing mistake to make
703 if "when_done" in req.args:
704 when_done = req.args["when_done"][0]
705 if "when_done" in req.fields:
706 when_done = req.fields["when_done"].value
708 if "replace" in req.fields:
709 if req.fields["replace"].value.lower() in ("false", "0"):
710 self._replace = False
714 raise RuntimeError("mkdir requires a name")
715 d = self._check_replacement(name)
716 d.addCallback(lambda res: self._node.create_empty_directory(name))
718 return "directory created"
722 raise RuntimeError("set-uri requires a name")
723 if "uri" in req.args:
724 newuri = req.args["uri"][0].strip()
726 newuri = req.fields["uri"].value.strip()
727 d = self._check_replacement(name)
728 d.addCallback(lambda res: self._node.set_uri(name, newuri))
734 # apparently an <input type="hidden" name="name" value="">
735 # won't show up in the resulting encoded form.. the 'name'
736 # field is completely missing. So to allow deletion of an
737 # empty file, we have to pretend that None means ''. The only
738 # downide of this is a slightly confusing error message if
739 # someone does a POST without a name= field. For our own HTML
740 # thisn't a big deal, because we create the 'delete' POST
743 d = self._node.delete(name)
745 return "thing deleted"
748 from_name = 'from_name' in req.fields and req.fields["from_name"].value
749 if from_name is not None:
750 from_name = from_name.strip()
751 to_name = 'to_name' in req.fields and req.fields["to_name"].value
752 if to_name is not None:
753 to_name = to_name.strip()
754 if not from_name or not to_name:
755 raise RuntimeError("rename requires from_name and to_name")
756 if not IDirectoryNode.providedBy(self._node):
757 raise RuntimeError("rename must only be called on directories")
758 for k,v in [ ('from_name', from_name), ('to_name', to_name) ]:
760 req.setResponseCode(http.BAD_REQUEST)
761 req.setHeader("content-type", "text/plain")
762 return "%s= may not contain a slash" % (k,)
763 d = self._check_replacement(to_name)
764 d.addCallback(lambda res: self._node.get(from_name))
766 uri = child.get_uri()
767 # now actually do the rename
768 return self._node.set_uri(to_name, uri)
769 d.addCallback(add_dest)
771 return self._node.delete(from_name)
772 d.addCallback(rm_src)
774 return "thing renamed"
778 if "mutable" in req.fields:
779 contents = req.fields["file"]
780 name = name or contents.filename
784 raise RuntimeError("upload-mutable requires a name")
785 # SDMF: files are small, and we can only upload data.
786 contents.file.seek(0)
787 data = contents.file.read()
788 uploadable = upload.FileHandle(contents.file)
789 d = self._check_replacement(name)
790 d.addCallback(lambda res: self._node.has_child(name))
791 def _checked(present):
793 # modify the existing one instead of creating a new
795 d2 = self._node.get(name)
796 def _got_newnode(newnode):
797 d3 = newnode.replace(data)
798 d3.addCallback(lambda res: newnode.get_uri())
800 d2.addCallback(_got_newnode)
802 d2 = IClient(ctx).create_mutable_file(data)
803 def _uploaded(newnode):
804 d1 = self._node.set_node(name, newnode)
805 d1.addCallback(lambda res: newnode.get_uri())
807 d2.addCallback(_uploaded)
809 d.addCallback(_checked)
811 contents = req.fields["file"]
812 name = name or contents.filename
816 raise RuntimeError("upload requires a name")
817 uploadable = upload.FileHandle(contents.file)
818 d = self._check_replacement(name)
819 d.addCallback(lambda res: self._node.add_file(name, uploadable))
821 return newnode.get_uri()
824 elif t == "overwrite":
825 contents = req.fields["file"]
826 # SDMF: files are small, and we can only upload data.
827 contents.file.seek(0)
828 data = contents.file.read()
829 # TODO: 'name' handling needs review
830 d = defer.succeed(self._node)
831 def _got_child(child_node):
832 child_node.replace(data)
833 return child_node.get_uri()
834 d.addCallback(_got_child)
837 d = self._node.get(name)
838 def _got_child(child_node):
839 d2 = child_node.check()
841 log.msg("checked %s, results %s" % (child_node, res))
843 d2.addCallback(_done)
845 d.addCallback(_got_child)
848 return "BAD t=%s" % t
850 d.addCallback(lambda res: url.URL.fromString(when_done))
851 def _check_replacement(f):
852 # TODO: make this more human-friendly: maybe send them to the
853 # when_done page but with an extra query-arg that will display
854 # the error message in a big box at the top of the page. The
855 # directory page that when_done= usually points to accepts a
856 # result= argument.. use that.
857 f.trap(NoReplacementError)
858 req.setResponseCode(http.CONFLICT)
859 req.setHeader("content-type", "text/plain")
861 d.addErrback(_check_replacement)
864 class DELETEHandler(rend.Page):
865 def __init__(self, node, name):
869 def renderHTTP(self, ctx):
870 req = inevow.IRequest(ctx)
871 d = self._node.delete(self._name)
873 # what should this return??
874 return "%s deleted" % self._name
876 def _trap_missing(f):
878 req.setResponseCode(http.NOT_FOUND)
879 req.setHeader("content-type", "text/plain")
880 return "no such child %s" % self._name
881 d.addErrback(_trap_missing)
884 class PUTHandler(rend.Page):
885 def __init__(self, node, path, t, localfile, localdir, replace):
889 self._localfile = localfile
890 self._localdir = localdir
891 self._replace = replace
893 def renderHTTP(self, ctx):
894 req = inevow.IRequest(ctx)
896 localfile = self._localfile
897 localdir = self._localdir
899 # we must traverse the path, creating new directories as necessary
900 d = self._get_or_create_directories(self._node, self._path[:-1])
901 name = self._path[-1]
902 d.addCallback(self._check_replacement, name, self._replace)
905 d.addCallback(self._upload_localfile, localfile, name)
908 d.addCallback(self._get_or_create_directories, self._path[-1:])
909 d.addCallback(self._upload_localdir, localdir)
911 raise RuntimeError("t=upload requires localfile= or localdir=")
913 d.addCallback(self._attach_uri, req.content, name)
915 d.addCallback(self._mkdir, name)
917 d.addCallback(self._upload_file, req.content, name)
918 def _check_blocking(f):
919 f.trap(BlockingFileError)
920 req.setResponseCode(http.BAD_REQUEST)
921 req.setHeader("content-type", "text/plain")
923 d.addErrback(_check_blocking)
924 def _check_replacement(f):
925 f.trap(NoReplacementError)
926 req.setResponseCode(http.CONFLICT)
927 req.setHeader("content-type", "text/plain")
929 d.addErrback(_check_replacement)
932 def _get_or_create_directories(self, node, path):
933 if not IDirectoryNode.providedBy(node):
934 # unfortunately it is too late to provide the name of the
935 # blocking directory in the error message.
936 raise BlockingFileError("cannot create directory because there "
937 "is a file in the way")
939 return defer.succeed(node)
940 d = node.get(path[0])
941 def _maybe_create(f):
943 return node.create_empty_directory(path[0])
944 d.addErrback(_maybe_create)
945 d.addCallback(self._get_or_create_directories, path[1:])
948 def _check_replacement(self, node, name, replace):
951 d = node.has_child(name)
954 raise NoReplacementError("There was already a child by that "
955 "name, and you asked me to not "
961 def _mkdir(self, node, name):
962 d = node.create_empty_directory(name)
964 return newnode.get_uri()
968 def _upload_file(self, node, contents, name):
969 uploadable = upload.FileHandle(contents)
970 d = node.add_file(name, uploadable)
972 log.msg("webish upload complete")
973 return filenode.get_uri()
977 def _upload_localfile(self, node, localfile, name):
978 uploadable = upload.FileName(localfile)
979 d = node.add_file(name, uploadable)
980 d.addCallback(lambda filenode: filenode.get_uri())
983 def _attach_uri(self, parentnode, contents, name):
984 newuri = contents.read().strip()
985 d = parentnode.set_uri(name, newuri)
991 def _upload_localdir(self, node, localdir):
992 # build up a list of files to upload
995 msg = "No files to upload! %s is empty" % localdir
996 if not os.path.exists(localdir):
997 msg = "%s doesn't exist!" % localdir
998 for root, dirs, files in os.walk(localdir):
1002 relative_root = root[len(localdir)+1:]
1003 path = tuple(relative_root.split(os.sep))
1005 all_dirs.append(path + (d,))
1007 all_files.append(path + (f,))
1008 d = defer.succeed(msg)
1009 for dir in all_dirs:
1011 d.addCallback(self._makedir, node, dir)
1013 d.addCallback(self._upload_one_file, node, localdir, f)
1016 def _makedir(self, res, node, dir):
1017 d = defer.succeed(None)
1018 # get the parent. As long as os.walk gives us parents before
1019 # children, this ought to work
1020 d.addCallback(lambda res: node.get_child_at_path(dir[:-1]))
1021 # then create the child directory
1022 d.addCallback(lambda parent: parent.create_empty_directory(dir[-1]))
1025 def _upload_one_file(self, res, node, localdir, f):
1026 # get the parent. We can be sure this exists because we already
1027 # went through and created all the directories we require.
1028 localfile = os.path.join(localdir, *f)
1029 d = node.get_child_at_path(f[:-1])
1030 d.addCallback(self._upload_localfile, localfile, f[-1])
1034 class Manifest(rend.Page):
1035 docFactory = getxmlfile("manifest.xhtml")
1036 def __init__(self, dirnode, dirpath):
1037 self._dirnode = dirnode
1038 self._dirpath = dirpath
1040 def dirpath_as_string(self):
1041 return "/" + "/".join(self._dirpath)
1043 def render_title(self, ctx):
1044 return T.title["Manifest of %s" % self.dirpath_as_string()]
1046 def render_header(self, ctx):
1047 return T.p["Manifest of %s" % self.dirpath_as_string()]
1049 def data_items(self, ctx, data):
1050 return self._dirnode.build_manifest()
1052 def render_row(self, ctx, refresh_cap):
1053 ctx.fillSlots("refresh_capability", refresh_cap)
1056 class VDrive(rend.Page):
1058 def __init__(self, node, name):
1062 def get_child_at_path(self, path):
1064 return self.node.get_child_at_path(path)
1065 return defer.succeed(self.node)
1067 def locateChild(self, ctx, segments):
1068 req = inevow.IRequest(ctx)
1072 # when we're pointing at a directory (like /uri/$DIR_URI/my_pix),
1073 # Directory.addSlash causes a redirect to /uri/$DIR_URI/my_pix/,
1074 # which appears here as ['my_pix', '']. This is supposed to hit the
1075 # same Directory as ['my_pix'].
1076 if path and path[-1] == '':
1081 t = req.args["t"][0]
1084 if "localfile" in req.args:
1085 localfile = req.args["localfile"][0]
1086 if localfile != os.path.abspath(localfile):
1087 return NeedAbsolutePathError(), ()
1089 if "localdir" in req.args:
1090 localdir = req.args["localdir"][0]
1091 if localdir != os.path.abspath(localdir):
1092 return NeedAbsolutePathError(), ()
1093 if localfile or localdir:
1094 if not ILocalAccess(ctx).local_access_is_allowed():
1095 return LocalAccessDisabledError(), ()
1096 if req.getHost().host != LOCALHOST:
1097 return NeedLocalhostError(), ()
1098 # TODO: think about clobbering/revealing config files and node secrets
1101 if "replace" in req.args:
1102 if req.args["replace"][0].lower() in ("false", "0"):
1106 # the node must exist, and our operation will be performed on the
1108 d = self.get_child_at_path(path)
1109 def file_or_dir(node):
1110 if (IFileNode.providedBy(node)
1111 or IMutableFileNode.providedBy(node)):
1112 filename = "unknown"
1115 if "filename" in req.args:
1116 filename = req.args["filename"][0]
1119 # write contents to a local file
1120 return LocalFileDownloader(node, localfile), ()
1121 # send contents as the result
1122 return FileDownloader(node, filename), ()
1124 # send contents as the result
1125 return FileDownloader(node, filename), ()
1127 return FileJSONMetadata(node), ()
1129 return FileURI(node), ()
1130 elif t == "readonly-uri":
1131 return FileReadOnlyURI(node), ()
1133 raise RuntimeError("bad t=%s" % t)
1134 elif IDirectoryNode.providedBy(node):
1137 # recursive download to a local directory
1138 return LocalDirectoryDownloader(node, localdir), ()
1139 raise RuntimeError("t=download requires localdir=")
1141 # send an HTML representation of the directory
1142 return Directory(self.name, node, path), ()
1144 return DirectoryJSONMetadata(node), ()
1146 return DirectoryURI(node), ()
1147 elif t == "readonly-uri":
1148 return DirectoryReadonlyURI(node), ()
1149 elif t == "manifest":
1150 return Manifest(node, path), ()
1151 elif t == 'rename-form':
1152 return RenameForm(self.name, node, path), ()
1154 raise RuntimeError("bad t=%s" % t)
1156 raise RuntimeError("unknown node type")
1157 d.addCallback(file_or_dir)
1158 elif method == "POST":
1159 # the node must exist, and our operation will be performed on the
1161 d = self.get_child_at_path(path)
1163 return POSTHandler(node, replace), ()
1165 elif method == "DELETE":
1166 # the node must exist, and our operation will be performed on its
1168 assert path # you can't delete the root
1169 d = self.get_child_at_path(path[:-1])
1171 return DELETEHandler(node, path[-1]), ()
1173 elif method in ("PUT",):
1174 # the node may or may not exist, and our operation may involve
1175 # all the ancestors of the node.
1176 return PUTHandler(self.node, path, t, localfile, localdir, replace), ()
1178 return rend.NotFound
1179 def _trap_KeyError(f):
1181 return rend.FourOhFour(), ()
1182 d.addErrback(_trap_KeyError)
1185 class URIPUTHandler(rend.Page):
1186 def renderHTTP(self, ctx):
1187 req = inevow.IRequest(ctx)
1188 assert req.method == "PUT"
1192 t = req.args["t"][0]
1195 # "PUT /uri", to create an unlinked file. This is like PUT but
1196 # without the associated set_uri.
1197 uploadable = upload.FileHandle(req.content)
1198 d = IClient(ctx).upload(uploadable)
1199 # that fires with the URI of the new file
1203 # "PUT /uri?t=mkdir", to create an unlinked directory.
1204 d = IClient(ctx).create_empty_dirnode()
1205 d.addCallback(lambda dirnode: dirnode.get_uri())
1208 req.setResponseCode(http.BAD_REQUEST)
1209 req.setHeader("content-type", "text/plain")
1210 return "/uri only accepts PUT and PUT?t=mkdir"
1213 class Root(rend.Page):
1216 docFactory = getxmlfile("welcome.xhtml")
1218 def locateChild(self, ctx, segments):
1219 client = IClient(ctx)
1220 req = inevow.IRequest(ctx)
1222 if segments[0] == "uri":
1223 if len(segments) == 1 or segments[1] == '':
1224 if "uri" in req.args:
1225 uri = req.args["uri"][0].replace("/", "!")
1226 there = url.URL.fromContext(ctx)
1227 there = there.clear("uri")
1228 there = there.child("uri").child(uri)
1230 if len(segments) == 1 and req.method == "PUT":
1232 # either "PUT /uri" to create an unlinked file, or
1233 # "PUT /uri?t=mkdir" to create an unlinked directory
1234 return URIPUTHandler(), ()
1235 if len(segments) < 2:
1236 return rend.NotFound
1237 uri = segments[1].replace("!", "/")
1238 d = defer.maybeDeferred(client.create_node_from_uri, uri)
1239 d.addCallback(lambda node: VDrive(node, "from-uri"))
1240 d.addCallback(lambda vd: vd.locateChild(ctx, segments[2:]))
1241 def _trap_KeyError(f):
1243 return rend.FourOhFour(), ()
1244 d.addErrback(_trap_KeyError)
1246 elif segments[0] == "xmlrpc":
1247 raise NotImplementedError()
1248 return rend.Page.locateChild(self, ctx, segments)
1250 child_webform_css = webform.defaultCSS
1251 child_tahoe_css = nevow_File(util.sibpath(__file__, "web/tahoe.css"))
1253 child_provisioning = provisioning.ProvisioningTool()
1255 def data_version(self, ctx, data):
1256 v = IClient(ctx).get_versions()
1257 return "tahoe: %s, zfec: %s, foolscap: %s, twisted: %s" % \
1258 (v['allmydata'], v['zfec'], v['foolscap'], v['twisted'])
1260 def data_my_nodeid(self, ctx, data):
1261 return b32encode(IClient(ctx).nodeid).lower()
1262 def data_introducer_furl(self, ctx, data):
1263 return IClient(ctx).introducer_furl
1264 def data_connected_to_introducer(self, ctx, data):
1265 if IClient(ctx).connected_to_introducer():
1268 def data_num_peers(self, ctx, data):
1269 #client = inevow.ISite(ctx)._client
1270 client = IClient(ctx)
1271 return len(list(client.get_all_peerids()))
1273 def data_peers(self, ctx, data):
1275 client = IClient(ctx)
1276 for nodeid in sorted(client.get_all_peerids()):
1277 row = (b32encode(nodeid).lower(),)
1281 def render_row(self, ctx, data):
1283 ctx.fillSlots("peerid", nodeid_a)
1286 def render_private_vdrive(self, ctx, data):
1287 basedir = IClient(ctx).basedir
1288 start_html = os.path.abspath(os.path.join(basedir, "start.html"))
1289 if os.path.exists(start_html):
1290 return T.p["To view your personal private non-shared filestore, ",
1291 "use this browser to open the following file from ",
1292 "your local filesystem:",
1295 return T.p["personal vdrive not available."]
1297 # this is a form where users can download files by URI
1299 def render_download_form(self, ctx, data):
1300 form = T.form(action="uri", method="get",
1301 enctype="multipart/form-data")[
1303 T.legend(class_="freeform-form-label")["Download a file"],
1304 "URI of file to download: ",
1305 T.input(type="text", name="uri"), " ",
1306 "Filename to download as: ",
1307 T.input(type="text", name="filename"), " ",
1308 T.input(type="submit", value="Download"),
1314 implements(ILocalAccess)
1316 self.local_access = False
1317 def local_access_is_allowed(self):
1318 return self.local_access
1320 class WebishServer(service.MultiService):
1323 def __init__(self, webport):
1324 service.MultiService.__init__(self)
1325 self.webport = webport
1327 self.site = site = appserver.NevowSite(self.root)
1328 self.site.requestFactory = MyRequest
1329 self.allow_local = LocalAccess()
1330 self.site.remember(self.allow_local, ILocalAccess)
1331 s = strports.service(webport, site)
1332 s.setServiceParent(self)
1333 self.listener = s # stash it so the tests can query for the portnum
1334 self._started = defer.Deferred()
1336 def allow_local_access(self, enable=True):
1337 self.allow_local.local_access = enable
1339 def startService(self):
1340 service.MultiService.startService(self)
1341 # to make various services available to render_* methods, we stash a
1342 # reference to the client on the NevowSite. This will be available by
1343 # adapting the 'context' argument to a special marker interface named
1345 self.site.remember(self.parent, IClient)
1346 # I thought you could do the same with an existing interface, but
1347 # apparently 'ISite' does not exist
1348 #self.site._client = self.parent
1349 self._started.callback(None)
1351 def create_start_html(self, private_uri, startfile, nodeurl_file):
1353 Returns a deferred that eventually fires once the start.html page has
1356 self._started.addCallback(self._create_start_html, private_uri, startfile, nodeurl_file)
1357 return self._started
1359 def _create_start_html(self, dummy, private_uri, startfile, nodeurl_file):
1360 f = open(startfile, "w")
1361 os.chmod(startfile, 0600)
1362 template = open(util.sibpath(__file__, "web/start.html"), "r").read()
1363 # what is our webport?
1365 if isinstance(s, internet.TCPServer):
1366 base_url = "http://localhost:%d" % s._port.getHost().port
1367 elif isinstance(s, internet.SSLServer):
1368 base_url = "https://localhost:%d" % s._port.getHost().port
1370 base_url = "UNKNOWN" # this will break the href
1371 # TODO: emit a start.html that explains that we don't know
1372 # how to create a suitable URL
1374 private_uri = private_uri.replace("/","!")
1375 link_to_private_uri = "View <a href=\"%s/uri/%s\">your personal private non-shared filestore</a>." % (base_url, private_uri)
1376 fields = {"link_to_private_uri": link_to_private_uri,
1377 "base_url": base_url,
1380 fields = {"link_to_private_uri": "",
1381 "base_url": base_url,
1383 f.write(template % fields)
1386 f = open(nodeurl_file, "w")
1387 # this file is world-readable
1388 f.write(base_url + "\n")