3 from twisted.application import service, strports, internet
4 from twisted.web import static, resource, server, html, http
5 from twisted.python import log
6 from twisted.internet import defer, address
7 from twisted.internet.interfaces import IConsumer
8 from nevow import inevow, rend, loaders, appserver, url, tags as T
9 from nevow.static import File as nevow_File # TODO: merge with static.File?
10 from allmydata.util import fileutil, idlib, observer
12 from allmydata.interfaces import IDownloadTarget, IDirectoryNode, IFileNode, \
14 import allmydata # to display import path
15 from allmydata import download
16 from allmydata.upload import FileHandle, FileName
17 from allmydata import provisioning
18 from allmydata import get_package_versions_string
19 from zope.interface import implements, Interface
21 from formless import webform
23 from nevow.util import resource_filename
26 return loaders.xmlfile(resource_filename('allmydata.web', '%s' % name))
28 class IClient(Interface):
30 class ILocalAccess(Interface):
31 def local_access_is_allowed():
32 """Return True if t=upload&localdir= is allowed, giving anyone who
33 can talk to the webserver control over the local (disk) filesystem."""
35 def boolean_of_arg(arg):
36 assert arg.lower() in ("true", "t", "1", "false", "f", "0")
37 return arg.lower() in ("true", "t", "1")
39 def get_arg(req, argname, default=None, multiple=False):
40 """Extract an argument from either the query args (req.args) or the form
41 body fields (req.fields). If multiple=False, this returns a single value
42 (or the default, which defaults to None), and the query args take
43 precedence. If multiple=True, this returns a tuple of arguments (possibly
44 empty), starting with all those in the query args.
47 if argname in req.args:
48 results.extend(req.args[argname])
49 if req.fields and argname in req.fields:
50 results.append(req.fields[argname].value)
57 # we must override twisted.web.http.Request.requestReceived with a version
58 # that doesn't use cgi.parse_multipart() . Since we actually use Nevow, we
59 # override the nevow-specific subclass, nevow.appserver.NevowRequest . This
60 # is an exact copy of twisted.web.http.Request (from SVN HEAD on 10-Aug-2007)
61 # that modifies the way form arguments are parsed. Note that this sort of
62 # surgery may induce a dependency upon a particular version of twisted.web
64 parse_qs = http.parse_qs
65 class MyRequest(appserver.NevowRequest):
67 def requestReceived(self, command, path, version):
68 """Called by channel when all data has been received.
70 This method is not intended for users.
72 self.content.seek(0,0)
76 self.method, self.uri = command, path
77 self.clientproto = version
78 x = self.uri.split('?', 1)
83 self.path, argstring = x
84 self.args = parse_qs(argstring, 1)
86 # cache the client and server information, we'll need this later to be
87 # serialized and sent with the request so CGIs will work remotely
88 self.client = self.channel.transport.getPeer()
89 self.host = self.channel.transport.getHost()
91 # Argument processing.
93 ## The original twisted.web.http.Request.requestReceived code parsed the
94 ## content and added the form fields it found there to self.args . It
95 ## did this with cgi.parse_multipart, which holds the arguments in RAM
96 ## and is thus unsuitable for large file uploads. The Nevow subclass
97 ## (nevow.appserver.NevowRequest) uses cgi.FieldStorage instead (putting
98 ## the results in self.fields), which is much more memory-efficient.
99 ## Since we know we're using Nevow, we can anticipate these arguments
100 ## appearing in self.fields instead of self.args, and thus skip the
101 ## parse-content-into-self.args step.
104 ## ctype = self.getHeader('content-type')
105 ## if self.method == "POST" and ctype:
106 ## mfd = 'multipart/form-data'
107 ## key, pdict = cgi.parse_header(ctype)
108 ## if key == 'application/x-www-form-urlencoded':
109 ## args.update(parse_qs(self.content.read(), 1))
112 ## args.update(cgi.parse_multipart(self.content, pdict))
113 ## except KeyError, e:
114 ## if e.args[0] == 'content-disposition':
115 ## # Parse_multipart can't cope with missing
116 ## # content-dispostion headers in multipart/form-data
117 ## # parts, so we catch the exception and tell the client
118 ## # it was a bad request.
119 ## self.channel.transport.write(
120 ## "HTTP/1.1 400 Bad Request\r\n\r\n")
121 ## self.channel.transport.loseConnection()
127 class Directory(rend.Page):
129 docFactory = getxmlfile("directory.xhtml")
131 def __init__(self, rootname, dirnode, dirpath):
132 self._rootname = rootname
133 self._dirnode = dirnode
134 self._dirpath = dirpath
136 def dirpath_as_string(self):
137 return "/" + "/".join(self._dirpath)
139 def render_title(self, ctx, data):
140 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
142 def render_header(self, ctx, data):
143 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
144 num_dirs = len(parent_directories)
146 header = ["Directory '"]
147 for i,d in enumerate(parent_directories):
148 upness = num_dirs - i - 1
150 link = "/".join( ("..",) * upness )
153 header.append(T.a(href=link)[d])
158 if self._dirnode.is_readonly():
159 header.append(" (readonly)")
161 return ctx.tag[header]
163 def render_welcome(self, ctx, data):
164 depth = len(self._dirpath) + 2
165 link = "/".join([".."] * depth)
166 return T.div[T.a(href=link)["Return to Welcome page"]]
168 def data_children(self, ctx, data):
169 d = self._dirnode.list()
170 d.addCallback(lambda dict: sorted(dict.items()))
173 def render_row(self, ctx, data):
174 name, (target, metadata) = data
176 if self._dirnode.is_readonly():
180 # this creates a button which will cause our child__delete method
181 # to be invoked, which deletes the file and then redirects the
182 # browser back to this directory
183 delete = T.form(action=url.here, method="post")[
184 T.input(type='hidden', name='t', value='delete'),
185 T.input(type='hidden', name='name', value=name),
186 T.input(type='hidden', name='when_done', value=url.here),
187 T.input(type='submit', value='del', name="del"),
190 rename = T.form(action=url.here, method="get")[
191 T.input(type='hidden', name='t', value='rename-form'),
192 T.input(type='hidden', name='name', value=name),
193 T.input(type='hidden', name='when_done', value=url.here),
194 T.input(type='submit', value='rename', name="rename"),
197 ctx.fillSlots("delete", delete)
198 ctx.fillSlots("rename", rename)
199 check = T.form(action=url.here, method="post")[
200 T.input(type='hidden', name='t', value='check'),
201 T.input(type='hidden', name='name', value=name),
202 T.input(type='hidden', name='when_done', value=url.here),
203 T.input(type='submit', value='check', name="check"),
205 ctx.fillSlots("overwrite", self.build_overwrite(ctx, (name, target)))
206 ctx.fillSlots("check", check)
209 TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
210 if "ctime" in metadata:
211 ctime = time.strftime(TIME_FORMAT,
212 time.localtime(metadata["ctime"]))
213 times.append("c: " + ctime)
214 if "mtime" in metadata:
215 mtime = time.strftime(TIME_FORMAT,
216 time.localtime(metadata["mtime"]))
219 times.append("m: " + mtime)
220 ctx.fillSlots("times", times)
223 # build the base of the uri_link link url
224 uri_link = "/uri/" + urllib.quote(target.get_uri())
226 assert (IFileNode.providedBy(target)
227 or IDirectoryNode.providedBy(target)
228 or IMutableFileNode.providedBy(target)), target
230 if IMutableFileNode.providedBy(target):
233 # add the filename to the uri_link url
234 uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
236 # to prevent javascript in displayed .html files from stealing a
237 # secret directory URI from the URL, send the browser to a URI-based
238 # page that doesn't know about the directory at all
239 #dlurl = urllib.quote(name)
242 ctx.fillSlots("filename",
243 T.a(href=dlurl)[html.escape(name)])
244 ctx.fillSlots("type", "SSK")
246 ctx.fillSlots("size", "?")
248 text_plain_link = uri_link + "?filename=foo.txt"
249 text_plain_tag = T.a(href=text_plain_link)["text/plain"]
251 elif IFileNode.providedBy(target):
254 # add the filename to the uri_link url
255 uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
257 # to prevent javascript in displayed .html files from stealing a
258 # secret directory URI from the URL, send the browser to a URI-based
259 # page that doesn't know about the directory at all
260 #dlurl = urllib.quote(name)
263 ctx.fillSlots("filename",
264 T.a(href=dlurl)[html.escape(name)])
265 ctx.fillSlots("type", "FILE")
267 ctx.fillSlots("size", target.get_size())
269 text_plain_link = uri_link + "?filename=foo.txt"
270 text_plain_tag = T.a(href=text_plain_link)["text/plain"]
272 elif IDirectoryNode.providedBy(target):
274 ctx.fillSlots("filename",
275 T.a(href=uri_link)[html.escape(name)])
276 if target.is_readonly():
280 ctx.fillSlots("type", dirtype)
281 ctx.fillSlots("size", "-")
282 text_plain_tag = None
284 childdata = [T.a(href="%s?t=json" % name)["JSON"], ", ",
285 T.a(href="%s?t=uri" % name)["URI"], ", ",
286 T.a(href="%s?t=readonly-uri" % name)["readonly-URI"],
289 childdata.extend([", ", text_plain_tag])
291 ctx.fillSlots("data", childdata)
294 checker = IClient(ctx).getServiceNamed("checker")
298 d = defer.maybeDeferred(checker.checker_results_for,
299 target.get_verifier())
300 def _got(checker_results):
301 recent_results = reversed(checker_results[-5:])
302 if IFileNode.providedBy(target):
304 ", ".join(["%d/%d" % (found, needed)
306 (needed, total, found, sharemap))
307 in recent_results]) +
309 elif IDirectoryNode.providedBy(target):
311 "".join([{True:"+",False:"-"}[res]
312 for (when, res) in recent_results]) +
315 results = "%d results" % len(checker_results)
321 # TODO: include a link to see more results, including timestamps
322 # TODO: use a sparkline
323 ctx.fillSlots("checker_results", results)
327 def render_forms(self, ctx, data):
328 if self._dirnode.is_readonly():
329 return T.div["No upload forms: directory is read-only"]
330 mkdir = T.form(action=".", method="post",
331 enctype="multipart/form-data")[
333 T.input(type="hidden", name="t", value="mkdir"),
334 T.input(type="hidden", name="when_done", value=url.here),
335 T.legend(class_="freeform-form-label")["Create a new directory"],
336 "New directory name: ",
337 T.input(type="text", name="name"), " ",
338 T.input(type="submit", value="Create"),
341 upload = T.form(action=".", method="post",
342 enctype="multipart/form-data")[
344 T.input(type="hidden", name="t", value="upload"),
345 T.input(type="hidden", name="when_done", value=url.here),
346 T.legend(class_="freeform-form-label")["Upload a file to this directory"],
347 "Choose a file to upload: ",
348 T.input(type="file", name="file", class_="freeform-input-file"),
350 T.input(type="submit", value="Upload"),
352 T.input(type="checkbox", name="mutable"),
355 mount = T.form(action=".", method="post",
356 enctype="multipart/form-data")[
358 T.input(type="hidden", name="t", value="uri"),
359 T.input(type="hidden", name="when_done", value=url.here),
360 T.legend(class_="freeform-form-label")["Attach a file or directory"
364 T.input(type="text", name="name"), " ",
365 "URI of new child: ",
366 T.input(type="text", name="uri"), " ",
367 T.input(type="submit", value="Attach"),
369 return [T.div(class_="freeform-form")[mkdir],
370 T.div(class_="freeform-form")[upload],
371 T.div(class_="freeform-form")[mount],
374 def build_overwrite(self, ctx, data):
376 if IMutableFileNode.providedBy(target) and not target.is_readonly():
377 action="/uri/" + urllib.quote(target.get_uri())
378 overwrite = T.form(action=action, method="post",
379 enctype="multipart/form-data")[
381 T.input(type="hidden", name="t", value="overwrite"),
382 T.input(type='hidden', name='name', value=name),
383 T.input(type='hidden', name='when_done', value=url.here),
384 T.legend(class_="freeform-form-label")["Overwrite"],
386 T.input(type="file", name="file", class_="freeform-input-file"),
388 T.input(type="submit", value="Overwrite")
390 return [T.div(class_="freeform-form")[overwrite],]
394 def render_results(self, ctx, data):
395 req = inevow.IRequest(ctx)
396 return get_arg(req, "results", "")
398 class WebDownloadTarget:
399 implements(IDownloadTarget, IConsumer)
400 def __init__(self, req, content_type, content_encoding, save_to_file):
402 self._content_type = content_type
403 self._content_encoding = content_encoding
405 self._producer = None
406 self._save_to_file = save_to_file
408 def registerProducer(self, producer, streaming):
409 self._req.registerProducer(producer, streaming)
410 def unregisterProducer(self):
411 self._req.unregisterProducer()
413 def open(self, size):
415 self._req.setHeader("content-type", self._content_type)
416 if self._content_encoding:
417 self._req.setHeader("content-encoding", self._content_encoding)
418 self._req.setHeader("content-length", str(size))
419 if self._save_to_file is not None:
420 # tell the browser to save the file rather display it
421 # TODO: quote save_to_file properly
422 self._req.setHeader("content-disposition",
423 'attachment; filename="%s"'
424 % self._save_to_file)
426 def write(self, data):
427 self._req.write(data)
433 # The content-type is already set, and the response code
434 # has already been sent, so we can't provide a clean error
435 # indication. We can emit text (which a browser might interpret
436 # as something else), and if we sent a Size header, they might
437 # notice that we've truncated the data. Keep the error message
438 # small to improve the chances of having our error response be
439 # shorter than the intended results.
441 # We don't have a lot of options, unfortunately.
442 self._req.write("problem during download\n")
444 # We haven't written anything yet, so we can provide a sensible
447 msg.replace("\n", "|")
448 self._req.setResponseCode(http.GONE, msg)
449 self._req.setHeader("content-type", "text/plain")
450 # TODO: HTML-formatted exception?
451 self._req.write(str(why))
454 def register_canceller(self, cb):
459 class FileDownloader(resource.Resource):
460 def __init__(self, filenode, name):
461 assert (IFileNode.providedBy(filenode)
462 or IMutableFileNode.providedBy(filenode))
463 self._filenode = filenode
466 def render(self, req):
467 gte = static.getTypeAndEncoding
468 type, encoding = gte(self._name,
469 static.File.contentTypes,
470 static.File.contentEncodings,
471 defaultType="text/plain")
473 if get_arg(req, "save", False):
474 # TODO: make the API specification clear: should "save=" or
475 # "save=false" count?
476 save_to_file = self._name
477 wdt = WebDownloadTarget(req, type, encoding, save_to_file)
478 d = self._filenode.download(wdt)
479 # exceptions during download are handled by the WebDownloadTarget
480 d.addErrback(lambda why: None)
481 return server.NOT_DONE_YET
483 class BlockingFileError(Exception):
484 """We cannot auto-create a parent directory, because there is a file in
486 class NoReplacementError(Exception):
487 """There was already a child by that name, and you asked me to not replace it"""
488 class NoLocalDirectoryError(Exception):
489 """The localdir= directory didn't exist"""
491 LOCALHOST = "127.0.0.1"
493 class NeedLocalhostError:
494 implements(inevow.IResource)
496 def renderHTTP(self, ctx):
497 req = inevow.IRequest(ctx)
498 req.setResponseCode(http.FORBIDDEN)
499 req.setHeader("content-type", "text/plain")
500 return "localfile= or localdir= requires a local connection"
502 class NeedAbsolutePathError:
503 implements(inevow.IResource)
505 def renderHTTP(self, ctx):
506 req = inevow.IRequest(ctx)
507 req.setResponseCode(http.FORBIDDEN)
508 req.setHeader("content-type", "text/plain")
509 return "localfile= or localdir= requires an absolute path"
511 class LocalAccessDisabledError:
512 implements(inevow.IResource)
514 def renderHTTP(self, ctx):
515 req = inevow.IRequest(ctx)
516 req.setResponseCode(http.FORBIDDEN)
517 req.setHeader("content-type", "text/plain")
518 return "local file access is disabled"
521 implements(inevow.IResource)
522 def __init__(self, response_code, errmsg):
523 self._response_code = response_code
524 self._errmsg = errmsg
526 def renderHTTP(self, ctx):
527 req = inevow.IRequest(ctx)
528 req.setResponseCode(self._response_code)
529 req.setHeader("content-type", "text/plain")
533 class LocalFileDownloader(resource.Resource):
534 def __init__(self, filenode, local_filename):
535 self._local_filename = local_filename
537 self._filenode = filenode
539 def render(self, req):
540 target = download.FileName(self._local_filename)
541 d = self._filenode.download(target)
543 req.write(self._filenode.get_uri())
546 return server.NOT_DONE_YET
549 class FileJSONMetadata(rend.Page):
550 def __init__(self, filenode):
551 self._filenode = filenode
553 def renderHTTP(self, ctx):
554 req = inevow.IRequest(ctx)
555 req.setHeader("content-type", "text/plain")
556 return self.renderNode(self._filenode)
558 def renderNode(self, filenode):
559 file_uri = filenode.get_uri()
562 'size': filenode.get_size(),
564 return simplejson.dumps(data, indent=1)
566 class FileURI(FileJSONMetadata):
567 def renderNode(self, filenode):
568 file_uri = filenode.get_uri()
571 class FileReadOnlyURI(FileJSONMetadata):
572 def renderNode(self, filenode):
573 if filenode.is_readonly():
574 return filenode.get_uri()
576 return filenode.get_readonly().get_uri()
578 class DirnodeWalkerMixin:
579 """Visit all nodes underneath (and including) the rootnode, one at a
580 time. For each one, call the visitor. The visitor will see the
581 IDirectoryNode before it sees any of the IFileNodes inside. If the
582 visitor returns a Deferred, I do not call the visitor again until it has
586 ## def _walk_if_we_could_use_generators(self, rootnode, rootpath=()):
587 ## # this is what we'd be doing if we didn't have the Deferreds and
588 ## # thus could use generators
589 ## yield rootpath, rootnode
590 ## for childname, childnode in rootnode.list().items():
591 ## childpath = rootpath + (childname,)
592 ## if IFileNode.providedBy(childnode):
593 ## yield childpath, childnode
594 ## elif IDirectoryNode.providedBy(childnode):
595 ## for res in self._walk_if_we_could_use_generators(childnode,
599 def walk(self, rootnode, visitor, rootpath=()):
601 def _listed(listing):
602 return listing.items()
603 d.addCallback(_listed)
604 d.addCallback(self._handle_items, visitor, rootpath)
607 def _handle_items(self, items, visitor, rootpath):
610 childname, (childnode, metadata) = items[0]
611 childpath = rootpath + (childname,)
612 d = defer.maybeDeferred(visitor, childpath, childnode, metadata)
613 if IDirectoryNode.providedBy(childnode):
614 d.addCallback(lambda res: self.walk(childnode, visitor, childpath))
615 d.addCallback(lambda res:
616 self._handle_items(items[1:], visitor, rootpath))
619 class LocalDirectoryDownloader(resource.Resource, DirnodeWalkerMixin):
620 def __init__(self, dirnode, localdir):
621 self._dirnode = dirnode
622 self._localdir = localdir
624 def _handle(self, path, node, metadata):
625 localfile = os.path.join(self._localdir, os.sep.join(path))
626 if IDirectoryNode.providedBy(node):
627 fileutil.make_dirs(localfile)
628 elif IFileNode.providedBy(node):
629 target = download.FileName(localfile)
630 return node.download(target)
632 def render(self, req):
633 d = self.walk(self._dirnode, self._handle)
635 req.setHeader("content-type", "text/plain")
636 return "operation complete"
640 class DirectoryJSONMetadata(rend.Page):
641 def __init__(self, dirnode):
642 self._dirnode = dirnode
644 def renderHTTP(self, ctx):
645 req = inevow.IRequest(ctx)
646 req.setHeader("content-type", "text/plain")
647 return self.renderNode(self._dirnode)
649 def renderNode(self, node):
653 for name, (childnode, metadata) in children.iteritems():
654 if IFileNode.providedBy(childnode):
655 kiduri = childnode.get_uri()
656 kiddata = ("filenode",
658 'size': childnode.get_size(),
659 'metadata': metadata,
662 assert IDirectoryNode.providedBy(childnode), (childnode, children,)
663 kiddata = ("dirnode",
664 {'ro_uri': childnode.get_readonly_uri(),
665 'metadata': metadata,
667 if not childnode.is_readonly():
668 kiddata[1]['rw_uri'] = childnode.get_uri()
670 contents = { 'children': kids,
671 'ro_uri': node.get_readonly_uri(),
673 if not node.is_readonly():
674 contents['rw_uri'] = node.get_uri()
675 data = ("dirnode", contents)
676 return simplejson.dumps(data, indent=1)
680 class DirectoryURI(DirectoryJSONMetadata):
681 def renderNode(self, node):
682 return node.get_uri()
684 class DirectoryReadonlyURI(DirectoryJSONMetadata):
685 def renderNode(self, node):
686 return node.get_readonly_uri()
688 class RenameForm(rend.Page):
690 docFactory = getxmlfile("rename-form.xhtml")
692 def __init__(self, rootname, dirnode, dirpath):
693 self._rootname = rootname
694 self._dirnode = dirnode
695 self._dirpath = dirpath
697 def dirpath_as_string(self):
698 return "/" + "/".join(self._dirpath)
700 def render_title(self, ctx, data):
701 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
703 def render_header(self, ctx, data):
704 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
705 num_dirs = len(parent_directories)
707 header = [ "Rename in directory '",
708 "<%s>/" % self._rootname,
709 "/".join(self._dirpath),
712 if self._dirnode.is_readonly():
713 header.append(" (readonly)")
714 return ctx.tag[header]
716 def render_when_done(self, ctx, data):
717 return T.input(type="hidden", name="when_done", value=url.here)
719 def render_get_name(self, ctx, data):
720 req = inevow.IRequest(ctx)
721 name = get_arg(req, "name", "")
722 ctx.tag.attributes['value'] = name
725 class POSTHandler(rend.Page):
726 def __init__(self, node, replace):
728 self._replace = replace
730 def _check_replacement(self, name):
732 return defer.succeed(None)
733 d = self._node.has_child(name)
736 raise NoReplacementError("There was already a child by that "
737 "name, and you asked me to not "
743 def renderHTTP(self, ctx):
744 req = inevow.IRequest(ctx)
746 t = get_arg(req, "t")
749 name = get_arg(req, "name", None)
750 if name and "/" in name:
751 req.setResponseCode(http.BAD_REQUEST)
752 req.setHeader("content-type", "text/plain")
753 return "name= may not contain a slash"
756 # we allow the user to delete an empty-named file, but not to create
757 # them, since that's an easy and confusing mistake to make
759 when_done = get_arg(req, "when_done", None)
760 if not boolean_of_arg(get_arg(req, "replace", "true")):
761 self._replace = False
765 raise RuntimeError("mkdir requires a name")
766 d = self._check_replacement(name)
767 d.addCallback(lambda res: self._node.create_empty_directory(name))
768 d.addCallback(lambda res: "directory created")
771 raise RuntimeError("set-uri requires a name")
772 newuri = get_arg(req, "uri")
773 assert newuri is not None
774 d = self._check_replacement(name)
775 d.addCallback(lambda res: self._node.set_uri(name, newuri))
776 d.addCallback(lambda res: newuri)
779 # apparently an <input type="hidden" name="name" value="">
780 # won't show up in the resulting encoded form.. the 'name'
781 # field is completely missing. So to allow deletion of an
782 # empty file, we have to pretend that None means ''. The only
783 # downide of this is a slightly confusing error message if
784 # someone does a POST without a name= field. For our own HTML
785 # thisn't a big deal, because we create the 'delete' POST
788 d = self._node.delete(name)
789 d.addCallback(lambda res: "thing deleted")
791 from_name = 'from_name' in req.fields and req.fields["from_name"].value
792 if from_name is not None:
793 from_name = from_name.strip()
794 to_name = 'to_name' in req.fields and req.fields["to_name"].value
795 if to_name is not None:
796 to_name = to_name.strip()
797 if not from_name or not to_name:
798 raise RuntimeError("rename requires from_name and to_name")
799 if not IDirectoryNode.providedBy(self._node):
800 raise RuntimeError("rename must only be called on directories")
801 for k,v in [ ('from_name', from_name), ('to_name', to_name) ]:
803 req.setResponseCode(http.BAD_REQUEST)
804 req.setHeader("content-type", "text/plain")
805 return "%s= may not contain a slash" % (k,)
806 d = self._check_replacement(to_name)
807 d.addCallback(lambda res: self._node.get(from_name))
809 uri = child.get_uri()
810 # now actually do the rename
811 return self._node.set_uri(to_name, uri)
812 d.addCallback(add_dest)
814 return self._node.delete(from_name)
815 d.addCallback(rm_src)
816 d.addCallback(lambda res: "thing renamed")
819 if "mutable" in req.fields:
820 contents = req.fields["file"]
821 name = name or contents.filename
825 raise RuntimeError("upload-mutable requires a name")
826 # SDMF: files are small, and we can only upload data.
827 contents.file.seek(0)
828 data = contents.file.read()
829 #uploadable = FileHandle(contents.file)
830 d = self._check_replacement(name)
831 d.addCallback(lambda res: self._node.has_child(name))
832 def _checked(present):
834 # modify the existing one instead of creating a new
836 d2 = self._node.get(name)
837 def _got_newnode(newnode):
838 d3 = newnode.replace(data)
839 d3.addCallback(lambda res: newnode.get_uri())
841 d2.addCallback(_got_newnode)
843 d2 = IClient(ctx).create_mutable_file(data)
844 def _uploaded(newnode):
845 d1 = self._node.set_node(name, newnode)
846 d1.addCallback(lambda res: newnode.get_uri())
848 d2.addCallback(_uploaded)
850 d.addCallback(_checked)
852 contents = req.fields["file"]
853 name = name or contents.filename
857 raise RuntimeError("upload requires a name")
858 uploadable = FileHandle(contents.file)
859 d = self._check_replacement(name)
860 d.addCallback(lambda res: self._node.add_file(name, uploadable))
862 return newnode.get_uri()
865 elif t == "overwrite":
866 contents = req.fields["file"]
867 # SDMF: files are small, and we can only upload data.
868 contents.file.seek(0)
869 data = contents.file.read()
870 # TODO: 'name' handling needs review
871 d = defer.succeed(self._node)
872 def _got_child_overwrite(child_node):
873 child_node.replace(data)
874 return child_node.get_uri()
875 d.addCallback(_got_child_overwrite)
878 d = self._node.get(name)
879 def _got_child_check(child_node):
880 d2 = child_node.check()
882 log.msg("checked %s, results %s" % (child_node, res))
884 d2.addCallback(_done)
886 d.addCallback(_got_child_check)
889 return "BAD t=%s" % t
891 d.addCallback(lambda res: url.URL.fromString(when_done))
892 def _check_replacement(f):
893 # TODO: make this more human-friendly: maybe send them to the
894 # when_done page but with an extra query-arg that will display
895 # the error message in a big box at the top of the page. The
896 # directory page that when_done= usually points to accepts a
897 # result= argument.. use that.
898 f.trap(NoReplacementError)
899 req.setResponseCode(http.CONFLICT)
900 req.setHeader("content-type", "text/plain")
902 d.addErrback(_check_replacement)
905 class DELETEHandler(rend.Page):
906 def __init__(self, node, name):
910 def renderHTTP(self, ctx):
911 req = inevow.IRequest(ctx)
912 d = self._node.delete(self._name)
914 # what should this return??
915 return "%s deleted" % self._name
917 def _trap_missing(f):
919 req.setResponseCode(http.NOT_FOUND)
920 req.setHeader("content-type", "text/plain")
921 return "no such child %s" % self._name
922 d.addErrback(_trap_missing)
925 class PUTHandler(rend.Page):
926 def __init__(self, node, path, t, localfile, localdir, replace):
930 self._localfile = localfile
931 self._localdir = localdir
932 self._replace = replace
934 def renderHTTP(self, ctx):
935 req = inevow.IRequest(ctx)
937 localfile = self._localfile
938 localdir = self._localdir
940 if t == "upload" and not (localfile or localdir):
941 req.setResponseCode(http.BAD_REQUEST)
942 req.setHeader("content-type", "text/plain")
943 return "t=upload requires localfile= or localdir="
945 # we must traverse the path, creating new directories as necessary
946 d = self._get_or_create_directories(self._node, self._path[:-1])
947 name = self._path[-1]
948 d.addCallback(self._check_replacement, name, self._replace)
951 d.addCallback(self._upload_localfile, localfile, name)
955 d.addCallback(self._get_or_create_directories, self._path[-1:])
956 d.addCallback(self._upload_localdir, localdir)
958 d.addCallback(self._attach_uri, req.content, name)
960 d.addCallback(self._mkdir, name)
962 d.addCallback(self._upload_file, req.content, name)
964 def _transform_error(f):
965 errors = {BlockingFileError: http.BAD_REQUEST,
966 NoReplacementError: http.CONFLICT,
967 NoLocalDirectoryError: http.BAD_REQUEST,
969 for k,v in errors.items():
971 req.setResponseCode(v)
972 req.setHeader("content-type", "text/plain")
975 d.addErrback(_transform_error)
978 def _get_or_create_directories(self, node, path):
979 if not IDirectoryNode.providedBy(node):
980 # unfortunately it is too late to provide the name of the
981 # blocking directory in the error message.
982 raise BlockingFileError("cannot create directory because there "
983 "is a file in the way")
985 return defer.succeed(node)
986 d = node.get(path[0])
987 def _maybe_create(f):
989 return node.create_empty_directory(path[0])
990 d.addErrback(_maybe_create)
991 d.addCallback(self._get_or_create_directories, path[1:])
994 def _check_replacement(self, node, name, replace):
997 d = node.has_child(name)
1000 raise NoReplacementError("There was already a child by that "
1001 "name, and you asked me to not "
1007 def _mkdir(self, node, name):
1008 d = node.create_empty_directory(name)
1010 return newnode.get_uri()
1011 d.addCallback(_done)
1014 def _upload_file(self, node, contents, name):
1015 uploadable = FileHandle(contents)
1016 d = node.add_file(name, uploadable)
1017 def _done(filenode):
1018 log.msg("webish upload complete")
1019 return filenode.get_uri()
1020 d.addCallback(_done)
1023 def _upload_localfile(self, node, localfile, name):
1024 uploadable = FileName(localfile)
1025 d = node.add_file(name, uploadable)
1026 d.addCallback(lambda filenode: filenode.get_uri())
1029 def _attach_uri(self, parentnode, contents, name):
1030 newuri = contents.read().strip()
1031 d = parentnode.set_uri(name, newuri)
1034 d.addCallback(_done)
1037 def _upload_localdir(self, node, localdir):
1038 # build up a list of files to upload
1041 msg = "No files to upload! %s is empty" % localdir
1042 if not os.path.exists(localdir):
1043 msg = "%s doesn't exist!" % localdir
1044 raise NoLocalDirectoryError(msg)
1045 for root, dirs, files in os.walk(localdir):
1046 if root == localdir:
1049 relative_root = root[len(localdir)+1:]
1050 path = tuple(relative_root.split(os.sep))
1052 all_dirs.append(path + (d,))
1054 all_files.append(path + (f,))
1055 d = defer.succeed(msg)
1056 for dir in all_dirs:
1058 d.addCallback(self._makedir, node, dir)
1060 d.addCallback(self._upload_one_file, node, localdir, f)
1063 def _makedir(self, res, node, dir):
1064 d = defer.succeed(None)
1065 # get the parent. As long as os.walk gives us parents before
1066 # children, this ought to work
1067 d.addCallback(lambda res: node.get_child_at_path(dir[:-1]))
1068 # then create the child directory
1069 d.addCallback(lambda parent: parent.create_empty_directory(dir[-1]))
1072 def _upload_one_file(self, res, node, localdir, f):
1073 # get the parent. We can be sure this exists because we already
1074 # went through and created all the directories we require.
1075 localfile = os.path.join(localdir, *f)
1076 d = node.get_child_at_path(f[:-1])
1077 d.addCallback(self._upload_localfile, localfile, f[-1])
1081 class Manifest(rend.Page):
1082 docFactory = getxmlfile("manifest.xhtml")
1083 def __init__(self, dirnode, dirpath):
1084 self._dirnode = dirnode
1085 self._dirpath = dirpath
1087 def dirpath_as_string(self):
1088 return "/" + "/".join(self._dirpath)
1090 def render_title(self, ctx):
1091 return T.title["Manifest of %s" % self.dirpath_as_string()]
1093 def render_header(self, ctx):
1094 return T.p["Manifest of %s" % self.dirpath_as_string()]
1096 def data_items(self, ctx, data):
1097 return self._dirnode.build_manifest()
1099 def render_row(self, ctx, refresh_cap):
1100 ctx.fillSlots("refresh_capability", refresh_cap)
1104 implements(inevow.IResource)
1105 def renderHTTP(self, ctx):
1106 req = inevow.IRequest(ctx)
1107 req.setResponseCode(http.BAD_REQUEST)
1108 req.setHeader("content-type", "text/plain")
1111 def child_error(text):
1116 class VDrive(rend.Page):
1118 def __init__(self, node, name):
1122 def get_child_at_path(self, path):
1124 return self.node.get_child_at_path(path)
1125 return defer.succeed(self.node)
1127 def locateChild(self, ctx, segments):
1128 req = inevow.IRequest(ctx)
1132 t = get_arg(req, "t", "")
1133 localfile = get_arg(req, "localfile", None)
1134 if localfile is not None:
1135 if localfile != os.path.abspath(localfile):
1136 return NeedAbsolutePathError(), ()
1137 localdir = get_arg(req, "localdir", None)
1138 if localdir is not None:
1139 if localdir != os.path.abspath(localdir):
1140 return NeedAbsolutePathError(), ()
1141 if localfile or localdir:
1142 if not ILocalAccess(ctx).local_access_is_allowed():
1143 return LocalAccessDisabledError(), ()
1144 if req.getHost().host != LOCALHOST:
1145 return NeedLocalhostError(), ()
1146 # TODO: think about clobbering/revealing config files and node secrets
1148 replace = boolean_of_arg(get_arg(req, "replace", "true"))
1151 # the node must exist, and our operation will be performed on the
1153 d = self.get_child_at_path(path)
1154 def file_or_dir(node):
1155 if (IFileNode.providedBy(node)
1156 or IMutableFileNode.providedBy(node)):
1157 filename = "unknown"
1160 filename = get_arg(req, "filename", filename)
1163 # write contents to a local file
1164 return LocalFileDownloader(node, localfile), ()
1165 # send contents as the result
1166 return FileDownloader(node, filename), ()
1168 # send contents as the result
1169 return FileDownloader(node, filename), ()
1171 return FileJSONMetadata(node), ()
1173 return FileURI(node), ()
1174 elif t == "readonly-uri":
1175 return FileReadOnlyURI(node), ()
1177 return child_error("bad t=%s" % t)
1178 elif IDirectoryNode.providedBy(node):
1181 # recursive download to a local directory
1182 return LocalDirectoryDownloader(node, localdir), ()
1183 return child_error("t=download requires localdir=")
1185 # send an HTML representation of the directory
1186 return Directory(self.name, node, path), ()
1188 return DirectoryJSONMetadata(node), ()
1190 return DirectoryURI(node), ()
1191 elif t == "readonly-uri":
1192 return DirectoryReadonlyURI(node), ()
1193 elif t == "manifest":
1194 return Manifest(node, path), ()
1195 elif t == 'rename-form':
1196 return RenameForm(self.name, node, path), ()
1198 return child_error("bad t=%s" % t)
1200 return child_error("unknown node type")
1201 d.addCallback(file_or_dir)
1202 elif method == "POST":
1203 # the node must exist, and our operation will be performed on the
1205 d = self.get_child_at_path(path)
1206 def _got_POST(node):
1207 return POSTHandler(node, replace), ()
1208 d.addCallback(_got_POST)
1209 elif method == "DELETE":
1210 # the node must exist, and our operation will be performed on its
1212 assert path # you can't delete the root
1213 d = self.get_child_at_path(path[:-1])
1214 def _got_DELETE(node):
1215 return DELETEHandler(node, path[-1]), ()
1216 d.addCallback(_got_DELETE)
1217 elif method in ("PUT",):
1218 # the node may or may not exist, and our operation may involve
1219 # all the ancestors of the node.
1220 return PUTHandler(self.node, path, t, localfile, localdir, replace), ()
1222 return rend.NotFound
1225 class UnlinkedPUTCHKUploader(rend.Page):
1226 def renderHTTP(self, ctx):
1227 req = inevow.IRequest(ctx)
1228 assert req.method == "PUT"
1229 # "PUT /uri", to create an unlinked file. This is like PUT but
1230 # without the associated set_uri.
1232 uploadable = FileHandle(req.content)
1233 d = IClient(ctx).upload(uploadable)
1234 d.addCallback(lambda results: results.uri)
1235 # that fires with the URI of the new file
1238 class UnlinkedPUTSSKUploader(rend.Page):
1239 def renderHTTP(self, ctx):
1240 req = inevow.IRequest(ctx)
1241 assert req.method == "PUT"
1242 # SDMF: files are small, and we can only upload data
1243 contents = req.content
1245 data = contents.read()
1246 d = IClient(ctx).create_mutable_file(data)
1247 d.addCallback(lambda n: n.get_uri())
1250 class UnlinkedPUTCreateDirectory(rend.Page):
1251 def renderHTTP(self, ctx):
1252 req = inevow.IRequest(ctx)
1253 assert req.method == "PUT"
1254 # "PUT /uri?t=mkdir", to create an unlinked directory.
1255 d = IClient(ctx).create_empty_dirnode()
1256 d.addCallback(lambda dirnode: dirnode.get_uri())
1257 # XXX add redirect_to_result
1261 class UnlinkedPOSTCHKUploader(rend.Page):
1262 """'POST /uri', to create an unlinked file."""
1263 docFactory = getxmlfile("unlinked-upload.xhtml")
1265 def __init__(self, client, req):
1266 rend.Page.__init__(self)
1267 # we start the upload now, and distribute notification of its
1268 # completion to render_ methods with an ObserverList
1269 assert req.method == "POST"
1270 self._done = observer.OneShotObserverList()
1271 fileobj = req.fields["file"].file
1272 uploadable = FileHandle(fileobj)
1273 d = client.upload(uploadable)
1274 d.addBoth(self._done.fire)
1276 def renderHTTP(self, ctx):
1277 req = inevow.IRequest(ctx)
1278 when_done = get_arg(req, "when_done", None)
1280 # if when_done= is provided, return a redirect instead of our
1281 # usual upload-results page
1282 d = self._done.when_fired()
1283 d.addCallback(lambda res: url.URL.fromString(when_done))
1285 return rend.Page.renderHTTP(self, ctx)
1287 def upload_results(self):
1288 return self._done.when_fired()
1290 def data_done(self, ctx, data):
1291 d = self.upload_results()
1292 d.addCallback(lambda res: "done!")
1295 def data_uri(self, ctx, data):
1296 d = self.upload_results()
1297 d.addCallback(lambda res: res.uri)
1300 def render_download_link(self, ctx, data):
1301 d = self.upload_results()
1302 d.addCallback(lambda res: T.a(href="/uri/" + urllib.quote(res.uri))
1303 ["/uri/" + res.uri])
1306 def render_sharemap(self, ctx, data):
1307 d = self.upload_results()
1308 d.addCallback(lambda res: res.sharemap)
1309 def _render(sharemap):
1310 if sharemap is None:
1313 for shnum in sorted(sharemap.keys()):
1314 l[T.li["%d -> %s" % (shnum, sharemap[shnum])]]
1316 d.addCallback(_render)
1319 def render_servermap(self, ctx, data):
1320 d = self.upload_results()
1321 d.addCallback(lambda res: res.servermap)
1322 def _render(servermap):
1323 if servermap is None:
1326 for peerid in sorted(servermap.keys()):
1327 peerid_s = idlib.shortnodeid_b2a(peerid)
1328 shares_s = ",".join([str(shnum) for shnum in servermap[peerid]])
1329 l[T.li["[%s] got shares: %s" % (peerid_s, shares_s)]]
1331 d.addCallback(_render)
1334 def data_file_size(self, ctx, data):
1335 d = self.upload_results()
1336 d.addCallback(lambda res: res.file_size)
1339 def render_time(self, ctx, data):
1340 # 1.23s, 790ms, 132us
1347 return "%dms" % (1000*s)
1349 return "%.1fms" % (1000*s)
1350 return "%dus" % (1000000*s)
1352 def render_rate(self, ctx, data):
1353 # 21.8kBps, 554.4kBps 4.37MBps
1358 return "%1.2fMBps" % (r/1000000)
1360 return "%.1fkBps" % (r/1000)
1363 def _get_time(self, name):
1364 d = self.upload_results()
1365 d.addCallback(lambda res: res.timings.get(name))
1368 def data_time_total(self, ctx, data):
1369 return self._get_time("total")
1371 def data_time_storage_index(self, ctx, data):
1372 return self._get_time("storage_index")
1374 def data_time_contacting_helper(self, ctx, data):
1375 return self._get_time("contacting_helper")
1377 def data_time_existence_check(self, ctx, data):
1378 return self._get_time("existence_check")
1380 def data_time_cumulative_fetch(self, ctx, data):
1381 return self._get_time("cumulative_fetch")
1383 def data_time_helper_total(self, ctx, data):
1384 return self._get_time("helper_total")
1386 def data_time_peer_selection(self, ctx, data):
1387 return self._get_time("peer_selection")
1389 def data_time_total_encode_and_push(self, ctx, data):
1390 return self._get_time("total_encode_and_push")
1392 def data_time_cumulative_encoding(self, ctx, data):
1393 return self._get_time("cumulative_encoding")
1395 def data_time_cumulative_sending(self, ctx, data):
1396 return self._get_time("cumulative_sending")
1398 def data_time_hashes_and_close(self, ctx, data):
1399 return self._get_time("hashes_and_close")
1401 def _get_rate(self, name):
1402 d = self.upload_results()
1404 file_size = r.file_size
1405 time = r.timings.get(name)
1409 return 1.0 * file_size / time
1410 except ZeroDivisionError:
1412 d.addCallback(_convert)
1415 def data_rate_total(self, ctx, data):
1416 return self._get_rate("total")
1418 def data_rate_storage_index(self, ctx, data):
1419 return self._get_rate("storage_index")
1421 def data_rate_encode(self, ctx, data):
1422 return self._get_rate("cumulative_encoding")
1424 def data_rate_push(self, ctx, data):
1425 return self._get_rate("cumulative_sending")
1427 def data_rate_encode_and_push(self, ctx, data):
1428 d = self.upload_results()
1430 file_size = r.file_size
1431 if file_size is None:
1433 time1 = r.timings.get("cumulative_encoding")
1436 time2 = r.timings.get("cumulative_sending")
1440 return 1.0 * file_size / (time1+time2)
1441 except ZeroDivisionError:
1443 d.addCallback(_convert)
1446 def data_rate_ciphertext_fetch(self, ctx, data):
1447 d = self.upload_results()
1449 fetch_size = r.ciphertext_fetched
1450 if fetch_size is None:
1452 time = r.timings.get("cumulative_fetch")
1456 return 1.0 * fetch_size / time
1457 except ZeroDivisionError:
1459 d.addCallback(_convert)
1462 class UnlinkedPOSTSSKUploader(rend.Page):
1463 def renderHTTP(self, ctx):
1464 req = inevow.IRequest(ctx)
1465 assert req.method == "POST"
1467 # "POST /uri", to create an unlinked file.
1468 # SDMF: files are small, and we can only upload data
1469 contents = req.fields["file"]
1470 contents.file.seek(0)
1471 data = contents.file.read()
1472 d = IClient(ctx).create_mutable_file(data)
1473 d.addCallback(lambda n: n.get_uri())
1476 class UnlinkedPOSTCreateDirectory(rend.Page):
1477 def renderHTTP(self, ctx):
1478 req = inevow.IRequest(ctx)
1479 assert req.method == "POST"
1481 # "POST /uri?t=mkdir", to create an unlinked directory.
1482 d = IClient(ctx).create_empty_dirnode()
1483 redirect = get_arg(req, "redirect_to_result", "false")
1484 if boolean_of_arg(redirect):
1485 def _then_redir(res):
1486 new_url = "uri/" + urllib.quote(res.get_uri())
1487 req.setResponseCode(http.SEE_OTHER) # 303
1488 req.setHeader('location', new_url)
1491 d.addCallback(_then_redir)
1493 d.addCallback(lambda dirnode: dirnode.get_uri())
1497 class Root(rend.Page):
1500 docFactory = getxmlfile("welcome.xhtml")
1502 def locateChild(self, ctx, segments):
1503 client = IClient(ctx)
1504 req = inevow.IRequest(ctx)
1506 segments = list(segments) # XXX HELP I AM YUCKY!
1507 while segments and not segments[-1]:
1511 segments = tuple(segments)
1513 if segments[0] == "uri":
1514 if len(segments) == 1 or segments[1] == '':
1515 uri = get_arg(req, "uri", None)
1517 there = url.URL.fromContext(ctx)
1518 there = there.clear("uri")
1519 there = there.child("uri").child(uri)
1521 if len(segments) == 1:
1523 if req.method == "PUT":
1524 # either "PUT /uri" to create an unlinked file, or
1525 # "PUT /uri?t=mkdir" to create an unlinked directory
1526 t = get_arg(req, "t", "").strip()
1528 mutable = bool(get_arg(req, "mutable", "").strip())
1530 return UnlinkedPUTSSKUploader(), ()
1532 return UnlinkedPUTCHKUploader(), ()
1534 return UnlinkedPUTCreateDirectory(), ()
1535 errmsg = "/uri only accepts PUT and PUT?t=mkdir"
1536 return WebError(http.BAD_REQUEST, errmsg), ()
1538 elif req.method == "POST":
1539 # "POST /uri?t=upload&file=newfile" to upload an
1540 # unlinked file or "POST /uri?t=mkdir" to create a
1542 t = get_arg(req, "t", "").strip()
1543 if t in ("", "upload"):
1544 mutable = bool(get_arg(req, "mutable", "").strip())
1546 return UnlinkedPOSTSSKUploader(), ()
1548 return UnlinkedPOSTCHKUploader(client, req), ()
1550 return UnlinkedPOSTCreateDirectory(), ()
1551 errmsg = "/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, and POST?t=mkdir"
1552 return WebError(http.BAD_REQUEST, errmsg), ()
1553 if len(segments) < 2:
1554 return rend.NotFound
1556 d = defer.maybeDeferred(client.create_node_from_uri, uri)
1557 d.addCallback(lambda node: VDrive(node, uri))
1558 d.addCallback(lambda vd: vd.locateChild(ctx, segments[2:]))
1559 def _trap_KeyError(f):
1561 return rend.FourOhFour(), ()
1562 d.addErrback(_trap_KeyError)
1564 elif segments[0] == "xmlrpc":
1565 raise NotImplementedError()
1566 return rend.Page.locateChild(self, ctx, segments)
1568 child_webform_css = webform.defaultCSS
1569 child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
1571 child_provisioning = provisioning.ProvisioningTool()
1573 def data_version(self, ctx, data):
1574 return get_package_versions_string()
1575 def data_import_path(self, ctx, data):
1576 return str(allmydata)
1577 def data_my_nodeid(self, ctx, data):
1578 return idlib.nodeid_b2a(IClient(ctx).nodeid)
1579 def data_storage(self, ctx, data):
1580 client = IClient(ctx)
1582 ss = client.getServiceNamed("storage")
1584 return "Not running"
1585 allocated = ss.allocated_size()
1586 return "about %d bytes allocated" % allocated
1588 def data_introducer_furl(self, ctx, data):
1589 return IClient(ctx).introducer_furl
1590 def data_connected_to_introducer(self, ctx, data):
1591 if IClient(ctx).connected_to_introducer():
1595 def data_helper_furl(self, ctx, data):
1597 uploader = IClient(ctx).getServiceNamed("uploader")
1600 furl, connected = uploader.get_helper_info()
1602 def data_connected_to_helper(self, ctx, data):
1604 uploader = IClient(ctx).getServiceNamed("uploader")
1606 return "no" # we don't even have an Uploader
1607 furl, connected = uploader.get_helper_info()
1612 def data_known_storage_servers(self, ctx, data):
1613 ic = IClient(ctx).introducer_client
1615 for c in ic.get_all_connectors().values()
1616 if c.service_name == "storage"]
1619 def data_connected_storage_servers(self, ctx, data):
1620 ic = IClient(ctx).introducer_client
1621 return len(ic.get_all_connections_for("storage"))
1623 def data_services(self, ctx, data):
1624 ic = IClient(ctx).introducer_client
1625 c = [ (service_name, nodeid, rsc)
1626 for (nodeid, service_name), rsc
1627 in ic.get_all_connectors().items() ]
1631 def render_service_row(self, ctx, data):
1632 (service_name, nodeid, rsc) = data
1633 ctx.fillSlots("peerid", "%s %s" % (idlib.nodeid_b2a(nodeid),
1636 rhost = rsc.remote_host
1637 if nodeid == IClient(ctx).nodeid:
1638 rhost_s = "(loopback)"
1639 elif isinstance(rhost, address.IPv4Address):
1640 rhost_s = "%s:%d" % (rhost.host, rhost.port)
1642 rhost_s = str(rhost)
1643 connected = "Yes: to " + rhost_s
1644 since = rsc.last_connect_time
1647 since = rsc.last_loss_time
1649 TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
1650 ctx.fillSlots("connected", connected)
1651 ctx.fillSlots("since", time.strftime(TIME_FORMAT, time.localtime(since)))
1652 ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
1653 time.localtime(rsc.announcement_time)))
1654 ctx.fillSlots("version", rsc.version)
1655 ctx.fillSlots("service_name", rsc.service_name)
1659 def render_download_form(self, ctx, data):
1660 # this is a form where users can download files by URI
1661 form = T.form(action="uri", method="get",
1662 enctype="multipart/form-data")[
1664 T.legend(class_="freeform-form-label")["Download a file"],
1665 "URI to download: ",
1666 T.input(type="text", name="uri"), " ",
1667 "Filename to download as: ",
1668 T.input(type="text", name="filename"), " ",
1669 T.input(type="submit", value="Download!"),
1673 def render_view_form(self, ctx, data):
1674 # this is a form where users can download files by URI, or jump to a
1676 form = T.form(action="uri", method="get",
1677 enctype="multipart/form-data")[
1679 T.legend(class_="freeform-form-label")["View a file or directory"],
1681 T.input(type="text", name="uri"), " ",
1682 T.input(type="submit", value="View!"),
1686 def render_upload_form(self, ctx, data):
1687 # this is a form where users can upload unlinked files
1688 form = T.form(action="uri", method="post",
1689 enctype="multipart/form-data")[
1691 T.legend(class_="freeform-form-label")["Upload a file"],
1693 T.input(type="file", name="file", class_="freeform-input-file"),
1694 T.input(type="hidden", name="t", value="upload"),
1695 " Mutable?:", T.input(type="checkbox", name="mutable"),
1696 T.input(type="submit", value="Upload!"),
1700 def render_mkdir_form(self, ctx, data):
1701 # this is a form where users can create new directories
1702 form = T.form(action="uri", method="post",
1703 enctype="multipart/form-data")[
1705 T.legend(class_="freeform-form-label")["Create a directory"],
1706 T.input(type="hidden", name="t", value="mkdir"),
1707 T.input(type="hidden", name="redirect_to_result", value="true"),
1708 T.input(type="submit", value="Create Directory!"),
1714 implements(ILocalAccess)
1716 self.local_access = False
1717 def local_access_is_allowed(self):
1718 return self.local_access
1720 class WebishServer(service.MultiService):
1723 def __init__(self, webport, nodeurl_path=None):
1724 service.MultiService.__init__(self)
1725 self.webport = webport
1727 self.site = site = appserver.NevowSite(self.root)
1728 self.site.requestFactory = MyRequest
1729 self.allow_local = LocalAccess()
1730 self.site.remember(self.allow_local, ILocalAccess)
1731 s = strports.service(webport, site)
1732 s.setServiceParent(self)
1733 self.listener = s # stash it so the tests can query for the portnum
1734 self._started = defer.Deferred()
1736 self._started.addCallback(self._write_nodeurl_file, nodeurl_path)
1738 def allow_local_access(self, enable=True):
1739 self.allow_local.local_access = enable
1741 def startService(self):
1742 service.MultiService.startService(self)
1743 # to make various services available to render_* methods, we stash a
1744 # reference to the client on the NevowSite. This will be available by
1745 # adapting the 'context' argument to a special marker interface named
1747 self.site.remember(self.parent, IClient)
1748 # I thought you could do the same with an existing interface, but
1749 # apparently 'ISite' does not exist
1750 #self.site._client = self.parent
1751 self._started.callback(None)
1753 def _write_nodeurl_file(self, junk, nodeurl_path):
1754 # what is our webport?
1756 if isinstance(s, internet.TCPServer):
1757 base_url = "http://localhost:%d" % s._port.getHost().port
1758 elif isinstance(s, internet.SSLServer):
1759 base_url = "https://localhost:%d" % s._port.getHost().port
1763 f = open(nodeurl_path, 'wb')
1764 # this file is world-readable
1765 f.write(base_url + "\n")