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, \
12 IMutableFileNode, IUploadStatus, IDownloadStatus
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", "on", "off")
37 return arg.lower() in ("true", "t", "1", "on")
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()
128 # we build up a log string that hides most of the cap, to preserve
129 # user privacy. We retain the query args so we can identify things
130 # like t=json. Then we send it to the flog. We make no attempt to
131 # match apache formatting. TODO: when we move to DSA dirnodes and
132 # shorter caps, consider exposing a few characters of the cap, or
133 # maybe a few characters of its hash.
134 x = self.uri.split("?", 1)
141 # there is a form handler which redirects POST /uri?uri=FOO into
142 # GET /uri/FOO so folks can paste in non-HTTP-prefixed uris. Make
143 # sure we censor these too.
144 if queryargs.startswith("uri="):
145 queryargs = "[uri=CENSORED]"
146 queryargs = "?" + queryargs
147 if path.startswith("/uri"):
148 path = "/uri/[CENSORED].."
149 uri = path + queryargs
151 log.msg(format="web: %(clientip)s %(method)s %(uri)s %(code)s %(length)s",
152 clientip=self.getClientIP(),
156 length=(self.sentLength or "-"),
157 facility="tahoe.webish",
158 level=log.OPERATIONAL,
161 class Directory(rend.Page):
163 docFactory = getxmlfile("directory.xhtml")
165 def __init__(self, rootname, dirnode, dirpath):
166 self._rootname = rootname
167 self._dirnode = dirnode
168 self._dirpath = dirpath
170 def dirpath_as_string(self):
171 return "/" + "/".join(self._dirpath)
173 def render_title(self, ctx, data):
174 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
176 def render_header(self, ctx, data):
177 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
178 num_dirs = len(parent_directories)
180 header = ["Directory '"]
181 for i,d in enumerate(parent_directories):
182 upness = num_dirs - i - 1
184 link = "/".join( ("..",) * upness )
187 header.append(T.a(href=link)[d])
192 if self._dirnode.is_readonly():
193 header.append(" (readonly)")
195 return ctx.tag[header]
197 def render_welcome(self, ctx, data):
198 depth = len(self._dirpath) + 2
199 link = "/".join([".."] * depth)
200 return T.div[T.a(href=link)["Return to Welcome page"]]
202 def data_children(self, ctx, data):
203 d = self._dirnode.list()
204 d.addCallback(lambda dict: sorted(dict.items()))
205 def _stall_some(items):
206 # Deferreds don't optimize out tail recursion, and the way
207 # Nevow's flattener handles Deferreds doesn't take this into
208 # account. As a result, large lists of Deferreds that fire in the
209 # same turn (i.e. the output of defer.succeed) will cause a stack
210 # overflow. To work around this, we insert a turn break after
211 # every 100 items, using foolscap's fireEventually(). This gives
212 # the stack a chance to be popped. It would also work to put
213 # every item in its own turn, but that'd be a lot more
214 # inefficient. This addresses ticket #237, for which I was never
215 # able to create a failing unit test.
217 for i,item in enumerate(items):
219 output.append(fireEventually(item))
223 d.addCallback(_stall_some)
226 def render_row(self, ctx, data):
227 name, (target, metadata) = data
228 name = name.encode("utf-8")
229 assert not isinstance(name, unicode)
231 if self._dirnode.is_readonly():
235 # this creates a button which will cause our child__delete method
236 # to be invoked, which deletes the file and then redirects the
237 # browser back to this directory
238 delete = T.form(action=url.here, method="post")[
239 T.input(type='hidden', name='t', value='delete'),
240 T.input(type='hidden', name='name', value=name),
241 T.input(type='hidden', name='when_done', value=url.here),
242 T.input(type='submit', value='del', name="del"),
245 rename = T.form(action=url.here, method="get")[
246 T.input(type='hidden', name='t', value='rename-form'),
247 T.input(type='hidden', name='name', value=name),
248 T.input(type='hidden', name='when_done', value=url.here),
249 T.input(type='submit', value='rename', name="rename"),
252 ctx.fillSlots("delete", delete)
253 ctx.fillSlots("rename", rename)
254 check = T.form(action=url.here, method="post")[
255 T.input(type='hidden', name='t', value='check'),
256 T.input(type='hidden', name='name', value=name),
257 T.input(type='hidden', name='when_done', value=url.here),
258 T.input(type='submit', value='check', name="check"),
260 ctx.fillSlots("overwrite", self.build_overwrite(ctx, (name, target)))
261 ctx.fillSlots("check", check)
264 TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
265 if "ctime" in metadata:
266 ctime = time.strftime(TIME_FORMAT,
267 time.localtime(metadata["ctime"]))
268 times.append("c: " + ctime)
269 if "mtime" in metadata:
270 mtime = time.strftime(TIME_FORMAT,
271 time.localtime(metadata["mtime"]))
274 times.append("m: " + mtime)
275 ctx.fillSlots("times", times)
278 # build the base of the uri_link link url
279 uri_link = "/uri/" + urllib.quote(target.get_uri())
281 assert (IFileNode.providedBy(target)
282 or IDirectoryNode.providedBy(target)
283 or IMutableFileNode.providedBy(target)), target
285 if IMutableFileNode.providedBy(target):
288 # add the filename to the uri_link url
289 uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
291 # to prevent javascript in displayed .html files from stealing a
292 # secret directory URI from the URL, send the browser to a URI-based
293 # page that doesn't know about the directory at all
294 #dlurl = urllib.quote(name)
297 ctx.fillSlots("filename",
298 T.a(href=dlurl)[html.escape(name)])
299 ctx.fillSlots("type", "SSK")
301 ctx.fillSlots("size", "?")
303 text_plain_link = uri_link + "?filename=foo.txt"
304 text_plain_tag = T.a(href=text_plain_link)["text/plain"]
306 elif IFileNode.providedBy(target):
309 # add the filename to the uri_link url
310 uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
312 # to prevent javascript in displayed .html files from stealing a
313 # secret directory URI from the URL, send the browser to a URI-based
314 # page that doesn't know about the directory at all
315 #dlurl = urllib.quote(name)
318 ctx.fillSlots("filename",
319 T.a(href=dlurl)[html.escape(name)])
320 ctx.fillSlots("type", "FILE")
322 ctx.fillSlots("size", target.get_size())
324 text_plain_link = uri_link + "?filename=foo.txt"
325 text_plain_tag = T.a(href=text_plain_link)["text/plain"]
327 elif IDirectoryNode.providedBy(target):
329 ctx.fillSlots("filename",
330 T.a(href=uri_link)[html.escape(name)])
331 if target.is_readonly():
335 ctx.fillSlots("type", dirtype)
336 ctx.fillSlots("size", "-")
337 text_plain_tag = None
339 childdata = [T.a(href="%s?t=json" % name)["JSON"], ", ",
340 T.a(href="%s?t=uri" % name)["URI"], ", ",
341 T.a(href="%s?t=readonly-uri" % name)["readonly-URI"],
344 childdata.extend([", ", text_plain_tag])
346 ctx.fillSlots("data", childdata)
349 checker = IClient(ctx).getServiceNamed("checker")
353 d = defer.maybeDeferred(checker.checker_results_for,
354 target.get_verifier())
355 def _got(checker_results):
356 recent_results = reversed(checker_results[-5:])
357 if IFileNode.providedBy(target):
359 ", ".join(["%d/%d" % (found, needed)
361 (needed, total, found, sharemap))
362 in recent_results]) +
364 elif IDirectoryNode.providedBy(target):
366 "".join([{True:"+",False:"-"}[res]
367 for (when, res) in recent_results]) +
370 results = "%d results" % len(checker_results)
376 # TODO: include a link to see more results, including timestamps
377 # TODO: use a sparkline
378 ctx.fillSlots("checker_results", results)
382 def render_forms(self, ctx, data):
383 if self._dirnode.is_readonly():
384 return T.div["No upload forms: directory is read-only"]
385 mkdir = T.form(action=".", method="post",
386 enctype="multipart/form-data")[
388 T.input(type="hidden", name="t", value="mkdir"),
389 T.input(type="hidden", name="when_done", value=url.here),
390 T.legend(class_="freeform-form-label")["Create a new directory"],
391 "New directory name: ",
392 T.input(type="text", name="name"), " ",
393 T.input(type="submit", value="Create"),
396 upload = T.form(action=".", method="post",
397 enctype="multipart/form-data")[
399 T.input(type="hidden", name="t", value="upload"),
400 T.input(type="hidden", name="when_done", value=url.here),
401 T.legend(class_="freeform-form-label")["Upload a file to this directory"],
402 "Choose a file to upload: ",
403 T.input(type="file", name="file", class_="freeform-input-file"),
405 T.input(type="submit", value="Upload"),
407 T.input(type="checkbox", name="mutable"),
410 mount = T.form(action=".", method="post",
411 enctype="multipart/form-data")[
413 T.input(type="hidden", name="t", value="uri"),
414 T.input(type="hidden", name="when_done", value=url.here),
415 T.legend(class_="freeform-form-label")["Attach a file or directory"
419 T.input(type="text", name="name"), " ",
420 "URI of new child: ",
421 T.input(type="text", name="uri"), " ",
422 T.input(type="submit", value="Attach"),
424 return [T.div(class_="freeform-form")[mkdir],
425 T.div(class_="freeform-form")[upload],
426 T.div(class_="freeform-form")[mount],
429 def build_overwrite(self, ctx, data):
431 if IMutableFileNode.providedBy(target) and not target.is_readonly():
432 action="/uri/" + urllib.quote(target.get_uri())
433 overwrite = T.form(action=action, method="post",
434 enctype="multipart/form-data")[
436 T.input(type="hidden", name="t", value="overwrite"),
437 T.input(type='hidden', name='name', value=name),
438 T.input(type='hidden', name='when_done', value=url.here),
439 T.legend(class_="freeform-form-label")["Overwrite"],
441 T.input(type="file", name="file", class_="freeform-input-file"),
443 T.input(type="submit", value="Overwrite")
445 return [T.div(class_="freeform-form")[overwrite],]
449 def render_results(self, ctx, data):
450 req = inevow.IRequest(ctx)
451 return get_arg(req, "results", "")
453 class WebDownloadTarget:
454 implements(IDownloadTarget, IConsumer)
455 def __init__(self, req, content_type, content_encoding, save_to_file):
457 self._content_type = content_type
458 self._content_encoding = content_encoding
460 self._producer = None
461 self._save_to_file = save_to_file
463 def registerProducer(self, producer, streaming):
464 self._req.registerProducer(producer, streaming)
465 def unregisterProducer(self):
466 self._req.unregisterProducer()
468 def open(self, size):
470 self._req.setHeader("content-type", self._content_type)
471 if self._content_encoding:
472 self._req.setHeader("content-encoding", self._content_encoding)
473 self._req.setHeader("content-length", str(size))
474 if self._save_to_file is not None:
475 # tell the browser to save the file rather display it
476 # TODO: quote save_to_file properly
477 filename = self._save_to_file.encode("utf-8")
478 self._req.setHeader("content-disposition",
479 'attachment; filename="%s"'
482 def write(self, data):
483 self._req.write(data)
489 # The content-type is already set, and the response code
490 # has already been sent, so we can't provide a clean error
491 # indication. We can emit text (which a browser might interpret
492 # as something else), and if we sent a Size header, they might
493 # notice that we've truncated the data. Keep the error message
494 # small to improve the chances of having our error response be
495 # shorter than the intended results.
497 # We don't have a lot of options, unfortunately.
498 self._req.write("problem during download\n")
500 # We haven't written anything yet, so we can provide a sensible
503 msg.replace("\n", "|")
504 self._req.setResponseCode(http.GONE, msg)
505 self._req.setHeader("content-type", "text/plain")
506 # TODO: HTML-formatted exception?
507 self._req.write(str(why))
510 def register_canceller(self, cb):
515 class FileDownloader(resource.Resource):
516 def __init__(self, filenode, name):
517 assert (IFileNode.providedBy(filenode)
518 or IMutableFileNode.providedBy(filenode))
519 self._filenode = filenode
522 def render(self, req):
523 gte = static.getTypeAndEncoding
524 ctype, encoding = gte(self._name,
525 static.File.contentTypes,
526 static.File.contentEncodings,
527 defaultType="text/plain")
529 if get_arg(req, "save", False):
530 # TODO: make the API specification clear: should "save=" or
531 # "save=false" count?
532 save_to_file = self._name
533 wdt = WebDownloadTarget(req, ctype, encoding, save_to_file)
534 d = self._filenode.download(wdt)
535 # exceptions during download are handled by the WebDownloadTarget
536 d.addErrback(lambda why: None)
537 return server.NOT_DONE_YET
539 class BlockingFileError(Exception):
540 """We cannot auto-create a parent directory, because there is a file in
542 class NoReplacementError(Exception):
543 """There was already a child by that name, and you asked me to not replace it"""
544 class NoLocalDirectoryError(Exception):
545 """The localdir= directory didn't exist"""
547 LOCALHOST = "127.0.0.1"
549 class NeedLocalhostError:
550 implements(inevow.IResource)
552 def renderHTTP(self, ctx):
553 req = inevow.IRequest(ctx)
554 req.setResponseCode(http.FORBIDDEN)
555 req.setHeader("content-type", "text/plain")
556 return "localfile= or localdir= requires a local connection"
558 class NeedAbsolutePathError:
559 implements(inevow.IResource)
561 def renderHTTP(self, ctx):
562 req = inevow.IRequest(ctx)
563 req.setResponseCode(http.FORBIDDEN)
564 req.setHeader("content-type", "text/plain")
565 return "localfile= or localdir= requires an absolute path"
567 class LocalAccessDisabledError:
568 implements(inevow.IResource)
570 def renderHTTP(self, ctx):
571 req = inevow.IRequest(ctx)
572 req.setResponseCode(http.FORBIDDEN)
573 req.setHeader("content-type", "text/plain")
574 return "local file access is disabled"
577 implements(inevow.IResource)
578 def __init__(self, response_code, errmsg):
579 self._response_code = response_code
580 self._errmsg = errmsg
582 def renderHTTP(self, ctx):
583 req = inevow.IRequest(ctx)
584 req.setResponseCode(self._response_code)
585 req.setHeader("content-type", "text/plain")
589 class LocalFileDownloader(resource.Resource):
590 def __init__(self, filenode, local_filename):
591 self._local_filename = local_filename
593 self._filenode = filenode
595 def render(self, req):
596 target = download.FileName(self._local_filename)
597 d = self._filenode.download(target)
599 req.write(self._filenode.get_uri())
602 return server.NOT_DONE_YET
605 class FileJSONMetadata(rend.Page):
606 def __init__(self, filenode):
607 self._filenode = filenode
609 def renderHTTP(self, ctx):
610 req = inevow.IRequest(ctx)
611 req.setHeader("content-type", "text/plain")
612 return self.renderNode(self._filenode)
614 def renderNode(self, filenode):
615 file_uri = filenode.get_uri()
618 'size': filenode.get_size(),
620 return simplejson.dumps(data, indent=1)
622 class FileURI(FileJSONMetadata):
623 def renderNode(self, filenode):
624 file_uri = filenode.get_uri()
627 class FileReadOnlyURI(FileJSONMetadata):
628 def renderNode(self, filenode):
629 if filenode.is_readonly():
630 return filenode.get_uri()
632 return filenode.get_readonly().get_uri()
634 class DirnodeWalkerMixin:
635 """Visit all nodes underneath (and including) the rootnode, one at a
636 time. For each one, call the visitor. The visitor will see the
637 IDirectoryNode before it sees any of the IFileNodes inside. If the
638 visitor returns a Deferred, I do not call the visitor again until it has
642 ## def _walk_if_we_could_use_generators(self, rootnode, rootpath=()):
643 ## # this is what we'd be doing if we didn't have the Deferreds and
644 ## # thus could use generators
645 ## yield rootpath, rootnode
646 ## for childname, childnode in rootnode.list().items():
647 ## childpath = rootpath + (childname,)
648 ## if IFileNode.providedBy(childnode):
649 ## yield childpath, childnode
650 ## elif IDirectoryNode.providedBy(childnode):
651 ## for res in self._walk_if_we_could_use_generators(childnode,
655 def walk(self, rootnode, visitor, rootpath=()):
657 def _listed(listing):
658 return listing.items()
659 d.addCallback(_listed)
660 d.addCallback(self._handle_items, visitor, rootpath)
663 def _handle_items(self, items, visitor, rootpath):
666 childname, (childnode, metadata) = items[0]
667 childpath = rootpath + (childname,)
668 d = defer.maybeDeferred(visitor, childpath, childnode, metadata)
669 if IDirectoryNode.providedBy(childnode):
670 d.addCallback(lambda res: self.walk(childnode, visitor, childpath))
671 d.addCallback(lambda res:
672 self._handle_items(items[1:], visitor, rootpath))
675 class LocalDirectoryDownloader(resource.Resource, DirnodeWalkerMixin):
676 def __init__(self, dirnode, localdir):
677 self._dirnode = dirnode
678 self._localdir = localdir
680 def _handle(self, path, node, metadata):
681 path = tuple([p.encode("utf-8") for p in path])
682 localfile = os.path.join(self._localdir, os.sep.join(path))
683 if IDirectoryNode.providedBy(node):
684 fileutil.make_dirs(localfile)
685 elif IFileNode.providedBy(node):
686 target = download.FileName(localfile)
687 return node.download(target)
689 def render(self, req):
690 d = self.walk(self._dirnode, self._handle)
692 req.setHeader("content-type", "text/plain")
693 return "operation complete"
697 class DirectoryJSONMetadata(rend.Page):
698 def __init__(self, dirnode):
699 self._dirnode = dirnode
701 def renderHTTP(self, ctx):
702 req = inevow.IRequest(ctx)
703 req.setHeader("content-type", "text/plain")
704 return self.renderNode(self._dirnode)
706 def renderNode(self, node):
710 for name, (childnode, metadata) in children.iteritems():
711 if IFileNode.providedBy(childnode):
712 kiduri = childnode.get_uri()
713 kiddata = ("filenode",
715 'size': childnode.get_size(),
716 'metadata': metadata,
719 assert IDirectoryNode.providedBy(childnode), (childnode, children,)
720 kiddata = ("dirnode",
721 {'ro_uri': childnode.get_readonly_uri(),
722 'metadata': metadata,
724 if not childnode.is_readonly():
725 kiddata[1]['rw_uri'] = childnode.get_uri()
727 contents = { 'children': kids,
728 'ro_uri': node.get_readonly_uri(),
730 if not node.is_readonly():
731 contents['rw_uri'] = node.get_uri()
732 data = ("dirnode", contents)
733 return simplejson.dumps(data, indent=1)
737 class DirectoryURI(DirectoryJSONMetadata):
738 def renderNode(self, node):
739 return node.get_uri()
741 class DirectoryReadonlyURI(DirectoryJSONMetadata):
742 def renderNode(self, node):
743 return node.get_readonly_uri()
745 class RenameForm(rend.Page):
747 docFactory = getxmlfile("rename-form.xhtml")
749 def __init__(self, rootname, dirnode, dirpath):
750 self._rootname = rootname
751 self._dirnode = dirnode
752 self._dirpath = dirpath
754 def dirpath_as_string(self):
755 return "/" + "/".join(self._dirpath)
757 def render_title(self, ctx, data):
758 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
760 def render_header(self, ctx, data):
761 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
762 num_dirs = len(parent_directories)
764 header = [ "Rename in directory '",
765 "<%s>/" % self._rootname,
766 "/".join(self._dirpath),
769 if self._dirnode.is_readonly():
770 header.append(" (readonly)")
771 return ctx.tag[header]
773 def render_when_done(self, ctx, data):
774 return T.input(type="hidden", name="when_done", value=url.here)
776 def render_get_name(self, ctx, data):
777 req = inevow.IRequest(ctx)
778 name = get_arg(req, "name", "")
779 ctx.tag.attributes['value'] = name
782 class POSTHandler(rend.Page):
783 def __init__(self, node, replace):
785 self._replace = replace
787 def _check_replacement(self, name):
789 return defer.succeed(None)
790 d = self._node.has_child(name)
793 raise NoReplacementError("There was already a child by that "
794 "name, and you asked me to not "
800 def _POST_mkdir(self, name):
801 d = self._check_replacement(name)
802 d.addCallback(lambda res: self._node.create_empty_directory(name))
803 d.addCallback(lambda res: "directory created")
806 def _POST_uri(self, name, newuri):
807 d = self._check_replacement(name)
808 d.addCallback(lambda res: self._node.set_uri(name, newuri))
809 d.addCallback(lambda res: newuri)
812 def _POST_delete(self, name):
814 # apparently an <input type="hidden" name="name" value="">
815 # won't show up in the resulting encoded form.. the 'name'
816 # field is completely missing. So to allow deletion of an
817 # empty file, we have to pretend that None means ''. The only
818 # downide of this is a slightly confusing error message if
819 # someone does a POST without a name= field. For our own HTML
820 # thisn't a big deal, because we create the 'delete' POST
823 d = self._node.delete(name)
824 d.addCallback(lambda res: "thing deleted")
827 def _POST_rename(self, name, from_name, to_name):
828 d = self._check_replacement(to_name)
829 d.addCallback(lambda res: self._node.get(from_name))
831 uri = child.get_uri()
832 # now actually do the rename
833 return self._node.set_uri(to_name, uri)
834 d.addCallback(add_dest)
836 return self._node.delete(from_name)
837 d.addCallback(rm_src)
838 d.addCallback(lambda res: "thing renamed")
841 def _POST_upload(self, contents, name, mutable, client):
843 # SDMF: files are small, and we can only upload data.
844 contents.file.seek(0)
845 data = contents.file.read()
846 #uploadable = FileHandle(contents.file)
847 d = self._check_replacement(name)
848 d.addCallback(lambda res: self._node.has_child(name))
849 def _checked(present):
851 # modify the existing one instead of creating a new
853 d2 = self._node.get(name)
854 def _got_newnode(newnode):
855 d3 = newnode.replace(data)
856 d3.addCallback(lambda res: newnode.get_uri())
858 d2.addCallback(_got_newnode)
860 d2 = client.create_mutable_file(data)
861 def _uploaded(newnode):
862 d1 = self._node.set_node(name, newnode)
863 d1.addCallback(lambda res: newnode.get_uri())
865 d2.addCallback(_uploaded)
867 d.addCallback(_checked)
869 uploadable = FileHandle(contents.file)
870 d = self._check_replacement(name)
871 d.addCallback(lambda res: self._node.add_file(name, uploadable))
873 return newnode.get_uri()
877 def _POST_overwrite(self, contents):
878 # SDMF: files are small, and we can only upload data.
879 contents.file.seek(0)
880 data = contents.file.read()
881 # TODO: 'name' handling needs review
882 d = defer.succeed(self._node)
883 def _got_child_overwrite(child_node):
884 child_node.replace(data)
885 return child_node.get_uri()
886 d.addCallback(_got_child_overwrite)
889 def _POST_check(self, name):
890 d = self._node.get(name)
891 def _got_child_check(child_node):
892 d2 = child_node.check()
894 log.msg("checked %s, results %s" % (child_node, res),
895 facility="tahoe.webish", level=log.NOISY)
897 d2.addCallback(_done)
899 d.addCallback(_got_child_check)
902 def _POST_set_children(self, children):
904 for name, (file_or_dir, mddict) in children.iteritems():
905 cap = str(mddict.get('rw_uri') or mddict.get('ro_uri'))
906 cs.append((name, cap, mddict.get('metadata')))
908 d = self._node.set_children(cs)
909 d.addCallback(lambda res: "Okay so I did it.")
912 def renderHTTP(self, ctx):
913 req = inevow.IRequest(ctx)
915 t = get_arg(req, "t")
918 charset = get_arg(req, "_charset", "utf-8")
920 name = get_arg(req, "name", None)
921 if name and "/" in name:
922 req.setResponseCode(http.BAD_REQUEST)
923 req.setHeader("content-type", "text/plain")
924 return "name= may not contain a slash"
927 name = name.decode(charset)
928 assert isinstance(name, unicode)
929 # we allow the user to delete an empty-named file, but not to create
930 # them, since that's an easy and confusing mistake to make
932 when_done = get_arg(req, "when_done", None)
933 if not boolean_of_arg(get_arg(req, "replace", "true")):
934 self._replace = False
938 raise RuntimeError("mkdir requires a name")
939 d = self._POST_mkdir(name)
942 raise RuntimeError("set-uri requires a name")
943 newuri = get_arg(req, "uri")
944 assert newuri is not None
945 d = self._POST_uri(name, newuri)
947 d = self._POST_delete(name)
949 from_name = get_arg(req, "from_name")
950 if from_name is not None:
951 from_name = from_name.strip()
952 from_name = from_name.decode(charset)
953 assert isinstance(from_name, unicode)
954 to_name = get_arg(req, "to_name")
955 if to_name is not None:
956 to_name = to_name.strip()
957 to_name = to_name.decode(charset)
958 assert isinstance(to_name, unicode)
959 if not from_name or not to_name:
960 raise RuntimeError("rename requires from_name and to_name")
961 if not IDirectoryNode.providedBy(self._node):
962 raise RuntimeError("rename must only be called on directories")
963 for k,v in [ ('from_name', from_name), ('to_name', to_name) ]:
965 req.setResponseCode(http.BAD_REQUEST)
966 req.setHeader("content-type", "text/plain")
967 return "%s= may not contain a slash" % (k,)
968 d = self._POST_rename(name, from_name, to_name)
970 contents = req.fields["file"]
971 name = name or contents.filename
975 # this prohibts empty, missing, and all-whitespace filenames
976 raise RuntimeError("upload requires a name")
977 name = name.decode(charset)
978 assert isinstance(name, unicode)
979 mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
980 d = self._POST_upload(contents, name, mutable, IClient(ctx))
981 elif t == "overwrite":
982 contents = req.fields["file"]
983 d = self._POST_overwrite(contents)
985 d = self._POST_check(name)
986 elif t == "set_children":
988 body = req.content.read()
990 children = simplejson.loads(body)
991 except ValueError, le:
992 le.args = tuple(le.args + (body,))
993 # TODO test handling of bad JSON
995 d = self._POST_set_children(children)
998 return "BAD t=%s" % t
1000 d.addCallback(lambda res: url.URL.fromString(when_done))
1001 def _check_replacement(f):
1002 # TODO: make this more human-friendly: maybe send them to the
1003 # when_done page but with an extra query-arg that will display
1004 # the error message in a big box at the top of the page. The
1005 # directory page that when_done= usually points to accepts a
1006 # result= argument.. use that.
1007 f.trap(NoReplacementError)
1008 req.setResponseCode(http.CONFLICT)
1009 req.setHeader("content-type", "text/plain")
1011 d.addErrback(_check_replacement)
1014 class DELETEHandler(rend.Page):
1015 def __init__(self, node, name):
1019 def renderHTTP(self, ctx):
1020 req = inevow.IRequest(ctx)
1021 d = self._node.delete(self._name)
1023 # what should this return??
1024 return "%s deleted" % self._name.encode("utf-8")
1025 d.addCallback(_done)
1026 def _trap_missing(f):
1028 req.setResponseCode(http.NOT_FOUND)
1029 req.setHeader("content-type", "text/plain")
1030 return "no such child %s" % self._name.encode("utf-8")
1031 d.addErrback(_trap_missing)
1034 class PUTHandler(rend.Page):
1035 def __init__(self, node, path, t, localfile, localdir, replace):
1039 self._localfile = localfile
1040 self._localdir = localdir
1041 self._replace = replace
1043 def renderHTTP(self, ctx):
1044 req = inevow.IRequest(ctx)
1046 localfile = self._localfile
1047 localdir = self._localdir
1049 if t == "upload" and not (localfile or localdir):
1050 req.setResponseCode(http.BAD_REQUEST)
1051 req.setHeader("content-type", "text/plain")
1052 return "t=upload requires localfile= or localdir="
1054 # we must traverse the path, creating new directories as necessary
1055 d = self._get_or_create_directories(self._node, self._path[:-1])
1056 name = self._path[-1]
1057 d.addCallback(self._check_replacement, name, self._replace)
1060 d.addCallback(self._upload_localfile, localfile, name)
1063 # take the last step
1064 d.addCallback(self._get_or_create_directories, self._path[-1:])
1065 d.addCallback(self._upload_localdir, localdir)
1067 d.addCallback(self._attach_uri, req.content, name)
1069 d.addCallback(self._mkdir, name)
1071 d.addCallback(self._upload_file, req.content, name)
1073 def _transform_error(f):
1074 errors = {BlockingFileError: http.BAD_REQUEST,
1075 NoReplacementError: http.CONFLICT,
1076 NoLocalDirectoryError: http.BAD_REQUEST,
1078 for k,v in errors.items():
1080 req.setResponseCode(v)
1081 req.setHeader("content-type", "text/plain")
1084 d.addErrback(_transform_error)
1087 def _get_or_create_directories(self, node, path):
1088 if not IDirectoryNode.providedBy(node):
1089 # unfortunately it is too late to provide the name of the
1090 # blocking directory in the error message.
1091 raise BlockingFileError("cannot create directory because there "
1092 "is a file in the way")
1094 return defer.succeed(node)
1095 d = node.get(path[0])
1096 def _maybe_create(f):
1098 return node.create_empty_directory(path[0])
1099 d.addErrback(_maybe_create)
1100 d.addCallback(self._get_or_create_directories, path[1:])
1103 def _check_replacement(self, node, name, replace):
1106 d = node.has_child(name)
1109 raise NoReplacementError("There was already a child by that "
1110 "name, and you asked me to not "
1116 def _mkdir(self, node, name):
1117 d = node.create_empty_directory(name)
1119 return newnode.get_uri()
1120 d.addCallback(_done)
1123 def _upload_file(self, node, contents, name):
1124 uploadable = FileHandle(contents)
1125 d = node.add_file(name, uploadable)
1126 def _done(filenode):
1127 log.msg("webish upload complete",
1128 facility="tahoe.webish", level=log.NOISY)
1129 return filenode.get_uri()
1130 d.addCallback(_done)
1133 def _upload_localfile(self, node, localfile, name):
1134 uploadable = FileName(localfile)
1135 d = node.add_file(name, uploadable)
1136 d.addCallback(lambda filenode: filenode.get_uri())
1139 def _attach_uri(self, parentnode, contents, name):
1140 newuri = contents.read().strip()
1141 d = parentnode.set_uri(name, newuri)
1144 d.addCallback(_done)
1147 def _upload_localdir(self, node, localdir):
1148 # build up a list of files to upload. TODO: for now, these files and
1149 # directories must have UTF-8 encoded filenames: anything else will
1150 # cause the upload to break.
1153 msg = "No files to upload! %s is empty" % localdir
1154 if not os.path.exists(localdir):
1155 msg = "%s doesn't exist!" % localdir
1156 raise NoLocalDirectoryError(msg)
1157 for root, dirs, files in os.walk(localdir):
1158 if root == localdir:
1161 relative_root = root[len(localdir)+1:]
1162 path = tuple(relative_root.split(os.sep))
1164 this_dir = path + (d,)
1165 this_dir = tuple([p.decode("utf-8") for p in this_dir])
1166 all_dirs.append(this_dir)
1168 this_file = path + (f,)
1169 this_file = tuple([p.decode("utf-8") for p in this_file])
1170 all_files.append(this_file)
1171 d = defer.succeed(msg)
1172 for dir in all_dirs:
1174 d.addCallback(self._makedir, node, dir)
1176 d.addCallback(self._upload_one_file, node, localdir, f)
1179 def _makedir(self, res, node, dir):
1180 d = defer.succeed(None)
1181 # get the parent. As long as os.walk gives us parents before
1182 # children, this ought to work
1183 d.addCallback(lambda res: node.get_child_at_path(dir[:-1]))
1184 # then create the child directory
1185 d.addCallback(lambda parent: parent.create_empty_directory(dir[-1]))
1188 def _upload_one_file(self, res, node, localdir, f):
1189 # get the parent. We can be sure this exists because we already
1190 # went through and created all the directories we require.
1191 localfile = os.path.join(localdir, *f)
1192 d = node.get_child_at_path(f[:-1])
1193 d.addCallback(self._upload_localfile, localfile, f[-1])
1197 class Manifest(rend.Page):
1198 docFactory = getxmlfile("manifest.xhtml")
1199 def __init__(self, dirnode, dirpath):
1200 self._dirnode = dirnode
1201 self._dirpath = dirpath
1203 def dirpath_as_string(self):
1204 return "/" + "/".join(self._dirpath)
1206 def render_title(self, ctx):
1207 return T.title["Manifest of %s" % self.dirpath_as_string()]
1209 def render_header(self, ctx):
1210 return T.p["Manifest of %s" % self.dirpath_as_string()]
1212 def data_items(self, ctx, data):
1213 return self._dirnode.build_manifest()
1215 def render_row(self, ctx, refresh_cap):
1216 ctx.fillSlots("refresh_capability", refresh_cap)
1220 implements(inevow.IResource)
1221 def renderHTTP(self, ctx):
1222 req = inevow.IRequest(ctx)
1223 req.setResponseCode(http.BAD_REQUEST)
1224 req.setHeader("content-type", "text/plain")
1227 def child_error(text):
1232 class VDrive(rend.Page):
1234 def __init__(self, node, name):
1238 def get_child_at_path(self, path):
1240 return self.node.get_child_at_path(path)
1241 return defer.succeed(self.node)
1243 def locateChild(self, ctx, segments):
1244 req = inevow.IRequest(ctx)
1246 path = tuple([seg.decode("utf-8") for seg in segments])
1248 t = get_arg(req, "t", "")
1249 localfile = get_arg(req, "localfile", None)
1250 if localfile is not None:
1251 if localfile != os.path.abspath(localfile):
1252 return NeedAbsolutePathError(), ()
1253 localdir = get_arg(req, "localdir", None)
1254 if localdir is not None:
1255 if localdir != os.path.abspath(localdir):
1256 return NeedAbsolutePathError(), ()
1257 if localfile or localdir:
1258 if not ILocalAccess(ctx).local_access_is_allowed():
1259 return LocalAccessDisabledError(), ()
1260 if req.getHost().host != LOCALHOST:
1261 return NeedLocalhostError(), ()
1262 # TODO: think about clobbering/revealing config files and node secrets
1264 replace = boolean_of_arg(get_arg(req, "replace", "true"))
1267 # the node must exist, and our operation will be performed on the
1269 d = self.get_child_at_path(path)
1270 def file_or_dir(node):
1271 if (IFileNode.providedBy(node)
1272 or IMutableFileNode.providedBy(node)):
1273 filename = "unknown"
1276 filename = get_arg(req, "filename", filename)
1279 # write contents to a local file
1280 return LocalFileDownloader(node, localfile), ()
1281 # send contents as the result
1282 return FileDownloader(node, filename), ()
1284 # send contents as the result
1285 return FileDownloader(node, filename), ()
1287 return FileJSONMetadata(node), ()
1289 return FileURI(node), ()
1290 elif t == "readonly-uri":
1291 return FileReadOnlyURI(node), ()
1293 return child_error("bad t=%s" % t)
1294 elif IDirectoryNode.providedBy(node):
1297 # recursive download to a local directory
1298 return LocalDirectoryDownloader(node, localdir), ()
1299 return child_error("t=download requires localdir=")
1301 # send an HTML representation of the directory
1302 return Directory(self.name, node, path), ()
1304 return DirectoryJSONMetadata(node), ()
1306 return DirectoryURI(node), ()
1307 elif t == "readonly-uri":
1308 return DirectoryReadonlyURI(node), ()
1309 elif t == "manifest":
1310 return Manifest(node, path), ()
1311 elif t == 'rename-form':
1312 return RenameForm(self.name, node, path), ()
1314 return child_error("bad t=%s" % t)
1316 return child_error("unknown node type")
1317 d.addCallback(file_or_dir)
1318 elif method == "POST":
1319 # the node must exist, and our operation will be performed on the
1321 d = self.get_child_at_path(path)
1322 def _got_POST(node):
1323 return POSTHandler(node, replace), ()
1324 d.addCallback(_got_POST)
1325 elif method == "DELETE":
1326 # the node must exist, and our operation will be performed on its
1328 assert path # you can't delete the root
1329 d = self.get_child_at_path(path[:-1])
1330 def _got_DELETE(node):
1331 return DELETEHandler(node, path[-1]), ()
1332 d.addCallback(_got_DELETE)
1333 elif method in ("PUT",):
1334 # the node may or may not exist, and our operation may involve
1335 # all the ancestors of the node.
1336 return PUTHandler(self.node, path, t, localfile, localdir, replace), ()
1338 return rend.NotFound
1341 class UnlinkedPUTCHKUploader(rend.Page):
1342 def renderHTTP(self, ctx):
1343 req = inevow.IRequest(ctx)
1344 assert req.method == "PUT"
1345 # "PUT /uri", to create an unlinked file. This is like PUT but
1346 # without the associated set_uri.
1348 uploadable = FileHandle(req.content)
1349 d = IClient(ctx).upload(uploadable)
1350 d.addCallback(lambda results: results.uri)
1351 # that fires with the URI of the new file
1354 class UnlinkedPUTSSKUploader(rend.Page):
1355 def renderHTTP(self, ctx):
1356 req = inevow.IRequest(ctx)
1357 assert req.method == "PUT"
1358 # SDMF: files are small, and we can only upload data
1360 data = req.content.read()
1361 d = IClient(ctx).create_mutable_file(data)
1362 d.addCallback(lambda n: n.get_uri())
1365 class UnlinkedPUTCreateDirectory(rend.Page):
1366 def renderHTTP(self, ctx):
1367 req = inevow.IRequest(ctx)
1368 assert req.method == "PUT"
1369 # "PUT /uri?t=mkdir", to create an unlinked directory.
1370 d = IClient(ctx).create_empty_dirnode()
1371 d.addCallback(lambda dirnode: dirnode.get_uri())
1372 # XXX add redirect_to_result
1375 def plural(sequence):
1376 if len(sequence) == 1:
1380 class UploadResultsRendererMixin:
1381 # this requires a method named 'upload_results'
1383 def render_sharemap(self, ctx, data):
1384 d = self.upload_results()
1385 d.addCallback(lambda res: res.sharemap)
1386 def _render(sharemap):
1387 if sharemap is None:
1390 for shnum in sorted(sharemap.keys()):
1391 l[T.li["%d -> %s" % (shnum, sharemap[shnum])]]
1393 d.addCallback(_render)
1396 def render_servermap(self, ctx, data):
1397 d = self.upload_results()
1398 d.addCallback(lambda res: res.servermap)
1399 def _render(servermap):
1400 if servermap is None:
1403 for peerid in sorted(servermap.keys()):
1404 peerid_s = idlib.shortnodeid_b2a(peerid)
1405 shares_s = ",".join(["#%d" % shnum
1406 for shnum in servermap[peerid]])
1407 l[T.li["[%s] got share%s: %s" % (peerid_s,
1408 plural(servermap[peerid]),
1411 d.addCallback(_render)
1414 def data_file_size(self, ctx, data):
1415 d = self.upload_results()
1416 d.addCallback(lambda res: res.file_size)
1419 def render_time(self, ctx, data):
1420 # 1.23s, 790ms, 132us
1427 return "%dms" % (1000*s)
1429 return "%.1fms" % (1000*s)
1430 return "%dus" % (1000000*s)
1432 def render_rate(self, ctx, data):
1433 # 21.8kBps, 554.4kBps 4.37MBps
1438 return "%1.2fMBps" % (r/1000000)
1440 return "%.1fkBps" % (r/1000)
1443 def _get_time(self, name):
1444 d = self.upload_results()
1445 d.addCallback(lambda res: res.timings.get(name))
1448 def data_time_total(self, ctx, data):
1449 return self._get_time("total")
1451 def data_time_storage_index(self, ctx, data):
1452 return self._get_time("storage_index")
1454 def data_time_contacting_helper(self, ctx, data):
1455 return self._get_time("contacting_helper")
1457 def data_time_existence_check(self, ctx, data):
1458 return self._get_time("existence_check")
1460 def data_time_cumulative_fetch(self, ctx, data):
1461 return self._get_time("cumulative_fetch")
1463 def data_time_helper_total(self, ctx, data):
1464 return self._get_time("helper_total")
1466 def data_time_peer_selection(self, ctx, data):
1467 return self._get_time("peer_selection")
1469 def data_time_total_encode_and_push(self, ctx, data):
1470 return self._get_time("total_encode_and_push")
1472 def data_time_cumulative_encoding(self, ctx, data):
1473 return self._get_time("cumulative_encoding")
1475 def data_time_cumulative_sending(self, ctx, data):
1476 return self._get_time("cumulative_sending")
1478 def data_time_hashes_and_close(self, ctx, data):
1479 return self._get_time("hashes_and_close")
1481 def _get_rate(self, name):
1482 d = self.upload_results()
1484 file_size = r.file_size
1485 time = r.timings.get(name)
1489 return 1.0 * file_size / time
1490 except ZeroDivisionError:
1492 d.addCallback(_convert)
1495 def data_rate_total(self, ctx, data):
1496 return self._get_rate("total")
1498 def data_rate_storage_index(self, ctx, data):
1499 return self._get_rate("storage_index")
1501 def data_rate_encode(self, ctx, data):
1502 return self._get_rate("cumulative_encoding")
1504 def data_rate_push(self, ctx, data):
1505 return self._get_rate("cumulative_sending")
1507 def data_rate_encode_and_push(self, ctx, data):
1508 d = self.upload_results()
1510 file_size = r.file_size
1511 if file_size is None:
1513 time1 = r.timings.get("cumulative_encoding")
1516 time2 = r.timings.get("cumulative_sending")
1520 return 1.0 * file_size / (time1+time2)
1521 except ZeroDivisionError:
1523 d.addCallback(_convert)
1526 def data_rate_ciphertext_fetch(self, ctx, data):
1527 d = self.upload_results()
1529 fetch_size = r.ciphertext_fetched
1530 if fetch_size is None:
1532 time = r.timings.get("cumulative_fetch")
1536 return 1.0 * fetch_size / time
1537 except ZeroDivisionError:
1539 d.addCallback(_convert)
1542 class UnlinkedPOSTCHKUploader(UploadResultsRendererMixin, rend.Page):
1543 """'POST /uri', to create an unlinked file."""
1544 docFactory = getxmlfile("upload-results.xhtml")
1546 def __init__(self, client, req):
1547 rend.Page.__init__(self)
1548 # we start the upload now, and distribute notification of its
1549 # completion to render_ methods with an ObserverList
1550 assert req.method == "POST"
1551 self._done = observer.OneShotObserverList()
1552 fileobj = req.fields["file"].file
1553 uploadable = FileHandle(fileobj)
1554 d = client.upload(uploadable)
1555 d.addBoth(self._done.fire)
1557 def renderHTTP(self, ctx):
1558 req = inevow.IRequest(ctx)
1559 when_done = get_arg(req, "when_done", None)
1561 # if when_done= is provided, return a redirect instead of our
1562 # usual upload-results page
1563 d = self._done.when_fired()
1564 d.addCallback(lambda res: url.URL.fromString(when_done))
1566 return rend.Page.renderHTTP(self, ctx)
1568 def upload_results(self):
1569 return self._done.when_fired()
1571 def data_done(self, ctx, data):
1572 d = self.upload_results()
1573 d.addCallback(lambda res: "done!")
1576 def data_uri(self, ctx, data):
1577 d = self.upload_results()
1578 d.addCallback(lambda res: res.uri)
1581 def render_download_link(self, ctx, data):
1582 d = self.upload_results()
1583 d.addCallback(lambda res: T.a(href="/uri/" + urllib.quote(res.uri))
1584 ["/uri/" + res.uri])
1587 class UnlinkedPOSTSSKUploader(rend.Page):
1588 def renderHTTP(self, ctx):
1589 req = inevow.IRequest(ctx)
1590 assert req.method == "POST"
1592 # "POST /uri", to create an unlinked file.
1593 # SDMF: files are small, and we can only upload data
1594 contents = req.fields["file"]
1595 contents.file.seek(0)
1596 data = contents.file.read()
1597 d = IClient(ctx).create_mutable_file(data)
1598 d.addCallback(lambda n: n.get_uri())
1601 class UnlinkedPOSTCreateDirectory(rend.Page):
1602 def renderHTTP(self, ctx):
1603 req = inevow.IRequest(ctx)
1604 assert req.method == "POST"
1606 # "POST /uri?t=mkdir", to create an unlinked directory.
1607 d = IClient(ctx).create_empty_dirnode()
1608 redirect = get_arg(req, "redirect_to_result", "false")
1609 if boolean_of_arg(redirect):
1610 def _then_redir(res):
1611 new_url = "uri/" + urllib.quote(res.get_uri())
1612 req.setResponseCode(http.SEE_OTHER) # 303
1613 req.setHeader('location', new_url)
1616 d.addCallback(_then_redir)
1618 d.addCallback(lambda dirnode: dirnode.get_uri())
1621 class UploadStatusPage(UploadResultsRendererMixin, rend.Page):
1622 docFactory = getxmlfile("upload-status.xhtml")
1624 def __init__(self, data):
1625 rend.Page.__init__(self, data)
1626 self.upload_status = data
1628 def upload_results(self):
1629 return defer.maybeDeferred(self.upload_status.get_results)
1631 def render_results(self, ctx, data):
1632 d = self.upload_results()
1633 def _got_results(results):
1637 d.addCallback(_got_results)
1640 def render_si(self, ctx, data):
1641 si_s = base32.b2a_or_none(data.get_storage_index())
1646 def render_helper(self, ctx, data):
1647 return {True: "Yes",
1648 False: "No"}[data.using_helper()]
1650 def render_total_size(self, ctx, data):
1651 size = data.get_size()
1656 def render_progress_hash(self, ctx, data):
1657 progress = data.get_progress()[0]
1658 # TODO: make an ascii-art bar
1659 return "%.1f%%" % (100.0 * progress)
1661 def render_progress_ciphertext(self, ctx, data):
1662 progress = data.get_progress()[1]
1663 # TODO: make an ascii-art bar
1664 return "%.1f%%" % (100.0 * progress)
1666 def render_progress_encode_push(self, ctx, data):
1667 progress = data.get_progress()[2]
1668 # TODO: make an ascii-art bar
1669 return "%.1f%%" % (100.0 * progress)
1671 def render_status(self, ctx, data):
1672 return data.get_status()
1674 class DownloadResultsRendererMixin:
1675 # this requires a method named 'download_results'
1677 def render_servers_used(self, ctx, data):
1680 def render_servermap(self, ctx, data):
1681 d = self.download_results()
1682 d.addCallback(lambda res: res.servermap)
1683 def _render(servermap):
1684 if servermap is None:
1687 for peerid in sorted(servermap.keys()):
1688 peerid_s = idlib.shortnodeid_b2a(peerid)
1689 shares_s = ",".join(["#%d" % shnum
1690 for shnum in servermap[peerid]])
1691 l[T.li["[%s] has share%s: %s" % (peerid_s,
1692 plural(servermap[peerid]),
1695 d.addCallback(_render)
1698 def render_problems(self, ctx, data):
1699 d = self.download_results()
1700 d.addCallback(lambda res: res.server_problems)
1701 def _got(server_problems):
1702 if not server_problems:
1705 for peerid in sorted(server_problems.keys()):
1706 peerid_s = idlib.shortnodeid_b2a(peerid)
1707 l[T.li["[%s]: %s" % (peerid_s, server_problems[peerid])]]
1708 return T.li["Server Problems:", l]
1712 def data_file_size(self, ctx, data):
1713 d = self.download_results()
1714 d.addCallback(lambda res: res.file_size)
1717 def render_time(self, ctx, data):
1718 # 1.23s, 790ms, 132us
1725 return "%dms" % (1000*s)
1727 return "%.1fms" % (1000*s)
1728 return "%dus" % (1000000*s)
1730 def render_rate(self, ctx, data):
1731 # 21.8kBps, 554.4kBps 4.37MBps
1736 return "%1.2fMBps" % (r/1000000)
1738 return "%.1fkBps" % (r/1000)
1741 def _get_time(self, name):
1742 d = self.download_results()
1743 d.addCallback(lambda res: res.timings.get(name))
1746 def data_time_total(self, ctx, data):
1747 return self._get_time("total")
1749 def data_time_peer_selection(self, ctx, data):
1750 return self._get_time("peer_selection")
1752 def data_time_uri_extension(self, ctx, data):
1753 return self._get_time("uri_extension")
1755 def data_time_hashtrees(self, ctx, data):
1756 return self._get_time("hashtrees")
1758 def data_time_segments(self, ctx, data):
1759 return self._get_time("segments")
1761 def data_time_cumulative_fetch(self, ctx, data):
1762 return self._get_time("cumulative_fetch")
1764 def data_time_cumulative_decode(self, ctx, data):
1765 return self._get_time("cumulative_decode")
1767 def data_time_cumulative_decrypt(self, ctx, data):
1768 return self._get_time("cumulative_decrypt")
1770 def _get_rate(self, name):
1771 d = self.download_results()
1773 file_size = r.file_size
1774 time = r.timings.get(name)
1778 return 1.0 * file_size / time
1779 except ZeroDivisionError:
1781 d.addCallback(_convert)
1784 def data_rate_total(self, ctx, data):
1785 return self._get_rate("total")
1787 def data_rate_segments(self, ctx, data):
1788 return self._get_rate("segments")
1790 def data_rate_fetch(self, ctx, data):
1791 return self._get_rate("cumulative_fetch")
1793 def data_rate_decode(self, ctx, data):
1794 return self._get_rate("cumulative_decode")
1796 def data_rate_decrypt(self, ctx, data):
1797 return self._get_rate("cumulative_decrypt")
1799 class DownloadStatusPage(DownloadResultsRendererMixin, rend.Page):
1800 docFactory = getxmlfile("download-status.xhtml")
1802 def __init__(self, data):
1803 rend.Page.__init__(self, data)
1804 self.download_status = data
1806 def download_results(self):
1807 return defer.maybeDeferred(self.download_status.get_results)
1809 def render_results(self, ctx, data):
1810 d = self.download_results()
1811 def _got_results(results):
1815 d.addCallback(_got_results)
1818 def render_si(self, ctx, data):
1819 si_s = base32.b2a_or_none(data.get_storage_index())
1824 def render_helper(self, ctx, data):
1825 return {True: "Yes",
1826 False: "No"}[data.using_helper()]
1828 def render_total_size(self, ctx, data):
1829 size = data.get_size()
1834 def render_progress(self, ctx, data):
1835 progress = data.get_progress()
1836 # TODO: make an ascii-art bar
1837 return "%.1f%%" % (100.0 * progress)
1839 def render_status(self, ctx, data):
1840 return data.get_status()
1842 class Status(rend.Page):
1843 docFactory = getxmlfile("status.xhtml")
1846 def data_active_uploads(self, ctx, data):
1847 return [u for u in IClient(ctx).list_active_uploads()]
1848 def data_active_downloads(self, ctx, data):
1849 return [d for d in IClient(ctx).list_active_downloads()]
1850 def data_recent_uploads(self, ctx, data):
1851 return [u for u in IClient(ctx).list_recent_uploads()
1852 if not u.get_active()]
1853 def data_recent_downloads(self, ctx, data):
1854 return [d for d in IClient(ctx).list_recent_downloads()
1855 if not d.get_active()]
1857 def childFactory(self, ctx, name):
1858 client = IClient(ctx)
1859 stype,count_s = name.split("-")
1860 count = int(count_s)
1862 for s in client.list_recent_uploads():
1863 if s.get_counter() == count:
1864 return UploadStatusPage(s)
1865 for s in client.list_all_uploads():
1866 if s.get_counter() == count:
1867 return UploadStatusPage(s)
1869 for s in client.list_recent_downloads():
1870 if s.get_counter() == count:
1871 return DownloadStatusPage(s)
1872 for s in client.list_all_downloads():
1873 if s.get_counter() == count:
1874 return DownloadStatusPage(s)
1876 def _render_common(self, ctx, data):
1878 si_s = base32.b2a_or_none(s.get_storage_index())
1881 ctx.fillSlots("si", si_s)
1882 ctx.fillSlots("helper", {True: "Yes",
1883 False: "No"}[s.using_helper()])
1887 ctx.fillSlots("total_size", size)
1888 if IUploadStatus.providedBy(data):
1889 link = "up-%d" % data.get_counter()
1891 assert IDownloadStatus.providedBy(data)
1892 link = "down-%d" % data.get_counter()
1893 ctx.fillSlots("status", T.a(href=link)[s.get_status()])
1895 def render_row_upload(self, ctx, data):
1896 self._render_common(ctx, data)
1897 (chk, ciphertext, encandpush) = data.get_progress()
1898 # TODO: make an ascii-art bar
1899 ctx.fillSlots("progress_hash", "%.1f%%" % (100.0 * chk))
1900 ctx.fillSlots("progress_ciphertext", "%.1f%%" % (100.0 * ciphertext))
1901 ctx.fillSlots("progress_encode", "%.1f%%" % (100.0 * encandpush))
1904 def render_row_download(self, ctx, data):
1905 self._render_common(ctx, data)
1906 progress = data.get_progress()
1907 # TODO: make an ascii-art bar
1908 ctx.fillSlots("progress", "%.1f%%" % (100.0 * progress))
1912 class Root(rend.Page):
1915 docFactory = getxmlfile("welcome.xhtml")
1917 def locateChild(self, ctx, segments):
1918 client = IClient(ctx)
1919 req = inevow.IRequest(ctx)
1921 segments = list(segments) # XXX HELP I AM YUCKY!
1922 while segments and not segments[-1]:
1926 segments = tuple(segments)
1928 if segments[0] == "uri":
1929 if len(segments) == 1 or segments[1] == '':
1930 uri = get_arg(req, "uri", None)
1932 there = url.URL.fromContext(ctx)
1933 there = there.clear("uri")
1934 there = there.child("uri").child(uri)
1936 if len(segments) == 1:
1938 if req.method == "PUT":
1939 # either "PUT /uri" to create an unlinked file, or
1940 # "PUT /uri?t=mkdir" to create an unlinked directory
1941 t = get_arg(req, "t", "").strip()
1943 mutable = bool(get_arg(req, "mutable", "").strip())
1945 return UnlinkedPUTSSKUploader(), ()
1947 return UnlinkedPUTCHKUploader(), ()
1949 return UnlinkedPUTCreateDirectory(), ()
1950 errmsg = "/uri only accepts PUT and PUT?t=mkdir"
1951 return WebError(http.BAD_REQUEST, errmsg), ()
1953 elif req.method == "POST":
1954 # "POST /uri?t=upload&file=newfile" to upload an
1955 # unlinked file or "POST /uri?t=mkdir" to create a
1957 t = get_arg(req, "t", "").strip()
1958 if t in ("", "upload"):
1959 mutable = bool(get_arg(req, "mutable", "").strip())
1961 return UnlinkedPOSTSSKUploader(), ()
1963 return UnlinkedPOSTCHKUploader(client, req), ()
1965 return UnlinkedPOSTCreateDirectory(), ()
1966 errmsg = "/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, and POST?t=mkdir"
1967 return WebError(http.BAD_REQUEST, errmsg), ()
1968 if len(segments) < 2:
1969 return rend.NotFound
1971 d = defer.maybeDeferred(client.create_node_from_uri, uri)
1972 d.addCallback(lambda node: VDrive(node, uri))
1973 d.addCallback(lambda vd: vd.locateChild(ctx, segments[2:]))
1974 def _trap_KeyError(f):
1976 return rend.FourOhFour(), ()
1977 d.addErrback(_trap_KeyError)
1979 elif segments[0] == "xmlrpc":
1980 raise NotImplementedError()
1981 return rend.Page.locateChild(self, ctx, segments)
1983 child_webform_css = webform.defaultCSS
1984 child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
1986 child_provisioning = provisioning.ProvisioningTool()
1987 child_status = Status()
1989 def data_version(self, ctx, data):
1990 return get_package_versions_string()
1991 def data_import_path(self, ctx, data):
1992 return str(allmydata)
1993 def data_my_nodeid(self, ctx, data):
1994 return idlib.nodeid_b2a(IClient(ctx).nodeid)
1995 def data_storage(self, ctx, data):
1996 client = IClient(ctx)
1998 ss = client.getServiceNamed("storage")
2000 return "Not running"
2001 allocated = ss.allocated_size()
2002 return "about %d bytes allocated" % allocated
2004 def data_introducer_furl(self, ctx, data):
2005 return IClient(ctx).introducer_furl
2006 def data_connected_to_introducer(self, ctx, data):
2007 if IClient(ctx).connected_to_introducer():
2011 def data_helper_furl(self, ctx, data):
2013 uploader = IClient(ctx).getServiceNamed("uploader")
2016 furl, connected = uploader.get_helper_info()
2018 def data_connected_to_helper(self, ctx, data):
2020 uploader = IClient(ctx).getServiceNamed("uploader")
2022 return "no" # we don't even have an Uploader
2023 furl, connected = uploader.get_helper_info()
2028 def data_known_storage_servers(self, ctx, data):
2029 ic = IClient(ctx).introducer_client
2031 for c in ic.get_all_connectors().values()
2032 if c.service_name == "storage"]
2035 def data_connected_storage_servers(self, ctx, data):
2036 ic = IClient(ctx).introducer_client
2037 return len(ic.get_all_connections_for("storage"))
2039 def data_services(self, ctx, data):
2040 ic = IClient(ctx).introducer_client
2041 c = [ (service_name, nodeid, rsc)
2042 for (nodeid, service_name), rsc
2043 in ic.get_all_connectors().items() ]
2047 def render_service_row(self, ctx, data):
2048 (service_name, nodeid, rsc) = data
2049 ctx.fillSlots("peerid", "%s %s" % (idlib.nodeid_b2a(nodeid),
2052 rhost = rsc.remote_host
2053 if nodeid == IClient(ctx).nodeid:
2054 rhost_s = "(loopback)"
2055 elif isinstance(rhost, address.IPv4Address):
2056 rhost_s = "%s:%d" % (rhost.host, rhost.port)
2058 rhost_s = str(rhost)
2059 connected = "Yes: to " + rhost_s
2060 since = rsc.last_connect_time
2063 since = rsc.last_loss_time
2065 TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
2066 ctx.fillSlots("connected", connected)
2067 ctx.fillSlots("since", time.strftime(TIME_FORMAT, time.localtime(since)))
2068 ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
2069 time.localtime(rsc.announcement_time)))
2070 ctx.fillSlots("version", rsc.version)
2071 ctx.fillSlots("service_name", rsc.service_name)
2075 def render_download_form(self, ctx, data):
2076 # this is a form where users can download files by URI
2077 form = T.form(action="uri", method="get",
2078 enctype="multipart/form-data")[
2080 T.legend(class_="freeform-form-label")["Download a file"],
2081 "URI to download: ",
2082 T.input(type="text", name="uri"), " ",
2083 "Filename to download as: ",
2084 T.input(type="text", name="filename"), " ",
2085 T.input(type="submit", value="Download!"),
2089 def render_view_form(self, ctx, data):
2090 # this is a form where users can download files by URI, or jump to a
2092 form = T.form(action="uri", method="get",
2093 enctype="multipart/form-data")[
2095 T.legend(class_="freeform-form-label")["View a file or directory"],
2097 T.input(type="text", name="uri"), " ",
2098 T.input(type="submit", value="View!"),
2102 def render_upload_form(self, ctx, data):
2103 # this is a form where users can upload unlinked files
2104 form = T.form(action="uri", method="post",
2105 enctype="multipart/form-data")[
2107 T.legend(class_="freeform-form-label")["Upload a file"],
2109 T.input(type="file", name="file", class_="freeform-input-file"),
2110 T.input(type="hidden", name="t", value="upload"),
2111 " Mutable?:", T.input(type="checkbox", name="mutable"),
2112 T.input(type="submit", value="Upload!"),
2116 def render_mkdir_form(self, ctx, data):
2117 # this is a form where users can create new directories
2118 form = T.form(action="uri", method="post",
2119 enctype="multipart/form-data")[
2121 T.legend(class_="freeform-form-label")["Create a directory"],
2122 T.input(type="hidden", name="t", value="mkdir"),
2123 T.input(type="hidden", name="redirect_to_result", value="true"),
2124 T.input(type="submit", value="Create Directory!"),
2130 implements(ILocalAccess)
2132 self.local_access = False
2133 def local_access_is_allowed(self):
2134 return self.local_access
2136 class WebishServer(service.MultiService):
2139 def __init__(self, webport, nodeurl_path=None):
2140 service.MultiService.__init__(self)
2141 self.webport = webport
2143 self.site = site = appserver.NevowSite(self.root)
2144 self.site.requestFactory = MyRequest
2145 self.allow_local = LocalAccess()
2146 self.site.remember(self.allow_local, ILocalAccess)
2147 s = strports.service(webport, site)
2148 s.setServiceParent(self)
2149 self.listener = s # stash it so the tests can query for the portnum
2150 self._started = defer.Deferred()
2152 self._started.addCallback(self._write_nodeurl_file, nodeurl_path)
2154 def allow_local_access(self, enable=True):
2155 self.allow_local.local_access = enable
2157 def startService(self):
2158 service.MultiService.startService(self)
2159 # to make various services available to render_* methods, we stash a
2160 # reference to the client on the NevowSite. This will be available by
2161 # adapting the 'context' argument to a special marker interface named
2163 self.site.remember(self.parent, IClient)
2164 # I thought you could do the same with an existing interface, but
2165 # apparently 'ISite' does not exist
2166 #self.site._client = self.parent
2167 self._started.callback(None)
2169 def _write_nodeurl_file(self, junk, nodeurl_path):
2170 # what is our webport?
2172 if isinstance(s, internet.TCPServer):
2173 base_url = "http://localhost:%d" % s._port.getHost().port
2174 elif isinstance(s, internet.SSLServer):
2175 base_url = "https://localhost:%d" % s._port.getHost().port
2179 f = open(nodeurl_path, 'wb')
2180 # this file is world-readable
2181 f.write(base_url + "\n")