3 from twisted.application import service, strports, internet
4 from twisted.web import static, resource, server, html, http
5 from twisted.internet import defer, address
6 from twisted.internet.interfaces import IConsumer
7 from nevow import inevow, rend, loaders, appserver, url, tags as T
8 from nevow.static import File as nevow_File # TODO: merge with static.File?
9 from allmydata.util import fileutil, idlib, observer, log
11 from allmydata.interfaces import IDownloadTarget, IDirectoryNode, IFileNode, \
13 import allmydata # to display import path
14 from allmydata import download
15 from allmydata.upload import FileHandle, FileName
16 from allmydata import provisioning
17 from allmydata import get_package_versions_string
18 from zope.interface import implements, Interface
20 from formless import webform
21 from foolscap.eventual import fireEventually
23 from nevow.util import resource_filename
25 from allmydata.web import status
26 from allmydata.web.common import IClient
29 return loaders.xmlfile(resource_filename('allmydata.web', '%s' % name))
31 class ILocalAccess(Interface):
32 def local_access_is_allowed():
33 """Return True if t=upload&localdir= is allowed, giving anyone who
34 can talk to the webserver control over the local (disk) filesystem."""
36 def boolean_of_arg(arg):
37 assert arg.lower() in ("true", "t", "1", "false", "f", "0", "on", "off")
38 return arg.lower() in ("true", "t", "1", "on")
40 def get_arg(req, argname, default=None, multiple=False):
41 """Extract an argument from either the query args (req.args) or the form
42 body fields (req.fields). If multiple=False, this returns a single value
43 (or the default, which defaults to None), and the query args take
44 precedence. If multiple=True, this returns a tuple of arguments (possibly
45 empty), starting with all those in the query args.
48 if argname in req.args:
49 results.extend(req.args[argname])
50 if req.fields and argname in req.fields:
51 results.append(req.fields[argname].value)
58 # we must override twisted.web.http.Request.requestReceived with a version
59 # that doesn't use cgi.parse_multipart() . Since we actually use Nevow, we
60 # override the nevow-specific subclass, nevow.appserver.NevowRequest . This
61 # is an exact copy of twisted.web.http.Request (from SVN HEAD on 10-Aug-2007)
62 # that modifies the way form arguments are parsed. Note that this sort of
63 # surgery may induce a dependency upon a particular version of twisted.web
65 parse_qs = http.parse_qs
66 class MyRequest(appserver.NevowRequest):
68 def requestReceived(self, command, path, version):
69 """Called by channel when all data has been received.
71 This method is not intended for users.
73 self.content.seek(0,0)
77 self.method, self.uri = command, path
78 self.clientproto = version
79 x = self.uri.split('?', 1)
84 self.path, argstring = x
85 self.args = parse_qs(argstring, 1)
87 # cache the client and server information, we'll need this later to be
88 # serialized and sent with the request so CGIs will work remotely
89 self.client = self.channel.transport.getPeer()
90 self.host = self.channel.transport.getHost()
92 # Argument processing.
94 ## The original twisted.web.http.Request.requestReceived code parsed the
95 ## content and added the form fields it found there to self.args . It
96 ## did this with cgi.parse_multipart, which holds the arguments in RAM
97 ## and is thus unsuitable for large file uploads. The Nevow subclass
98 ## (nevow.appserver.NevowRequest) uses cgi.FieldStorage instead (putting
99 ## the results in self.fields), which is much more memory-efficient.
100 ## Since we know we're using Nevow, we can anticipate these arguments
101 ## appearing in self.fields instead of self.args, and thus skip the
102 ## parse-content-into-self.args step.
105 ## ctype = self.getHeader('content-type')
106 ## if self.method == "POST" and ctype:
107 ## mfd = 'multipart/form-data'
108 ## key, pdict = cgi.parse_header(ctype)
109 ## if key == 'application/x-www-form-urlencoded':
110 ## args.update(parse_qs(self.content.read(), 1))
113 ## args.update(cgi.parse_multipart(self.content, pdict))
114 ## except KeyError, e:
115 ## if e.args[0] == 'content-disposition':
116 ## # Parse_multipart can't cope with missing
117 ## # content-dispostion headers in multipart/form-data
118 ## # parts, so we catch the exception and tell the client
119 ## # it was a bad request.
120 ## self.channel.transport.write(
121 ## "HTTP/1.1 400 Bad Request\r\n\r\n")
122 ## self.channel.transport.loseConnection()
129 # we build up a log string that hides most of the cap, to preserve
130 # user privacy. We retain the query args so we can identify things
131 # like t=json. Then we send it to the flog. We make no attempt to
132 # match apache formatting. TODO: when we move to DSA dirnodes and
133 # shorter caps, consider exposing a few characters of the cap, or
134 # maybe a few characters of its hash.
135 x = self.uri.split("?", 1)
142 # there is a form handler which redirects POST /uri?uri=FOO into
143 # GET /uri/FOO so folks can paste in non-HTTP-prefixed uris. Make
144 # sure we censor these too.
145 if queryargs.startswith("uri="):
146 queryargs = "[uri=CENSORED]"
147 queryargs = "?" + queryargs
148 if path.startswith("/uri"):
149 path = "/uri/[CENSORED].."
150 uri = path + queryargs
152 log.msg(format="web: %(clientip)s %(method)s %(uri)s %(code)s %(length)s",
153 clientip=self.getClientIP(),
157 length=(self.sentLength or "-"),
158 facility="tahoe.webish",
159 level=log.OPERATIONAL,
162 class Directory(rend.Page):
164 docFactory = getxmlfile("directory.xhtml")
166 def __init__(self, rootname, dirnode, dirpath):
167 self._rootname = rootname
168 self._dirnode = dirnode
169 self._dirpath = dirpath
171 def dirpath_as_string(self):
172 return "/" + "/".join(self._dirpath)
174 def render_title(self, ctx, data):
175 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
177 def render_header(self, ctx, data):
178 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
179 num_dirs = len(parent_directories)
181 header = ["Directory '"]
182 for i,d in enumerate(parent_directories):
183 upness = num_dirs - i - 1
185 link = "/".join( ("..",) * upness )
188 header.append(T.a(href=link)[d])
193 if self._dirnode.is_readonly():
194 header.append(" (readonly)")
196 return ctx.tag[header]
198 def render_welcome(self, ctx, data):
199 depth = len(self._dirpath) + 2
200 link = "/".join([".."] * depth)
201 return T.div[T.a(href=link)["Return to Welcome page"]]
203 def data_children(self, ctx, data):
204 d = self._dirnode.list()
205 d.addCallback(lambda dict: sorted(dict.items()))
206 def _stall_some(items):
207 # Deferreds don't optimize out tail recursion, and the way
208 # Nevow's flattener handles Deferreds doesn't take this into
209 # account. As a result, large lists of Deferreds that fire in the
210 # same turn (i.e. the output of defer.succeed) will cause a stack
211 # overflow. To work around this, we insert a turn break after
212 # every 100 items, using foolscap's fireEventually(). This gives
213 # the stack a chance to be popped. It would also work to put
214 # every item in its own turn, but that'd be a lot more
215 # inefficient. This addresses ticket #237, for which I was never
216 # able to create a failing unit test.
218 for i,item in enumerate(items):
220 output.append(fireEventually(item))
224 d.addCallback(_stall_some)
227 def render_row(self, ctx, data):
228 name, (target, metadata) = data
229 name = name.encode("utf-8")
230 assert not isinstance(name, unicode)
232 if self._dirnode.is_readonly():
236 # this creates a button which will cause our child__delete method
237 # to be invoked, which deletes the file and then redirects the
238 # browser back to this directory
239 delete = T.form(action=url.here, method="post")[
240 T.input(type='hidden', name='t', value='delete'),
241 T.input(type='hidden', name='name', value=name),
242 T.input(type='hidden', name='when_done', value=url.here),
243 T.input(type='submit', value='del', name="del"),
246 rename = T.form(action=url.here, method="get")[
247 T.input(type='hidden', name='t', value='rename-form'),
248 T.input(type='hidden', name='name', value=name),
249 T.input(type='hidden', name='when_done', value=url.here),
250 T.input(type='submit', value='rename', name="rename"),
253 ctx.fillSlots("delete", delete)
254 ctx.fillSlots("rename", rename)
255 check = T.form(action=url.here, method="post")[
256 T.input(type='hidden', name='t', value='check'),
257 T.input(type='hidden', name='name', value=name),
258 T.input(type='hidden', name='when_done', value=url.here),
259 T.input(type='submit', value='check', name="check"),
261 ctx.fillSlots("overwrite", self.build_overwrite(ctx, (name, target)))
262 ctx.fillSlots("check", check)
265 TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
266 if "ctime" in metadata:
267 ctime = time.strftime(TIME_FORMAT,
268 time.localtime(metadata["ctime"]))
269 times.append("c: " + ctime)
270 if "mtime" in metadata:
271 mtime = time.strftime(TIME_FORMAT,
272 time.localtime(metadata["mtime"]))
275 times.append("m: " + mtime)
276 ctx.fillSlots("times", times)
279 # build the base of the uri_link link url
280 uri_link = "/uri/" + urllib.quote(target.get_uri())
282 assert (IFileNode.providedBy(target)
283 or IDirectoryNode.providedBy(target)
284 or IMutableFileNode.providedBy(target)), target
286 if IMutableFileNode.providedBy(target):
289 # add the filename to the uri_link url
290 uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
292 # to prevent javascript in displayed .html files from stealing a
293 # secret directory URI from the URL, send the browser to a URI-based
294 # page that doesn't know about the directory at all
295 #dlurl = urllib.quote(name)
298 ctx.fillSlots("filename",
299 T.a(href=dlurl)[html.escape(name)])
300 ctx.fillSlots("type", "SSK")
302 ctx.fillSlots("size", "?")
304 text_plain_link = uri_link + "?filename=foo.txt"
305 text_plain_tag = T.a(href=text_plain_link)["text/plain"]
307 elif IFileNode.providedBy(target):
310 # add the filename to the uri_link url
311 uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
313 # to prevent javascript in displayed .html files from stealing a
314 # secret directory URI from the URL, send the browser to a URI-based
315 # page that doesn't know about the directory at all
316 #dlurl = urllib.quote(name)
319 ctx.fillSlots("filename",
320 T.a(href=dlurl)[html.escape(name)])
321 ctx.fillSlots("type", "FILE")
323 ctx.fillSlots("size", target.get_size())
325 text_plain_link = uri_link + "?filename=foo.txt"
326 text_plain_tag = T.a(href=text_plain_link)["text/plain"]
328 elif IDirectoryNode.providedBy(target):
330 ctx.fillSlots("filename",
331 T.a(href=uri_link)[html.escape(name)])
332 if target.is_readonly():
336 ctx.fillSlots("type", dirtype)
337 ctx.fillSlots("size", "-")
338 text_plain_tag = None
340 childdata = [T.a(href="%s?t=json" % name)["JSON"], ", ",
341 T.a(href="%s?t=uri" % name)["URI"], ", ",
342 T.a(href="%s?t=readonly-uri" % name)["readonly-URI"],
345 childdata.extend([", ", text_plain_tag])
347 ctx.fillSlots("data", childdata)
350 checker = IClient(ctx).getServiceNamed("checker")
354 d = defer.maybeDeferred(checker.checker_results_for,
355 target.get_verifier())
356 def _got(checker_results):
357 recent_results = reversed(checker_results[-5:])
358 if IFileNode.providedBy(target):
360 ", ".join(["%d/%d" % (found, needed)
362 (needed, total, found, sharemap))
363 in recent_results]) +
365 elif IDirectoryNode.providedBy(target):
367 "".join([{True:"+",False:"-"}[res]
368 for (when, res) in recent_results]) +
371 results = "%d results" % len(checker_results)
377 # TODO: include a link to see more results, including timestamps
378 # TODO: use a sparkline
379 ctx.fillSlots("checker_results", results)
383 def render_forms(self, ctx, data):
384 if self._dirnode.is_readonly():
385 return T.div["No upload forms: directory is read-only"]
386 mkdir = T.form(action=".", method="post",
387 enctype="multipart/form-data")[
389 T.input(type="hidden", name="t", value="mkdir"),
390 T.input(type="hidden", name="when_done", value=url.here),
391 T.legend(class_="freeform-form-label")["Create a new directory"],
392 "New directory name: ",
393 T.input(type="text", name="name"), " ",
394 T.input(type="submit", value="Create"),
397 upload = T.form(action=".", method="post",
398 enctype="multipart/form-data")[
400 T.input(type="hidden", name="t", value="upload"),
401 T.input(type="hidden", name="when_done", value=url.here),
402 T.legend(class_="freeform-form-label")["Upload a file to this directory"],
403 "Choose a file to upload: ",
404 T.input(type="file", name="file", class_="freeform-input-file"),
406 T.input(type="submit", value="Upload"),
408 T.input(type="checkbox", name="mutable"),
411 mount = T.form(action=".", method="post",
412 enctype="multipart/form-data")[
414 T.input(type="hidden", name="t", value="uri"),
415 T.input(type="hidden", name="when_done", value=url.here),
416 T.legend(class_="freeform-form-label")["Attach a file or directory"
420 T.input(type="text", name="name"), " ",
421 "URI of new child: ",
422 T.input(type="text", name="uri"), " ",
423 T.input(type="submit", value="Attach"),
425 return [T.div(class_="freeform-form")[mkdir],
426 T.div(class_="freeform-form")[upload],
427 T.div(class_="freeform-form")[mount],
430 def build_overwrite(self, ctx, data):
432 if IMutableFileNode.providedBy(target) and not target.is_readonly():
433 action="/uri/" + urllib.quote(target.get_uri())
434 overwrite = T.form(action=action, method="post",
435 enctype="multipart/form-data")[
437 T.input(type="hidden", name="t", value="overwrite"),
438 T.input(type='hidden', name='name', value=name),
439 T.input(type='hidden', name='when_done', value=url.here),
440 T.legend(class_="freeform-form-label")["Overwrite"],
442 T.input(type="file", name="file", class_="freeform-input-file"),
444 T.input(type="submit", value="Overwrite")
446 return [T.div(class_="freeform-form")[overwrite],]
450 def render_results(self, ctx, data):
451 req = inevow.IRequest(ctx)
452 return get_arg(req, "results", "")
454 class WebDownloadTarget:
455 implements(IDownloadTarget, IConsumer)
456 def __init__(self, req, content_type, content_encoding, save_to_file):
458 self._content_type = content_type
459 self._content_encoding = content_encoding
461 self._producer = None
462 self._save_to_file = save_to_file
464 def registerProducer(self, producer, streaming):
465 self._req.registerProducer(producer, streaming)
466 def unregisterProducer(self):
467 self._req.unregisterProducer()
469 def open(self, size):
471 self._req.setHeader("content-type", self._content_type)
472 if self._content_encoding:
473 self._req.setHeader("content-encoding", self._content_encoding)
474 self._req.setHeader("content-length", str(size))
475 if self._save_to_file is not None:
476 # tell the browser to save the file rather display it
477 # TODO: quote save_to_file properly
478 filename = self._save_to_file.encode("utf-8")
479 self._req.setHeader("content-disposition",
480 'attachment; filename="%s"'
483 def write(self, data):
484 self._req.write(data)
490 # The content-type is already set, and the response code
491 # has already been sent, so we can't provide a clean error
492 # indication. We can emit text (which a browser might interpret
493 # as something else), and if we sent a Size header, they might
494 # notice that we've truncated the data. Keep the error message
495 # small to improve the chances of having our error response be
496 # shorter than the intended results.
498 # We don't have a lot of options, unfortunately.
499 self._req.write("problem during download\n")
501 # We haven't written anything yet, so we can provide a sensible
504 msg.replace("\n", "|")
505 self._req.setResponseCode(http.GONE, msg)
506 self._req.setHeader("content-type", "text/plain")
507 # TODO: HTML-formatted exception?
508 self._req.write(str(why))
511 def register_canceller(self, cb):
516 class FileDownloader(resource.Resource):
517 def __init__(self, filenode, name):
518 assert (IFileNode.providedBy(filenode)
519 or IMutableFileNode.providedBy(filenode))
520 self._filenode = filenode
523 def render(self, req):
524 gte = static.getTypeAndEncoding
525 ctype, encoding = gte(self._name,
526 static.File.contentTypes,
527 static.File.contentEncodings,
528 defaultType="text/plain")
530 if get_arg(req, "save", False):
531 # TODO: make the API specification clear: should "save=" or
532 # "save=false" count?
533 save_to_file = self._name
534 wdt = WebDownloadTarget(req, ctype, encoding, save_to_file)
535 d = self._filenode.download(wdt)
536 # exceptions during download are handled by the WebDownloadTarget
537 d.addErrback(lambda why: None)
538 return server.NOT_DONE_YET
540 class BlockingFileError(Exception):
541 """We cannot auto-create a parent directory, because there is a file in
543 class NoReplacementError(Exception):
544 """There was already a child by that name, and you asked me to not replace it"""
545 class NoLocalDirectoryError(Exception):
546 """The localdir= directory didn't exist"""
548 LOCALHOST = "127.0.0.1"
550 class NeedLocalhostError:
551 implements(inevow.IResource)
553 def renderHTTP(self, ctx):
554 req = inevow.IRequest(ctx)
555 req.setResponseCode(http.FORBIDDEN)
556 req.setHeader("content-type", "text/plain")
557 return "localfile= or localdir= requires a local connection"
559 class NeedAbsolutePathError:
560 implements(inevow.IResource)
562 def renderHTTP(self, ctx):
563 req = inevow.IRequest(ctx)
564 req.setResponseCode(http.FORBIDDEN)
565 req.setHeader("content-type", "text/plain")
566 return "localfile= or localdir= requires an absolute path"
568 class LocalAccessDisabledError:
569 implements(inevow.IResource)
571 def renderHTTP(self, ctx):
572 req = inevow.IRequest(ctx)
573 req.setResponseCode(http.FORBIDDEN)
574 req.setHeader("content-type", "text/plain")
575 return "local file access is disabled"
578 implements(inevow.IResource)
579 def __init__(self, response_code, errmsg):
580 self._response_code = response_code
581 self._errmsg = errmsg
583 def renderHTTP(self, ctx):
584 req = inevow.IRequest(ctx)
585 req.setResponseCode(self._response_code)
586 req.setHeader("content-type", "text/plain")
590 class LocalFileDownloader(resource.Resource):
591 def __init__(self, filenode, local_filename):
592 self._local_filename = local_filename
594 self._filenode = filenode
596 def render(self, req):
597 target = download.FileName(self._local_filename)
598 d = self._filenode.download(target)
600 req.write(self._filenode.get_uri())
603 return server.NOT_DONE_YET
606 class FileJSONMetadata(rend.Page):
607 def __init__(self, filenode):
608 self._filenode = filenode
610 def renderHTTP(self, ctx):
611 req = inevow.IRequest(ctx)
612 req.setHeader("content-type", "text/plain")
613 return self.renderNode(self._filenode)
615 def renderNode(self, filenode):
616 file_uri = filenode.get_uri()
619 'size': filenode.get_size(),
621 return simplejson.dumps(data, indent=1)
623 class FileURI(FileJSONMetadata):
624 def renderNode(self, filenode):
625 file_uri = filenode.get_uri()
628 class FileReadOnlyURI(FileJSONMetadata):
629 def renderNode(self, filenode):
630 if filenode.is_readonly():
631 return filenode.get_uri()
633 return filenode.get_readonly().get_uri()
635 class DirnodeWalkerMixin:
636 """Visit all nodes underneath (and including) the rootnode, one at a
637 time. For each one, call the visitor. The visitor will see the
638 IDirectoryNode before it sees any of the IFileNodes inside. If the
639 visitor returns a Deferred, I do not call the visitor again until it has
643 ## def _walk_if_we_could_use_generators(self, rootnode, rootpath=()):
644 ## # this is what we'd be doing if we didn't have the Deferreds and
645 ## # thus could use generators
646 ## yield rootpath, rootnode
647 ## for childname, childnode in rootnode.list().items():
648 ## childpath = rootpath + (childname,)
649 ## if IFileNode.providedBy(childnode):
650 ## yield childpath, childnode
651 ## elif IDirectoryNode.providedBy(childnode):
652 ## for res in self._walk_if_we_could_use_generators(childnode,
656 def walk(self, rootnode, visitor, rootpath=()):
658 def _listed(listing):
659 return listing.items()
660 d.addCallback(_listed)
661 d.addCallback(self._handle_items, visitor, rootpath)
664 def _handle_items(self, items, visitor, rootpath):
667 childname, (childnode, metadata) = items[0]
668 childpath = rootpath + (childname,)
669 d = defer.maybeDeferred(visitor, childpath, childnode, metadata)
670 if IDirectoryNode.providedBy(childnode):
671 d.addCallback(lambda res: self.walk(childnode, visitor, childpath))
672 d.addCallback(lambda res:
673 self._handle_items(items[1:], visitor, rootpath))
676 class LocalDirectoryDownloader(resource.Resource, DirnodeWalkerMixin):
677 def __init__(self, dirnode, localdir):
678 self._dirnode = dirnode
679 self._localdir = localdir
681 def _handle(self, path, node, metadata):
682 path = tuple([p.encode("utf-8") for p in path])
683 localfile = os.path.join(self._localdir, os.sep.join(path))
684 if IDirectoryNode.providedBy(node):
685 fileutil.make_dirs(localfile)
686 elif IFileNode.providedBy(node):
687 target = download.FileName(localfile)
688 return node.download(target)
690 def render(self, req):
691 d = self.walk(self._dirnode, self._handle)
693 req.setHeader("content-type", "text/plain")
694 return "operation complete"
698 class DirectoryJSONMetadata(rend.Page):
699 def __init__(self, dirnode):
700 self._dirnode = dirnode
702 def renderHTTP(self, ctx):
703 req = inevow.IRequest(ctx)
704 req.setHeader("content-type", "text/plain")
705 return self.renderNode(self._dirnode)
707 def renderNode(self, node):
711 for name, (childnode, metadata) in children.iteritems():
712 if IFileNode.providedBy(childnode):
713 kiduri = childnode.get_uri()
714 kiddata = ("filenode",
716 'size': childnode.get_size(),
717 'metadata': metadata,
720 assert IDirectoryNode.providedBy(childnode), (childnode, children,)
721 kiddata = ("dirnode",
722 {'ro_uri': childnode.get_readonly_uri(),
723 'metadata': metadata,
725 if not childnode.is_readonly():
726 kiddata[1]['rw_uri'] = childnode.get_uri()
728 contents = { 'children': kids,
729 'ro_uri': node.get_readonly_uri(),
731 if not node.is_readonly():
732 contents['rw_uri'] = node.get_uri()
733 data = ("dirnode", contents)
734 return simplejson.dumps(data, indent=1)
738 class DirectoryURI(DirectoryJSONMetadata):
739 def renderNode(self, node):
740 return node.get_uri()
742 class DirectoryReadonlyURI(DirectoryJSONMetadata):
743 def renderNode(self, node):
744 return node.get_readonly_uri()
746 class RenameForm(rend.Page):
748 docFactory = getxmlfile("rename-form.xhtml")
750 def __init__(self, rootname, dirnode, dirpath):
751 self._rootname = rootname
752 self._dirnode = dirnode
753 self._dirpath = dirpath
755 def dirpath_as_string(self):
756 return "/" + "/".join(self._dirpath)
758 def render_title(self, ctx, data):
759 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
761 def render_header(self, ctx, data):
762 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
763 num_dirs = len(parent_directories)
765 header = [ "Rename in directory '",
766 "<%s>/" % self._rootname,
767 "/".join(self._dirpath),
770 if self._dirnode.is_readonly():
771 header.append(" (readonly)")
772 return ctx.tag[header]
774 def render_when_done(self, ctx, data):
775 return T.input(type="hidden", name="when_done", value=url.here)
777 def render_get_name(self, ctx, data):
778 req = inevow.IRequest(ctx)
779 name = get_arg(req, "name", "")
780 ctx.tag.attributes['value'] = name
783 class POSTHandler(rend.Page):
784 def __init__(self, node, replace):
786 self._replace = replace
788 def _check_replacement(self, name):
790 return defer.succeed(None)
791 d = self._node.has_child(name)
794 raise NoReplacementError("There was already a child by that "
795 "name, and you asked me to not "
801 def _POST_mkdir(self, name):
802 d = self._check_replacement(name)
803 d.addCallback(lambda res: self._node.create_empty_directory(name))
804 d.addCallback(lambda res: "directory created")
807 def _POST_uri(self, name, newuri):
808 d = self._check_replacement(name)
809 d.addCallback(lambda res: self._node.set_uri(name, newuri))
810 d.addCallback(lambda res: newuri)
813 def _POST_delete(self, name):
815 # apparently an <input type="hidden" name="name" value="">
816 # won't show up in the resulting encoded form.. the 'name'
817 # field is completely missing. So to allow deletion of an
818 # empty file, we have to pretend that None means ''. The only
819 # downide of this is a slightly confusing error message if
820 # someone does a POST without a name= field. For our own HTML
821 # thisn't a big deal, because we create the 'delete' POST
824 d = self._node.delete(name)
825 d.addCallback(lambda res: "thing deleted")
828 def _POST_rename(self, name, from_name, to_name):
829 d = self._check_replacement(to_name)
830 d.addCallback(lambda res: self._node.get(from_name))
832 uri = child.get_uri()
833 # now actually do the rename
834 return self._node.set_uri(to_name, uri)
835 d.addCallback(add_dest)
837 return self._node.delete(from_name)
838 d.addCallback(rm_src)
839 d.addCallback(lambda res: "thing renamed")
842 def _POST_upload(self, contents, name, mutable, client):
844 # SDMF: files are small, and we can only upload data.
845 contents.file.seek(0)
846 data = contents.file.read()
847 #uploadable = FileHandle(contents.file)
848 d = self._check_replacement(name)
849 d.addCallback(lambda res: self._node.has_child(name))
850 def _checked(present):
852 # modify the existing one instead of creating a new
854 d2 = self._node.get(name)
855 def _got_newnode(newnode):
856 d3 = newnode.replace(data)
857 d3.addCallback(lambda res: newnode.get_uri())
859 d2.addCallback(_got_newnode)
861 d2 = client.create_mutable_file(data)
862 def _uploaded(newnode):
863 d1 = self._node.set_node(name, newnode)
864 d1.addCallback(lambda res: newnode.get_uri())
866 d2.addCallback(_uploaded)
868 d.addCallback(_checked)
870 uploadable = FileHandle(contents.file)
871 d = self._check_replacement(name)
872 d.addCallback(lambda res: self._node.add_file(name, uploadable))
874 return newnode.get_uri()
878 def _POST_overwrite(self, contents):
879 # SDMF: files are small, and we can only upload data.
880 contents.file.seek(0)
881 data = contents.file.read()
882 # TODO: 'name' handling needs review
883 d = defer.succeed(self._node)
884 def _got_child_overwrite(child_node):
885 child_node.replace(data)
886 return child_node.get_uri()
887 d.addCallback(_got_child_overwrite)
890 def _POST_check(self, name):
891 d = self._node.get(name)
892 def _got_child_check(child_node):
893 d2 = child_node.check()
895 log.msg("checked %s, results %s" % (child_node, res),
896 facility="tahoe.webish", level=log.NOISY)
898 d2.addCallback(_done)
900 d.addCallback(_got_child_check)
903 def _POST_set_children(self, children):
905 for name, (file_or_dir, mddict) in children.iteritems():
906 cap = str(mddict.get('rw_uri') or mddict.get('ro_uri'))
907 cs.append((name, cap, mddict.get('metadata')))
909 d = self._node.set_children(cs)
910 d.addCallback(lambda res: "Okay so I did it.")
913 def renderHTTP(self, ctx):
914 req = inevow.IRequest(ctx)
916 t = get_arg(req, "t")
919 charset = get_arg(req, "_charset", "utf-8")
921 name = get_arg(req, "name", None)
922 if name and "/" in name:
923 req.setResponseCode(http.BAD_REQUEST)
924 req.setHeader("content-type", "text/plain")
925 return "name= may not contain a slash"
928 name = name.decode(charset)
929 assert isinstance(name, unicode)
930 # we allow the user to delete an empty-named file, but not to create
931 # them, since that's an easy and confusing mistake to make
933 when_done = get_arg(req, "when_done", None)
934 if not boolean_of_arg(get_arg(req, "replace", "true")):
935 self._replace = False
939 raise RuntimeError("mkdir requires a name")
940 d = self._POST_mkdir(name)
943 raise RuntimeError("set-uri requires a name")
944 newuri = get_arg(req, "uri")
945 assert newuri is not None
946 d = self._POST_uri(name, newuri)
948 d = self._POST_delete(name)
950 from_name = get_arg(req, "from_name")
951 if from_name is not None:
952 from_name = from_name.strip()
953 from_name = from_name.decode(charset)
954 assert isinstance(from_name, unicode)
955 to_name = get_arg(req, "to_name")
956 if to_name is not None:
957 to_name = to_name.strip()
958 to_name = to_name.decode(charset)
959 assert isinstance(to_name, unicode)
960 if not from_name or not to_name:
961 raise RuntimeError("rename requires from_name and to_name")
962 if not IDirectoryNode.providedBy(self._node):
963 raise RuntimeError("rename must only be called on directories")
964 for k,v in [ ('from_name', from_name), ('to_name', to_name) ]:
966 req.setResponseCode(http.BAD_REQUEST)
967 req.setHeader("content-type", "text/plain")
968 return "%s= may not contain a slash" % (k,)
969 d = self._POST_rename(name, from_name, to_name)
971 contents = req.fields["file"]
972 name = name or contents.filename
976 # this prohibts empty, missing, and all-whitespace filenames
977 raise RuntimeError("upload requires a name")
978 name = name.decode(charset)
979 assert isinstance(name, unicode)
980 mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
981 d = self._POST_upload(contents, name, mutable, IClient(ctx))
982 elif t == "overwrite":
983 contents = req.fields["file"]
984 d = self._POST_overwrite(contents)
986 d = self._POST_check(name)
987 elif t == "set_children":
989 body = req.content.read()
991 children = simplejson.loads(body)
992 except ValueError, le:
993 le.args = tuple(le.args + (body,))
994 # TODO test handling of bad JSON
996 d = self._POST_set_children(children)
999 return "BAD t=%s" % t
1001 d.addCallback(lambda res: url.URL.fromString(when_done))
1002 def _check_replacement(f):
1003 # TODO: make this more human-friendly: maybe send them to the
1004 # when_done page but with an extra query-arg that will display
1005 # the error message in a big box at the top of the page. The
1006 # directory page that when_done= usually points to accepts a
1007 # result= argument.. use that.
1008 f.trap(NoReplacementError)
1009 req.setResponseCode(http.CONFLICT)
1010 req.setHeader("content-type", "text/plain")
1012 d.addErrback(_check_replacement)
1015 class DELETEHandler(rend.Page):
1016 def __init__(self, node, name):
1020 def renderHTTP(self, ctx):
1021 req = inevow.IRequest(ctx)
1022 d = self._node.delete(self._name)
1024 # what should this return??
1025 return "%s deleted" % self._name.encode("utf-8")
1026 d.addCallback(_done)
1027 def _trap_missing(f):
1029 req.setResponseCode(http.NOT_FOUND)
1030 req.setHeader("content-type", "text/plain")
1031 return "no such child %s" % self._name.encode("utf-8")
1032 d.addErrback(_trap_missing)
1035 class PUTHandler(rend.Page):
1036 def __init__(self, node, path, t, localfile, localdir, replace):
1040 self._localfile = localfile
1041 self._localdir = localdir
1042 self._replace = replace
1044 def renderHTTP(self, ctx):
1045 req = inevow.IRequest(ctx)
1047 localfile = self._localfile
1048 localdir = self._localdir
1050 if t == "upload" and not (localfile or localdir):
1051 req.setResponseCode(http.BAD_REQUEST)
1052 req.setHeader("content-type", "text/plain")
1053 return "t=upload requires localfile= or localdir="
1055 # we must traverse the path, creating new directories as necessary
1056 d = self._get_or_create_directories(self._node, self._path[:-1])
1057 name = self._path[-1]
1058 d.addCallback(self._check_replacement, name, self._replace)
1061 d.addCallback(self._upload_localfile, localfile, name)
1064 # take the last step
1065 d.addCallback(self._get_or_create_directories, self._path[-1:])
1066 d.addCallback(self._upload_localdir, localdir)
1068 d.addCallback(self._attach_uri, req.content, name)
1070 d.addCallback(self._mkdir, name)
1072 d.addCallback(self._upload_file, req.content, name)
1074 def _transform_error(f):
1075 errors = {BlockingFileError: http.BAD_REQUEST,
1076 NoReplacementError: http.CONFLICT,
1077 NoLocalDirectoryError: http.BAD_REQUEST,
1079 for k,v in errors.items():
1081 req.setResponseCode(v)
1082 req.setHeader("content-type", "text/plain")
1085 d.addErrback(_transform_error)
1088 def _get_or_create_directories(self, node, path):
1089 if not IDirectoryNode.providedBy(node):
1090 # unfortunately it is too late to provide the name of the
1091 # blocking directory in the error message.
1092 raise BlockingFileError("cannot create directory because there "
1093 "is a file in the way")
1095 return defer.succeed(node)
1096 d = node.get(path[0])
1097 def _maybe_create(f):
1099 return node.create_empty_directory(path[0])
1100 d.addErrback(_maybe_create)
1101 d.addCallback(self._get_or_create_directories, path[1:])
1104 def _check_replacement(self, node, name, replace):
1107 d = node.has_child(name)
1110 raise NoReplacementError("There was already a child by that "
1111 "name, and you asked me to not "
1117 def _mkdir(self, node, name):
1118 d = node.create_empty_directory(name)
1120 return newnode.get_uri()
1121 d.addCallback(_done)
1124 def _upload_file(self, node, contents, name):
1125 uploadable = FileHandle(contents)
1126 d = node.add_file(name, uploadable)
1127 def _done(filenode):
1128 log.msg("webish upload complete",
1129 facility="tahoe.webish", level=log.NOISY)
1130 return filenode.get_uri()
1131 d.addCallback(_done)
1134 def _upload_localfile(self, node, localfile, name):
1135 uploadable = FileName(localfile)
1136 d = node.add_file(name, uploadable)
1137 d.addCallback(lambda filenode: filenode.get_uri())
1140 def _attach_uri(self, parentnode, contents, name):
1141 newuri = contents.read().strip()
1142 d = parentnode.set_uri(name, newuri)
1145 d.addCallback(_done)
1148 def _upload_localdir(self, node, localdir):
1149 # build up a list of files to upload. TODO: for now, these files and
1150 # directories must have UTF-8 encoded filenames: anything else will
1151 # cause the upload to break.
1154 msg = "No files to upload! %s is empty" % localdir
1155 if not os.path.exists(localdir):
1156 msg = "%s doesn't exist!" % localdir
1157 raise NoLocalDirectoryError(msg)
1158 for root, dirs, files in os.walk(localdir):
1159 if root == localdir:
1162 relative_root = root[len(localdir)+1:]
1163 path = tuple(relative_root.split(os.sep))
1165 this_dir = path + (d,)
1166 this_dir = tuple([p.decode("utf-8") for p in this_dir])
1167 all_dirs.append(this_dir)
1169 this_file = path + (f,)
1170 this_file = tuple([p.decode("utf-8") for p in this_file])
1171 all_files.append(this_file)
1172 d = defer.succeed(msg)
1173 for dir in all_dirs:
1175 d.addCallback(self._makedir, node, dir)
1177 d.addCallback(self._upload_one_file, node, localdir, f)
1180 def _makedir(self, res, node, dir):
1181 d = defer.succeed(None)
1182 # get the parent. As long as os.walk gives us parents before
1183 # children, this ought to work
1184 d.addCallback(lambda res: node.get_child_at_path(dir[:-1]))
1185 # then create the child directory
1186 d.addCallback(lambda parent: parent.create_empty_directory(dir[-1]))
1189 def _upload_one_file(self, res, node, localdir, f):
1190 # get the parent. We can be sure this exists because we already
1191 # went through and created all the directories we require.
1192 localfile = os.path.join(localdir, *f)
1193 d = node.get_child_at_path(f[:-1])
1194 d.addCallback(self._upload_localfile, localfile, f[-1])
1198 class Manifest(rend.Page):
1199 docFactory = getxmlfile("manifest.xhtml")
1200 def __init__(self, dirnode, dirpath):
1201 self._dirnode = dirnode
1202 self._dirpath = dirpath
1204 def dirpath_as_string(self):
1205 return "/" + "/".join(self._dirpath)
1207 def render_title(self, ctx):
1208 return T.title["Manifest of %s" % self.dirpath_as_string()]
1210 def render_header(self, ctx):
1211 return T.p["Manifest of %s" % self.dirpath_as_string()]
1213 def data_items(self, ctx, data):
1214 return self._dirnode.build_manifest()
1216 def render_row(self, ctx, refresh_cap):
1217 ctx.fillSlots("refresh_capability", refresh_cap)
1221 implements(inevow.IResource)
1222 def renderHTTP(self, ctx):
1223 req = inevow.IRequest(ctx)
1224 req.setResponseCode(http.BAD_REQUEST)
1225 req.setHeader("content-type", "text/plain")
1228 def child_error(text):
1233 class VDrive(rend.Page):
1235 def __init__(self, node, name):
1239 def get_child_at_path(self, path):
1241 return self.node.get_child_at_path(path)
1242 return defer.succeed(self.node)
1244 def locateChild(self, ctx, segments):
1245 req = inevow.IRequest(ctx)
1247 path = tuple([seg.decode("utf-8") for seg in segments])
1249 t = get_arg(req, "t", "")
1250 localfile = get_arg(req, "localfile", None)
1251 if localfile is not None:
1252 if localfile != os.path.abspath(localfile):
1253 return NeedAbsolutePathError(), ()
1254 localdir = get_arg(req, "localdir", None)
1255 if localdir is not None:
1256 if localdir != os.path.abspath(localdir):
1257 return NeedAbsolutePathError(), ()
1258 if localfile or localdir:
1259 if not ILocalAccess(ctx).local_access_is_allowed():
1260 return LocalAccessDisabledError(), ()
1261 if req.getHost().host != LOCALHOST:
1262 return NeedLocalhostError(), ()
1263 # TODO: think about clobbering/revealing config files and node secrets
1265 replace = boolean_of_arg(get_arg(req, "replace", "true"))
1268 # the node must exist, and our operation will be performed on the
1270 d = self.get_child_at_path(path)
1271 def file_or_dir(node):
1272 if (IFileNode.providedBy(node)
1273 or IMutableFileNode.providedBy(node)):
1274 filename = "unknown"
1277 filename = get_arg(req, "filename", filename)
1280 # write contents to a local file
1281 return LocalFileDownloader(node, localfile), ()
1282 # send contents as the result
1283 return FileDownloader(node, filename), ()
1285 # send contents as the result
1286 return FileDownloader(node, filename), ()
1288 return FileJSONMetadata(node), ()
1290 return FileURI(node), ()
1291 elif t == "readonly-uri":
1292 return FileReadOnlyURI(node), ()
1294 return child_error("bad t=%s" % t)
1295 elif IDirectoryNode.providedBy(node):
1298 # recursive download to a local directory
1299 return LocalDirectoryDownloader(node, localdir), ()
1300 return child_error("t=download requires localdir=")
1302 # send an HTML representation of the directory
1303 return Directory(self.name, node, path), ()
1305 return DirectoryJSONMetadata(node), ()
1307 return DirectoryURI(node), ()
1308 elif t == "readonly-uri":
1309 return DirectoryReadonlyURI(node), ()
1310 elif t == "manifest":
1311 return Manifest(node, path), ()
1312 elif t == 'rename-form':
1313 return RenameForm(self.name, node, path), ()
1315 return child_error("bad t=%s" % t)
1317 return child_error("unknown node type")
1318 d.addCallback(file_or_dir)
1319 elif method == "POST":
1320 # the node must exist, and our operation will be performed on the
1322 d = self.get_child_at_path(path)
1323 def _got_POST(node):
1324 return POSTHandler(node, replace), ()
1325 d.addCallback(_got_POST)
1326 elif method == "DELETE":
1327 # the node must exist, and our operation will be performed on its
1329 assert path # you can't delete the root
1330 d = self.get_child_at_path(path[:-1])
1331 def _got_DELETE(node):
1332 return DELETEHandler(node, path[-1]), ()
1333 d.addCallback(_got_DELETE)
1334 elif method in ("PUT",):
1335 # the node may or may not exist, and our operation may involve
1336 # all the ancestors of the node.
1337 return PUTHandler(self.node, path, t, localfile, localdir, replace), ()
1339 return rend.NotFound
1342 class UnlinkedPUTCHKUploader(rend.Page):
1343 def renderHTTP(self, ctx):
1344 req = inevow.IRequest(ctx)
1345 assert req.method == "PUT"
1346 # "PUT /uri", to create an unlinked file. This is like PUT but
1347 # without the associated set_uri.
1349 uploadable = FileHandle(req.content)
1350 d = IClient(ctx).upload(uploadable)
1351 d.addCallback(lambda results: results.uri)
1352 # that fires with the URI of the new file
1355 class UnlinkedPUTSSKUploader(rend.Page):
1356 def renderHTTP(self, ctx):
1357 req = inevow.IRequest(ctx)
1358 assert req.method == "PUT"
1359 # SDMF: files are small, and we can only upload data
1361 data = req.content.read()
1362 d = IClient(ctx).create_mutable_file(data)
1363 d.addCallback(lambda n: n.get_uri())
1366 class UnlinkedPUTCreateDirectory(rend.Page):
1367 def renderHTTP(self, ctx):
1368 req = inevow.IRequest(ctx)
1369 assert req.method == "PUT"
1370 # "PUT /uri?t=mkdir", to create an unlinked directory.
1371 d = IClient(ctx).create_empty_dirnode()
1372 d.addCallback(lambda dirnode: dirnode.get_uri())
1373 # XXX add redirect_to_result
1376 class UnlinkedPOSTCHKUploader(status.UploadResultsRendererMixin, rend.Page):
1377 """'POST /uri', to create an unlinked file."""
1378 docFactory = getxmlfile("upload-results.xhtml")
1380 def __init__(self, client, req):
1381 rend.Page.__init__(self)
1382 # we start the upload now, and distribute notification of its
1383 # completion to render_ methods with an ObserverList
1384 assert req.method == "POST"
1385 self._done = observer.OneShotObserverList()
1386 fileobj = req.fields["file"].file
1387 uploadable = FileHandle(fileobj)
1388 d = client.upload(uploadable)
1389 d.addBoth(self._done.fire)
1391 def renderHTTP(self, ctx):
1392 req = inevow.IRequest(ctx)
1393 when_done = get_arg(req, "when_done", None)
1395 # if when_done= is provided, return a redirect instead of our
1396 # usual upload-results page
1397 d = self._done.when_fired()
1398 d.addCallback(lambda res: url.URL.fromString(when_done))
1400 return rend.Page.renderHTTP(self, ctx)
1402 def upload_results(self):
1403 return self._done.when_fired()
1405 def data_done(self, ctx, data):
1406 d = self.upload_results()
1407 d.addCallback(lambda res: "done!")
1410 def data_uri(self, ctx, data):
1411 d = self.upload_results()
1412 d.addCallback(lambda res: res.uri)
1415 def render_download_link(self, ctx, data):
1416 d = self.upload_results()
1417 d.addCallback(lambda res: T.a(href="/uri/" + urllib.quote(res.uri))
1418 ["/uri/" + res.uri])
1421 class UnlinkedPOSTSSKUploader(rend.Page):
1422 def renderHTTP(self, ctx):
1423 req = inevow.IRequest(ctx)
1424 assert req.method == "POST"
1426 # "POST /uri", to create an unlinked file.
1427 # SDMF: files are small, and we can only upload data
1428 contents = req.fields["file"]
1429 contents.file.seek(0)
1430 data = contents.file.read()
1431 d = IClient(ctx).create_mutable_file(data)
1432 d.addCallback(lambda n: n.get_uri())
1435 class UnlinkedPOSTCreateDirectory(rend.Page):
1436 def renderHTTP(self, ctx):
1437 req = inevow.IRequest(ctx)
1438 assert req.method == "POST"
1440 # "POST /uri?t=mkdir", to create an unlinked directory.
1441 d = IClient(ctx).create_empty_dirnode()
1442 redirect = get_arg(req, "redirect_to_result", "false")
1443 if boolean_of_arg(redirect):
1444 def _then_redir(res):
1445 new_url = "uri/" + urllib.quote(res.get_uri())
1446 req.setResponseCode(http.SEE_OTHER) # 303
1447 req.setHeader('location', new_url)
1450 d.addCallback(_then_redir)
1452 d.addCallback(lambda dirnode: dirnode.get_uri())
1455 class Root(rend.Page):
1458 docFactory = getxmlfile("welcome.xhtml")
1460 def locateChild(self, ctx, segments):
1461 client = IClient(ctx)
1462 req = inevow.IRequest(ctx)
1464 segments = list(segments) # XXX HELP I AM YUCKY!
1465 while segments and not segments[-1]:
1469 segments = tuple(segments)
1471 if segments[0] == "uri":
1472 if len(segments) == 1 or segments[1] == '':
1473 uri = get_arg(req, "uri", None)
1475 there = url.URL.fromContext(ctx)
1476 there = there.clear("uri")
1477 there = there.child("uri").child(uri)
1479 if len(segments) == 1:
1481 if req.method == "PUT":
1482 # either "PUT /uri" to create an unlinked file, or
1483 # "PUT /uri?t=mkdir" to create an unlinked directory
1484 t = get_arg(req, "t", "").strip()
1486 mutable = bool(get_arg(req, "mutable", "").strip())
1488 return UnlinkedPUTSSKUploader(), ()
1490 return UnlinkedPUTCHKUploader(), ()
1492 return UnlinkedPUTCreateDirectory(), ()
1493 errmsg = "/uri only accepts PUT and PUT?t=mkdir"
1494 return WebError(http.BAD_REQUEST, errmsg), ()
1496 elif req.method == "POST":
1497 # "POST /uri?t=upload&file=newfile" to upload an
1498 # unlinked file or "POST /uri?t=mkdir" to create a
1500 t = get_arg(req, "t", "").strip()
1501 if t in ("", "upload"):
1502 mutable = bool(get_arg(req, "mutable", "").strip())
1504 return UnlinkedPOSTSSKUploader(), ()
1506 return UnlinkedPOSTCHKUploader(client, req), ()
1508 return UnlinkedPOSTCreateDirectory(), ()
1509 errmsg = "/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, and POST?t=mkdir"
1510 return WebError(http.BAD_REQUEST, errmsg), ()
1511 if len(segments) < 2:
1512 return rend.NotFound
1514 d = defer.maybeDeferred(client.create_node_from_uri, uri)
1515 d.addCallback(lambda node: VDrive(node, uri))
1516 d.addCallback(lambda vd: vd.locateChild(ctx, segments[2:]))
1517 def _trap_KeyError(f):
1519 return rend.FourOhFour(), ()
1520 d.addErrback(_trap_KeyError)
1522 elif segments[0] == "xmlrpc":
1523 raise NotImplementedError()
1524 return rend.Page.locateChild(self, ctx, segments)
1526 child_webform_css = webform.defaultCSS
1527 child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
1529 child_provisioning = provisioning.ProvisioningTool()
1530 child_status = status.Status()
1532 def data_version(self, ctx, data):
1533 return get_package_versions_string()
1534 def data_import_path(self, ctx, data):
1535 return str(allmydata)
1536 def data_my_nodeid(self, ctx, data):
1537 return idlib.nodeid_b2a(IClient(ctx).nodeid)
1538 def data_storage(self, ctx, data):
1539 client = IClient(ctx)
1541 ss = client.getServiceNamed("storage")
1543 return "Not running"
1544 allocated = ss.allocated_size()
1545 return "about %d bytes allocated" % allocated
1547 def data_introducer_furl(self, ctx, data):
1548 return IClient(ctx).introducer_furl
1549 def data_connected_to_introducer(self, ctx, data):
1550 if IClient(ctx).connected_to_introducer():
1554 def data_helper_furl(self, ctx, data):
1556 uploader = IClient(ctx).getServiceNamed("uploader")
1559 furl, connected = uploader.get_helper_info()
1561 def data_connected_to_helper(self, ctx, data):
1563 uploader = IClient(ctx).getServiceNamed("uploader")
1565 return "no" # we don't even have an Uploader
1566 furl, connected = uploader.get_helper_info()
1571 def data_known_storage_servers(self, ctx, data):
1572 ic = IClient(ctx).introducer_client
1574 for c in ic.get_all_connectors().values()
1575 if c.service_name == "storage"]
1578 def data_connected_storage_servers(self, ctx, data):
1579 ic = IClient(ctx).introducer_client
1580 return len(ic.get_all_connections_for("storage"))
1582 def data_services(self, ctx, data):
1583 ic = IClient(ctx).introducer_client
1584 c = [ (service_name, nodeid, rsc)
1585 for (nodeid, service_name), rsc
1586 in ic.get_all_connectors().items() ]
1590 def render_service_row(self, ctx, data):
1591 (service_name, nodeid, rsc) = data
1592 ctx.fillSlots("peerid", "%s %s" % (idlib.nodeid_b2a(nodeid),
1595 rhost = rsc.remote_host
1596 if nodeid == IClient(ctx).nodeid:
1597 rhost_s = "(loopback)"
1598 elif isinstance(rhost, address.IPv4Address):
1599 rhost_s = "%s:%d" % (rhost.host, rhost.port)
1601 rhost_s = str(rhost)
1602 connected = "Yes: to " + rhost_s
1603 since = rsc.last_connect_time
1606 since = rsc.last_loss_time
1608 TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
1609 ctx.fillSlots("connected", connected)
1610 ctx.fillSlots("since", time.strftime(TIME_FORMAT, time.localtime(since)))
1611 ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
1612 time.localtime(rsc.announcement_time)))
1613 ctx.fillSlots("version", rsc.version)
1614 ctx.fillSlots("service_name", rsc.service_name)
1618 def render_download_form(self, ctx, data):
1619 # this is a form where users can download files by URI
1620 form = T.form(action="uri", method="get",
1621 enctype="multipart/form-data")[
1623 T.legend(class_="freeform-form-label")["Download a file"],
1624 "URI to download: ",
1625 T.input(type="text", name="uri"), " ",
1626 "Filename to download as: ",
1627 T.input(type="text", name="filename"), " ",
1628 T.input(type="submit", value="Download!"),
1632 def render_view_form(self, ctx, data):
1633 # this is a form where users can download files by URI, or jump to a
1635 form = T.form(action="uri", method="get",
1636 enctype="multipart/form-data")[
1638 T.legend(class_="freeform-form-label")["View a file or directory"],
1640 T.input(type="text", name="uri"), " ",
1641 T.input(type="submit", value="View!"),
1645 def render_upload_form(self, ctx, data):
1646 # this is a form where users can upload unlinked files
1647 form = T.form(action="uri", method="post",
1648 enctype="multipart/form-data")[
1650 T.legend(class_="freeform-form-label")["Upload a file"],
1652 T.input(type="file", name="file", class_="freeform-input-file"),
1653 T.input(type="hidden", name="t", value="upload"),
1654 " Mutable?:", T.input(type="checkbox", name="mutable"),
1655 T.input(type="submit", value="Upload!"),
1659 def render_mkdir_form(self, ctx, data):
1660 # this is a form where users can create new directories
1661 form = T.form(action="uri", method="post",
1662 enctype="multipart/form-data")[
1664 T.legend(class_="freeform-form-label")["Create a directory"],
1665 T.input(type="hidden", name="t", value="mkdir"),
1666 T.input(type="hidden", name="redirect_to_result", value="true"),
1667 T.input(type="submit", value="Create Directory!"),
1673 implements(ILocalAccess)
1675 self.local_access = False
1676 def local_access_is_allowed(self):
1677 return self.local_access
1679 class WebishServer(service.MultiService):
1682 def __init__(self, webport, nodeurl_path=None):
1683 service.MultiService.__init__(self)
1684 self.webport = webport
1686 self.site = site = appserver.NevowSite(self.root)
1687 self.site.requestFactory = MyRequest
1688 self.allow_local = LocalAccess()
1689 self.site.remember(self.allow_local, ILocalAccess)
1690 s = strports.service(webport, site)
1691 s.setServiceParent(self)
1692 self.listener = s # stash it so the tests can query for the portnum
1693 self._started = defer.Deferred()
1695 self._started.addCallback(self._write_nodeurl_file, nodeurl_path)
1697 def allow_local_access(self, enable=True):
1698 self.allow_local.local_access = enable
1700 def startService(self):
1701 service.MultiService.startService(self)
1702 # to make various services available to render_* methods, we stash a
1703 # reference to the client on the NevowSite. This will be available by
1704 # adapting the 'context' argument to a special marker interface named
1706 self.site.remember(self.parent, IClient)
1707 # I thought you could do the same with an existing interface, but
1708 # apparently 'ISite' does not exist
1709 #self.site._client = self.parent
1710 self._started.callback(None)
1712 def _write_nodeurl_file(self, junk, nodeurl_path):
1713 # what is our webport?
1715 if isinstance(s, internet.TCPServer):
1716 base_url = "http://localhost:%d" % s._port.getHost().port
1717 elif isinstance(s, internet.SSLServer):
1718 base_url = "https://localhost:%d" % s._port.getHost().port
1722 f = open(nodeurl_path, 'wb')
1723 # this file is world-readable
1724 f.write(base_url + "\n")