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 allmydata import get_package_versions_string
18 from zope.interface import implements, Interface
20 from formless import webform
23 return loaders.xmlfile(util.sibpath(__file__, "web/%s" % name))
25 class IClient(Interface):
27 class ILocalAccess(Interface):
28 def local_access_is_allowed():
29 """Return True if t=upload&localdir= is allowed, giving anyone who
30 can talk to the webserver control over the local (disk) filesystem."""
33 # we must override twisted.web.http.Request.requestReceived with a version
34 # that doesn't use cgi.parse_multipart() . Since we actually use Nevow, we
35 # override the nevow-specific subclass, nevow.appserver.NevowRequest . This
36 # is an exact copy of twisted.web.http.Request (from SVN HEAD on 10-Aug-2007)
37 # that modifies the way form arguments are parsed. Note that this sort of
38 # surgery may induce a dependency upon a particular version of twisted.web
40 parse_qs = http.parse_qs
41 class MyRequest(appserver.NevowRequest):
42 def requestReceived(self, command, path, version):
43 """Called by channel when all data has been received.
45 This method is not intended for users.
47 self.content.seek(0,0)
51 self.method, self.uri = command, path
52 self.clientproto = version
53 x = self.uri.split('?', 1)
58 self.path, argstring = x
59 self.args = parse_qs(argstring, 1)
61 # cache the client and server information, we'll need this later to be
62 # serialized and sent with the request so CGIs will work remotely
63 self.client = self.channel.transport.getPeer()
64 self.host = self.channel.transport.getHost()
66 # Argument processing.
68 ## The original twisted.web.http.Request.requestReceived code parsed the
69 ## content and added the form fields it found there to self.args . It
70 ## did this with cgi.parse_multipart, which holds the arguments in RAM
71 ## and is thus unsuitable for large file uploads. The Nevow subclass
72 ## (nevow.appserver.NevowRequest) uses cgi.FieldStorage instead (putting
73 ## the results in self.fields), which is much more memory-efficient.
74 ## Since we know we're using Nevow, we can anticipate these arguments
75 ## appearing in self.fields instead of self.args, and thus skip the
76 ## parse-content-into-self.args step.
79 ## ctype = self.getHeader('content-type')
80 ## if self.method == "POST" and ctype:
81 ## mfd = 'multipart/form-data'
82 ## key, pdict = cgi.parse_header(ctype)
83 ## if key == 'application/x-www-form-urlencoded':
84 ## args.update(parse_qs(self.content.read(), 1))
87 ## args.update(cgi.parse_multipart(self.content, pdict))
88 ## except KeyError, e:
89 ## if e.args[0] == 'content-disposition':
90 ## # Parse_multipart can't cope with missing
91 ## # content-dispostion headers in multipart/form-data
92 ## # parts, so we catch the exception and tell the client
93 ## # it was a bad request.
94 ## self.channel.transport.write(
95 ## "HTTP/1.1 400 Bad Request\r\n\r\n")
96 ## self.channel.transport.loseConnection()
102 class Directory(rend.Page):
104 docFactory = getxmlfile("directory.xhtml")
106 def __init__(self, rootname, dirnode, dirpath):
107 self._rootname = rootname
108 self._dirnode = dirnode
109 self._dirpath = dirpath
111 def dirpath_as_string(self):
112 return "/" + "/".join(self._dirpath)
114 def render_title(self, ctx, data):
115 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
117 def render_header(self, ctx, data):
118 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
119 num_dirs = len(parent_directories)
121 header = ["Directory '"]
122 for i,d in enumerate(parent_directories):
123 upness = num_dirs - i - 1
125 link = "/".join( ("..",) * upness )
128 header.append(T.a(href=link)[d])
133 if self._dirnode.is_readonly():
134 header.append(" (readonly)")
136 return ctx.tag[header]
138 def render_welcome(self, ctx, data):
139 depth = len(self._dirpath) + 2
140 link = "/".join([".."] * depth)
141 return T.div[T.a(href=link)["Return to Welcome page"]]
143 def data_children(self, ctx, data):
144 d = self._dirnode.list()
145 d.addCallback(lambda dict: sorted(dict.items()))
148 def render_row(self, ctx, data):
149 name, (target, metadata) = data
151 if self._dirnode.is_readonly():
155 # this creates a button which will cause our child__delete method
156 # to be invoked, which deletes the file and then redirects the
157 # browser back to this directory
158 delete = T.form(action=url.here, method="post")[
159 T.input(type='hidden', name='t', value='delete'),
160 T.input(type='hidden', name='name', value=name),
161 T.input(type='hidden', name='when_done', value=url.here),
162 T.input(type='submit', value='del', name="del"),
165 rename = T.form(action=url.here, method="get")[
166 T.input(type='hidden', name='t', value='rename-form'),
167 T.input(type='hidden', name='name', value=name),
168 T.input(type='hidden', name='when_done', value=url.here),
169 T.input(type='submit', value='rename', name="rename"),
172 ctx.fillSlots("delete", delete)
173 ctx.fillSlots("rename", rename)
174 check = T.form(action=url.here, method="post")[
175 T.input(type='hidden', name='t', value='check'),
176 T.input(type='hidden', name='name', value=name),
177 T.input(type='hidden', name='when_done', value=url.here),
178 T.input(type='submit', value='check', name="check"),
180 ctx.fillSlots("overwrite", self.build_overwrite(ctx, (name, target)))
181 ctx.fillSlots("check", check)
183 # build the base of the uri_link link url
184 uri_link = "/uri/" + urllib.quote(target.get_uri())
186 assert (IFileNode.providedBy(target)
187 or IDirectoryNode.providedBy(target)
188 or IMutableFileNode.providedBy(target)), target
190 if IMutableFileNode.providedBy(target):
193 # add the filename to the uri_link url
194 uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
196 # to prevent javascript in displayed .html files from stealing a
197 # secret directory URI from the URL, send the browser to a URI-based
198 # page that doesn't know about the directory at all
199 #dlurl = urllib.quote(name)
202 ctx.fillSlots("filename",
203 T.a(href=dlurl)[html.escape(name)])
204 ctx.fillSlots("type", "SSK")
206 ctx.fillSlots("size", "?")
208 text_plain_link = uri_link + "?filename=foo.txt"
209 text_plain_tag = T.a(href=text_plain_link)["text/plain"]
211 elif IFileNode.providedBy(target):
214 # add the filename to the uri_link url
215 uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
217 # to prevent javascript in displayed .html files from stealing a
218 # secret directory URI from the URL, send the browser to a URI-based
219 # page that doesn't know about the directory at all
220 #dlurl = urllib.quote(name)
223 ctx.fillSlots("filename",
224 T.a(href=dlurl)[html.escape(name)])
225 ctx.fillSlots("type", "FILE")
227 ctx.fillSlots("size", target.get_size())
229 text_plain_link = uri_link + "?filename=foo.txt"
230 text_plain_tag = T.a(href=text_plain_link)["text/plain"]
232 elif IDirectoryNode.providedBy(target):
234 subdir_url = urllib.quote(name)
235 ctx.fillSlots("filename",
236 T.a(href=subdir_url)[html.escape(name)])
237 if target.is_readonly():
241 ctx.fillSlots("type", dirtype)
242 ctx.fillSlots("size", "-")
243 text_plain_tag = None
245 childdata = [T.a(href="%s?t=json" % name)["JSON"], ", ",
246 T.a(href="%s?t=uri" % name)["URI"], ", ",
247 T.a(href="%s?t=readonly-uri" % name)["readonly-URI"], ", ",
248 T.a(href=uri_link)["URI-link"],
251 childdata.extend([", ", text_plain_tag])
253 ctx.fillSlots("data", childdata)
256 checker = IClient(ctx).getServiceNamed("checker")
260 d = defer.maybeDeferred(checker.checker_results_for,
261 target.get_verifier())
262 def _got(checker_results):
263 recent_results = reversed(checker_results[-5:])
264 if IFileNode.providedBy(target):
266 ", ".join(["%d/%d" % (found, needed)
268 (needed, total, found, sharemap))
269 in recent_results]) +
271 elif IDirectoryNode.providedBy(target):
273 "".join([{True:"+",False:"-"}[res]
274 for (when, res) in recent_results]) +
277 results = "%d results" % len(checker_results)
283 # TODO: include a link to see more results, including timestamps
284 # TODO: use a sparkline
285 ctx.fillSlots("checker_results", results)
289 def render_forms(self, ctx, data):
290 if self._dirnode.is_readonly():
291 return T.div["No upload forms: directory is read-only"]
292 mkdir = T.form(action=".", method="post",
293 enctype="multipart/form-data")[
295 T.input(type="hidden", name="t", value="mkdir"),
296 T.input(type="hidden", name="when_done", value=url.here),
297 T.legend(class_="freeform-form-label")["Create a new directory"],
298 "New directory name: ",
299 T.input(type="text", name="name"), " ",
300 T.input(type="submit", value="Create"),
303 upload = T.form(action=".", method="post",
304 enctype="multipart/form-data")[
306 T.input(type="hidden", name="t", value="upload"),
307 T.input(type="hidden", name="when_done", value=url.here),
308 T.legend(class_="freeform-form-label")["Upload a file to this directory"],
309 "Choose a file to upload: ",
310 T.input(type="file", name="file", class_="freeform-input-file"),
312 T.input(type="submit", value="Upload"),
314 T.input(type="checkbox", name="mutable"),
317 mount = T.form(action=".", method="post",
318 enctype="multipart/form-data")[
320 T.input(type="hidden", name="t", value="uri"),
321 T.input(type="hidden", name="when_done", value=url.here),
322 T.legend(class_="freeform-form-label")["Attach a file or directory"
326 T.input(type="text", name="name"), " ",
327 "URI of new child: ",
328 T.input(type="text", name="uri"), " ",
329 T.input(type="submit", value="Attach"),
331 return [T.div(class_="freeform-form")[mkdir],
332 T.div(class_="freeform-form")[upload],
333 T.div(class_="freeform-form")[mount],
336 def build_overwrite(self, ctx, data):
338 if IMutableFileNode.providedBy(target) and not target.is_readonly():
339 action="/uri/" + urllib.quote(target.get_uri())
340 overwrite = T.form(action=action, method="post",
341 enctype="multipart/form-data")[
343 T.input(type="hidden", name="t", value="overwrite"),
344 T.input(type='hidden', name='name', value=name),
345 T.input(type='hidden', name='when_done', value=url.here),
346 T.legend(class_="freeform-form-label")["Overwrite"],
348 T.input(type="file", name="file", class_="freeform-input-file"),
350 T.input(type="submit", value="Overwrite")
352 return [T.div(class_="freeform-form")[overwrite],]
356 def render_results(self, ctx, data):
357 req = inevow.IRequest(ctx)
358 if "results" in req.args:
359 return req.args["results"]
363 class WebDownloadTarget:
364 implements(IDownloadTarget, IConsumer)
365 def __init__(self, req, content_type, content_encoding, save_to_file):
367 self._content_type = content_type
368 self._content_encoding = content_encoding
370 self._producer = None
371 self._save_to_file = save_to_file
373 def registerProducer(self, producer, streaming):
374 self._req.registerProducer(producer, streaming)
375 def unregisterProducer(self):
376 self._req.unregisterProducer()
378 def open(self, size):
380 self._req.setHeader("content-type", self._content_type)
381 if self._content_encoding:
382 self._req.setHeader("content-encoding", self._content_encoding)
383 self._req.setHeader("content-length", str(size))
384 if self._save_to_file is not None:
385 # tell the browser to save the file rather display it
386 # TODO: quote save_to_file properly
387 self._req.setHeader("content-disposition",
388 'attachment; filename="%s"'
389 % self._save_to_file)
391 def write(self, data):
392 self._req.write(data)
398 # The content-type is already set, and the response code
399 # has already been sent, so we can't provide a clean error
400 # indication. We can emit text (which a browser might interpret
401 # as something else), and if we sent a Size header, they might
402 # notice that we've truncated the data. Keep the error message
403 # small to improve the chances of having our error response be
404 # shorter than the intended results.
406 # We don't have a lot of options, unfortunately.
407 self._req.write("problem during download\n")
409 # We haven't written anything yet, so we can provide a sensible
412 msg.replace("\n", "|")
413 self._req.setResponseCode(http.GONE, msg)
414 self._req.setHeader("content-type", "text/plain")
415 # TODO: HTML-formatted exception?
416 self._req.write(str(why))
419 def register_canceller(self, cb):
424 class FileDownloader(resource.Resource):
425 def __init__(self, filenode, name):
426 assert (IFileNode.providedBy(filenode)
427 or IMutableFileNode.providedBy(filenode))
428 self._filenode = filenode
431 def render(self, req):
432 gte = static.getTypeAndEncoding
433 type, encoding = gte(self._name,
434 static.File.contentTypes,
435 static.File.contentEncodings,
436 defaultType="text/plain")
438 if "save" in req.args:
439 save_to_file = self._name
440 wdt = WebDownloadTarget(req, type, encoding, save_to_file)
441 d = self._filenode.download(wdt)
442 # exceptions during download are handled by the WebDownloadTarget
443 d.addErrback(lambda why: None)
444 return server.NOT_DONE_YET
446 class BlockingFileError(Exception):
447 """We cannot auto-create a parent directory, because there is a file in
449 class NoReplacementError(Exception):
450 """There was already a child by that name, and you asked me to not replace it"""
452 LOCALHOST = "127.0.0.1"
454 class NeedLocalhostError:
455 implements(inevow.IResource)
457 def renderHTTP(self, ctx):
458 req = inevow.IRequest(ctx)
459 req.setResponseCode(http.FORBIDDEN)
460 req.setHeader("content-type", "text/plain")
461 return "localfile= or localdir= requires a local connection"
463 class NeedAbsolutePathError:
464 implements(inevow.IResource)
466 def renderHTTP(self, ctx):
467 req = inevow.IRequest(ctx)
468 req.setResponseCode(http.FORBIDDEN)
469 req.setHeader("content-type", "text/plain")
470 return "localfile= or localdir= requires an absolute path"
472 class LocalAccessDisabledError:
473 implements(inevow.IResource)
475 def renderHTTP(self, ctx):
476 req = inevow.IRequest(ctx)
477 req.setResponseCode(http.FORBIDDEN)
478 req.setHeader("content-type", "text/plain")
479 return "local file access is disabled"
482 class LocalFileDownloader(resource.Resource):
483 def __init__(self, filenode, local_filename):
484 self._local_filename = local_filename
486 self._filenode = filenode
488 def render(self, req):
489 target = download.FileName(self._local_filename)
490 d = self._filenode.download(target)
492 req.write(self._filenode.get_uri())
495 return server.NOT_DONE_YET
498 class FileJSONMetadata(rend.Page):
499 def __init__(self, filenode):
500 self._filenode = filenode
502 def renderHTTP(self, ctx):
503 req = inevow.IRequest(ctx)
504 req.setHeader("content-type", "text/plain")
505 return self.renderNode(self._filenode)
507 def renderNode(self, filenode):
508 file_uri = filenode.get_uri()
511 'size': filenode.get_size(),
513 return simplejson.dumps(data, indent=1)
515 class FileURI(FileJSONMetadata):
516 def renderNode(self, filenode):
517 file_uri = filenode.get_uri()
520 class FileReadOnlyURI(FileJSONMetadata):
521 def renderNode(self, filenode):
522 if filenode.is_readonly():
523 return filenode.get_uri()
525 return filenode.get_readonly().get_uri()
527 class DirnodeWalkerMixin:
528 """Visit all nodes underneath (and including) the rootnode, one at a
529 time. For each one, call the visitor. The visitor will see the
530 IDirectoryNode before it sees any of the IFileNodes inside. If the
531 visitor returns a Deferred, I do not call the visitor again until it has
535 ## def _walk_if_we_could_use_generators(self, rootnode, rootpath=()):
536 ## # this is what we'd be doing if we didn't have the Deferreds and
537 ## # thus could use generators
538 ## yield rootpath, rootnode
539 ## for childname, childnode in rootnode.list().items():
540 ## childpath = rootpath + (childname,)
541 ## if IFileNode.providedBy(childnode):
542 ## yield childpath, childnode
543 ## elif IDirectoryNode.providedBy(childnode):
544 ## for res in self._walk_if_we_could_use_generators(childnode,
548 def walk(self, rootnode, visitor, rootpath=()):
550 def _listed(listing):
551 return listing.items()
552 d.addCallback(_listed)
553 d.addCallback(self._handle_items, visitor, rootpath)
556 def _handle_items(self, items, visitor, rootpath):
559 childname, (childnode, metadata) = items[0]
560 childpath = rootpath + (childname,)
561 d = defer.maybeDeferred(visitor, childpath, childnode, metadata)
562 if IDirectoryNode.providedBy(childnode):
563 d.addCallback(lambda res: self.walk(childnode, visitor, childpath))
564 d.addCallback(lambda res:
565 self._handle_items(items[1:], visitor, rootpath))
568 class LocalDirectoryDownloader(resource.Resource, DirnodeWalkerMixin):
569 def __init__(self, dirnode, localdir):
570 self._dirnode = dirnode
571 self._localdir = localdir
573 def _handle(self, path, node, metadata):
574 localfile = os.path.join(self._localdir, os.sep.join(path))
575 if IDirectoryNode.providedBy(node):
576 fileutil.make_dirs(localfile)
577 elif IFileNode.providedBy(node):
578 target = download.FileName(localfile)
579 return node.download(target)
581 def render(self, req):
582 d = self.walk(self._dirnode, self._handle)
584 req.setHeader("content-type", "text/plain")
585 return "operation complete"
589 class DirectoryJSONMetadata(rend.Page):
590 def __init__(self, dirnode):
591 self._dirnode = dirnode
593 def renderHTTP(self, ctx):
594 req = inevow.IRequest(ctx)
595 req.setHeader("content-type", "text/plain")
596 return self.renderNode(self._dirnode)
598 def renderNode(self, node):
602 for name, (childnode, metadata) in children.iteritems():
603 if IFileNode.providedBy(childnode):
604 kiduri = childnode.get_uri()
605 kiddata = ("filenode",
607 'size': childnode.get_size(),
610 assert IDirectoryNode.providedBy(childnode), (childnode, children,)
611 kiddata = ("dirnode",
612 {'ro_uri': childnode.get_readonly_uri(),
614 if not childnode.is_readonly():
615 kiddata[1]['rw_uri'] = childnode.get_uri()
617 contents = { 'children': kids,
618 'ro_uri': node.get_readonly_uri(),
620 if not node.is_readonly():
621 contents['rw_uri'] = node.get_uri()
622 data = ("dirnode", contents)
623 return simplejson.dumps(data, indent=1)
627 class DirectoryURI(DirectoryJSONMetadata):
628 def renderNode(self, node):
629 return node.get_uri()
631 class DirectoryReadonlyURI(DirectoryJSONMetadata):
632 def renderNode(self, node):
633 return node.get_readonly_uri()
635 class RenameForm(rend.Page):
637 docFactory = getxmlfile("rename-form.xhtml")
639 def __init__(self, rootname, dirnode, dirpath):
640 self._rootname = rootname
641 self._dirnode = dirnode
642 self._dirpath = dirpath
644 def dirpath_as_string(self):
645 return "/" + "/".join(self._dirpath)
647 def render_title(self, ctx, data):
648 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
650 def render_header(self, ctx, data):
651 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
652 num_dirs = len(parent_directories)
654 header = [ "Rename in directory '",
655 "<%s>/" % self._rootname,
656 "/".join(self._dirpath),
659 if self._dirnode.is_readonly():
660 header.append(" (readonly)")
661 return ctx.tag[header]
663 def render_when_done(self, ctx, data):
664 return T.input(type="hidden", name="when_done", value=url.here)
666 def render_get_name(self, ctx, data):
667 req = inevow.IRequest(ctx)
668 if 'name' in req.args:
669 name = req.args['name'][0]
672 ctx.tag.attributes['value'] = name
675 class POSTHandler(rend.Page):
676 def __init__(self, node, replace):
678 self._replace = replace
680 def _check_replacement(self, name):
682 return defer.succeed(None)
683 d = self._node.has_child(name)
686 raise NoReplacementError("There was already a child by that "
687 "name, and you asked me to not "
693 def renderHTTP(self, ctx):
694 req = inevow.IRequest(ctx)
699 t = req.fields["t"].value
702 if "name" in req.args:
703 name = req.args["name"][0]
704 elif "name" in req.fields:
705 name = req.fields["name"].value
706 if name and "/" in name:
707 req.setResponseCode(http.BAD_REQUEST)
708 req.setHeader("content-type", "text/plain")
709 return "name= may not contain a slash"
712 # we allow the user to delete an empty-named file, but not to create
713 # them, since that's an easy and confusing mistake to make
716 if "when_done" in req.args:
717 when_done = req.args["when_done"][0]
718 if "when_done" in req.fields:
719 when_done = req.fields["when_done"].value
721 if "replace" in req.fields:
722 if req.fields["replace"].value.lower() in ("false", "0"):
723 self._replace = False
727 raise RuntimeError("mkdir requires a name")
728 d = self._check_replacement(name)
729 d.addCallback(lambda res: self._node.create_empty_directory(name))
731 return "directory created"
735 raise RuntimeError("set-uri requires a name")
736 if "uri" in req.args:
737 newuri = req.args["uri"][0].strip()
739 newuri = req.fields["uri"].value.strip()
740 d = self._check_replacement(name)
741 d.addCallback(lambda res: self._node.set_uri(name, newuri))
747 # apparently an <input type="hidden" name="name" value="">
748 # won't show up in the resulting encoded form.. the 'name'
749 # field is completely missing. So to allow deletion of an
750 # empty file, we have to pretend that None means ''. The only
751 # downide of this is a slightly confusing error message if
752 # someone does a POST without a name= field. For our own HTML
753 # thisn't a big deal, because we create the 'delete' POST
756 d = self._node.delete(name)
758 return "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)
787 return "thing renamed"
791 if "mutable" in req.fields:
792 contents = req.fields["file"]
793 name = name or contents.filename
797 raise RuntimeError("upload-mutable requires a name")
798 # SDMF: files are small, and we can only upload data.
799 contents.file.seek(0)
800 data = contents.file.read()
801 uploadable = upload.FileHandle(contents.file)
802 d = self._check_replacement(name)
803 d.addCallback(lambda res: self._node.has_child(name))
804 def _checked(present):
806 # modify the existing one instead of creating a new
808 d2 = self._node.get(name)
809 def _got_newnode(newnode):
810 d3 = newnode.replace(data)
811 d3.addCallback(lambda res: newnode.get_uri())
813 d2.addCallback(_got_newnode)
815 d2 = IClient(ctx).create_mutable_file(data)
816 def _uploaded(newnode):
817 d1 = self._node.set_node(name, newnode)
818 d1.addCallback(lambda res: newnode.get_uri())
820 d2.addCallback(_uploaded)
822 d.addCallback(_checked)
824 contents = req.fields["file"]
825 name = name or contents.filename
829 raise RuntimeError("upload requires a name")
830 uploadable = upload.FileHandle(contents.file)
831 d = self._check_replacement(name)
832 d.addCallback(lambda res: self._node.add_file(name, uploadable))
834 return newnode.get_uri()
837 elif t == "overwrite":
838 contents = req.fields["file"]
839 # SDMF: files are small, and we can only upload data.
840 contents.file.seek(0)
841 data = contents.file.read()
842 # TODO: 'name' handling needs review
843 d = defer.succeed(self._node)
844 def _got_child(child_node):
845 child_node.replace(data)
846 return child_node.get_uri()
847 d.addCallback(_got_child)
850 d = self._node.get(name)
851 def _got_child(child_node):
852 d2 = child_node.check()
854 log.msg("checked %s, results %s" % (child_node, res))
856 d2.addCallback(_done)
858 d.addCallback(_got_child)
861 return "BAD t=%s" % t
863 d.addCallback(lambda res: url.URL.fromString(when_done))
864 def _check_replacement(f):
865 # TODO: make this more human-friendly: maybe send them to the
866 # when_done page but with an extra query-arg that will display
867 # the error message in a big box at the top of the page. The
868 # directory page that when_done= usually points to accepts a
869 # result= argument.. use that.
870 f.trap(NoReplacementError)
871 req.setResponseCode(http.CONFLICT)
872 req.setHeader("content-type", "text/plain")
874 d.addErrback(_check_replacement)
877 class DELETEHandler(rend.Page):
878 def __init__(self, node, name):
882 def renderHTTP(self, ctx):
883 req = inevow.IRequest(ctx)
884 d = self._node.delete(self._name)
886 # what should this return??
887 return "%s deleted" % self._name
889 def _trap_missing(f):
891 req.setResponseCode(http.NOT_FOUND)
892 req.setHeader("content-type", "text/plain")
893 return "no such child %s" % self._name
894 d.addErrback(_trap_missing)
897 class PUTHandler(rend.Page):
898 def __init__(self, node, path, t, localfile, localdir, replace):
902 self._localfile = localfile
903 self._localdir = localdir
904 self._replace = replace
906 def renderHTTP(self, ctx):
907 req = inevow.IRequest(ctx)
909 localfile = self._localfile
910 localdir = self._localdir
912 # we must traverse the path, creating new directories as necessary
913 d = self._get_or_create_directories(self._node, self._path[:-1])
914 name = self._path[-1]
915 d.addCallback(self._check_replacement, name, self._replace)
918 d.addCallback(self._upload_localfile, localfile, name)
921 d.addCallback(self._get_or_create_directories, self._path[-1:])
922 d.addCallback(self._upload_localdir, localdir)
924 raise RuntimeError("t=upload requires localfile= or localdir=")
926 d.addCallback(self._attach_uri, req.content, name)
928 d.addCallback(self._mkdir, name)
930 d.addCallback(self._upload_file, req.content, name)
931 def _check_blocking(f):
932 f.trap(BlockingFileError)
933 req.setResponseCode(http.BAD_REQUEST)
934 req.setHeader("content-type", "text/plain")
936 d.addErrback(_check_blocking)
937 def _check_replacement(f):
938 f.trap(NoReplacementError)
939 req.setResponseCode(http.CONFLICT)
940 req.setHeader("content-type", "text/plain")
942 d.addErrback(_check_replacement)
945 def _get_or_create_directories(self, node, path):
946 if not IDirectoryNode.providedBy(node):
947 # unfortunately it is too late to provide the name of the
948 # blocking directory in the error message.
949 raise BlockingFileError("cannot create directory because there "
950 "is a file in the way")
952 return defer.succeed(node)
953 d = node.get(path[0])
954 def _maybe_create(f):
956 return node.create_empty_directory(path[0])
957 d.addErrback(_maybe_create)
958 d.addCallback(self._get_or_create_directories, path[1:])
961 def _check_replacement(self, node, name, replace):
964 d = node.has_child(name)
967 raise NoReplacementError("There was already a child by that "
968 "name, and you asked me to not "
974 def _mkdir(self, node, name):
975 d = node.create_empty_directory(name)
977 return newnode.get_uri()
981 def _upload_file(self, node, contents, name):
982 uploadable = upload.FileHandle(contents)
983 d = node.add_file(name, uploadable)
985 log.msg("webish upload complete")
986 return filenode.get_uri()
990 def _upload_localfile(self, node, localfile, name):
991 uploadable = upload.FileName(localfile)
992 d = node.add_file(name, uploadable)
993 d.addCallback(lambda filenode: filenode.get_uri())
996 def _attach_uri(self, parentnode, contents, name):
997 newuri = contents.read().strip()
998 d = parentnode.set_uri(name, newuri)
1001 d.addCallback(_done)
1004 def _upload_localdir(self, node, localdir):
1005 # build up a list of files to upload
1008 msg = "No files to upload! %s is empty" % localdir
1009 if not os.path.exists(localdir):
1010 msg = "%s doesn't exist!" % localdir
1011 for root, dirs, files in os.walk(localdir):
1012 if root == localdir:
1015 relative_root = root[len(localdir)+1:]
1016 path = tuple(relative_root.split(os.sep))
1018 all_dirs.append(path + (d,))
1020 all_files.append(path + (f,))
1021 d = defer.succeed(msg)
1022 for dir in all_dirs:
1024 d.addCallback(self._makedir, node, dir)
1026 d.addCallback(self._upload_one_file, node, localdir, f)
1029 def _makedir(self, res, node, dir):
1030 d = defer.succeed(None)
1031 # get the parent. As long as os.walk gives us parents before
1032 # children, this ought to work
1033 d.addCallback(lambda res: node.get_child_at_path(dir[:-1]))
1034 # then create the child directory
1035 d.addCallback(lambda parent: parent.create_empty_directory(dir[-1]))
1038 def _upload_one_file(self, res, node, localdir, f):
1039 # get the parent. We can be sure this exists because we already
1040 # went through and created all the directories we require.
1041 localfile = os.path.join(localdir, *f)
1042 d = node.get_child_at_path(f[:-1])
1043 d.addCallback(self._upload_localfile, localfile, f[-1])
1047 class Manifest(rend.Page):
1048 docFactory = getxmlfile("manifest.xhtml")
1049 def __init__(self, dirnode, dirpath):
1050 self._dirnode = dirnode
1051 self._dirpath = dirpath
1053 def dirpath_as_string(self):
1054 return "/" + "/".join(self._dirpath)
1056 def render_title(self, ctx):
1057 return T.title["Manifest of %s" % self.dirpath_as_string()]
1059 def render_header(self, ctx):
1060 return T.p["Manifest of %s" % self.dirpath_as_string()]
1062 def data_items(self, ctx, data):
1063 return self._dirnode.build_manifest()
1065 def render_row(self, ctx, refresh_cap):
1066 ctx.fillSlots("refresh_capability", refresh_cap)
1069 class VDrive(rend.Page):
1071 def __init__(self, node, name):
1075 def get_child_at_path(self, path):
1077 return self.node.get_child_at_path(path)
1078 return defer.succeed(self.node)
1080 def locateChild(self, ctx, segments):
1081 req = inevow.IRequest(ctx)
1085 # when we're pointing at a directory (like /uri/$DIR_URI/my_pix),
1086 # Directory.addSlash causes a redirect to /uri/$DIR_URI/my_pix/,
1087 # which appears here as ['my_pix', '']. This is supposed to hit the
1088 # same Directory as ['my_pix'].
1089 if path and path[-1] == '':
1094 t = req.args["t"][0]
1097 if "localfile" in req.args:
1098 localfile = req.args["localfile"][0]
1099 if localfile != os.path.abspath(localfile):
1100 return NeedAbsolutePathError(), ()
1102 if "localdir" in req.args:
1103 localdir = req.args["localdir"][0]
1104 if localdir != os.path.abspath(localdir):
1105 return NeedAbsolutePathError(), ()
1106 if localfile or localdir:
1107 if not ILocalAccess(ctx).local_access_is_allowed():
1108 return LocalAccessDisabledError(), ()
1109 if req.getHost().host != LOCALHOST:
1110 return NeedLocalhostError(), ()
1111 # TODO: think about clobbering/revealing config files and node secrets
1114 if "replace" in req.args:
1115 if req.args["replace"][0].lower() in ("false", "0"):
1119 # the node must exist, and our operation will be performed on the
1121 d = self.get_child_at_path(path)
1122 def file_or_dir(node):
1123 if (IFileNode.providedBy(node)
1124 or IMutableFileNode.providedBy(node)):
1125 filename = "unknown"
1128 if "filename" in req.args:
1129 filename = req.args["filename"][0]
1132 # write contents to a local file
1133 return LocalFileDownloader(node, localfile), ()
1134 # send contents as the result
1135 return FileDownloader(node, filename), ()
1137 # send contents as the result
1138 return FileDownloader(node, filename), ()
1140 return FileJSONMetadata(node), ()
1142 return FileURI(node), ()
1143 elif t == "readonly-uri":
1144 return FileReadOnlyURI(node), ()
1146 raise RuntimeError("bad t=%s" % t)
1147 elif IDirectoryNode.providedBy(node):
1150 # recursive download to a local directory
1151 return LocalDirectoryDownloader(node, localdir), ()
1152 raise RuntimeError("t=download requires localdir=")
1154 # send an HTML representation of the directory
1155 return Directory(self.name, node, path), ()
1157 return DirectoryJSONMetadata(node), ()
1159 return DirectoryURI(node), ()
1160 elif t == "readonly-uri":
1161 return DirectoryReadonlyURI(node), ()
1162 elif t == "manifest":
1163 return Manifest(node, path), ()
1164 elif t == 'rename-form':
1165 return RenameForm(self.name, node, path), ()
1167 raise RuntimeError("bad t=%s" % t)
1169 raise RuntimeError("unknown node type")
1170 d.addCallback(file_or_dir)
1171 elif method == "POST":
1172 # the node must exist, and our operation will be performed on the
1174 d = self.get_child_at_path(path)
1176 return POSTHandler(node, replace), ()
1178 elif method == "DELETE":
1179 # the node must exist, and our operation will be performed on its
1181 assert path # you can't delete the root
1182 d = self.get_child_at_path(path[:-1])
1184 return DELETEHandler(node, path[-1]), ()
1186 elif method in ("PUT",):
1187 # the node may or may not exist, and our operation may involve
1188 # all the ancestors of the node.
1189 return PUTHandler(self.node, path, t, localfile, localdir, replace), ()
1191 return rend.NotFound
1192 def _trap_KeyError(f):
1194 return rend.FourOhFour(), ()
1195 d.addErrback(_trap_KeyError)
1198 class URIPUTHandler(rend.Page):
1199 def renderHTTP(self, ctx):
1200 req = inevow.IRequest(ctx)
1201 assert req.method == "PUT"
1205 t = req.args["t"][0]
1208 # "PUT /uri", to create an unlinked file. This is like PUT but
1209 # without the associated set_uri.
1210 uploadable = upload.FileHandle(req.content)
1211 d = IClient(ctx).upload(uploadable)
1212 # that fires with the URI of the new file
1216 # "PUT /uri?t=mkdir", to create an unlinked directory.
1217 d = IClient(ctx).create_empty_dirnode()
1218 d.addCallback(lambda dirnode: dirnode.get_uri())
1221 req.setResponseCode(http.BAD_REQUEST)
1222 req.setHeader("content-type", "text/plain")
1223 return "/uri only accepts PUT and PUT?t=mkdir"
1225 class URIPOSTHandler(rend.Page):
1226 def renderHTTP(self, ctx):
1227 req = inevow.IRequest(ctx)
1228 assert req.method == "POST"
1232 t = req.args["t"][0]
1234 if t in ("", "upload"):
1235 # "POST /uri", to create an unlinked file.
1236 fileobj = req.fields["file"].file
1237 uploadable = upload.FileHandle(fileobj)
1238 d = IClient(ctx).upload(uploadable)
1239 # that fires with the URI of the new file
1243 # "PUT /uri?t=mkdir", to create an unlinked directory.
1244 d = IClient(ctx).create_empty_dirnode()
1245 d.addCallback(lambda dirnode: dirnode.get_uri())
1248 req.setResponseCode(http.BAD_REQUEST)
1249 req.setHeader("content-type", "text/plain")
1250 return "/uri accepts only PUT, PUT?t=mkdir, POST?t=upload" # XXX check this -- what about POST?t=mkdir?
1253 class Root(rend.Page):
1256 docFactory = getxmlfile("welcome.xhtml")
1258 def locateChild(self, ctx, segments):
1259 client = IClient(ctx)
1260 req = inevow.IRequest(ctx)
1262 segments = list(segments) # XXX HELP I AM YUCKY!
1263 while segments and not segments[-1]:
1267 segments = tuple(segments)
1269 if segments[0] == "uri":
1270 if len(segments) == 1 or segments[1] == '':
1271 if "uri" in req.args:
1272 uri = req.args["uri"][0]
1273 there = url.URL.fromContext(ctx)
1274 there = there.clear("uri")
1275 there = there.child("uri").child(uri)
1277 if len(segments) == 1:
1279 if req.method == "PUT":
1280 # either "PUT /uri" to create an unlinked file, or
1281 # "PUT /uri?t=mkdir" to create an unlinked directory
1282 return URIPUTHandler(), ()
1283 elif req.method == "POST":
1284 # "POST /uri?t=upload&file=newfile" to upload an unlinked
1286 return URIPOSTHandler(), ()
1287 if len(segments) < 2:
1288 return rend.NotFound
1290 d = defer.maybeDeferred(client.create_node_from_uri, uri)
1291 d.addCallback(lambda node: VDrive(node, "from-uri"))
1292 d.addCallback(lambda vd: vd.locateChild(ctx, segments[2:]))
1293 def _trap_KeyError(f):
1295 return rend.FourOhFour(), ()
1296 d.addErrback(_trap_KeyError)
1298 elif segments[0] == "xmlrpc":
1299 raise NotImplementedError()
1300 return rend.Page.locateChild(self, ctx, segments)
1302 child_webform_css = webform.defaultCSS
1303 child_tahoe_css = nevow_File(util.sibpath(__file__, "web/tahoe.css"))
1305 child_provisioning = provisioning.ProvisioningTool()
1307 def data_version(self, ctx, data):
1308 return get_package_versions_string()
1310 def data_my_nodeid(self, ctx, data):
1311 return b32encode(IClient(ctx).nodeid).lower()
1312 def data_introducer_furl(self, ctx, data):
1313 return IClient(ctx).introducer_furl
1314 def data_connected_to_introducer(self, ctx, data):
1315 if IClient(ctx).connected_to_introducer():
1318 def data_num_peers(self, ctx, data):
1319 #client = inevow.ISite(ctx)._client
1320 client = IClient(ctx)
1321 return len(list(client.get_all_peerids()))
1323 def data_peers(self, ctx, data):
1325 client = IClient(ctx)
1326 for nodeid in sorted(client.get_all_peerids()):
1327 row = (b32encode(nodeid).lower(),)
1331 def render_row(self, ctx, data):
1333 ctx.fillSlots("peerid", nodeid_a)
1336 def render_private_vdrive(self, ctx, data):
1337 basedir = IClient(ctx).basedir
1338 start_html = os.path.abspath(os.path.join(basedir, "private", "start.html"))
1339 basedir = IClient(ctx).basedir
1340 if os.path.exists(start_html) and os.path.exists(os.path.join(basedir, "private", "my_private_dir.cap")):
1341 return T.p["To view your personal private non-shared filestore, ",
1342 "use this browser to open the following file from ",
1343 "your local filesystem:",
1346 return T.p["personal vdrive not available."]
1348 # this is a form where users can download files by URI
1350 def render_download_form(self, ctx, data):
1351 form = T.form(action="uri", method="get",
1352 enctype="multipart/form-data")[
1354 T.legend(class_="freeform-form-label")["Download a file"],
1355 "URI of file to download: ",
1356 T.input(type="text", name="uri"), " ",
1357 "Filename to download as: ",
1358 T.input(type="text", name="filename"), " ",
1359 T.input(type="submit", value="Download"),
1365 implements(ILocalAccess)
1367 self.local_access = False
1368 def local_access_is_allowed(self):
1369 return self.local_access
1371 class WebishServer(service.MultiService):
1374 def __init__(self, webport):
1375 service.MultiService.__init__(self)
1376 self.webport = webport
1378 self.site = site = appserver.NevowSite(self.root)
1379 self.site.requestFactory = MyRequest
1380 self.allow_local = LocalAccess()
1381 self.site.remember(self.allow_local, ILocalAccess)
1382 s = strports.service(webport, site)
1383 s.setServiceParent(self)
1384 self.listener = s # stash it so the tests can query for the portnum
1385 self._started = defer.Deferred()
1387 def allow_local_access(self, enable=True):
1388 self.allow_local.local_access = enable
1390 def startService(self):
1391 service.MultiService.startService(self)
1392 # to make various services available to render_* methods, we stash a
1393 # reference to the client on the NevowSite. This will be available by
1394 # adapting the 'context' argument to a special marker interface named
1396 self.site.remember(self.parent, IClient)
1397 # I thought you could do the same with an existing interface, but
1398 # apparently 'ISite' does not exist
1399 #self.site._client = self.parent
1400 self._started.callback(None)
1402 def create_start_html(self, private_uri, startfile, nodeurl_file):
1404 Returns a deferred that eventually fires once the start.html page has
1407 self._started.addCallback(self._create_start_html, private_uri, startfile, nodeurl_file)
1408 return self._started
1410 def _create_start_html(self, dummy, private_uri, startfile, nodeurl_file):
1411 f = open(startfile, "w")
1412 template = open(util.sibpath(__file__, "web/start.html"), "r").read()
1413 # what is our webport?
1415 if isinstance(s, internet.TCPServer):
1416 base_url = "http://localhost:%d" % s._port.getHost().port
1417 elif isinstance(s, internet.SSLServer):
1418 base_url = "https://localhost:%d" % s._port.getHost().port
1420 base_url = "UNKNOWN" # this will break the href
1421 # TODO: emit a start.html that explains that we don't know
1422 # how to create a suitable URL
1424 link_to_private_uri = "View <a href=\"%s/uri/%s\">your personal private non-shared filestore</a>." % (base_url, private_uri)
1425 fields = {"link_to_private_uri": link_to_private_uri,
1426 "base_url": base_url,
1429 fields = {"link_to_private_uri": "",
1430 "base_url": base_url,
1432 f.write(template % fields)
1435 f = open(nodeurl_file, "w")
1436 # this file is world-readable
1437 f.write(base_url + "\n")