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 base32, 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
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 def _escape(self, s):
128 # pain in the ass. Return a string like python repr, but always
129 # escaped as if surrounding quotes were "".
132 return r[1:-1].replace('"', '\\"').replace("\\'", "'")
136 # we build up a log string that hides most of the cap, to preserve
137 # user privacy. We retain the query args so we can identify things
138 # like t=json. Then we send it to the flog. We make no attempt to
139 # match apache formatting. TODO: when we move to DSA dirnodes and
140 # shorter caps, consider exposing a few characters of the cap, or
141 # maybe a few characters of its hash.
142 x = self.uri.split("?", 1)
149 # there is a form handler which redirects POST /uri?uri=FOO into
150 # GET /uri/FOO so folks can paste in non-HTTP-prefixed uris. Make
151 # sure we censor these too.
152 if queryargs.startswith("uri="):
153 queryargs = "[uri=CENSORED]"
154 queryargs = "?" + queryargs
155 if path.startswith("/uri"):
156 path = "/uri/[CENSORED].."
157 uri = path + queryargs
159 log.msg(format="web: %(clientip)s %(method)s %(uri)s %(code)s %(length)s",
160 clientip=self.getClientIP(),
164 length=(self.sentLength or "-"),
165 facility="tahoe.webish",
166 level=log.OPERATIONAL,
169 class Directory(rend.Page):
171 docFactory = getxmlfile("directory.xhtml")
173 def __init__(self, rootname, dirnode, dirpath):
174 self._rootname = rootname
175 self._dirnode = dirnode
176 self._dirpath = dirpath
178 def dirpath_as_string(self):
179 return "/" + "/".join(self._dirpath)
181 def render_title(self, ctx, data):
182 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
184 def render_header(self, ctx, data):
185 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
186 num_dirs = len(parent_directories)
188 header = ["Directory '"]
189 for i,d in enumerate(parent_directories):
190 upness = num_dirs - i - 1
192 link = "/".join( ("..",) * upness )
195 header.append(T.a(href=link)[d])
200 if self._dirnode.is_readonly():
201 header.append(" (readonly)")
203 return ctx.tag[header]
205 def render_welcome(self, ctx, data):
206 depth = len(self._dirpath) + 2
207 link = "/".join([".."] * depth)
208 return T.div[T.a(href=link)["Return to Welcome page"]]
210 def data_children(self, ctx, data):
211 d = self._dirnode.list()
212 d.addCallback(lambda dict: sorted(dict.items()))
213 def _stall_some(items):
214 # Deferreds don't optimize out tail recursion, and the way
215 # Nevow's flattener handles Deferreds doesn't take this into
216 # account. As a result, large lists of Deferreds that fire in the
217 # same turn (i.e. the output of defer.succeed) will cause a stack
218 # overflow. To work around this, we insert a turn break after
219 # every 100 items, using foolscap's fireEventually(). This gives
220 # the stack a chance to be popped. It would also work to put
221 # every item in its own turn, but that'd be a lot more
222 # inefficient. This addresses ticket #237, for which I was never
223 # able to create a failing unit test.
225 for i,item in enumerate(items):
227 output.append(fireEventually(item))
231 d.addCallback(_stall_some)
234 def render_row(self, ctx, data):
235 name, (target, metadata) = data
237 if self._dirnode.is_readonly():
241 # this creates a button which will cause our child__delete method
242 # to be invoked, which deletes the file and then redirects the
243 # browser back to this directory
244 delete = T.form(action=url.here, method="post")[
245 T.input(type='hidden', name='t', value='delete'),
246 T.input(type='hidden', name='name', value=name),
247 T.input(type='hidden', name='when_done', value=url.here),
248 T.input(type='submit', value='del', name="del"),
251 rename = T.form(action=url.here, method="get")[
252 T.input(type='hidden', name='t', value='rename-form'),
253 T.input(type='hidden', name='name', value=name),
254 T.input(type='hidden', name='when_done', value=url.here),
255 T.input(type='submit', value='rename', name="rename"),
258 ctx.fillSlots("delete", delete)
259 ctx.fillSlots("rename", rename)
260 check = T.form(action=url.here, method="post")[
261 T.input(type='hidden', name='t', value='check'),
262 T.input(type='hidden', name='name', value=name),
263 T.input(type='hidden', name='when_done', value=url.here),
264 T.input(type='submit', value='check', name="check"),
266 ctx.fillSlots("overwrite", self.build_overwrite(ctx, (name, target)))
267 ctx.fillSlots("check", check)
270 TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
271 if "ctime" in metadata:
272 ctime = time.strftime(TIME_FORMAT,
273 time.localtime(metadata["ctime"]))
274 times.append("c: " + ctime)
275 if "mtime" in metadata:
276 mtime = time.strftime(TIME_FORMAT,
277 time.localtime(metadata["mtime"]))
280 times.append("m: " + mtime)
281 ctx.fillSlots("times", times)
284 # build the base of the uri_link link url
285 uri_link = "/uri/" + urllib.quote(target.get_uri())
287 assert (IFileNode.providedBy(target)
288 or IDirectoryNode.providedBy(target)
289 or IMutableFileNode.providedBy(target)), target
291 if IMutableFileNode.providedBy(target):
294 # add the filename to the uri_link url
295 uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
297 # to prevent javascript in displayed .html files from stealing a
298 # secret directory URI from the URL, send the browser to a URI-based
299 # page that doesn't know about the directory at all
300 #dlurl = urllib.quote(name)
303 ctx.fillSlots("filename",
304 T.a(href=dlurl)[html.escape(name)])
305 ctx.fillSlots("type", "SSK")
307 ctx.fillSlots("size", "?")
309 text_plain_link = uri_link + "?filename=foo.txt"
310 text_plain_tag = T.a(href=text_plain_link)["text/plain"]
312 elif IFileNode.providedBy(target):
315 # add the filename to the uri_link url
316 uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
318 # to prevent javascript in displayed .html files from stealing a
319 # secret directory URI from the URL, send the browser to a URI-based
320 # page that doesn't know about the directory at all
321 #dlurl = urllib.quote(name)
324 ctx.fillSlots("filename",
325 T.a(href=dlurl)[html.escape(name)])
326 ctx.fillSlots("type", "FILE")
328 ctx.fillSlots("size", target.get_size())
330 text_plain_link = uri_link + "?filename=foo.txt"
331 text_plain_tag = T.a(href=text_plain_link)["text/plain"]
333 elif IDirectoryNode.providedBy(target):
335 ctx.fillSlots("filename",
336 T.a(href=uri_link)[html.escape(name)])
337 if target.is_readonly():
341 ctx.fillSlots("type", dirtype)
342 ctx.fillSlots("size", "-")
343 text_plain_tag = None
345 childdata = [T.a(href="%s?t=json" % name)["JSON"], ", ",
346 T.a(href="%s?t=uri" % name)["URI"], ", ",
347 T.a(href="%s?t=readonly-uri" % name)["readonly-URI"],
350 childdata.extend([", ", text_plain_tag])
352 ctx.fillSlots("data", childdata)
355 checker = IClient(ctx).getServiceNamed("checker")
359 d = defer.maybeDeferred(checker.checker_results_for,
360 target.get_verifier())
361 def _got(checker_results):
362 recent_results = reversed(checker_results[-5:])
363 if IFileNode.providedBy(target):
365 ", ".join(["%d/%d" % (found, needed)
367 (needed, total, found, sharemap))
368 in recent_results]) +
370 elif IDirectoryNode.providedBy(target):
372 "".join([{True:"+",False:"-"}[res]
373 for (when, res) in recent_results]) +
376 results = "%d results" % len(checker_results)
382 # TODO: include a link to see more results, including timestamps
383 # TODO: use a sparkline
384 ctx.fillSlots("checker_results", results)
388 def render_forms(self, ctx, data):
389 if self._dirnode.is_readonly():
390 return T.div["No upload forms: directory is read-only"]
391 mkdir = T.form(action=".", method="post",
392 enctype="multipart/form-data")[
394 T.input(type="hidden", name="t", value="mkdir"),
395 T.input(type="hidden", name="when_done", value=url.here),
396 T.legend(class_="freeform-form-label")["Create a new directory"],
397 "New directory name: ",
398 T.input(type="text", name="name"), " ",
399 T.input(type="submit", value="Create"),
402 upload = T.form(action=".", method="post",
403 enctype="multipart/form-data")[
405 T.input(type="hidden", name="t", value="upload"),
406 T.input(type="hidden", name="when_done", value=url.here),
407 T.legend(class_="freeform-form-label")["Upload a file to this directory"],
408 "Choose a file to upload: ",
409 T.input(type="file", name="file", class_="freeform-input-file"),
411 T.input(type="submit", value="Upload"),
413 T.input(type="checkbox", name="mutable"),
416 mount = T.form(action=".", method="post",
417 enctype="multipart/form-data")[
419 T.input(type="hidden", name="t", value="uri"),
420 T.input(type="hidden", name="when_done", value=url.here),
421 T.legend(class_="freeform-form-label")["Attach a file or directory"
425 T.input(type="text", name="name"), " ",
426 "URI of new child: ",
427 T.input(type="text", name="uri"), " ",
428 T.input(type="submit", value="Attach"),
430 return [T.div(class_="freeform-form")[mkdir],
431 T.div(class_="freeform-form")[upload],
432 T.div(class_="freeform-form")[mount],
435 def build_overwrite(self, ctx, data):
437 if IMutableFileNode.providedBy(target) and not target.is_readonly():
438 action="/uri/" + urllib.quote(target.get_uri())
439 overwrite = T.form(action=action, method="post",
440 enctype="multipart/form-data")[
442 T.input(type="hidden", name="t", value="overwrite"),
443 T.input(type='hidden', name='name', value=name),
444 T.input(type='hidden', name='when_done', value=url.here),
445 T.legend(class_="freeform-form-label")["Overwrite"],
447 T.input(type="file", name="file", class_="freeform-input-file"),
449 T.input(type="submit", value="Overwrite")
451 return [T.div(class_="freeform-form")[overwrite],]
455 def render_results(self, ctx, data):
456 req = inevow.IRequest(ctx)
457 return get_arg(req, "results", "")
459 class WebDownloadTarget:
460 implements(IDownloadTarget, IConsumer)
461 def __init__(self, req, content_type, content_encoding, save_to_file):
463 self._content_type = content_type
464 self._content_encoding = content_encoding
466 self._producer = None
467 self._save_to_file = save_to_file
469 def registerProducer(self, producer, streaming):
470 self._req.registerProducer(producer, streaming)
471 def unregisterProducer(self):
472 self._req.unregisterProducer()
474 def open(self, size):
476 self._req.setHeader("content-type", self._content_type)
477 if self._content_encoding:
478 self._req.setHeader("content-encoding", self._content_encoding)
479 self._req.setHeader("content-length", str(size))
480 if self._save_to_file is not None:
481 # tell the browser to save the file rather display it
482 # TODO: quote save_to_file properly
483 self._req.setHeader("content-disposition",
484 'attachment; filename="%s"'
485 % self._save_to_file)
487 def write(self, data):
488 self._req.write(data)
494 # The content-type is already set, and the response code
495 # has already been sent, so we can't provide a clean error
496 # indication. We can emit text (which a browser might interpret
497 # as something else), and if we sent a Size header, they might
498 # notice that we've truncated the data. Keep the error message
499 # small to improve the chances of having our error response be
500 # shorter than the intended results.
502 # We don't have a lot of options, unfortunately.
503 self._req.write("problem during download\n")
505 # We haven't written anything yet, so we can provide a sensible
508 msg.replace("\n", "|")
509 self._req.setResponseCode(http.GONE, msg)
510 self._req.setHeader("content-type", "text/plain")
511 # TODO: HTML-formatted exception?
512 self._req.write(str(why))
515 def register_canceller(self, cb):
520 class FileDownloader(resource.Resource):
521 def __init__(self, filenode, name):
522 assert (IFileNode.providedBy(filenode)
523 or IMutableFileNode.providedBy(filenode))
524 self._filenode = filenode
527 def render(self, req):
528 gte = static.getTypeAndEncoding
529 type, encoding = gte(self._name,
530 static.File.contentTypes,
531 static.File.contentEncodings,
532 defaultType="text/plain")
534 if get_arg(req, "save", False):
535 # TODO: make the API specification clear: should "save=" or
536 # "save=false" count?
537 save_to_file = self._name
538 wdt = WebDownloadTarget(req, type, encoding, save_to_file)
539 d = self._filenode.download(wdt)
540 # exceptions during download are handled by the WebDownloadTarget
541 d.addErrback(lambda why: None)
542 return server.NOT_DONE_YET
544 class BlockingFileError(Exception):
545 """We cannot auto-create a parent directory, because there is a file in
547 class NoReplacementError(Exception):
548 """There was already a child by that name, and you asked me to not replace it"""
549 class NoLocalDirectoryError(Exception):
550 """The localdir= directory didn't exist"""
552 LOCALHOST = "127.0.0.1"
554 class NeedLocalhostError:
555 implements(inevow.IResource)
557 def renderHTTP(self, ctx):
558 req = inevow.IRequest(ctx)
559 req.setResponseCode(http.FORBIDDEN)
560 req.setHeader("content-type", "text/plain")
561 return "localfile= or localdir= requires a local connection"
563 class NeedAbsolutePathError:
564 implements(inevow.IResource)
566 def renderHTTP(self, ctx):
567 req = inevow.IRequest(ctx)
568 req.setResponseCode(http.FORBIDDEN)
569 req.setHeader("content-type", "text/plain")
570 return "localfile= or localdir= requires an absolute path"
572 class LocalAccessDisabledError:
573 implements(inevow.IResource)
575 def renderHTTP(self, ctx):
576 req = inevow.IRequest(ctx)
577 req.setResponseCode(http.FORBIDDEN)
578 req.setHeader("content-type", "text/plain")
579 return "local file access is disabled"
582 implements(inevow.IResource)
583 def __init__(self, response_code, errmsg):
584 self._response_code = response_code
585 self._errmsg = errmsg
587 def renderHTTP(self, ctx):
588 req = inevow.IRequest(ctx)
589 req.setResponseCode(self._response_code)
590 req.setHeader("content-type", "text/plain")
594 class LocalFileDownloader(resource.Resource):
595 def __init__(self, filenode, local_filename):
596 self._local_filename = local_filename
598 self._filenode = filenode
600 def render(self, req):
601 target = download.FileName(self._local_filename)
602 d = self._filenode.download(target)
604 req.write(self._filenode.get_uri())
607 return server.NOT_DONE_YET
610 class FileJSONMetadata(rend.Page):
611 def __init__(self, filenode):
612 self._filenode = filenode
614 def renderHTTP(self, ctx):
615 req = inevow.IRequest(ctx)
616 req.setHeader("content-type", "text/plain")
617 return self.renderNode(self._filenode)
619 def renderNode(self, filenode):
620 file_uri = filenode.get_uri()
623 'size': filenode.get_size(),
625 return simplejson.dumps(data, indent=1)
627 class FileURI(FileJSONMetadata):
628 def renderNode(self, filenode):
629 file_uri = filenode.get_uri()
632 class FileReadOnlyURI(FileJSONMetadata):
633 def renderNode(self, filenode):
634 if filenode.is_readonly():
635 return filenode.get_uri()
637 return filenode.get_readonly().get_uri()
639 class DirnodeWalkerMixin:
640 """Visit all nodes underneath (and including) the rootnode, one at a
641 time. For each one, call the visitor. The visitor will see the
642 IDirectoryNode before it sees any of the IFileNodes inside. If the
643 visitor returns a Deferred, I do not call the visitor again until it has
647 ## def _walk_if_we_could_use_generators(self, rootnode, rootpath=()):
648 ## # this is what we'd be doing if we didn't have the Deferreds and
649 ## # thus could use generators
650 ## yield rootpath, rootnode
651 ## for childname, childnode in rootnode.list().items():
652 ## childpath = rootpath + (childname,)
653 ## if IFileNode.providedBy(childnode):
654 ## yield childpath, childnode
655 ## elif IDirectoryNode.providedBy(childnode):
656 ## for res in self._walk_if_we_could_use_generators(childnode,
660 def walk(self, rootnode, visitor, rootpath=()):
662 def _listed(listing):
663 return listing.items()
664 d.addCallback(_listed)
665 d.addCallback(self._handle_items, visitor, rootpath)
668 def _handle_items(self, items, visitor, rootpath):
671 childname, (childnode, metadata) = items[0]
672 childpath = rootpath + (childname,)
673 d = defer.maybeDeferred(visitor, childpath, childnode, metadata)
674 if IDirectoryNode.providedBy(childnode):
675 d.addCallback(lambda res: self.walk(childnode, visitor, childpath))
676 d.addCallback(lambda res:
677 self._handle_items(items[1:], visitor, rootpath))
680 class LocalDirectoryDownloader(resource.Resource, DirnodeWalkerMixin):
681 def __init__(self, dirnode, localdir):
682 self._dirnode = dirnode
683 self._localdir = localdir
685 def _handle(self, path, node, metadata):
686 localfile = os.path.join(self._localdir, os.sep.join(path))
687 if IDirectoryNode.providedBy(node):
688 fileutil.make_dirs(localfile)
689 elif IFileNode.providedBy(node):
690 target = download.FileName(localfile)
691 return node.download(target)
693 def render(self, req):
694 d = self.walk(self._dirnode, self._handle)
696 req.setHeader("content-type", "text/plain")
697 return "operation complete"
701 class DirectoryJSONMetadata(rend.Page):
702 def __init__(self, dirnode):
703 self._dirnode = dirnode
705 def renderHTTP(self, ctx):
706 req = inevow.IRequest(ctx)
707 req.setHeader("content-type", "text/plain")
708 return self.renderNode(self._dirnode)
710 def renderNode(self, node):
714 for name, (childnode, metadata) in children.iteritems():
715 if IFileNode.providedBy(childnode):
716 kiduri = childnode.get_uri()
717 kiddata = ("filenode",
719 'size': childnode.get_size(),
720 'metadata': metadata,
723 assert IDirectoryNode.providedBy(childnode), (childnode, children,)
724 kiddata = ("dirnode",
725 {'ro_uri': childnode.get_readonly_uri(),
726 'metadata': metadata,
728 if not childnode.is_readonly():
729 kiddata[1]['rw_uri'] = childnode.get_uri()
731 contents = { 'children': kids,
732 'ro_uri': node.get_readonly_uri(),
734 if not node.is_readonly():
735 contents['rw_uri'] = node.get_uri()
736 data = ("dirnode", contents)
737 return simplejson.dumps(data, indent=1)
741 class DirectoryURI(DirectoryJSONMetadata):
742 def renderNode(self, node):
743 return node.get_uri()
745 class DirectoryReadonlyURI(DirectoryJSONMetadata):
746 def renderNode(self, node):
747 return node.get_readonly_uri()
749 class RenameForm(rend.Page):
751 docFactory = getxmlfile("rename-form.xhtml")
753 def __init__(self, rootname, dirnode, dirpath):
754 self._rootname = rootname
755 self._dirnode = dirnode
756 self._dirpath = dirpath
758 def dirpath_as_string(self):
759 return "/" + "/".join(self._dirpath)
761 def render_title(self, ctx, data):
762 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
764 def render_header(self, ctx, data):
765 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
766 num_dirs = len(parent_directories)
768 header = [ "Rename in directory '",
769 "<%s>/" % self._rootname,
770 "/".join(self._dirpath),
773 if self._dirnode.is_readonly():
774 header.append(" (readonly)")
775 return ctx.tag[header]
777 def render_when_done(self, ctx, data):
778 return T.input(type="hidden", name="when_done", value=url.here)
780 def render_get_name(self, ctx, data):
781 req = inevow.IRequest(ctx)
782 name = get_arg(req, "name", "")
783 ctx.tag.attributes['value'] = name
786 class POSTHandler(rend.Page):
787 def __init__(self, node, replace):
789 self._replace = replace
791 def _check_replacement(self, name):
793 return defer.succeed(None)
794 d = self._node.has_child(name)
797 raise NoReplacementError("There was already a child by that "
798 "name, and you asked me to not "
804 def renderHTTP(self, ctx):
805 req = inevow.IRequest(ctx)
807 t = get_arg(req, "t")
810 name = get_arg(req, "name", None)
811 if name and "/" in name:
812 req.setResponseCode(http.BAD_REQUEST)
813 req.setHeader("content-type", "text/plain")
814 return "name= may not contain a slash"
817 # we allow the user to delete an empty-named file, but not to create
818 # them, since that's an easy and confusing mistake to make
820 when_done = get_arg(req, "when_done", None)
821 if not boolean_of_arg(get_arg(req, "replace", "true")):
822 self._replace = False
826 raise RuntimeError("mkdir requires a name")
827 d = self._check_replacement(name)
828 d.addCallback(lambda res: self._node.create_empty_directory(name))
829 d.addCallback(lambda res: "directory created")
832 raise RuntimeError("set-uri requires a name")
833 newuri = get_arg(req, "uri")
834 assert newuri is not None
835 d = self._check_replacement(name)
836 d.addCallback(lambda res: self._node.set_uri(name, newuri))
837 d.addCallback(lambda res: newuri)
840 # apparently an <input type="hidden" name="name" value="">
841 # won't show up in the resulting encoded form.. the 'name'
842 # field is completely missing. So to allow deletion of an
843 # empty file, we have to pretend that None means ''. The only
844 # downide of this is a slightly confusing error message if
845 # someone does a POST without a name= field. For our own HTML
846 # thisn't a big deal, because we create the 'delete' POST
849 d = self._node.delete(name)
850 d.addCallback(lambda res: "thing deleted")
852 from_name = 'from_name' in req.fields and req.fields["from_name"].value
853 if from_name is not None:
854 from_name = from_name.strip()
855 to_name = 'to_name' in req.fields and req.fields["to_name"].value
856 if to_name is not None:
857 to_name = to_name.strip()
858 if not from_name or not to_name:
859 raise RuntimeError("rename requires from_name and to_name")
860 if not IDirectoryNode.providedBy(self._node):
861 raise RuntimeError("rename must only be called on directories")
862 for k,v in [ ('from_name', from_name), ('to_name', to_name) ]:
864 req.setResponseCode(http.BAD_REQUEST)
865 req.setHeader("content-type", "text/plain")
866 return "%s= may not contain a slash" % (k,)
867 d = self._check_replacement(to_name)
868 d.addCallback(lambda res: self._node.get(from_name))
870 uri = child.get_uri()
871 # now actually do the rename
872 return self._node.set_uri(to_name, uri)
873 d.addCallback(add_dest)
875 return self._node.delete(from_name)
876 d.addCallback(rm_src)
877 d.addCallback(lambda res: "thing renamed")
880 if "mutable" in req.fields:
881 contents = req.fields["file"]
882 name = name or contents.filename
886 raise RuntimeError("upload-mutable requires a name")
887 # SDMF: files are small, and we can only upload data.
888 contents.file.seek(0)
889 data = contents.file.read()
890 #uploadable = FileHandle(contents.file)
891 d = self._check_replacement(name)
892 d.addCallback(lambda res: self._node.has_child(name))
893 def _checked(present):
895 # modify the existing one instead of creating a new
897 d2 = self._node.get(name)
898 def _got_newnode(newnode):
899 d3 = newnode.replace(data)
900 d3.addCallback(lambda res: newnode.get_uri())
902 d2.addCallback(_got_newnode)
904 d2 = IClient(ctx).create_mutable_file(data)
905 def _uploaded(newnode):
906 d1 = self._node.set_node(name, newnode)
907 d1.addCallback(lambda res: newnode.get_uri())
909 d2.addCallback(_uploaded)
911 d.addCallback(_checked)
913 contents = req.fields["file"]
914 name = name or contents.filename
918 raise RuntimeError("upload requires a name")
919 uploadable = FileHandle(contents.file)
920 d = self._check_replacement(name)
921 d.addCallback(lambda res: self._node.add_file(name, uploadable))
923 return newnode.get_uri()
926 elif t == "overwrite":
927 contents = req.fields["file"]
928 # SDMF: files are small, and we can only upload data.
929 contents.file.seek(0)
930 data = contents.file.read()
931 # TODO: 'name' handling needs review
932 d = defer.succeed(self._node)
933 def _got_child_overwrite(child_node):
934 child_node.replace(data)
935 return child_node.get_uri()
936 d.addCallback(_got_child_overwrite)
939 d = self._node.get(name)
940 def _got_child_check(child_node):
941 d2 = child_node.check()
943 log.msg("checked %s, results %s" % (child_node, res),
944 facility="tahoe.webish", level=log.NOISY)
946 d2.addCallback(_done)
948 d.addCallback(_got_child_check)
951 return "BAD t=%s" % t
953 d.addCallback(lambda res: url.URL.fromString(when_done))
954 def _check_replacement(f):
955 # TODO: make this more human-friendly: maybe send them to the
956 # when_done page but with an extra query-arg that will display
957 # the error message in a big box at the top of the page. The
958 # directory page that when_done= usually points to accepts a
959 # result= argument.. use that.
960 f.trap(NoReplacementError)
961 req.setResponseCode(http.CONFLICT)
962 req.setHeader("content-type", "text/plain")
964 d.addErrback(_check_replacement)
967 class DELETEHandler(rend.Page):
968 def __init__(self, node, name):
972 def renderHTTP(self, ctx):
973 req = inevow.IRequest(ctx)
974 d = self._node.delete(self._name)
976 # what should this return??
977 return "%s deleted" % self._name
979 def _trap_missing(f):
981 req.setResponseCode(http.NOT_FOUND)
982 req.setHeader("content-type", "text/plain")
983 return "no such child %s" % self._name
984 d.addErrback(_trap_missing)
987 class PUTHandler(rend.Page):
988 def __init__(self, node, path, t, localfile, localdir, replace):
992 self._localfile = localfile
993 self._localdir = localdir
994 self._replace = replace
996 def renderHTTP(self, ctx):
997 req = inevow.IRequest(ctx)
999 localfile = self._localfile
1000 localdir = self._localdir
1002 if t == "upload" and not (localfile or localdir):
1003 req.setResponseCode(http.BAD_REQUEST)
1004 req.setHeader("content-type", "text/plain")
1005 return "t=upload requires localfile= or localdir="
1007 # we must traverse the path, creating new directories as necessary
1008 d = self._get_or_create_directories(self._node, self._path[:-1])
1009 name = self._path[-1]
1010 d.addCallback(self._check_replacement, name, self._replace)
1013 d.addCallback(self._upload_localfile, localfile, name)
1016 # take the last step
1017 d.addCallback(self._get_or_create_directories, self._path[-1:])
1018 d.addCallback(self._upload_localdir, localdir)
1020 d.addCallback(self._attach_uri, req.content, name)
1022 d.addCallback(self._mkdir, name)
1024 d.addCallback(self._upload_file, req.content, name)
1026 def _transform_error(f):
1027 errors = {BlockingFileError: http.BAD_REQUEST,
1028 NoReplacementError: http.CONFLICT,
1029 NoLocalDirectoryError: http.BAD_REQUEST,
1031 for k,v in errors.items():
1033 req.setResponseCode(v)
1034 req.setHeader("content-type", "text/plain")
1037 d.addErrback(_transform_error)
1040 def _get_or_create_directories(self, node, path):
1041 if not IDirectoryNode.providedBy(node):
1042 # unfortunately it is too late to provide the name of the
1043 # blocking directory in the error message.
1044 raise BlockingFileError("cannot create directory because there "
1045 "is a file in the way")
1047 return defer.succeed(node)
1048 d = node.get(path[0])
1049 def _maybe_create(f):
1051 return node.create_empty_directory(path[0])
1052 d.addErrback(_maybe_create)
1053 d.addCallback(self._get_or_create_directories, path[1:])
1056 def _check_replacement(self, node, name, replace):
1059 d = node.has_child(name)
1062 raise NoReplacementError("There was already a child by that "
1063 "name, and you asked me to not "
1069 def _mkdir(self, node, name):
1070 d = node.create_empty_directory(name)
1072 return newnode.get_uri()
1073 d.addCallback(_done)
1076 def _upload_file(self, node, contents, name):
1077 uploadable = FileHandle(contents)
1078 d = node.add_file(name, uploadable)
1079 def _done(filenode):
1080 log.msg("webish upload complete",
1081 facility="tahoe.webish", level=log.NOISY)
1082 return filenode.get_uri()
1083 d.addCallback(_done)
1086 def _upload_localfile(self, node, localfile, name):
1087 uploadable = FileName(localfile)
1088 d = node.add_file(name, uploadable)
1089 d.addCallback(lambda filenode: filenode.get_uri())
1092 def _attach_uri(self, parentnode, contents, name):
1093 newuri = contents.read().strip()
1094 d = parentnode.set_uri(name, newuri)
1097 d.addCallback(_done)
1100 def _upload_localdir(self, node, localdir):
1101 # build up a list of files to upload
1104 msg = "No files to upload! %s is empty" % localdir
1105 if not os.path.exists(localdir):
1106 msg = "%s doesn't exist!" % localdir
1107 raise NoLocalDirectoryError(msg)
1108 for root, dirs, files in os.walk(localdir):
1109 if root == localdir:
1112 relative_root = root[len(localdir)+1:]
1113 path = tuple(relative_root.split(os.sep))
1115 all_dirs.append(path + (d,))
1117 all_files.append(path + (f,))
1118 d = defer.succeed(msg)
1119 for dir in all_dirs:
1121 d.addCallback(self._makedir, node, dir)
1123 d.addCallback(self._upload_one_file, node, localdir, f)
1126 def _makedir(self, res, node, dir):
1127 d = defer.succeed(None)
1128 # get the parent. As long as os.walk gives us parents before
1129 # children, this ought to work
1130 d.addCallback(lambda res: node.get_child_at_path(dir[:-1]))
1131 # then create the child directory
1132 d.addCallback(lambda parent: parent.create_empty_directory(dir[-1]))
1135 def _upload_one_file(self, res, node, localdir, f):
1136 # get the parent. We can be sure this exists because we already
1137 # went through and created all the directories we require.
1138 localfile = os.path.join(localdir, *f)
1139 d = node.get_child_at_path(f[:-1])
1140 d.addCallback(self._upload_localfile, localfile, f[-1])
1144 class Manifest(rend.Page):
1145 docFactory = getxmlfile("manifest.xhtml")
1146 def __init__(self, dirnode, dirpath):
1147 self._dirnode = dirnode
1148 self._dirpath = dirpath
1150 def dirpath_as_string(self):
1151 return "/" + "/".join(self._dirpath)
1153 def render_title(self, ctx):
1154 return T.title["Manifest of %s" % self.dirpath_as_string()]
1156 def render_header(self, ctx):
1157 return T.p["Manifest of %s" % self.dirpath_as_string()]
1159 def data_items(self, ctx, data):
1160 return self._dirnode.build_manifest()
1162 def render_row(self, ctx, refresh_cap):
1163 ctx.fillSlots("refresh_capability", refresh_cap)
1167 implements(inevow.IResource)
1168 def renderHTTP(self, ctx):
1169 req = inevow.IRequest(ctx)
1170 req.setResponseCode(http.BAD_REQUEST)
1171 req.setHeader("content-type", "text/plain")
1174 def child_error(text):
1179 class VDrive(rend.Page):
1181 def __init__(self, node, name):
1185 def get_child_at_path(self, path):
1187 return self.node.get_child_at_path(path)
1188 return defer.succeed(self.node)
1190 def locateChild(self, ctx, segments):
1191 req = inevow.IRequest(ctx)
1195 t = get_arg(req, "t", "")
1196 localfile = get_arg(req, "localfile", None)
1197 if localfile is not None:
1198 if localfile != os.path.abspath(localfile):
1199 return NeedAbsolutePathError(), ()
1200 localdir = get_arg(req, "localdir", None)
1201 if localdir is not None:
1202 if localdir != os.path.abspath(localdir):
1203 return NeedAbsolutePathError(), ()
1204 if localfile or localdir:
1205 if not ILocalAccess(ctx).local_access_is_allowed():
1206 return LocalAccessDisabledError(), ()
1207 if req.getHost().host != LOCALHOST:
1208 return NeedLocalhostError(), ()
1209 # TODO: think about clobbering/revealing config files and node secrets
1211 replace = boolean_of_arg(get_arg(req, "replace", "true"))
1214 # the node must exist, and our operation will be performed on the
1216 d = self.get_child_at_path(path)
1217 def file_or_dir(node):
1218 if (IFileNode.providedBy(node)
1219 or IMutableFileNode.providedBy(node)):
1220 filename = "unknown"
1223 filename = get_arg(req, "filename", filename)
1226 # write contents to a local file
1227 return LocalFileDownloader(node, localfile), ()
1228 # send contents as the result
1229 return FileDownloader(node, filename), ()
1231 # send contents as the result
1232 return FileDownloader(node, filename), ()
1234 return FileJSONMetadata(node), ()
1236 return FileURI(node), ()
1237 elif t == "readonly-uri":
1238 return FileReadOnlyURI(node), ()
1240 return child_error("bad t=%s" % t)
1241 elif IDirectoryNode.providedBy(node):
1244 # recursive download to a local directory
1245 return LocalDirectoryDownloader(node, localdir), ()
1246 return child_error("t=download requires localdir=")
1248 # send an HTML representation of the directory
1249 return Directory(self.name, node, path), ()
1251 return DirectoryJSONMetadata(node), ()
1253 return DirectoryURI(node), ()
1254 elif t == "readonly-uri":
1255 return DirectoryReadonlyURI(node), ()
1256 elif t == "manifest":
1257 return Manifest(node, path), ()
1258 elif t == 'rename-form':
1259 return RenameForm(self.name, node, path), ()
1261 return child_error("bad t=%s" % t)
1263 return child_error("unknown node type")
1264 d.addCallback(file_or_dir)
1265 elif method == "POST":
1266 # the node must exist, and our operation will be performed on the
1268 d = self.get_child_at_path(path)
1269 def _got_POST(node):
1270 return POSTHandler(node, replace), ()
1271 d.addCallback(_got_POST)
1272 elif method == "DELETE":
1273 # the node must exist, and our operation will be performed on its
1275 assert path # you can't delete the root
1276 d = self.get_child_at_path(path[:-1])
1277 def _got_DELETE(node):
1278 return DELETEHandler(node, path[-1]), ()
1279 d.addCallback(_got_DELETE)
1280 elif method in ("PUT",):
1281 # the node may or may not exist, and our operation may involve
1282 # all the ancestors of the node.
1283 return PUTHandler(self.node, path, t, localfile, localdir, replace), ()
1285 return rend.NotFound
1288 class UnlinkedPUTCHKUploader(rend.Page):
1289 def renderHTTP(self, ctx):
1290 req = inevow.IRequest(ctx)
1291 assert req.method == "PUT"
1292 # "PUT /uri", to create an unlinked file. This is like PUT but
1293 # without the associated set_uri.
1295 uploadable = FileHandle(req.content)
1296 d = IClient(ctx).upload(uploadable)
1297 d.addCallback(lambda results: results.uri)
1298 # that fires with the URI of the new file
1301 class UnlinkedPUTSSKUploader(rend.Page):
1302 def renderHTTP(self, ctx):
1303 req = inevow.IRequest(ctx)
1304 assert req.method == "PUT"
1305 # SDMF: files are small, and we can only upload data
1306 contents = req.content
1308 data = contents.read()
1309 d = IClient(ctx).create_mutable_file(data)
1310 d.addCallback(lambda n: n.get_uri())
1313 class UnlinkedPUTCreateDirectory(rend.Page):
1314 def renderHTTP(self, ctx):
1315 req = inevow.IRequest(ctx)
1316 assert req.method == "PUT"
1317 # "PUT /uri?t=mkdir", to create an unlinked directory.
1318 d = IClient(ctx).create_empty_dirnode()
1319 d.addCallback(lambda dirnode: dirnode.get_uri())
1320 # XXX add redirect_to_result
1324 class UnlinkedPOSTCHKUploader(rend.Page):
1325 """'POST /uri', to create an unlinked file."""
1326 docFactory = getxmlfile("unlinked-upload.xhtml")
1328 def __init__(self, client, req):
1329 rend.Page.__init__(self)
1330 # we start the upload now, and distribute notification of its
1331 # completion to render_ methods with an ObserverList
1332 assert req.method == "POST"
1333 self._done = observer.OneShotObserverList()
1334 fileobj = req.fields["file"].file
1335 uploadable = FileHandle(fileobj)
1336 d = client.upload(uploadable)
1337 d.addBoth(self._done.fire)
1339 def renderHTTP(self, ctx):
1340 req = inevow.IRequest(ctx)
1341 when_done = get_arg(req, "when_done", None)
1343 # if when_done= is provided, return a redirect instead of our
1344 # usual upload-results page
1345 d = self._done.when_fired()
1346 d.addCallback(lambda res: url.URL.fromString(when_done))
1348 return rend.Page.renderHTTP(self, ctx)
1350 def upload_results(self):
1351 return self._done.when_fired()
1353 def data_done(self, ctx, data):
1354 d = self.upload_results()
1355 d.addCallback(lambda res: "done!")
1358 def data_uri(self, ctx, data):
1359 d = self.upload_results()
1360 d.addCallback(lambda res: res.uri)
1363 def render_download_link(self, ctx, data):
1364 d = self.upload_results()
1365 d.addCallback(lambda res: T.a(href="/uri/" + urllib.quote(res.uri))
1366 ["/uri/" + res.uri])
1369 def render_sharemap(self, ctx, data):
1370 d = self.upload_results()
1371 d.addCallback(lambda res: res.sharemap)
1372 def _render(sharemap):
1373 if sharemap is None:
1376 for shnum in sorted(sharemap.keys()):
1377 l[T.li["%d -> %s" % (shnum, sharemap[shnum])]]
1379 d.addCallback(_render)
1382 def render_servermap(self, ctx, data):
1383 d = self.upload_results()
1384 d.addCallback(lambda res: res.servermap)
1385 def _render(servermap):
1386 if servermap is None:
1389 for peerid in sorted(servermap.keys()):
1390 peerid_s = idlib.shortnodeid_b2a(peerid)
1391 shares_s = ",".join([str(shnum) for shnum in servermap[peerid]])
1392 l[T.li["[%s] got shares: %s" % (peerid_s, shares_s)]]
1394 d.addCallback(_render)
1397 def data_file_size(self, ctx, data):
1398 d = self.upload_results()
1399 d.addCallback(lambda res: res.file_size)
1402 def render_time(self, ctx, data):
1403 # 1.23s, 790ms, 132us
1410 return "%dms" % (1000*s)
1412 return "%.1fms" % (1000*s)
1413 return "%dus" % (1000000*s)
1415 def render_rate(self, ctx, data):
1416 # 21.8kBps, 554.4kBps 4.37MBps
1421 return "%1.2fMBps" % (r/1000000)
1423 return "%.1fkBps" % (r/1000)
1426 def _get_time(self, name):
1427 d = self.upload_results()
1428 d.addCallback(lambda res: res.timings.get(name))
1431 def data_time_total(self, ctx, data):
1432 return self._get_time("total")
1434 def data_time_storage_index(self, ctx, data):
1435 return self._get_time("storage_index")
1437 def data_time_contacting_helper(self, ctx, data):
1438 return self._get_time("contacting_helper")
1440 def data_time_existence_check(self, ctx, data):
1441 return self._get_time("existence_check")
1443 def data_time_cumulative_fetch(self, ctx, data):
1444 return self._get_time("cumulative_fetch")
1446 def data_time_helper_total(self, ctx, data):
1447 return self._get_time("helper_total")
1449 def data_time_peer_selection(self, ctx, data):
1450 return self._get_time("peer_selection")
1452 def data_time_total_encode_and_push(self, ctx, data):
1453 return self._get_time("total_encode_and_push")
1455 def data_time_cumulative_encoding(self, ctx, data):
1456 return self._get_time("cumulative_encoding")
1458 def data_time_cumulative_sending(self, ctx, data):
1459 return self._get_time("cumulative_sending")
1461 def data_time_hashes_and_close(self, ctx, data):
1462 return self._get_time("hashes_and_close")
1464 def _get_rate(self, name):
1465 d = self.upload_results()
1467 file_size = r.file_size
1468 time = r.timings.get(name)
1472 return 1.0 * file_size / time
1473 except ZeroDivisionError:
1475 d.addCallback(_convert)
1478 def data_rate_total(self, ctx, data):
1479 return self._get_rate("total")
1481 def data_rate_storage_index(self, ctx, data):
1482 return self._get_rate("storage_index")
1484 def data_rate_encode(self, ctx, data):
1485 return self._get_rate("cumulative_encoding")
1487 def data_rate_push(self, ctx, data):
1488 return self._get_rate("cumulative_sending")
1490 def data_rate_encode_and_push(self, ctx, data):
1491 d = self.upload_results()
1493 file_size = r.file_size
1494 if file_size is None:
1496 time1 = r.timings.get("cumulative_encoding")
1499 time2 = r.timings.get("cumulative_sending")
1503 return 1.0 * file_size / (time1+time2)
1504 except ZeroDivisionError:
1506 d.addCallback(_convert)
1509 def data_rate_ciphertext_fetch(self, ctx, data):
1510 d = self.upload_results()
1512 fetch_size = r.ciphertext_fetched
1513 if fetch_size is None:
1515 time = r.timings.get("cumulative_fetch")
1519 return 1.0 * fetch_size / time
1520 except ZeroDivisionError:
1522 d.addCallback(_convert)
1525 class UnlinkedPOSTSSKUploader(rend.Page):
1526 def renderHTTP(self, ctx):
1527 req = inevow.IRequest(ctx)
1528 assert req.method == "POST"
1530 # "POST /uri", to create an unlinked file.
1531 # SDMF: files are small, and we can only upload data
1532 contents = req.fields["file"]
1533 contents.file.seek(0)
1534 data = contents.file.read()
1535 d = IClient(ctx).create_mutable_file(data)
1536 d.addCallback(lambda n: n.get_uri())
1539 class UnlinkedPOSTCreateDirectory(rend.Page):
1540 def renderHTTP(self, ctx):
1541 req = inevow.IRequest(ctx)
1542 assert req.method == "POST"
1544 # "POST /uri?t=mkdir", to create an unlinked directory.
1545 d = IClient(ctx).create_empty_dirnode()
1546 redirect = get_arg(req, "redirect_to_result", "false")
1547 if boolean_of_arg(redirect):
1548 def _then_redir(res):
1549 new_url = "uri/" + urllib.quote(res.get_uri())
1550 req.setResponseCode(http.SEE_OTHER) # 303
1551 req.setHeader('location', new_url)
1554 d.addCallback(_then_redir)
1556 d.addCallback(lambda dirnode: dirnode.get_uri())
1559 class Status(rend.Page):
1560 docFactory = getxmlfile("status.xhtml")
1562 def data_uploads(self, ctx, data):
1563 return IClient(ctx).list_uploads()
1565 def data_downloads(self, ctx, data):
1566 return IClient(ctx).list_downloads()
1568 def _render_common(self, ctx, data):
1570 si_s = base32.b2a_or_none(s.get_storage_index())
1573 ctx.fillSlots("si", si_s)
1574 ctx.fillSlots("helper", {True: "Yes",
1575 False: "No"}[s.using_helper()])
1579 ctx.fillSlots("total_size", size)
1580 ctx.fillSlots("status", s.get_status())
1582 def render_row_upload(self, ctx, data):
1583 self._render_common(ctx, data)
1584 (chk, ciphertext, encandpush) = data.get_progress()
1585 # TODO: make an ascii-art bar
1586 ctx.fillSlots("progress_hash", "%.1f%%" % (100.0 * chk))
1587 ctx.fillSlots("progress_ciphertext", "%.1f%%" % (100.0 * ciphertext))
1588 ctx.fillSlots("progress_encode", "%.1f%%" % (100.0 * encandpush))
1591 def render_row_download(self, ctx, data):
1592 self._render_common(ctx, data)
1593 progress = data.get_progress()
1594 # TODO: make an ascii-art bar
1595 ctx.fillSlots("progress", "%.1f%%" % (100.0 * progress))
1599 class Root(rend.Page):
1602 docFactory = getxmlfile("welcome.xhtml")
1604 def locateChild(self, ctx, segments):
1605 client = IClient(ctx)
1606 req = inevow.IRequest(ctx)
1608 segments = list(segments) # XXX HELP I AM YUCKY!
1609 while segments and not segments[-1]:
1613 segments = tuple(segments)
1615 if segments[0] == "uri":
1616 if len(segments) == 1 or segments[1] == '':
1617 uri = get_arg(req, "uri", None)
1619 there = url.URL.fromContext(ctx)
1620 there = there.clear("uri")
1621 there = there.child("uri").child(uri)
1623 if len(segments) == 1:
1625 if req.method == "PUT":
1626 # either "PUT /uri" to create an unlinked file, or
1627 # "PUT /uri?t=mkdir" to create an unlinked directory
1628 t = get_arg(req, "t", "").strip()
1630 mutable = bool(get_arg(req, "mutable", "").strip())
1632 return UnlinkedPUTSSKUploader(), ()
1634 return UnlinkedPUTCHKUploader(), ()
1636 return UnlinkedPUTCreateDirectory(), ()
1637 errmsg = "/uri only accepts PUT and PUT?t=mkdir"
1638 return WebError(http.BAD_REQUEST, errmsg), ()
1640 elif req.method == "POST":
1641 # "POST /uri?t=upload&file=newfile" to upload an
1642 # unlinked file or "POST /uri?t=mkdir" to create a
1644 t = get_arg(req, "t", "").strip()
1645 if t in ("", "upload"):
1646 mutable = bool(get_arg(req, "mutable", "").strip())
1648 return UnlinkedPOSTSSKUploader(), ()
1650 return UnlinkedPOSTCHKUploader(client, req), ()
1652 return UnlinkedPOSTCreateDirectory(), ()
1653 errmsg = "/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, and POST?t=mkdir"
1654 return WebError(http.BAD_REQUEST, errmsg), ()
1655 if len(segments) < 2:
1656 return rend.NotFound
1658 d = defer.maybeDeferred(client.create_node_from_uri, uri)
1659 d.addCallback(lambda node: VDrive(node, uri))
1660 d.addCallback(lambda vd: vd.locateChild(ctx, segments[2:]))
1661 def _trap_KeyError(f):
1663 return rend.FourOhFour(), ()
1664 d.addErrback(_trap_KeyError)
1666 elif segments[0] == "xmlrpc":
1667 raise NotImplementedError()
1668 return rend.Page.locateChild(self, ctx, segments)
1670 child_webform_css = webform.defaultCSS
1671 child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
1673 child_provisioning = provisioning.ProvisioningTool()
1674 child_status = Status()
1676 def data_version(self, ctx, data):
1677 return get_package_versions_string()
1678 def data_import_path(self, ctx, data):
1679 return str(allmydata)
1680 def data_my_nodeid(self, ctx, data):
1681 return idlib.nodeid_b2a(IClient(ctx).nodeid)
1682 def data_storage(self, ctx, data):
1683 client = IClient(ctx)
1685 ss = client.getServiceNamed("storage")
1687 return "Not running"
1688 allocated = ss.allocated_size()
1689 return "about %d bytes allocated" % allocated
1691 def data_introducer_furl(self, ctx, data):
1692 return IClient(ctx).introducer_furl
1693 def data_connected_to_introducer(self, ctx, data):
1694 if IClient(ctx).connected_to_introducer():
1698 def data_helper_furl(self, ctx, data):
1700 uploader = IClient(ctx).getServiceNamed("uploader")
1703 furl, connected = uploader.get_helper_info()
1705 def data_connected_to_helper(self, ctx, data):
1707 uploader = IClient(ctx).getServiceNamed("uploader")
1709 return "no" # we don't even have an Uploader
1710 furl, connected = uploader.get_helper_info()
1715 def data_known_storage_servers(self, ctx, data):
1716 ic = IClient(ctx).introducer_client
1718 for c in ic.get_all_connectors().values()
1719 if c.service_name == "storage"]
1722 def data_connected_storage_servers(self, ctx, data):
1723 ic = IClient(ctx).introducer_client
1724 return len(ic.get_all_connections_for("storage"))
1726 def data_services(self, ctx, data):
1727 ic = IClient(ctx).introducer_client
1728 c = [ (service_name, nodeid, rsc)
1729 for (nodeid, service_name), rsc
1730 in ic.get_all_connectors().items() ]
1734 def render_service_row(self, ctx, data):
1735 (service_name, nodeid, rsc) = data
1736 ctx.fillSlots("peerid", "%s %s" % (idlib.nodeid_b2a(nodeid),
1739 rhost = rsc.remote_host
1740 if nodeid == IClient(ctx).nodeid:
1741 rhost_s = "(loopback)"
1742 elif isinstance(rhost, address.IPv4Address):
1743 rhost_s = "%s:%d" % (rhost.host, rhost.port)
1745 rhost_s = str(rhost)
1746 connected = "Yes: to " + rhost_s
1747 since = rsc.last_connect_time
1750 since = rsc.last_loss_time
1752 TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
1753 ctx.fillSlots("connected", connected)
1754 ctx.fillSlots("since", time.strftime(TIME_FORMAT, time.localtime(since)))
1755 ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
1756 time.localtime(rsc.announcement_time)))
1757 ctx.fillSlots("version", rsc.version)
1758 ctx.fillSlots("service_name", rsc.service_name)
1762 def render_download_form(self, ctx, data):
1763 # this is a form where users can download files by URI
1764 form = T.form(action="uri", method="get",
1765 enctype="multipart/form-data")[
1767 T.legend(class_="freeform-form-label")["Download a file"],
1768 "URI to download: ",
1769 T.input(type="text", name="uri"), " ",
1770 "Filename to download as: ",
1771 T.input(type="text", name="filename"), " ",
1772 T.input(type="submit", value="Download!"),
1776 def render_view_form(self, ctx, data):
1777 # this is a form where users can download files by URI, or jump to a
1779 form = T.form(action="uri", method="get",
1780 enctype="multipart/form-data")[
1782 T.legend(class_="freeform-form-label")["View a file or directory"],
1784 T.input(type="text", name="uri"), " ",
1785 T.input(type="submit", value="View!"),
1789 def render_upload_form(self, ctx, data):
1790 # this is a form where users can upload unlinked files
1791 form = T.form(action="uri", method="post",
1792 enctype="multipart/form-data")[
1794 T.legend(class_="freeform-form-label")["Upload a file"],
1796 T.input(type="file", name="file", class_="freeform-input-file"),
1797 T.input(type="hidden", name="t", value="upload"),
1798 " Mutable?:", T.input(type="checkbox", name="mutable"),
1799 T.input(type="submit", value="Upload!"),
1803 def render_mkdir_form(self, ctx, data):
1804 # this is a form where users can create new directories
1805 form = T.form(action="uri", method="post",
1806 enctype="multipart/form-data")[
1808 T.legend(class_="freeform-form-label")["Create a directory"],
1809 T.input(type="hidden", name="t", value="mkdir"),
1810 T.input(type="hidden", name="redirect_to_result", value="true"),
1811 T.input(type="submit", value="Create Directory!"),
1817 implements(ILocalAccess)
1819 self.local_access = False
1820 def local_access_is_allowed(self):
1821 return self.local_access
1823 class WebishServer(service.MultiService):
1826 def __init__(self, webport, nodeurl_path=None):
1827 service.MultiService.__init__(self)
1828 self.webport = webport
1830 self.site = site = appserver.NevowSite(self.root)
1831 self.site.requestFactory = MyRequest
1832 self.allow_local = LocalAccess()
1833 self.site.remember(self.allow_local, ILocalAccess)
1834 s = strports.service(webport, site)
1835 s.setServiceParent(self)
1836 self.listener = s # stash it so the tests can query for the portnum
1837 self._started = defer.Deferred()
1839 self._started.addCallback(self._write_nodeurl_file, nodeurl_path)
1841 def allow_local_access(self, enable=True):
1842 self.allow_local.local_access = enable
1844 def startService(self):
1845 service.MultiService.startService(self)
1846 # to make various services available to render_* methods, we stash a
1847 # reference to the client on the NevowSite. This will be available by
1848 # adapting the 'context' argument to a special marker interface named
1850 self.site.remember(self.parent, IClient)
1851 # I thought you could do the same with an existing interface, but
1852 # apparently 'ISite' does not exist
1853 #self.site._client = self.parent
1854 self._started.callback(None)
1856 def _write_nodeurl_file(self, junk, nodeurl_path):
1857 # what is our webport?
1859 if isinstance(s, internet.TCPServer):
1860 base_url = "http://localhost:%d" % s._port.getHost().port
1861 elif isinstance(s, internet.SSLServer):
1862 base_url = "https://localhost:%d" % s._port.getHost().port
1866 f = open(nodeurl_path, 'wb')
1867 # this file is world-readable
1868 f.write(base_url + "\n")