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, appserver, url, tags as T
8 from nevow.static import File as nevow_File # TODO: merge with static.File?
9 from allmydata.util import fileutil, idlib, log
11 from allmydata.interfaces import IDownloadTarget, IDirectoryNode, IFileNode, \
13 import allmydata # to display import path
14 from allmydata import download
15 from allmydata.uri import from_string_verifier, CHKFileVerifierURI
16 from allmydata.upload import FileHandle, FileName
17 from allmydata import provisioning
18 from allmydata import get_package_versions_string
19 from zope.interface import implements, Interface
21 from formless import webform
22 from foolscap.eventual import fireEventually
24 from nevow.util import resource_filename
26 from allmydata.web import status, unlinked, introweb
27 from allmydata.web.common import IClient, getxmlfile, get_arg, \
28 boolean_of_arg, abbreviate_size
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 # we must override twisted.web.http.Request.requestReceived with a version
36 # that doesn't use cgi.parse_multipart() . Since we actually use Nevow, we
37 # override the nevow-specific subclass, nevow.appserver.NevowRequest . This
38 # is an exact copy of twisted.web.http.Request (from SVN HEAD on 10-Aug-2007)
39 # that modifies the way form arguments are parsed. Note that this sort of
40 # surgery may induce a dependency upon a particular version of twisted.web
42 parse_qs = http.parse_qs
43 class MyRequest(appserver.NevowRequest):
45 def requestReceived(self, command, path, version):
46 """Called by channel when all data has been received.
48 This method is not intended for users.
50 self.content.seek(0,0)
54 self.method, self.uri = command, path
55 self.clientproto = version
56 x = self.uri.split('?', 1)
61 self.path, argstring = x
62 self.args = parse_qs(argstring, 1)
64 # cache the client and server information, we'll need this later to be
65 # serialized and sent with the request so CGIs will work remotely
66 self.client = self.channel.transport.getPeer()
67 self.host = self.channel.transport.getHost()
69 # Argument processing.
71 ## The original twisted.web.http.Request.requestReceived code parsed the
72 ## content and added the form fields it found there to self.args . It
73 ## did this with cgi.parse_multipart, which holds the arguments in RAM
74 ## and is thus unsuitable for large file uploads. The Nevow subclass
75 ## (nevow.appserver.NevowRequest) uses cgi.FieldStorage instead (putting
76 ## the results in self.fields), which is much more memory-efficient.
77 ## Since we know we're using Nevow, we can anticipate these arguments
78 ## appearing in self.fields instead of self.args, and thus skip the
79 ## parse-content-into-self.args step.
82 ## ctype = self.getHeader('content-type')
83 ## if self.method == "POST" and ctype:
84 ## mfd = 'multipart/form-data'
85 ## key, pdict = cgi.parse_header(ctype)
86 ## if key == 'application/x-www-form-urlencoded':
87 ## args.update(parse_qs(self.content.read(), 1))
90 ## args.update(cgi.parse_multipart(self.content, pdict))
91 ## except KeyError, e:
92 ## if e.args[0] == 'content-disposition':
93 ## # Parse_multipart can't cope with missing
94 ## # content-dispostion headers in multipart/form-data
95 ## # parts, so we catch the exception and tell the client
96 ## # it was a bad request.
97 ## self.channel.transport.write(
98 ## "HTTP/1.1 400 Bad Request\r\n\r\n")
99 ## self.channel.transport.loseConnection()
106 # we build up a log string that hides most of the cap, to preserve
107 # user privacy. We retain the query args so we can identify things
108 # like t=json. Then we send it to the flog. We make no attempt to
109 # match apache formatting. TODO: when we move to DSA dirnodes and
110 # shorter caps, consider exposing a few characters of the cap, or
111 # maybe a few characters of its hash.
112 x = self.uri.split("?", 1)
119 # there is a form handler which redirects POST /uri?uri=FOO into
120 # GET /uri/FOO so folks can paste in non-HTTP-prefixed uris. Make
121 # sure we censor these too.
122 if queryargs.startswith("uri="):
123 queryargs = "[uri=CENSORED]"
124 queryargs = "?" + queryargs
125 if path.startswith("/uri"):
126 path = "/uri/[CENSORED].."
127 uri = path + queryargs
129 log.msg(format="web: %(clientip)s %(method)s %(uri)s %(code)s %(length)s",
130 clientip=self.getClientIP(),
134 length=(self.sentLength or "-"),
135 facility="tahoe.webish",
136 level=log.OPERATIONAL,
139 class Directory(rend.Page):
141 docFactory = getxmlfile("directory.xhtml")
143 def __init__(self, rootname, dirnode, dirpath):
144 self._rootname = rootname
145 self._dirnode = dirnode
146 self._dirpath = dirpath
148 def dirpath_as_string(self):
149 return "/" + "/".join(self._dirpath)
151 def render_title(self, ctx, data):
152 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
154 def render_header(self, ctx, data):
155 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
156 num_dirs = len(parent_directories)
158 header = ["Directory '"]
159 for i,d in enumerate(parent_directories):
160 upness = num_dirs - i - 1
162 link = "/".join( ("..",) * upness )
165 header.append(T.a(href=link)[d])
170 if self._dirnode.is_readonly():
171 header.append(" (readonly)")
173 return ctx.tag[header]
175 def render_welcome(self, ctx, data):
176 depth = len(self._dirpath) + 2
177 link = "/".join([".."] * depth)
178 return T.div[T.a(href=link)["Return to Welcome page"]]
180 def data_children(self, ctx, data):
181 d = self._dirnode.list()
182 d.addCallback(lambda dict: sorted(dict.items()))
183 def _stall_some(items):
184 # Deferreds don't optimize out tail recursion, and the way
185 # Nevow's flattener handles Deferreds doesn't take this into
186 # account. As a result, large lists of Deferreds that fire in the
187 # same turn (i.e. the output of defer.succeed) will cause a stack
188 # overflow. To work around this, we insert a turn break after
189 # every 100 items, using foolscap's fireEventually(). This gives
190 # the stack a chance to be popped. It would also work to put
191 # every item in its own turn, but that'd be a lot more
192 # inefficient. This addresses ticket #237, for which I was never
193 # able to create a failing unit test.
195 for i,item in enumerate(items):
197 output.append(fireEventually(item))
201 d.addCallback(_stall_some)
204 def render_row(self, ctx, data):
205 name, (target, metadata) = data
206 name = name.encode("utf-8")
207 assert not isinstance(name, unicode)
209 if self._dirnode.is_readonly():
213 # this creates a button which will cause our child__delete method
214 # to be invoked, which deletes the file and then redirects the
215 # browser back to this directory
216 delete = T.form(action=url.here, method="post")[
217 T.input(type='hidden', name='t', value='delete'),
218 T.input(type='hidden', name='name', value=name),
219 T.input(type='hidden', name='when_done', value=url.here),
220 T.input(type='submit', value='del', name="del"),
223 rename = T.form(action=url.here, method="get")[
224 T.input(type='hidden', name='t', value='rename-form'),
225 T.input(type='hidden', name='name', value=name),
226 T.input(type='hidden', name='when_done', value=url.here),
227 T.input(type='submit', value='rename', name="rename"),
230 ctx.fillSlots("delete", delete)
231 ctx.fillSlots("rename", rename)
232 check = T.form(action=url.here, method="post")[
233 T.input(type='hidden', name='t', value='check'),
234 T.input(type='hidden', name='name', value=name),
235 T.input(type='hidden', name='when_done', value=url.here),
236 T.input(type='submit', value='check', name="check"),
238 ctx.fillSlots("overwrite", self.build_overwrite(ctx, (name, target)))
239 ctx.fillSlots("check", check)
242 TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
243 if "ctime" in metadata:
244 ctime = time.strftime(TIME_FORMAT,
245 time.localtime(metadata["ctime"]))
246 times.append("c: " + ctime)
247 if "mtime" in metadata:
248 mtime = time.strftime(TIME_FORMAT,
249 time.localtime(metadata["mtime"]))
252 times.append("m: " + mtime)
253 ctx.fillSlots("times", times)
255 assert (IFileNode.providedBy(target)
256 or IDirectoryNode.providedBy(target)
257 or IMutableFileNode.providedBy(target)), target
259 quoted_uri = urllib.quote(target.get_uri())
261 if IMutableFileNode.providedBy(target):
262 # to prevent javascript in displayed .html files from stealing a
263 # secret directory URI from the URL, send the browser to a URI-based
264 # page that doesn't know about the directory at all
265 dlurl = "/file/%s/@@named=/%s" % (quoted_uri, urllib.quote(name))
267 ctx.fillSlots("filename",
268 T.a(href=dlurl)[html.escape(name)])
269 ctx.fillSlots("type", "SSK")
271 ctx.fillSlots("size", "?")
273 text_plain_url = "/file/%s/@@named=/foo.txt" % quoted_uri
274 text_plain_tag = T.a(href=text_plain_url)["text/plain"]
276 elif IFileNode.providedBy(target):
277 dlurl = "/file/%s/@@named=/%s" % (quoted_uri, urllib.quote(name))
279 ctx.fillSlots("filename",
280 T.a(href=dlurl)[html.escape(name)])
281 ctx.fillSlots("type", "FILE")
283 ctx.fillSlots("size", target.get_size())
285 text_plain_url = "/file/%s/@@named=/foo.txt" % quoted_uri
286 text_plain_tag = T.a(href=text_plain_url)["text/plain"]
289 elif IDirectoryNode.providedBy(target):
291 uri_link = "/uri/" + urllib.quote(target.get_uri())
292 ctx.fillSlots("filename",
293 T.a(href=uri_link)[html.escape(name)])
294 if target.is_readonly():
298 ctx.fillSlots("type", dirtype)
299 ctx.fillSlots("size", "-")
300 text_plain_tag = None
302 childdata = [T.a(href="%s?t=json" % name)["JSON"], ", ",
303 T.a(href="%s?t=uri" % name)["URI"], ", ",
304 T.a(href="%s?t=readonly-uri" % name)["readonly-URI"],
307 childdata.extend([", ", text_plain_tag])
309 ctx.fillSlots("data", childdata)
312 checker = IClient(ctx).getServiceNamed("checker")
316 d = defer.maybeDeferred(checker.checker_results_for,
317 target.get_verifier())
318 def _got(checker_results):
319 recent_results = reversed(checker_results[-5:])
320 if IFileNode.providedBy(target):
322 ", ".join(["%d/%d" % (found, needed)
324 (needed, total, found, sharemap))
325 in recent_results]) +
327 elif IDirectoryNode.providedBy(target):
329 "".join([{True:"+",False:"-"}[res]
330 for (when, res) in recent_results]) +
333 results = "%d results" % len(checker_results)
339 # TODO: include a link to see more results, including timestamps
340 # TODO: use a sparkline
341 ctx.fillSlots("checker_results", results)
345 def render_forms(self, ctx, data):
346 if self._dirnode.is_readonly():
347 return T.div["No upload forms: directory is read-only"]
348 mkdir = T.form(action=".", method="post",
349 enctype="multipart/form-data")[
351 T.input(type="hidden", name="t", value="mkdir"),
352 T.input(type="hidden", name="when_done", value=url.here),
353 T.legend(class_="freeform-form-label")["Create a new directory"],
354 "New directory name: ",
355 T.input(type="text", name="name"), " ",
356 T.input(type="submit", value="Create"),
359 upload = T.form(action=".", method="post",
360 enctype="multipart/form-data")[
362 T.input(type="hidden", name="t", value="upload"),
363 T.input(type="hidden", name="when_done", value=url.here),
364 T.legend(class_="freeform-form-label")["Upload a file to this directory"],
365 "Choose a file to upload: ",
366 T.input(type="file", name="file", class_="freeform-input-file"),
368 T.input(type="submit", value="Upload"),
370 T.input(type="checkbox", name="mutable"),
373 mount = T.form(action=".", method="post",
374 enctype="multipart/form-data")[
376 T.input(type="hidden", name="t", value="uri"),
377 T.input(type="hidden", name="when_done", value=url.here),
378 T.legend(class_="freeform-form-label")["Attach a file or directory"
382 T.input(type="text", name="name"), " ",
383 "URI of new child: ",
384 T.input(type="text", name="uri"), " ",
385 T.input(type="submit", value="Attach"),
387 return [T.div(class_="freeform-form")[mkdir],
388 T.div(class_="freeform-form")[upload],
389 T.div(class_="freeform-form")[mount],
392 def build_overwrite(self, ctx, data):
394 if IMutableFileNode.providedBy(target) and not target.is_readonly():
395 action="/uri/" + urllib.quote(target.get_uri())
396 overwrite = T.form(action=action, method="post",
397 enctype="multipart/form-data")[
399 T.input(type="hidden", name="t", value="overwrite"),
400 T.input(type='hidden', name='name', value=name),
401 T.input(type='hidden', name='when_done', value=url.here),
402 T.legend(class_="freeform-form-label")["Overwrite"],
404 T.input(type="file", name="file", class_="freeform-input-file"),
406 T.input(type="submit", value="Overwrite")
408 return [T.div(class_="freeform-form")[overwrite],]
412 def render_results(self, ctx, data):
413 req = inevow.IRequest(ctx)
414 return get_arg(req, "results", "")
416 class WebDownloadTarget:
417 implements(IDownloadTarget, IConsumer)
418 def __init__(self, req, content_type, content_encoding, save_to_file):
420 self._content_type = content_type
421 self._content_encoding = content_encoding
423 self._producer = None
424 self._save_to_file = save_to_file
426 def registerProducer(self, producer, streaming):
427 self._req.registerProducer(producer, streaming)
428 def unregisterProducer(self):
429 self._req.unregisterProducer()
431 def open(self, size):
433 self._req.setHeader("content-type", self._content_type)
434 if self._content_encoding:
435 self._req.setHeader("content-encoding", self._content_encoding)
436 self._req.setHeader("content-length", str(size))
437 if self._save_to_file is not None:
438 # tell the browser to save the file rather display it
439 # TODO: quote save_to_file properly
440 filename = self._save_to_file.encode("utf-8")
441 self._req.setHeader("content-disposition",
442 'attachment; filename="%s"'
445 def write(self, data):
446 self._req.write(data)
452 # The content-type is already set, and the response code
453 # has already been sent, so we can't provide a clean error
454 # indication. We can emit text (which a browser might interpret
455 # as something else), and if we sent a Size header, they might
456 # notice that we've truncated the data. Keep the error message
457 # small to improve the chances of having our error response be
458 # shorter than the intended results.
460 # We don't have a lot of options, unfortunately.
461 self._req.write("problem during download\n")
463 # We haven't written anything yet, so we can provide a sensible
466 msg.replace("\n", "|")
467 self._req.setResponseCode(http.GONE, msg)
468 self._req.setHeader("content-type", "text/plain")
469 # TODO: HTML-formatted exception?
470 self._req.write(str(why))
473 def register_canceller(self, cb):
478 class FileDownloader(resource.Resource):
480 def __init__(self, filenode, name):
481 assert (IFileNode.providedBy(filenode)
482 or IMutableFileNode.providedBy(filenode))
483 self._filenode = filenode
486 def render(self, req):
487 gte = static.getTypeAndEncoding
488 ctype, encoding = gte(self._name,
489 static.File.contentTypes,
490 static.File.contentEncodings,
491 defaultType="text/plain")
493 if get_arg(req, "save", False):
494 # TODO: make the API specification clear: should "save=" or
495 # "save=false" count?
496 save_to_file = self._name
497 wdt = WebDownloadTarget(req, ctype, encoding, save_to_file)
498 d = self._filenode.download(wdt)
499 # exceptions during download are handled by the WebDownloadTarget
500 d.addErrback(lambda why: None)
501 return server.NOT_DONE_YET
503 class BlockingFileError(Exception):
504 """We cannot auto-create a parent directory, because there is a file in
506 class NoReplacementError(Exception):
507 """There was already a child by that name, and you asked me to not replace it"""
508 class NoLocalDirectoryError(Exception):
509 """The localdir= directory didn't exist"""
511 LOCALHOST = "127.0.0.1"
513 class NeedLocalhostError:
514 implements(inevow.IResource)
516 def renderHTTP(self, ctx):
517 req = inevow.IRequest(ctx)
518 req.setResponseCode(http.FORBIDDEN)
519 req.setHeader("content-type", "text/plain")
520 return "localfile= or localdir= requires a local connection"
522 class NeedAbsolutePathError:
523 implements(inevow.IResource)
525 def renderHTTP(self, ctx):
526 req = inevow.IRequest(ctx)
527 req.setResponseCode(http.FORBIDDEN)
528 req.setHeader("content-type", "text/plain")
529 return "localfile= or localdir= requires an absolute path"
531 class LocalAccessDisabledError:
532 implements(inevow.IResource)
534 def renderHTTP(self, ctx):
535 req = inevow.IRequest(ctx)
536 req.setResponseCode(http.FORBIDDEN)
537 req.setHeader("content-type", "text/plain")
538 return "local file access is disabled"
541 implements(inevow.IResource)
542 def __init__(self, response_code, errmsg):
543 self._response_code = response_code
544 self._errmsg = errmsg
546 def renderHTTP(self, ctx):
547 req = inevow.IRequest(ctx)
548 req.setResponseCode(self._response_code)
549 req.setHeader("content-type", "text/plain")
553 class LocalFileDownloader(resource.Resource):
554 def __init__(self, filenode, local_filename):
555 self._local_filename = local_filename
557 self._filenode = filenode
559 def render(self, req):
560 target = download.FileName(self._local_filename)
561 d = self._filenode.download(target)
563 req.write(self._filenode.get_uri())
566 return server.NOT_DONE_YET
569 class FileJSONMetadata(rend.Page):
570 def __init__(self, filenode):
571 self._filenode = filenode
573 def renderHTTP(self, ctx):
574 req = inevow.IRequest(ctx)
575 req.setHeader("content-type", "text/plain")
576 return self.renderNode(self._filenode)
578 def renderNode(self, filenode):
579 file_uri = filenode.get_uri()
582 'size': filenode.get_size(),
584 return simplejson.dumps(data, indent=1)
586 class FileURI(FileJSONMetadata):
587 def renderNode(self, filenode):
588 file_uri = filenode.get_uri()
591 class FileReadOnlyURI(FileJSONMetadata):
592 def renderNode(self, filenode):
593 if filenode.is_readonly():
594 return filenode.get_uri()
596 return filenode.get_readonly().get_uri()
598 class DirnodeWalkerMixin:
599 """Visit all nodes underneath (and including) the rootnode, one at a
600 time. For each one, call the visitor. The visitor will see the
601 IDirectoryNode before it sees any of the IFileNodes inside. If the
602 visitor returns a Deferred, I do not call the visitor again until it has
606 ## def _walk_if_we_could_use_generators(self, rootnode, rootpath=()):
607 ## # this is what we'd be doing if we didn't have the Deferreds and
608 ## # thus could use generators
609 ## yield rootpath, rootnode
610 ## for childname, childnode in rootnode.list().items():
611 ## childpath = rootpath + (childname,)
612 ## if IFileNode.providedBy(childnode):
613 ## yield childpath, childnode
614 ## elif IDirectoryNode.providedBy(childnode):
615 ## for res in self._walk_if_we_could_use_generators(childnode,
619 def walk(self, rootnode, visitor, rootpath=()):
621 def _listed(listing):
622 return listing.items()
623 d.addCallback(_listed)
624 d.addCallback(self._handle_items, visitor, rootpath)
627 def _handle_items(self, items, visitor, rootpath):
630 childname, (childnode, metadata) = items[0]
631 childpath = rootpath + (childname,)
632 d = defer.maybeDeferred(visitor, childpath, childnode, metadata)
633 if IDirectoryNode.providedBy(childnode):
634 d.addCallback(lambda res: self.walk(childnode, visitor, childpath))
635 d.addCallback(lambda res:
636 self._handle_items(items[1:], visitor, rootpath))
639 class LocalDirectoryDownloader(resource.Resource, DirnodeWalkerMixin):
640 def __init__(self, dirnode, localdir):
641 self._dirnode = dirnode
642 self._localdir = localdir
644 def _handle(self, path, node, metadata):
645 path = tuple([p.encode("utf-8") for p in path])
646 localfile = os.path.join(self._localdir, os.sep.join(path))
647 if IDirectoryNode.providedBy(node):
648 fileutil.make_dirs(localfile)
649 elif IFileNode.providedBy(node):
650 target = download.FileName(localfile)
651 return node.download(target)
653 def render(self, req):
654 d = self.walk(self._dirnode, self._handle)
656 req.setHeader("content-type", "text/plain")
657 return "operation complete"
661 class DirectoryJSONMetadata(rend.Page):
662 def __init__(self, dirnode):
663 self._dirnode = dirnode
665 def renderHTTP(self, ctx):
666 req = inevow.IRequest(ctx)
667 req.setHeader("content-type", "text/plain")
668 return self.renderNode(self._dirnode)
670 def renderNode(self, node):
674 for name, (childnode, metadata) in children.iteritems():
675 if IFileNode.providedBy(childnode):
676 kiduri = childnode.get_uri()
677 kiddata = ("filenode",
679 'size': childnode.get_size(),
680 'metadata': metadata,
683 assert IDirectoryNode.providedBy(childnode), (childnode, children,)
684 kiddata = ("dirnode",
685 {'ro_uri': childnode.get_readonly_uri(),
686 'metadata': metadata,
688 if not childnode.is_readonly():
689 kiddata[1]['rw_uri'] = childnode.get_uri()
691 contents = { 'children': kids,
692 'ro_uri': node.get_readonly_uri(),
694 if not node.is_readonly():
695 contents['rw_uri'] = node.get_uri()
696 data = ("dirnode", contents)
697 return simplejson.dumps(data, indent=1)
701 class DirectoryURI(DirectoryJSONMetadata):
702 def renderNode(self, node):
703 return node.get_uri()
705 class DirectoryReadonlyURI(DirectoryJSONMetadata):
706 def renderNode(self, node):
707 return node.get_readonly_uri()
709 class RenameForm(rend.Page):
711 docFactory = getxmlfile("rename-form.xhtml")
713 def __init__(self, rootname, dirnode, dirpath):
714 self._rootname = rootname
715 self._dirnode = dirnode
716 self._dirpath = dirpath
718 def dirpath_as_string(self):
719 return "/" + "/".join(self._dirpath)
721 def render_title(self, ctx, data):
722 return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
724 def render_header(self, ctx, data):
725 parent_directories = ("<%s>" % self._rootname,) + self._dirpath
726 num_dirs = len(parent_directories)
728 header = [ "Rename in directory '",
729 "<%s>/" % self._rootname,
730 "/".join(self._dirpath),
733 if self._dirnode.is_readonly():
734 header.append(" (readonly)")
735 return ctx.tag[header]
737 def render_when_done(self, ctx, data):
738 return T.input(type="hidden", name="when_done", value=url.here)
740 def render_get_name(self, ctx, data):
741 req = inevow.IRequest(ctx)
742 name = get_arg(req, "name", "")
743 ctx.tag.attributes['value'] = name
746 class POSTHandler(rend.Page):
747 def __init__(self, node, replace):
749 self._replace = replace
751 def _check_replacement(self, name):
753 return defer.succeed(None)
754 d = self._node.has_child(name)
757 raise NoReplacementError("There was already a child by that "
758 "name, and you asked me to not "
764 def _POST_mkdir(self, name):
765 d = self._check_replacement(name)
766 d.addCallback(lambda res: self._node.create_empty_directory(name))
767 d.addCallback(lambda res: "directory created")
770 def _POST_mkdir_p(self, path):
771 path_ = tuple([seg.decode("utf-8") for seg in path.split('/') if seg ])
772 d = self._get_or_create_directories(self._node, path_)
773 d.addCallback(lambda node: node.get_uri())
776 # this code stolen from PUTHandler: should be refactored to a more
777 # generally accesible place, perhaps...
778 def _get_or_create_directories(self, node, path):
779 if not IDirectoryNode.providedBy(node):
780 # unfortunately it is too late to provide the name of the
781 # blocking directory in the error message.
782 raise BlockingFileError("cannot create directory because there "
783 "is a file in the way")
785 return defer.succeed(node)
786 d = node.get(path[0])
787 def _maybe_create(f):
789 return node.create_empty_directory(path[0])
790 d.addErrback(_maybe_create)
791 d.addCallback(self._get_or_create_directories, path[1:])
794 def _POST_uri(self, name, newuri):
795 d = self._check_replacement(name)
796 d.addCallback(lambda res: self._node.set_uri(name, newuri))
797 d.addCallback(lambda res: newuri)
800 def _POST_delete(self, name):
802 # apparently an <input type="hidden" name="name" value="">
803 # won't show up in the resulting encoded form.. the 'name'
804 # field is completely missing. So to allow deletion of an
805 # empty file, we have to pretend that None means ''. The only
806 # downide of this is a slightly confusing error message if
807 # someone does a POST without a name= field. For our own HTML
808 # thisn't a big deal, because we create the 'delete' POST
811 d = self._node.delete(name)
812 d.addCallback(lambda res: "thing deleted")
815 def _POST_rename(self, name, from_name, to_name):
816 d = self._check_replacement(to_name)
817 d.addCallback(lambda res: self._node.get(from_name))
819 uri = child.get_uri()
820 # now actually do the rename
821 return self._node.set_uri(to_name, uri)
822 d.addCallback(add_dest)
824 return self._node.delete(from_name)
825 d.addCallback(rm_src)
826 d.addCallback(lambda res: "thing renamed")
829 def _POST_upload(self, contents, name, mutable, client):
831 # SDMF: files are small, and we can only upload data.
832 contents.file.seek(0)
833 data = contents.file.read()
834 #uploadable = FileHandle(contents.file)
835 d = self._check_replacement(name)
836 d.addCallback(lambda res: self._node.has_child(name))
837 def _checked(present):
839 # modify the existing one instead of creating a new
841 d2 = self._node.get(name)
842 def _got_newnode(newnode):
843 d3 = newnode.overwrite(data)
844 d3.addCallback(lambda res: newnode.get_uri())
846 d2.addCallback(_got_newnode)
848 d2 = client.create_mutable_file(data)
849 def _uploaded(newnode):
850 d1 = self._node.set_node(name, newnode)
851 d1.addCallback(lambda res: newnode.get_uri())
853 d2.addCallback(_uploaded)
855 d.addCallback(_checked)
857 uploadable = FileHandle(contents.file, convergence=client.convergence)
858 d = self._check_replacement(name)
859 d.addCallback(lambda res: self._node.add_file(name, uploadable))
861 return newnode.get_uri()
865 def _POST_overwrite(self, contents):
866 # SDMF: files are small, and we can only upload data.
867 contents.file.seek(0)
868 data = contents.file.read()
869 # TODO: 'name' handling needs review
870 d = defer.succeed(self._node)
871 def _got_child_overwrite(child_node):
872 child_node.overwrite(data)
873 return child_node.get_uri()
874 d.addCallback(_got_child_overwrite)
877 def _POST_check(self, name):
878 d = self._node.get(name)
879 def _got_child_check(child_node):
880 d2 = child_node.check()
882 log.msg("checked %s, results %s" % (child_node, res),
883 facility="tahoe.webish", level=log.NOISY)
885 d2.addCallback(_done)
887 d.addCallback(_got_child_check)
890 def _POST_set_children(self, children):
892 for name, (file_or_dir, mddict) in children.iteritems():
893 cap = str(mddict.get('rw_uri') or mddict.get('ro_uri'))
894 cs.append((name, cap, mddict.get('metadata')))
896 d = self._node.set_children(cs)
897 d.addCallback(lambda res: "Okay so I did it.")
900 def renderHTTP(self, ctx):
901 req = inevow.IRequest(ctx)
903 t = get_arg(req, "t")
906 charset = get_arg(req, "_charset", "utf-8")
908 name = get_arg(req, "name", None)
909 if name and "/" in name:
910 req.setResponseCode(http.BAD_REQUEST)
911 req.setHeader("content-type", "text/plain")
912 return "name= may not contain a slash"
915 name = name.decode(charset)
916 assert isinstance(name, unicode)
917 # we allow the user to delete an empty-named file, but not to create
918 # them, since that's an easy and confusing mistake to make
920 when_done = get_arg(req, "when_done", None)
921 if not boolean_of_arg(get_arg(req, "replace", "true")):
922 self._replace = False
926 raise RuntimeError("mkdir requires a name")
927 d = self._POST_mkdir(name)
929 path = get_arg(req, "path")
931 raise RuntimeError("mkdir-p requires a path")
932 d = self._POST_mkdir_p(path)
935 raise RuntimeError("set-uri requires a name")
936 newuri = get_arg(req, "uri")
937 assert newuri is not None
938 d = self._POST_uri(name, newuri)
940 d = self._POST_delete(name)
942 from_name = get_arg(req, "from_name")
943 if from_name is not None:
944 from_name = from_name.strip()
945 from_name = from_name.decode(charset)
946 assert isinstance(from_name, unicode)
947 to_name = get_arg(req, "to_name")
948 if to_name is not None:
949 to_name = to_name.strip()
950 to_name = to_name.decode(charset)
951 assert isinstance(to_name, unicode)
952 if not from_name or not to_name:
953 raise RuntimeError("rename requires from_name and to_name")
954 if not IDirectoryNode.providedBy(self._node):
955 raise RuntimeError("rename must only be called on directories")
956 for k,v in [ ('from_name', from_name), ('to_name', to_name) ]:
958 req.setResponseCode(http.BAD_REQUEST)
959 req.setHeader("content-type", "text/plain")
960 return "%s= may not contain a slash" % (k,)
961 d = self._POST_rename(name, from_name, to_name)
963 contents = req.fields["file"]
964 name = name or contents.filename
968 # this prohibts empty, missing, and all-whitespace filenames
969 raise RuntimeError("upload requires a name")
970 name = name.decode(charset)
971 assert isinstance(name, unicode)
972 mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
973 d = self._POST_upload(contents, name, mutable, IClient(ctx))
974 elif t == "overwrite":
975 contents = req.fields["file"]
976 d = self._POST_overwrite(contents)
978 d = self._POST_check(name)
979 elif t == "set_children":
981 body = req.content.read()
983 children = simplejson.loads(body)
984 except ValueError, le:
985 le.args = tuple(le.args + (body,))
986 # TODO test handling of bad JSON
988 d = self._POST_set_children(children)
990 req.setResponseCode(http.BAD_REQUEST)
991 req.setHeader("content-type", "text/plain")
992 return "BAD t=%s" % t
994 d.addCallback(lambda res: url.URL.fromString(when_done))
995 def _check_replacement(f):
996 # TODO: make this more human-friendly: maybe send them to the
997 # when_done page but with an extra query-arg that will display
998 # the error message in a big box at the top of the page. The
999 # directory page that when_done= usually points to accepts a
1000 # result= argument.. use that.
1001 f.trap(NoReplacementError)
1002 req.setResponseCode(http.CONFLICT)
1003 req.setHeader("content-type", "text/plain")
1005 d.addErrback(_check_replacement)
1008 class DELETEHandler(rend.Page):
1009 def __init__(self, node, name):
1013 def renderHTTP(self, ctx):
1014 req = inevow.IRequest(ctx)
1015 d = self._node.delete(self._name)
1017 # what should this return??
1018 return "%s deleted" % self._name.encode("utf-8")
1019 d.addCallback(_done)
1020 def _trap_missing(f):
1022 req.setResponseCode(http.NOT_FOUND)
1023 req.setHeader("content-type", "text/plain")
1024 return "no such child %s" % self._name.encode("utf-8")
1025 d.addErrback(_trap_missing)
1028 class PUTHandler(rend.Page):
1029 def __init__(self, node, path, t, localfile, localdir, replace):
1033 self._localfile = localfile
1034 self._localdir = localdir
1035 self._replace = replace
1037 def renderHTTP(self, ctx):
1038 client = IClient(ctx)
1039 req = inevow.IRequest(ctx)
1041 localfile = self._localfile
1042 localdir = self._localdir
1044 if t == "upload" and not (localfile or localdir):
1045 req.setResponseCode(http.BAD_REQUEST)
1046 req.setHeader("content-type", "text/plain")
1047 return "t=upload requires localfile= or localdir="
1049 # we must traverse the path, creating new directories as necessary
1050 d = self._get_or_create_directories(self._node, self._path[:-1])
1051 name = self._path[-1]
1052 d.addCallback(self._check_replacement, name, self._replace)
1055 d.addCallback(self._upload_localfile, localfile, name, convergence=client.convergence)
1058 # take the last step
1059 d.addCallback(self._get_or_create_directories, self._path[-1:])
1060 d.addCallback(self._upload_localdir, localdir, convergence=client.convergence)
1062 d.addCallback(self._attach_uri, req.content, name)
1064 d.addCallback(self._mkdir, name)
1066 d.addCallback(self._upload_file, req.content, name, convergence=client.convergence)
1068 def _transform_error(f):
1069 errors = {BlockingFileError: http.BAD_REQUEST,
1070 NoReplacementError: http.CONFLICT,
1071 NoLocalDirectoryError: http.BAD_REQUEST,
1073 for k,v in errors.items():
1075 req.setResponseCode(v)
1076 req.setHeader("content-type", "text/plain")
1079 d.addErrback(_transform_error)
1082 def _get_or_create_directories(self, node, path):
1083 if not IDirectoryNode.providedBy(node):
1084 # unfortunately it is too late to provide the name of the
1085 # blocking directory in the error message.
1086 raise BlockingFileError("cannot create directory because there "
1087 "is a file in the way")
1089 return defer.succeed(node)
1090 d = node.get(path[0])
1091 def _maybe_create(f):
1093 return node.create_empty_directory(path[0])
1094 d.addErrback(_maybe_create)
1095 d.addCallback(self._get_or_create_directories, path[1:])
1098 def _check_replacement(self, node, name, replace):
1101 d = node.has_child(name)
1104 raise NoReplacementError("There was already a child by that "
1105 "name, and you asked me to not "
1111 def _mkdir(self, node, name):
1112 d = node.create_empty_directory(name)
1114 return newnode.get_uri()
1115 d.addCallback(_done)
1118 def _upload_file(self, node, contents, name, convergence):
1119 uploadable = FileHandle(contents, convergence=convergence)
1120 d = node.add_file(name, uploadable)
1121 def _done(filenode):
1122 log.msg("webish upload complete",
1123 facility="tahoe.webish", level=log.NOISY)
1124 return filenode.get_uri()
1125 d.addCallback(_done)
1128 def _upload_localfile(self, node, localfile, name, convergence):
1129 uploadable = FileName(localfile, convergence=convergence)
1130 d = node.add_file(name, uploadable)
1131 d.addCallback(lambda filenode: filenode.get_uri())
1134 def _attach_uri(self, parentnode, contents, name):
1135 newuri = contents.read().strip()
1136 d = parentnode.set_uri(name, newuri)
1139 d.addCallback(_done)
1142 def _upload_localdir(self, node, localdir, convergence):
1143 # build up a list of files to upload. TODO: for now, these files and
1144 # directories must have UTF-8 encoded filenames: anything else will
1145 # cause the upload to break.
1148 msg = "No files to upload! %s is empty" % localdir
1149 if not os.path.exists(localdir):
1150 msg = "%s doesn't exist!" % localdir
1151 raise NoLocalDirectoryError(msg)
1152 for root, dirs, files in os.walk(localdir):
1153 if root == localdir:
1156 relative_root = root[len(localdir)+1:]
1157 path = tuple(relative_root.split(os.sep))
1159 this_dir = path + (d,)
1160 this_dir = tuple([p.decode("utf-8") for p in this_dir])
1161 all_dirs.append(this_dir)
1163 this_file = path + (f,)
1164 this_file = tuple([p.decode("utf-8") for p in this_file])
1165 all_files.append(this_file)
1166 d = defer.succeed(msg)
1167 for dir in all_dirs:
1169 d.addCallback(self._makedir, node, dir)
1171 d.addCallback(self._upload_one_file, node, localdir, f, convergence=convergence)
1174 def _makedir(self, res, node, dir):
1175 d = defer.succeed(None)
1176 # get the parent. As long as os.walk gives us parents before
1177 # children, this ought to work
1178 d.addCallback(lambda res: node.get_child_at_path(dir[:-1]))
1179 # then create the child directory
1180 d.addCallback(lambda parent: parent.create_empty_directory(dir[-1]))
1183 def _upload_one_file(self, res, node, localdir, f, convergence):
1184 # get the parent. We can be sure this exists because we already
1185 # went through and created all the directories we require.
1186 localfile = os.path.join(localdir, *f)
1187 d = node.get_child_at_path(f[:-1])
1188 d.addCallback(self._upload_localfile, localfile, f[-1], convergence=convergence)
1192 class Manifest(rend.Page):
1193 docFactory = getxmlfile("manifest.xhtml")
1194 def __init__(self, dirnode, dirpath):
1195 self._dirnode = dirnode
1196 self._dirpath = dirpath
1198 def dirpath_as_string(self):
1199 return "/" + "/".join(self._dirpath)
1201 def render_title(self, ctx):
1202 return T.title["Manifest of %s" % self.dirpath_as_string()]
1204 def render_header(self, ctx):
1205 return T.p["Manifest of %s" % self.dirpath_as_string()]
1207 def data_items(self, ctx, data):
1208 return self._dirnode.build_manifest()
1210 def render_row(self, ctx, refresh_cap):
1211 ctx.fillSlots("refresh_capability", refresh_cap)
1214 class DeepSize(rend.Page):
1216 def __init__(self, dirnode, dirpath):
1217 self._dirnode = dirnode
1218 self._dirpath = dirpath
1220 def renderHTTP(self, ctx):
1221 inevow.IRequest(ctx).setHeader("content-type", "text/plain")
1222 d = self._dirnode.build_manifest()
1223 def _measure_size(manifest):
1225 for verifiercap in manifest:
1226 u = from_string_verifier(verifiercap)
1227 if isinstance(u, CHKFileVerifierURI):
1230 d.addCallback(_measure_size)
1233 class DeepStats(rend.Page):
1235 def __init__(self, dirnode, dirpath):
1236 self._dirnode = dirnode
1237 self._dirpath = dirpath
1239 def renderHTTP(self, ctx):
1240 inevow.IRequest(ctx).setHeader("content-type", "text/plain")
1241 d = self._dirnode.deep_stats()
1242 d.addCallback(simplejson.dumps, indent=1)
1246 implements(inevow.IResource)
1247 def renderHTTP(self, ctx):
1248 req = inevow.IRequest(ctx)
1249 req.setResponseCode(http.BAD_REQUEST)
1250 req.setHeader("content-type", "text/plain")
1253 def child_error(text):
1258 class VDrive(rend.Page):
1260 def __init__(self, node, name):
1264 def get_child_at_path(self, path):
1266 return self.node.get_child_at_path(path)
1267 return defer.succeed(self.node)
1269 def locateChild(self, ctx, segments):
1270 req = inevow.IRequest(ctx)
1272 path = tuple([seg.decode("utf-8") for seg in segments])
1274 t = get_arg(req, "t", "")
1275 localfile = get_arg(req, "localfile", None)
1276 if localfile is not None:
1277 if localfile != os.path.abspath(localfile):
1278 return NeedAbsolutePathError(), ()
1279 localdir = get_arg(req, "localdir", None)
1280 if localdir is not None:
1281 if localdir != os.path.abspath(localdir):
1282 return NeedAbsolutePathError(), ()
1283 if localfile or localdir:
1284 if not ILocalAccess(ctx).local_access_is_allowed():
1285 return LocalAccessDisabledError(), ()
1286 if req.getHost().host != LOCALHOST:
1287 return NeedLocalhostError(), ()
1288 # TODO: think about clobbering/revealing config files and node secrets
1290 replace = boolean_of_arg(get_arg(req, "replace", "true"))
1293 # the node must exist, and our operation will be performed on the
1295 d = self.get_child_at_path(path)
1296 def file_or_dir(node):
1297 if (IFileNode.providedBy(node)
1298 or IMutableFileNode.providedBy(node)):
1299 filename = "unknown"
1302 filename = get_arg(req, "filename", filename)
1305 # write contents to a local file
1306 return LocalFileDownloader(node, localfile), ()
1307 # send contents as the result
1308 return FileDownloader(node, filename), ()
1310 # send contents as the result
1311 return FileDownloader(node, filename), ()
1313 return FileJSONMetadata(node), ()
1315 return FileURI(node), ()
1316 elif t == "readonly-uri":
1317 return FileReadOnlyURI(node), ()
1319 return child_error("bad t=%s" % t)
1320 elif IDirectoryNode.providedBy(node):
1323 # recursive download to a local directory
1324 return LocalDirectoryDownloader(node, localdir), ()
1325 return child_error("t=download requires localdir=")
1327 # send an HTML representation of the directory
1328 return Directory(self.name, node, path), ()
1330 return DirectoryJSONMetadata(node), ()
1332 return DirectoryURI(node), ()
1333 elif t == "readonly-uri":
1334 return DirectoryReadonlyURI(node), ()
1335 elif t == "manifest":
1336 return Manifest(node, path), ()
1337 elif t == "deep-size":
1338 return DeepSize(node, path), ()
1339 elif t == "deep-stats":
1340 return DeepStats(node, path), ()
1341 elif t == 'rename-form':
1342 return RenameForm(self.name, node, path), ()
1344 return child_error("bad t=%s" % t)
1346 return child_error("unknown node type")
1347 d.addCallback(file_or_dir)
1348 elif method == "POST":
1349 # the node must exist, and our operation will be performed on the
1351 d = self.get_child_at_path(path)
1352 def _got_POST(node):
1353 return POSTHandler(node, replace), ()
1354 d.addCallback(_got_POST)
1355 elif method == "DELETE":
1356 # the node must exist, and our operation will be performed on its
1358 assert path # you can't delete the root
1359 d = self.get_child_at_path(path[:-1])
1360 def _got_DELETE(node):
1361 return DELETEHandler(node, path[-1]), ()
1362 d.addCallback(_got_DELETE)
1363 elif method in ("PUT",):
1364 # the node may or may not exist, and our operation may involve
1365 # all the ancestors of the node.
1366 return PUTHandler(self.node, path, t, localfile, localdir, replace), ()
1368 return rend.NotFound
1371 class Root(rend.Page):
1374 docFactory = getxmlfile("welcome.xhtml")
1376 def locateChild(self, ctx, segments):
1377 client = IClient(ctx)
1378 req = inevow.IRequest(ctx)
1381 return rend.Page.locateChild(self, ctx, segments)
1383 if segments[0] == "file":
1384 if len(segments) < 2:
1385 return rend.Page.locateChild(self, ctx, segments)
1386 filecap = segments[1]
1387 node = client.create_node_from_uri(filecap)
1389 return FileDownloader(node, name), ()
1391 if segments[0] != "uri":
1392 return rend.Page.locateChild(self, ctx, segments)
1394 segments = list(segments)
1395 while segments and not segments[-1]:
1399 segments = tuple(segments)
1401 if len(segments) == 1 or segments[1] == '':
1402 uri = get_arg(req, "uri", None)
1404 there = url.URL.fromContext(ctx)
1405 there = there.clear("uri")
1406 there = there.child("uri").child(uri)
1409 if len(segments) == 1:
1411 if req.method == "PUT":
1412 # either "PUT /uri" to create an unlinked file, or
1413 # "PUT /uri?t=mkdir" to create an unlinked directory
1414 t = get_arg(req, "t", "").strip()
1416 mutable = bool(get_arg(req, "mutable", "").strip())
1418 return unlinked.UnlinkedPUTSSKUploader(), ()
1420 return unlinked.UnlinkedPUTCHKUploader(), ()
1422 return unlinked.UnlinkedPUTCreateDirectory(), ()
1423 errmsg = "/uri only accepts PUT and PUT?t=mkdir"
1424 return WebError(http.BAD_REQUEST, errmsg), ()
1426 elif req.method == "POST":
1427 # "POST /uri?t=upload&file=newfile" to upload an
1428 # unlinked file or "POST /uri?t=mkdir" to create a
1430 t = get_arg(req, "t", "").strip()
1431 if t in ("", "upload"):
1432 mutable = bool(get_arg(req, "mutable", "").strip())
1434 return unlinked.UnlinkedPOSTSSKUploader(), ()
1436 return unlinked.UnlinkedPOSTCHKUploader(client, req), ()
1438 return unlinked.UnlinkedPOSTCreateDirectory(), ()
1439 errmsg = "/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, and POST?t=mkdir"
1440 return WebError(http.BAD_REQUEST, errmsg), ()
1442 if len(segments) < 2:
1443 return rend.NotFound
1446 d = defer.maybeDeferred(client.create_node_from_uri, uri)
1447 d.addCallback(lambda node: VDrive(node, uri))
1448 d.addCallback(lambda vd: vd.locateChild(ctx, segments[2:]))
1449 def _trap_KeyError(f):
1451 return rend.FourOhFour(), ()
1452 d.addErrback(_trap_KeyError)
1455 child_webform_css = webform.defaultCSS
1456 child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
1458 child_provisioning = provisioning.ProvisioningTool()
1459 child_status = status.Status()
1460 child_helper_status = status.HelperStatus()
1461 child_statistics = status.Statistics()
1463 def data_version(self, ctx, data):
1464 return get_package_versions_string()
1465 def data_import_path(self, ctx, data):
1466 return str(allmydata)
1467 def data_my_nodeid(self, ctx, data):
1468 return idlib.nodeid_b2a(IClient(ctx).nodeid)
1470 def render_services(self, ctx, data):
1472 client = IClient(ctx)
1474 ss = client.getServiceNamed("storage")
1475 allocated_s = abbreviate_size(ss.allocated_size())
1476 allocated = "about %s allocated" % allocated_s
1477 sizelimit = "no size limit"
1478 if ss.sizelimit is not None:
1479 sizelimit = "size limit is %s" % abbreviate_size(ss.sizelimit)
1480 ul[T.li["Storage Server: %s, %s" % (allocated, sizelimit)]]
1482 ul[T.li["Not running storage server"]]
1485 h = client.getServiceNamed("helper")
1486 stats = h.get_stats()
1487 active_uploads = stats["chk_upload_helper.active_uploads"]
1488 ul[T.li["Helper: %d active uploads" % (active_uploads,)]]
1490 ul[T.li["Not running helper"]]
1494 def data_introducer_furl(self, ctx, data):
1495 return IClient(ctx).introducer_furl
1496 def data_connected_to_introducer(self, ctx, data):
1497 if IClient(ctx).connected_to_introducer():
1501 def data_helper_furl(self, ctx, data):
1503 uploader = IClient(ctx).getServiceNamed("uploader")
1506 furl, connected = uploader.get_helper_info()
1508 def data_connected_to_helper(self, ctx, data):
1510 uploader = IClient(ctx).getServiceNamed("uploader")
1512 return "no" # we don't even have an Uploader
1513 furl, connected = uploader.get_helper_info()
1518 def data_known_storage_servers(self, ctx, data):
1519 ic = IClient(ctx).introducer_client
1521 for c in ic.get_all_connectors().values()
1522 if c.service_name == "storage"]
1525 def data_connected_storage_servers(self, ctx, data):
1526 ic = IClient(ctx).introducer_client
1527 return len(ic.get_all_connections_for("storage"))
1529 def data_services(self, ctx, data):
1530 ic = IClient(ctx).introducer_client
1531 c = [ (service_name, nodeid, rsc)
1532 for (nodeid, service_name), rsc
1533 in ic.get_all_connectors().items() ]
1537 def render_service_row(self, ctx, data):
1538 (service_name, nodeid, rsc) = data
1539 ctx.fillSlots("peerid", "%s %s" % (idlib.nodeid_b2a(nodeid),
1542 rhost = rsc.remote_host
1543 if nodeid == IClient(ctx).nodeid:
1544 rhost_s = "(loopback)"
1545 elif isinstance(rhost, address.IPv4Address):
1546 rhost_s = "%s:%d" % (rhost.host, rhost.port)
1548 rhost_s = str(rhost)
1549 connected = "Yes: to " + rhost_s
1550 since = rsc.last_connect_time
1553 since = rsc.last_loss_time
1555 TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
1556 ctx.fillSlots("connected", connected)
1557 ctx.fillSlots("since", time.strftime(TIME_FORMAT, time.localtime(since)))
1558 ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
1559 time.localtime(rsc.announcement_time)))
1560 ctx.fillSlots("version", rsc.version)
1561 ctx.fillSlots("service_name", rsc.service_name)
1565 def render_download_form(self, ctx, data):
1566 # this is a form where users can download files by URI
1567 form = T.form(action="uri", method="get",
1568 enctype="multipart/form-data")[
1570 T.legend(class_="freeform-form-label")["Download a file"],
1571 "URI to download: ",
1572 T.input(type="text", name="uri"), " ",
1573 "Filename to download as: ",
1574 T.input(type="text", name="filename"), " ",
1575 T.input(type="submit", value="Download!"),
1579 def render_view_form(self, ctx, data):
1580 # this is a form where users can download files by URI, or jump to a
1582 form = T.form(action="uri", method="get",
1583 enctype="multipart/form-data")[
1585 T.legend(class_="freeform-form-label")["View a file or directory"],
1587 T.input(type="text", name="uri"), " ",
1588 T.input(type="submit", value="View!"),
1592 def render_upload_form(self, ctx, data):
1593 # this is a form where users can upload unlinked files
1594 form = T.form(action="uri", method="post",
1595 enctype="multipart/form-data")[
1597 T.legend(class_="freeform-form-label")["Upload a file"],
1599 T.input(type="file", name="file", class_="freeform-input-file"),
1600 T.input(type="hidden", name="t", value="upload"),
1601 " Mutable?:", T.input(type="checkbox", name="mutable"),
1602 T.input(type="submit", value="Upload!"),
1606 def render_mkdir_form(self, ctx, data):
1607 # this is a form where users can create new directories
1608 form = T.form(action="uri", method="post",
1609 enctype="multipart/form-data")[
1611 T.legend(class_="freeform-form-label")["Create a directory"],
1612 T.input(type="hidden", name="t", value="mkdir"),
1613 T.input(type="hidden", name="redirect_to_result", value="true"),
1614 T.input(type="submit", value="Create Directory!"),
1620 implements(ILocalAccess)
1622 self.local_access = False
1623 def local_access_is_allowed(self):
1624 return self.local_access
1626 class WebishServer(service.MultiService):
1630 def __init__(self, webport, nodeurl_path=None):
1631 service.MultiService.__init__(self)
1632 self.webport = webport
1633 self.root = self.root_class()
1634 self.site = site = appserver.NevowSite(self.root)
1635 self.site.requestFactory = MyRequest
1636 self.allow_local = LocalAccess()
1637 self.site.remember(self.allow_local, ILocalAccess)
1638 s = strports.service(webport, site)
1639 s.setServiceParent(self)
1640 self.listener = s # stash it so the tests can query for the portnum
1641 self._started = defer.Deferred()
1643 self._started.addCallback(self._write_nodeurl_file, nodeurl_path)
1645 def allow_local_access(self, enable=True):
1646 self.allow_local.local_access = enable
1648 def startService(self):
1649 service.MultiService.startService(self)
1650 # to make various services available to render_* methods, we stash a
1651 # reference to the client on the NevowSite. This will be available by
1652 # adapting the 'context' argument to a special marker interface named
1654 self.site.remember(self.parent, IClient)
1655 # I thought you could do the same with an existing interface, but
1656 # apparently 'ISite' does not exist
1657 #self.site._client = self.parent
1658 self._started.callback(None)
1660 def _write_nodeurl_file(self, junk, nodeurl_path):
1661 # what is our webport?
1663 if isinstance(s, internet.TCPServer):
1664 base_url = "http://127.0.0.1:%d/" % s._port.getHost().port
1665 elif isinstance(s, internet.SSLServer):
1666 base_url = "https://127.0.0.1:%d/" % s._port.getHost().port
1670 f = open(nodeurl_path, 'wb')
1671 # this file is world-readable
1672 f.write(base_url + "\n")
1675 class IntroducerWebishServer(WebishServer):
1676 root_class = introweb.IntroducerRoot