]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/webish.py
webish: display timestamps
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / webish.py
1
2 import time, os.path
3 from twisted.application import service, strports, internet
4 from twisted.web import static, resource, server, html, http
5 from twisted.python import log
6 from twisted.internet import defer, address
7 from twisted.internet.interfaces import IConsumer
8 from nevow import inevow, rend, loaders, appserver, url, tags as T
9 from nevow.static import File as nevow_File # TODO: merge with static.File?
10 from allmydata.util import fileutil, idlib, observer
11 import simplejson
12 from allmydata.interfaces import IDownloadTarget, IDirectoryNode, IFileNode, \
13      IMutableFileNode
14 import allmydata # to display import path
15 from allmydata import download
16 from allmydata.upload import FileHandle, FileName
17 from allmydata import provisioning
18 from allmydata import get_package_versions_string
19 from zope.interface import implements, Interface
20 import urllib
21 from formless import webform
22
23 from nevow.util import resource_filename
24
25 def getxmlfile(name):
26     return loaders.xmlfile(resource_filename('allmydata.web', '%s' % name))
27
28 class IClient(Interface):
29     pass
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."""
34
35 def boolean_of_arg(arg):
36     assert arg.lower() in ("true", "t", "1", "false", "f", "0")
37     return arg.lower() in ("true", "t", "1")
38
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.
45     """
46     results = []
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)
51     if multiple:
52         return tuple(results)
53     if results:
54         return results[0]
55     return default
56
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
63
64 parse_qs = http.parse_qs
65 class MyRequest(appserver.NevowRequest):
66     fields = None
67     def requestReceived(self, command, path, version):
68         """Called by channel when all data has been received.
69
70         This method is not intended for users.
71         """
72         self.content.seek(0,0)
73         self.args = {}
74         self.stack = []
75
76         self.method, self.uri = command, path
77         self.clientproto = version
78         x = self.uri.split('?', 1)
79
80         if len(x) == 1:
81             self.path = self.uri
82         else:
83             self.path, argstring = x
84             self.args = parse_qs(argstring, 1)
85
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()
90
91         # Argument processing.
92
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.
102
103 ##      args = self.args
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))
110 ##          elif key == mfd:
111 ##              try:
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()
122 ##                      return
123 ##                  raise
124
125         self.process()
126
127 class Directory(rend.Page):
128     addSlash = True
129     docFactory = getxmlfile("directory.xhtml")
130
131     def __init__(self, rootname, dirnode, dirpath):
132         self._rootname = rootname
133         self._dirnode = dirnode
134         self._dirpath = dirpath
135
136     def dirpath_as_string(self):
137         return "/" + "/".join(self._dirpath)
138
139     def render_title(self, ctx, data):
140         return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
141
142     def render_header(self, ctx, data):
143         parent_directories = ("<%s>" % self._rootname,) + self._dirpath
144         num_dirs = len(parent_directories)
145
146         header = ["Directory '"]
147         for i,d in enumerate(parent_directories):
148             upness = num_dirs - i - 1
149             if upness:
150                 link = "/".join( ("..",) * upness )
151             else:
152                 link = "."
153             header.append(T.a(href=link)[d])
154             if upness != 0:
155                 header.append("/")
156         header.append("'")
157
158         if self._dirnode.is_readonly():
159             header.append(" (readonly)")
160         header.append(":")
161         return ctx.tag[header]
162
163     def render_welcome(self, ctx, data):
164         depth = len(self._dirpath) + 2
165         link = "/".join([".."] * depth)
166         return T.div[T.a(href=link)["Return to Welcome page"]]
167
168     def data_children(self, ctx, data):
169         d = self._dirnode.list()
170         d.addCallback(lambda dict: sorted(dict.items()))
171         return d
172
173     def render_row(self, ctx, data):
174         name, (target, metadata) = data
175
176         if self._dirnode.is_readonly():
177             delete = "-"
178             rename = "-"
179         else:
180             # this creates a button which will cause our child__delete method
181             # to be invoked, which deletes the file and then redirects the
182             # browser back to this directory
183             delete = T.form(action=url.here, method="post")[
184                 T.input(type='hidden', name='t', value='delete'),
185                 T.input(type='hidden', name='name', value=name),
186                 T.input(type='hidden', name='when_done', value=url.here),
187                 T.input(type='submit', value='del', name="del"),
188                 ]
189
190             rename = T.form(action=url.here, method="get")[
191                 T.input(type='hidden', name='t', value='rename-form'),
192                 T.input(type='hidden', name='name', value=name),
193                 T.input(type='hidden', name='when_done', value=url.here),
194                 T.input(type='submit', value='rename', name="rename"),
195                 ]
196
197         ctx.fillSlots("delete", delete)
198         ctx.fillSlots("rename", rename)
199         check = T.form(action=url.here, method="post")[
200             T.input(type='hidden', name='t', value='check'),
201             T.input(type='hidden', name='name', value=name),
202             T.input(type='hidden', name='when_done', value=url.here),
203             T.input(type='submit', value='check', name="check"),
204             ]
205         ctx.fillSlots("overwrite", self.build_overwrite(ctx, (name, target)))
206         ctx.fillSlots("check", check)
207
208         times = []
209         TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
210         if "ctime" in metadata:
211             ctime = time.strftime(TIME_FORMAT,
212                                   time.localtime(metadata["ctime"]))
213             times.append("c: " + ctime)
214         if "mtime" in metadata:
215             mtime = time.strftime(TIME_FORMAT,
216                                   time.localtime(metadata["mtime"]))
217             if times:
218                 times.append(T.br())
219                 times.append("m: " + mtime)
220         ctx.fillSlots("times", times)
221
222
223         # build the base of the uri_link link url
224         uri_link = "/uri/" + urllib.quote(target.get_uri())
225
226         assert (IFileNode.providedBy(target)
227                 or IDirectoryNode.providedBy(target)
228                 or IMutableFileNode.providedBy(target)), target
229
230         if IMutableFileNode.providedBy(target):
231             # file
232
233             # add the filename to the uri_link url
234             uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
235
236             # to prevent javascript in displayed .html files from stealing a
237             # secret directory URI from the URL, send the browser to a URI-based
238             # page that doesn't know about the directory at all
239             #dlurl = urllib.quote(name)
240             dlurl = uri_link
241
242             ctx.fillSlots("filename",
243                           T.a(href=dlurl)[html.escape(name)])
244             ctx.fillSlots("type", "SSK")
245
246             ctx.fillSlots("size", "?")
247
248             text_plain_link = uri_link + "?filename=foo.txt"
249             text_plain_tag = T.a(href=text_plain_link)["text/plain"]
250
251         elif IFileNode.providedBy(target):
252             # file
253
254             # add the filename to the uri_link url
255             uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
256
257             # to prevent javascript in displayed .html files from stealing a
258             # secret directory URI from the URL, send the browser to a URI-based
259             # page that doesn't know about the directory at all
260             #dlurl = urllib.quote(name)
261             dlurl = uri_link
262
263             ctx.fillSlots("filename",
264                           T.a(href=dlurl)[html.escape(name)])
265             ctx.fillSlots("type", "FILE")
266
267             ctx.fillSlots("size", target.get_size())
268
269             text_plain_link = uri_link + "?filename=foo.txt"
270             text_plain_tag = T.a(href=text_plain_link)["text/plain"]
271
272         elif IDirectoryNode.providedBy(target):
273             # directory
274             ctx.fillSlots("filename",
275                           T.a(href=uri_link)[html.escape(name)])
276             if target.is_readonly():
277                 dirtype = "DIR-RO"
278             else:
279                 dirtype = "DIR"
280             ctx.fillSlots("type", dirtype)
281             ctx.fillSlots("size", "-")
282             text_plain_tag = None
283
284         childdata = [T.a(href="%s?t=json" % name)["JSON"], ", ",
285                      T.a(href="%s?t=uri" % name)["URI"], ", ",
286                      T.a(href="%s?t=readonly-uri" % name)["readonly-URI"],
287                      ]
288         if text_plain_tag:
289             childdata.extend([", ", text_plain_tag])
290
291         ctx.fillSlots("data", childdata)
292
293         try:
294             checker = IClient(ctx).getServiceNamed("checker")
295         except KeyError:
296             checker = None
297         if checker:
298             d = defer.maybeDeferred(checker.checker_results_for,
299                                     target.get_verifier())
300             def _got(checker_results):
301                 recent_results = reversed(checker_results[-5:])
302                 if IFileNode.providedBy(target):
303                     results = ("[" +
304                                ", ".join(["%d/%d" % (found, needed)
305                                           for (when,
306                                                (needed, total, found, sharemap))
307                                           in recent_results]) +
308                                "]")
309                 elif IDirectoryNode.providedBy(target):
310                     results = ("[" +
311                                "".join([{True:"+",False:"-"}[res]
312                                         for (when, res) in recent_results]) +
313                                "]")
314                 else:
315                     results = "%d results" % len(checker_results)
316                 return results
317             d.addCallback(_got)
318             results = d
319         else:
320             results = "--"
321         # TODO: include a link to see more results, including timestamps
322         # TODO: use a sparkline
323         ctx.fillSlots("checker_results", results)
324
325         return ctx.tag
326
327     def render_forms(self, ctx, data):
328         if self._dirnode.is_readonly():
329             return T.div["No upload forms: directory is read-only"]
330         mkdir = T.form(action=".", method="post",
331                        enctype="multipart/form-data")[
332             T.fieldset[
333             T.input(type="hidden", name="t", value="mkdir"),
334             T.input(type="hidden", name="when_done", value=url.here),
335             T.legend(class_="freeform-form-label")["Create a new directory"],
336             "New directory name: ",
337             T.input(type="text", name="name"), " ",
338             T.input(type="submit", value="Create"),
339             ]]
340
341         upload = T.form(action=".", method="post",
342                         enctype="multipart/form-data")[
343             T.fieldset[
344             T.input(type="hidden", name="t", value="upload"),
345             T.input(type="hidden", name="when_done", value=url.here),
346             T.legend(class_="freeform-form-label")["Upload a file to this directory"],
347             "Choose a file to upload: ",
348             T.input(type="file", name="file", class_="freeform-input-file"),
349             " ",
350             T.input(type="submit", value="Upload"),
351             " Mutable?:",
352             T.input(type="checkbox", name="mutable"),
353             ]]
354
355         mount = T.form(action=".", method="post",
356                         enctype="multipart/form-data")[
357             T.fieldset[
358             T.input(type="hidden", name="t", value="uri"),
359             T.input(type="hidden", name="when_done", value=url.here),
360             T.legend(class_="freeform-form-label")["Attach a file or directory"
361                                                    " (by URI) to this"
362                                                    " directory"],
363             "New child name: ",
364             T.input(type="text", name="name"), " ",
365             "URI of new child: ",
366             T.input(type="text", name="uri"), " ",
367             T.input(type="submit", value="Attach"),
368             ]]
369         return [T.div(class_="freeform-form")[mkdir],
370                 T.div(class_="freeform-form")[upload],
371                 T.div(class_="freeform-form")[mount],
372                 ]
373
374     def build_overwrite(self, ctx, data):
375         name, target = data
376         if IMutableFileNode.providedBy(target) and not target.is_readonly():
377             action="/uri/" + urllib.quote(target.get_uri())
378             overwrite = T.form(action=action, method="post",
379                                enctype="multipart/form-data")[
380                 T.fieldset[
381                 T.input(type="hidden", name="t", value="overwrite"),
382                 T.input(type='hidden', name='name', value=name),
383                 T.input(type='hidden', name='when_done', value=url.here),
384                 T.legend(class_="freeform-form-label")["Overwrite"],
385                 "Choose new file: ",
386                 T.input(type="file", name="file", class_="freeform-input-file"),
387                 " ",
388                 T.input(type="submit", value="Overwrite")
389                 ]]
390             return [T.div(class_="freeform-form")[overwrite],]
391         else:
392             return []
393
394     def render_results(self, ctx, data):
395         req = inevow.IRequest(ctx)
396         return get_arg(req, "results", "")
397
398 class WebDownloadTarget:
399     implements(IDownloadTarget, IConsumer)
400     def __init__(self, req, content_type, content_encoding, save_to_file):
401         self._req = req
402         self._content_type = content_type
403         self._content_encoding = content_encoding
404         self._opened = False
405         self._producer = None
406         self._save_to_file = save_to_file
407
408     def registerProducer(self, producer, streaming):
409         self._req.registerProducer(producer, streaming)
410     def unregisterProducer(self):
411         self._req.unregisterProducer()
412
413     def open(self, size):
414         self._opened = True
415         self._req.setHeader("content-type", self._content_type)
416         if self._content_encoding:
417             self._req.setHeader("content-encoding", self._content_encoding)
418         self._req.setHeader("content-length", str(size))
419         if self._save_to_file is not None:
420             # tell the browser to save the file rather display it
421             # TODO: quote save_to_file properly
422             self._req.setHeader("content-disposition",
423                                 'attachment; filename="%s"'
424                                 % self._save_to_file)
425
426     def write(self, data):
427         self._req.write(data)
428     def close(self):
429         self._req.finish()
430
431     def fail(self, why):
432         if self._opened:
433             # The content-type is already set, and the response code
434             # has already been sent, so we can't provide a clean error
435             # indication. We can emit text (which a browser might interpret
436             # as something else), and if we sent a Size header, they might
437             # notice that we've truncated the data. Keep the error message
438             # small to improve the chances of having our error response be
439             # shorter than the intended results.
440             #
441             # We don't have a lot of options, unfortunately.
442             self._req.write("problem during download\n")
443         else:
444             # We haven't written anything yet, so we can provide a sensible
445             # error message.
446             msg = str(why.type)
447             msg.replace("\n", "|")
448             self._req.setResponseCode(http.GONE, msg)
449             self._req.setHeader("content-type", "text/plain")
450             # TODO: HTML-formatted exception?
451             self._req.write(str(why))
452         self._req.finish()
453
454     def register_canceller(self, cb):
455         pass
456     def finish(self):
457         pass
458
459 class FileDownloader(resource.Resource):
460     def __init__(self, filenode, name):
461         assert (IFileNode.providedBy(filenode)
462                 or IMutableFileNode.providedBy(filenode))
463         self._filenode = filenode
464         self._name = name
465
466     def render(self, req):
467         gte = static.getTypeAndEncoding
468         type, encoding = gte(self._name,
469                              static.File.contentTypes,
470                              static.File.contentEncodings,
471                              defaultType="text/plain")
472         save_to_file = None
473         if get_arg(req, "save", False):
474             # TODO: make the API specification clear: should "save=" or
475             # "save=false" count?
476             save_to_file = self._name
477         wdt = WebDownloadTarget(req, type, encoding, save_to_file)
478         d = self._filenode.download(wdt)
479         # exceptions during download are handled by the WebDownloadTarget
480         d.addErrback(lambda why: None)
481         return server.NOT_DONE_YET
482
483 class BlockingFileError(Exception):
484     """We cannot auto-create a parent directory, because there is a file in
485     the way"""
486 class NoReplacementError(Exception):
487     """There was already a child by that name, and you asked me to not replace it"""
488 class NoLocalDirectoryError(Exception):
489     """The localdir= directory didn't exist"""
490
491 LOCALHOST = "127.0.0.1"
492
493 class NeedLocalhostError:
494     implements(inevow.IResource)
495
496     def renderHTTP(self, ctx):
497         req = inevow.IRequest(ctx)
498         req.setResponseCode(http.FORBIDDEN)
499         req.setHeader("content-type", "text/plain")
500         return "localfile= or localdir= requires a local connection"
501
502 class NeedAbsolutePathError:
503     implements(inevow.IResource)
504
505     def renderHTTP(self, ctx):
506         req = inevow.IRequest(ctx)
507         req.setResponseCode(http.FORBIDDEN)
508         req.setHeader("content-type", "text/plain")
509         return "localfile= or localdir= requires an absolute path"
510
511 class LocalAccessDisabledError:
512     implements(inevow.IResource)
513
514     def renderHTTP(self, ctx):
515         req = inevow.IRequest(ctx)
516         req.setResponseCode(http.FORBIDDEN)
517         req.setHeader("content-type", "text/plain")
518         return "local file access is disabled"
519
520 class WebError:
521     implements(inevow.IResource)
522     def __init__(self, response_code, errmsg):
523         self._response_code = response_code
524         self._errmsg = errmsg
525
526     def renderHTTP(self, ctx):
527         req = inevow.IRequest(ctx)
528         req.setResponseCode(self._response_code)
529         req.setHeader("content-type", "text/plain")
530         return self._errmsg
531
532
533 class LocalFileDownloader(resource.Resource):
534     def __init__(self, filenode, local_filename):
535         self._local_filename = local_filename
536         IFileNode(filenode)
537         self._filenode = filenode
538
539     def render(self, req):
540         target = download.FileName(self._local_filename)
541         d = self._filenode.download(target)
542         def _done(res):
543             req.write(self._filenode.get_uri())
544             req.finish()
545         d.addCallback(_done)
546         return server.NOT_DONE_YET
547
548
549 class FileJSONMetadata(rend.Page):
550     def __init__(self, filenode):
551         self._filenode = filenode
552
553     def renderHTTP(self, ctx):
554         req = inevow.IRequest(ctx)
555         req.setHeader("content-type", "text/plain")
556         return self.renderNode(self._filenode)
557
558     def renderNode(self, filenode):
559         file_uri = filenode.get_uri()
560         data = ("filenode",
561                 {'ro_uri': file_uri,
562                  'size': filenode.get_size(),
563                  })
564         return simplejson.dumps(data, indent=1)
565
566 class FileURI(FileJSONMetadata):
567     def renderNode(self, filenode):
568         file_uri = filenode.get_uri()
569         return file_uri
570
571 class FileReadOnlyURI(FileJSONMetadata):
572     def renderNode(self, filenode):
573         if filenode.is_readonly():
574             return filenode.get_uri()
575         else:
576             return filenode.get_readonly().get_uri()
577
578 class DirnodeWalkerMixin:
579     """Visit all nodes underneath (and including) the rootnode, one at a
580     time. For each one, call the visitor. The visitor will see the
581     IDirectoryNode before it sees any of the IFileNodes inside. If the
582     visitor returns a Deferred, I do not call the visitor again until it has
583     fired.
584     """
585
586 ##    def _walk_if_we_could_use_generators(self, rootnode, rootpath=()):
587 ##        # this is what we'd be doing if we didn't have the Deferreds and
588 ##        # thus could use generators
589 ##        yield rootpath, rootnode
590 ##        for childname, childnode in rootnode.list().items():
591 ##            childpath = rootpath + (childname,)
592 ##            if IFileNode.providedBy(childnode):
593 ##                yield childpath, childnode
594 ##            elif IDirectoryNode.providedBy(childnode):
595 ##                for res in self._walk_if_we_could_use_generators(childnode,
596 ##                                                                 childpath):
597 ##                    yield res
598
599     def walk(self, rootnode, visitor, rootpath=()):
600         d = rootnode.list()
601         def _listed(listing):
602             return listing.items()
603         d.addCallback(_listed)
604         d.addCallback(self._handle_items, visitor, rootpath)
605         return d
606
607     def _handle_items(self, items, visitor, rootpath):
608         if not items:
609             return
610         childname, (childnode, metadata) = items[0]
611         childpath = rootpath + (childname,)
612         d = defer.maybeDeferred(visitor, childpath, childnode, metadata)
613         if IDirectoryNode.providedBy(childnode):
614             d.addCallback(lambda res: self.walk(childnode, visitor, childpath))
615         d.addCallback(lambda res:
616                       self._handle_items(items[1:], visitor, rootpath))
617         return d
618
619 class LocalDirectoryDownloader(resource.Resource, DirnodeWalkerMixin):
620     def __init__(self, dirnode, localdir):
621         self._dirnode = dirnode
622         self._localdir = localdir
623
624     def _handle(self, path, node, metadata):
625         localfile = os.path.join(self._localdir, os.sep.join(path))
626         if IDirectoryNode.providedBy(node):
627             fileutil.make_dirs(localfile)
628         elif IFileNode.providedBy(node):
629             target = download.FileName(localfile)
630             return node.download(target)
631
632     def render(self, req):
633         d = self.walk(self._dirnode, self._handle)
634         def _done(res):
635             req.setHeader("content-type", "text/plain")
636             return "operation complete"
637         d.addCallback(_done)
638         return d
639
640 class DirectoryJSONMetadata(rend.Page):
641     def __init__(self, dirnode):
642         self._dirnode = dirnode
643
644     def renderHTTP(self, ctx):
645         req = inevow.IRequest(ctx)
646         req.setHeader("content-type", "text/plain")
647         return self.renderNode(self._dirnode)
648
649     def renderNode(self, node):
650         d = node.list()
651         def _got(children):
652             kids = {}
653             for name, (childnode, metadata) in children.iteritems():
654                 if IFileNode.providedBy(childnode):
655                     kiduri = childnode.get_uri()
656                     kiddata = ("filenode",
657                                {'ro_uri': kiduri,
658                                 'size': childnode.get_size(),
659                                 })
660                 else:
661                     assert IDirectoryNode.providedBy(childnode), (childnode, children,)
662                     kiddata = ("dirnode",
663                                {'ro_uri': childnode.get_readonly_uri(),
664                                 })
665                     if not childnode.is_readonly():
666                         kiddata[1]['rw_uri'] = childnode.get_uri()
667                 kids[name] = kiddata
668             contents = { 'children': kids,
669                          'ro_uri': node.get_readonly_uri(),
670                          }
671             if not node.is_readonly():
672                 contents['rw_uri'] = node.get_uri()
673             data = ("dirnode", contents)
674             return simplejson.dumps(data, indent=1)
675         d.addCallback(_got)
676         return d
677
678 class DirectoryURI(DirectoryJSONMetadata):
679     def renderNode(self, node):
680         return node.get_uri()
681
682 class DirectoryReadonlyURI(DirectoryJSONMetadata):
683     def renderNode(self, node):
684         return node.get_readonly_uri()
685
686 class RenameForm(rend.Page):
687     addSlash = True
688     docFactory = getxmlfile("rename-form.xhtml")
689
690     def __init__(self, rootname, dirnode, dirpath):
691         self._rootname = rootname
692         self._dirnode = dirnode
693         self._dirpath = dirpath
694
695     def dirpath_as_string(self):
696         return "/" + "/".join(self._dirpath)
697
698     def render_title(self, ctx, data):
699         return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
700
701     def render_header(self, ctx, data):
702         parent_directories = ("<%s>" % self._rootname,) + self._dirpath
703         num_dirs = len(parent_directories)
704
705         header = [ "Rename in directory '",
706                    "<%s>/" % self._rootname,
707                    "/".join(self._dirpath),
708                    "':", ]
709
710         if self._dirnode.is_readonly():
711             header.append(" (readonly)")
712         return ctx.tag[header]
713
714     def render_when_done(self, ctx, data):
715         return T.input(type="hidden", name="when_done", value=url.here)
716
717     def render_get_name(self, ctx, data):
718         req = inevow.IRequest(ctx)
719         name = get_arg(req, "name", "")
720         ctx.tag.attributes['value'] = name
721         return ctx.tag
722
723 class POSTHandler(rend.Page):
724     def __init__(self, node, replace):
725         self._node = node
726         self._replace = replace
727
728     def _check_replacement(self, name):
729         if self._replace:
730             return defer.succeed(None)
731         d = self._node.has_child(name)
732         def _got(present):
733             if present:
734                 raise NoReplacementError("There was already a child by that "
735                                          "name, and you asked me to not "
736                                          "replace it.")
737             return None
738         d.addCallback(_got)
739         return d
740
741     def renderHTTP(self, ctx):
742         req = inevow.IRequest(ctx)
743
744         t = get_arg(req, "t")
745         assert t is not None
746
747         name = get_arg(req, "name", None)
748         if name and "/" in name:
749             req.setResponseCode(http.BAD_REQUEST)
750             req.setHeader("content-type", "text/plain")
751             return "name= may not contain a slash"
752         if name is not None:
753             name = name.strip()
754         # we allow the user to delete an empty-named file, but not to create
755         # them, since that's an easy and confusing mistake to make
756
757         when_done = get_arg(req, "when_done", None)
758         if not boolean_of_arg(get_arg(req, "replace", "true")):
759             self._replace = False
760
761         if t == "mkdir":
762             if not name:
763                 raise RuntimeError("mkdir requires a name")
764             d = self._check_replacement(name)
765             d.addCallback(lambda res: self._node.create_empty_directory(name))
766             d.addCallback(lambda res: "directory created")
767         elif t == "uri":
768             if not name:
769                 raise RuntimeError("set-uri requires a name")
770             newuri = get_arg(req, "uri")
771             assert newuri is not None
772             d = self._check_replacement(name)
773             d.addCallback(lambda res: self._node.set_uri(name, newuri))
774             d.addCallback(lambda res: newuri)
775         elif t == "delete":
776             if name is None:
777                 # apparently an <input type="hidden" name="name" value="">
778                 # won't show up in the resulting encoded form.. the 'name'
779                 # field is completely missing. So to allow deletion of an
780                 # empty file, we have to pretend that None means ''. The only
781                 # downide of this is a slightly confusing error message if
782                 # someone does a POST without a name= field. For our own HTML
783                 # thisn't a big deal, because we create the 'delete' POST
784                 # buttons ourselves.
785                 name = ''
786             d = self._node.delete(name)
787             d.addCallback(lambda res: "thing deleted")
788         elif t == "rename":
789             from_name = 'from_name' in req.fields and req.fields["from_name"].value
790             if from_name is not None:
791                 from_name = from_name.strip()
792             to_name = 'to_name' in req.fields and req.fields["to_name"].value
793             if to_name is not None:
794                 to_name = to_name.strip()
795             if not from_name or not to_name:
796                 raise RuntimeError("rename requires from_name and to_name")
797             if not IDirectoryNode.providedBy(self._node):
798                 raise RuntimeError("rename must only be called on directories")
799             for k,v in [ ('from_name', from_name), ('to_name', to_name) ]:
800                 if v and "/" in v:
801                     req.setResponseCode(http.BAD_REQUEST)
802                     req.setHeader("content-type", "text/plain")
803                     return "%s= may not contain a slash" % (k,)
804             d = self._check_replacement(to_name)
805             d.addCallback(lambda res: self._node.get(from_name))
806             def add_dest(child):
807                 uri = child.get_uri()
808                 # now actually do the rename
809                 return self._node.set_uri(to_name, uri)
810             d.addCallback(add_dest)
811             def rm_src(junk):
812                 return self._node.delete(from_name)
813             d.addCallback(rm_src)
814             d.addCallback(lambda res: "thing renamed")
815
816         elif t == "upload":
817             if "mutable" in req.fields:
818                 contents = req.fields["file"]
819                 name = name or contents.filename
820                 if name is not None:
821                     name = name.strip()
822                 if not name:
823                     raise RuntimeError("upload-mutable requires a name")
824                 # SDMF: files are small, and we can only upload data.
825                 contents.file.seek(0)
826                 data = contents.file.read()
827                 #uploadable = FileHandle(contents.file)
828                 d = self._check_replacement(name)
829                 d.addCallback(lambda res: self._node.has_child(name))
830                 def _checked(present):
831                     if present:
832                         # modify the existing one instead of creating a new
833                         # one
834                         d2 = self._node.get(name)
835                         def _got_newnode(newnode):
836                             d3 = newnode.replace(data)
837                             d3.addCallback(lambda res: newnode.get_uri())
838                             return d3
839                         d2.addCallback(_got_newnode)
840                     else:
841                         d2 = IClient(ctx).create_mutable_file(data)
842                         def _uploaded(newnode):
843                             d1 = self._node.set_node(name, newnode)
844                             d1.addCallback(lambda res: newnode.get_uri())
845                             return d1
846                         d2.addCallback(_uploaded)
847                     return d2
848                 d.addCallback(_checked)
849             else:
850                 contents = req.fields["file"]
851                 name = name or contents.filename
852                 if name is not None:
853                     name = name.strip()
854                 if not name:
855                     raise RuntimeError("upload requires a name")
856                 uploadable = FileHandle(contents.file)
857                 d = self._check_replacement(name)
858                 d.addCallback(lambda res: self._node.add_file(name, uploadable))
859                 def _done(newnode):
860                     return newnode.get_uri()
861                 d.addCallback(_done)
862
863         elif t == "overwrite":
864             contents = req.fields["file"]
865             # SDMF: files are small, and we can only upload data.
866             contents.file.seek(0)
867             data = contents.file.read()
868             # TODO: 'name' handling needs review
869             d = defer.succeed(self._node)
870             def _got_child_overwrite(child_node):
871                 child_node.replace(data)
872                 return child_node.get_uri()
873             d.addCallback(_got_child_overwrite)
874
875         elif t == "check":
876             d = self._node.get(name)
877             def _got_child_check(child_node):
878                 d2 = child_node.check()
879                 def _done(res):
880                     log.msg("checked %s, results %s" % (child_node, res))
881                     return str(res)
882                 d2.addCallback(_done)
883                 return d2
884             d.addCallback(_got_child_check)
885         else:
886             print "BAD t=%s" % t
887             return "BAD t=%s" % t
888         if when_done:
889             d.addCallback(lambda res: url.URL.fromString(when_done))
890         def _check_replacement(f):
891             # TODO: make this more human-friendly: maybe send them to the
892             # when_done page but with an extra query-arg that will display
893             # the error message in a big box at the top of the page. The
894             # directory page that when_done= usually points to accepts a
895             # result= argument.. use that.
896             f.trap(NoReplacementError)
897             req.setResponseCode(http.CONFLICT)
898             req.setHeader("content-type", "text/plain")
899             return str(f.value)
900         d.addErrback(_check_replacement)
901         return d
902
903 class DELETEHandler(rend.Page):
904     def __init__(self, node, name):
905         self._node = node
906         self._name = name
907
908     def renderHTTP(self, ctx):
909         req = inevow.IRequest(ctx)
910         d = self._node.delete(self._name)
911         def _done(res):
912             # what should this return??
913             return "%s deleted" % self._name
914         d.addCallback(_done)
915         def _trap_missing(f):
916             f.trap(KeyError)
917             req.setResponseCode(http.NOT_FOUND)
918             req.setHeader("content-type", "text/plain")
919             return "no such child %s" % self._name
920         d.addErrback(_trap_missing)
921         return d
922
923 class PUTHandler(rend.Page):
924     def __init__(self, node, path, t, localfile, localdir, replace):
925         self._node = node
926         self._path = path
927         self._t = t
928         self._localfile = localfile
929         self._localdir = localdir
930         self._replace = replace
931
932     def renderHTTP(self, ctx):
933         req = inevow.IRequest(ctx)
934         t = self._t
935         localfile = self._localfile
936         localdir = self._localdir
937
938         if t == "upload" and not (localfile or localdir):
939             req.setResponseCode(http.BAD_REQUEST)
940             req.setHeader("content-type", "text/plain")
941             return "t=upload requires localfile= or localdir="
942
943         # we must traverse the path, creating new directories as necessary
944         d = self._get_or_create_directories(self._node, self._path[:-1])
945         name = self._path[-1]
946         d.addCallback(self._check_replacement, name, self._replace)
947         if t == "upload":
948             if localfile:
949                 d.addCallback(self._upload_localfile, localfile, name)
950             else:
951                 # localdir
952                 # take the last step
953                 d.addCallback(self._get_or_create_directories, self._path[-1:])
954                 d.addCallback(self._upload_localdir, localdir)
955         elif t == "uri":
956             d.addCallback(self._attach_uri, req.content, name)
957         elif t == "mkdir":
958             d.addCallback(self._mkdir, name)
959         else:
960             d.addCallback(self._upload_file, req.content, name)
961
962         def _transform_error(f):
963             errors = {BlockingFileError: http.BAD_REQUEST,
964                       NoReplacementError: http.CONFLICT,
965                       NoLocalDirectoryError: http.BAD_REQUEST,
966                       }
967             for k,v in errors.items():
968                 if f.check(k):
969                     req.setResponseCode(v)
970                     req.setHeader("content-type", "text/plain")
971                     return str(f.value)
972             return f
973         d.addErrback(_transform_error)
974         return d
975
976     def _get_or_create_directories(self, node, path):
977         if not IDirectoryNode.providedBy(node):
978             # unfortunately it is too late to provide the name of the
979             # blocking directory in the error message.
980             raise BlockingFileError("cannot create directory because there "
981                                     "is a file in the way")
982         if not path:
983             return defer.succeed(node)
984         d = node.get(path[0])
985         def _maybe_create(f):
986             f.trap(KeyError)
987             return node.create_empty_directory(path[0])
988         d.addErrback(_maybe_create)
989         d.addCallback(self._get_or_create_directories, path[1:])
990         return d
991
992     def _check_replacement(self, node, name, replace):
993         if replace:
994             return node
995         d = node.has_child(name)
996         def _got(present):
997             if present:
998                 raise NoReplacementError("There was already a child by that "
999                                          "name, and you asked me to not "
1000                                          "replace it.")
1001             return node
1002         d.addCallback(_got)
1003         return d
1004
1005     def _mkdir(self, node, name):
1006         d = node.create_empty_directory(name)
1007         def _done(newnode):
1008             return newnode.get_uri()
1009         d.addCallback(_done)
1010         return d
1011
1012     def _upload_file(self, node, contents, name):
1013         uploadable = FileHandle(contents)
1014         d = node.add_file(name, uploadable)
1015         def _done(filenode):
1016             log.msg("webish upload complete")
1017             return filenode.get_uri()
1018         d.addCallback(_done)
1019         return d
1020
1021     def _upload_localfile(self, node, localfile, name):
1022         uploadable = FileName(localfile)
1023         d = node.add_file(name, uploadable)
1024         d.addCallback(lambda filenode: filenode.get_uri())
1025         return d
1026
1027     def _attach_uri(self, parentnode, contents, name):
1028         newuri = contents.read().strip()
1029         d = parentnode.set_uri(name, newuri)
1030         def _done(res):
1031             return newuri
1032         d.addCallback(_done)
1033         return d
1034
1035     def _upload_localdir(self, node, localdir):
1036         # build up a list of files to upload
1037         all_files = []
1038         all_dirs = []
1039         msg = "No files to upload! %s is empty" % localdir
1040         if not os.path.exists(localdir):
1041             msg = "%s doesn't exist!" % localdir
1042             raise NoLocalDirectoryError(msg)
1043         for root, dirs, files in os.walk(localdir):
1044             if root == localdir:
1045                 path = ()
1046             else:
1047                 relative_root = root[len(localdir)+1:]
1048                 path = tuple(relative_root.split(os.sep))
1049             for d in dirs:
1050                 all_dirs.append(path + (d,))
1051             for f in files:
1052                 all_files.append(path + (f,))
1053         d = defer.succeed(msg)
1054         for dir in all_dirs:
1055             if dir:
1056                 d.addCallback(self._makedir, node, dir)
1057         for f in all_files:
1058             d.addCallback(self._upload_one_file, node, localdir, f)
1059         return d
1060
1061     def _makedir(self, res, node, dir):
1062         d = defer.succeed(None)
1063         # get the parent. As long as os.walk gives us parents before
1064         # children, this ought to work
1065         d.addCallback(lambda res: node.get_child_at_path(dir[:-1]))
1066         # then create the child directory
1067         d.addCallback(lambda parent: parent.create_empty_directory(dir[-1]))
1068         return d
1069
1070     def _upload_one_file(self, res, node, localdir, f):
1071         # get the parent. We can be sure this exists because we already
1072         # went through and created all the directories we require.
1073         localfile = os.path.join(localdir, *f)
1074         d = node.get_child_at_path(f[:-1])
1075         d.addCallback(self._upload_localfile, localfile, f[-1])
1076         return d
1077
1078
1079 class Manifest(rend.Page):
1080     docFactory = getxmlfile("manifest.xhtml")
1081     def __init__(self, dirnode, dirpath):
1082         self._dirnode = dirnode
1083         self._dirpath = dirpath
1084
1085     def dirpath_as_string(self):
1086         return "/" + "/".join(self._dirpath)
1087
1088     def render_title(self, ctx):
1089         return T.title["Manifest of %s" % self.dirpath_as_string()]
1090
1091     def render_header(self, ctx):
1092         return T.p["Manifest of %s" % self.dirpath_as_string()]
1093
1094     def data_items(self, ctx, data):
1095         return self._dirnode.build_manifest()
1096
1097     def render_row(self, ctx, refresh_cap):
1098         ctx.fillSlots("refresh_capability", refresh_cap)
1099         return ctx.tag
1100
1101 class ChildError:
1102     implements(inevow.IResource)
1103     def renderHTTP(self, ctx):
1104         req = inevow.IRequest(ctx)
1105         req.setResponseCode(http.BAD_REQUEST)
1106         req.setHeader("content-type", "text/plain")
1107         return self.text
1108
1109 def child_error(text):
1110     ce = ChildError()
1111     ce.text = text
1112     return ce, ()
1113
1114 class VDrive(rend.Page):
1115
1116     def __init__(self, node, name):
1117         self.node = node
1118         self.name = name
1119
1120     def get_child_at_path(self, path):
1121         if path:
1122             return self.node.get_child_at_path(path)
1123         return defer.succeed(self.node)
1124
1125     def locateChild(self, ctx, segments):
1126         req = inevow.IRequest(ctx)
1127         method = req.method
1128         path = segments
1129
1130         t = get_arg(req, "t", "")
1131         localfile = get_arg(req, "localfile", None)
1132         if localfile is not None:
1133             if localfile != os.path.abspath(localfile):
1134                 return NeedAbsolutePathError(), ()
1135         localdir = get_arg(req, "localdir", None)
1136         if localdir is not None:
1137             if localdir != os.path.abspath(localdir):
1138                 return NeedAbsolutePathError(), ()
1139         if localfile or localdir:
1140             if not ILocalAccess(ctx).local_access_is_allowed():
1141                 return LocalAccessDisabledError(), ()
1142             if req.getHost().host != LOCALHOST:
1143                 return NeedLocalhostError(), ()
1144         # TODO: think about clobbering/revealing config files and node secrets
1145
1146         replace = boolean_of_arg(get_arg(req, "replace", "true"))
1147
1148         if method == "GET":
1149             # the node must exist, and our operation will be performed on the
1150             # node itself.
1151             d = self.get_child_at_path(path)
1152             def file_or_dir(node):
1153                 if (IFileNode.providedBy(node)
1154                     or IMutableFileNode.providedBy(node)):
1155                     filename = "unknown"
1156                     if path:
1157                         filename = path[-1]
1158                     filename = get_arg(req, "filename", filename)
1159                     if t == "download":
1160                         if localfile:
1161                             # write contents to a local file
1162                             return LocalFileDownloader(node, localfile), ()
1163                         # send contents as the result
1164                         return FileDownloader(node, filename), ()
1165                     elif t == "":
1166                         # send contents as the result
1167                         return FileDownloader(node, filename), ()
1168                     elif t == "json":
1169                         return FileJSONMetadata(node), ()
1170                     elif t == "uri":
1171                         return FileURI(node), ()
1172                     elif t == "readonly-uri":
1173                         return FileReadOnlyURI(node), ()
1174                     else:
1175                         return child_error("bad t=%s" % t)
1176                 elif IDirectoryNode.providedBy(node):
1177                     if t == "download":
1178                         if localdir:
1179                             # recursive download to a local directory
1180                             return LocalDirectoryDownloader(node, localdir), ()
1181                         return child_error("t=download requires localdir=")
1182                     elif t == "":
1183                         # send an HTML representation of the directory
1184                         return Directory(self.name, node, path), ()
1185                     elif t == "json":
1186                         return DirectoryJSONMetadata(node), ()
1187                     elif t == "uri":
1188                         return DirectoryURI(node), ()
1189                     elif t == "readonly-uri":
1190                         return DirectoryReadonlyURI(node), ()
1191                     elif t == "manifest":
1192                         return Manifest(node, path), ()
1193                     elif t == 'rename-form':
1194                         return RenameForm(self.name, node, path), ()
1195                     else:
1196                         return child_error("bad t=%s" % t)
1197                 else:
1198                     return child_error("unknown node type")
1199             d.addCallback(file_or_dir)
1200         elif method == "POST":
1201             # the node must exist, and our operation will be performed on the
1202             # node itself.
1203             d = self.get_child_at_path(path)
1204             def _got_POST(node):
1205                 return POSTHandler(node, replace), ()
1206             d.addCallback(_got_POST)
1207         elif method == "DELETE":
1208             # the node must exist, and our operation will be performed on its
1209             # parent node.
1210             assert path # you can't delete the root
1211             d = self.get_child_at_path(path[:-1])
1212             def _got_DELETE(node):
1213                 return DELETEHandler(node, path[-1]), ()
1214             d.addCallback(_got_DELETE)
1215         elif method in ("PUT",):
1216             # the node may or may not exist, and our operation may involve
1217             # all the ancestors of the node.
1218             return PUTHandler(self.node, path, t, localfile, localdir, replace), ()
1219         else:
1220             return rend.NotFound
1221         return d
1222
1223 class UnlinkedPUTCHKUploader(rend.Page):
1224     def renderHTTP(self, ctx):
1225         req = inevow.IRequest(ctx)
1226         assert req.method == "PUT"
1227         # "PUT /uri", to create an unlinked file. This is like PUT but
1228         # without the associated set_uri.
1229
1230         uploadable = FileHandle(req.content)
1231         d = IClient(ctx).upload(uploadable)
1232         d.addCallback(lambda results: results.uri)
1233         # that fires with the URI of the new file
1234         return d
1235
1236 class UnlinkedPUTSSKUploader(rend.Page):
1237     def renderHTTP(self, ctx):
1238         req = inevow.IRequest(ctx)
1239         assert req.method == "PUT"
1240         # SDMF: files are small, and we can only upload data
1241         contents = req.content
1242         contents.seek(0)
1243         data = contents.read()
1244         d = IClient(ctx).create_mutable_file(data)
1245         d.addCallback(lambda n: n.get_uri())
1246         return d
1247
1248 class UnlinkedPUTCreateDirectory(rend.Page):
1249     def renderHTTP(self, ctx):
1250         req = inevow.IRequest(ctx)
1251         assert req.method == "PUT"
1252         # "PUT /uri?t=mkdir", to create an unlinked directory.
1253         d = IClient(ctx).create_empty_dirnode()
1254         d.addCallback(lambda dirnode: dirnode.get_uri())
1255         # XXX add redirect_to_result
1256         return d
1257
1258
1259 class UnlinkedPOSTCHKUploader(rend.Page):
1260     """'POST /uri', to create an unlinked file."""
1261     docFactory = getxmlfile("unlinked-upload.xhtml")
1262
1263     def __init__(self, client, req):
1264         rend.Page.__init__(self)
1265         # we start the upload now, and distribute notification of its
1266         # completion to render_ methods with an ObserverList
1267         assert req.method == "POST"
1268         self._done = observer.OneShotObserverList()
1269         fileobj = req.fields["file"].file
1270         uploadable = FileHandle(fileobj)
1271         d = client.upload(uploadable)
1272         d.addBoth(self._done.fire)
1273
1274     def renderHTTP(self, ctx):
1275         req = inevow.IRequest(ctx)
1276         when_done = get_arg(req, "when_done", None)
1277         if when_done:
1278             # if when_done= is provided, return a redirect instead of our
1279             # usual upload-results page
1280             d = self._done.when_fired()
1281             d.addCallback(lambda res: url.URL.fromString(when_done))
1282             return d
1283         return rend.Page.renderHTTP(self, ctx)
1284
1285     def upload_results(self):
1286         return self._done.when_fired()
1287
1288     def data_done(self, ctx, data):
1289         d = self.upload_results()
1290         d.addCallback(lambda res: "done!")
1291         return d
1292
1293     def data_uri(self, ctx, data):
1294         d = self.upload_results()
1295         d.addCallback(lambda res: res.uri)
1296         return d
1297
1298     def render_download_link(self, ctx, data):
1299         d = self.upload_results()
1300         d.addCallback(lambda res: T.a(href="/uri/" + urllib.quote(res.uri))
1301                       ["/uri/" + res.uri])
1302         return d
1303
1304     def render_sharemap(self, ctx, data):
1305         d = self.upload_results()
1306         d.addCallback(lambda res: res.sharemap)
1307         def _render(sharemap):
1308             if sharemap is None:
1309                 return "None"
1310             l = T.ul()
1311             for shnum in sorted(sharemap.keys()):
1312                 l[T.li["%d -> %s" % (shnum, sharemap[shnum])]]
1313             return l
1314         d.addCallback(_render)
1315         return d
1316
1317     def render_servermap(self, ctx, data):
1318         d = self.upload_results()
1319         d.addCallback(lambda res: res.servermap)
1320         def _render(servermap):
1321             if servermap is None:
1322                 return "None"
1323             l = T.ul()
1324             for peerid in sorted(servermap.keys()):
1325                 peerid_s = idlib.shortnodeid_b2a(peerid)
1326                 shares_s = ",".join([str(shnum) for shnum in servermap[peerid]])
1327                 l[T.li["[%s] got shares: %s" % (peerid_s, shares_s)]]
1328             return l
1329         d.addCallback(_render)
1330         return d
1331
1332     def data_file_size(self, ctx, data):
1333         d = self.upload_results()
1334         d.addCallback(lambda res: res.file_size)
1335         return d
1336
1337     def render_time(self, ctx, data):
1338         # 1.23s, 790ms, 132us
1339         if data is None:
1340             return ""
1341         s = float(data)
1342         if s >= 1.0:
1343             return "%.2fs" % s
1344         if s >= 0.01:
1345             return "%dms" % (1000*s)
1346         if s >= 0.001:
1347             return "%.1fms" % (1000*s)
1348         return "%dus" % (1000000*s)
1349
1350     def render_rate(self, ctx, data):
1351         # 21.8kBps, 554.4kBps 4.37MBps
1352         if data is None:
1353             return ""
1354         r = float(data)
1355         if r > 1000000:
1356             return "%1.2fMBps" % (r/1000000)
1357         if r > 1000:
1358             return "%.1fkBps" % (r/1000)
1359         return "%dBps" % r
1360
1361     def _get_time(self, name):
1362         d = self.upload_results()
1363         d.addCallback(lambda res: res.timings.get(name))
1364         return d
1365
1366     def data_time_total(self, ctx, data):
1367         return self._get_time("total")
1368
1369     def data_time_storage_index(self, ctx, data):
1370         return self._get_time("storage_index")
1371
1372     def data_time_contacting_helper(self, ctx, data):
1373         return self._get_time("contacting_helper")
1374
1375     def data_time_existence_check(self, ctx, data):
1376         return self._get_time("existence_check")
1377
1378     def data_time_cumulative_fetch(self, ctx, data):
1379         return self._get_time("cumulative_fetch")
1380
1381     def data_time_helper_total(self, ctx, data):
1382         return self._get_time("helper_total")
1383
1384     def data_time_peer_selection(self, ctx, data):
1385         return self._get_time("peer_selection")
1386
1387     def data_time_total_encode_and_push(self, ctx, data):
1388         return self._get_time("total_encode_and_push")
1389
1390     def data_time_cumulative_encoding(self, ctx, data):
1391         return self._get_time("cumulative_encoding")
1392
1393     def data_time_cumulative_sending(self, ctx, data):
1394         return self._get_time("cumulative_sending")
1395
1396     def data_time_hashes_and_close(self, ctx, data):
1397         return self._get_time("hashes_and_close")
1398
1399     def _get_rate(self, name):
1400         d = self.upload_results()
1401         def _convert(r):
1402             file_size = r.file_size
1403             time = r.timings.get(name)
1404             if time is None:
1405                 return None
1406             try:
1407                 return 1.0 * file_size / time
1408             except ZeroDivisionError:
1409                 return None
1410         d.addCallback(_convert)
1411         return d
1412
1413     def data_rate_total(self, ctx, data):
1414         return self._get_rate("total")
1415
1416     def data_rate_storage_index(self, ctx, data):
1417         return self._get_rate("storage_index")
1418
1419     def data_rate_encode(self, ctx, data):
1420         return self._get_rate("cumulative_encoding")
1421
1422     def data_rate_push(self, ctx, data):
1423         return self._get_rate("cumulative_sending")
1424
1425     def data_rate_encode_and_push(self, ctx, data):
1426         d = self.upload_results()
1427         def _convert(r):
1428             file_size = r.file_size
1429             if file_size is None:
1430                 return None
1431             time1 = r.timings.get("cumulative_encoding")
1432             if time1 is None:
1433                 return None
1434             time2 = r.timings.get("cumulative_sending")
1435             if time2 is None:
1436                 return None
1437             try:
1438                 return 1.0 * file_size / (time1+time2)
1439             except ZeroDivisionError:
1440                 return None
1441         d.addCallback(_convert)
1442         return d
1443
1444     def data_rate_ciphertext_fetch(self, ctx, data):
1445         d = self.upload_results()
1446         def _convert(r):
1447             fetch_size = r.ciphertext_fetched
1448             if fetch_size is None:
1449                 return None
1450             time = r.timings.get("cumulative_fetch")
1451             if time is None:
1452                 return None
1453             try:
1454                 return 1.0 * fetch_size / time
1455             except ZeroDivisionError:
1456                 return None
1457         d.addCallback(_convert)
1458         return d
1459
1460 class UnlinkedPOSTSSKUploader(rend.Page):
1461     def renderHTTP(self, ctx):
1462         req = inevow.IRequest(ctx)
1463         assert req.method == "POST"
1464
1465         # "POST /uri", to create an unlinked file.
1466         # SDMF: files are small, and we can only upload data
1467         contents = req.fields["file"]
1468         contents.file.seek(0)
1469         data = contents.file.read()
1470         d = IClient(ctx).create_mutable_file(data)
1471         d.addCallback(lambda n: n.get_uri())
1472         return d
1473
1474 class UnlinkedPOSTCreateDirectory(rend.Page):
1475     def renderHTTP(self, ctx):
1476         req = inevow.IRequest(ctx)
1477         assert req.method == "POST"
1478
1479         # "POST /uri?t=mkdir", to create an unlinked directory.
1480         d = IClient(ctx).create_empty_dirnode()
1481         redirect = get_arg(req, "redirect_to_result", "false")
1482         if boolean_of_arg(redirect):
1483             def _then_redir(res):
1484                 new_url = "uri/" + urllib.quote(res.get_uri())
1485                 req.setResponseCode(http.SEE_OTHER) # 303
1486                 req.setHeader('location', new_url)
1487                 req.finish()
1488                 return ''
1489             d.addCallback(_then_redir)
1490         else:
1491             d.addCallback(lambda dirnode: dirnode.get_uri())
1492         return d
1493
1494
1495 class Root(rend.Page):
1496
1497     addSlash = True
1498     docFactory = getxmlfile("welcome.xhtml")
1499
1500     def locateChild(self, ctx, segments):
1501         client = IClient(ctx)
1502         req = inevow.IRequest(ctx)
1503
1504         segments = list(segments) # XXX HELP I AM YUCKY!
1505         while segments and not segments[-1]:
1506             segments.pop()
1507         if not segments:
1508             segments.append('')
1509         segments = tuple(segments)
1510         if segments:
1511             if segments[0] == "uri":
1512                 if len(segments) == 1 or segments[1] == '':
1513                     uri = get_arg(req, "uri", None)
1514                     if uri is not None:
1515                         there = url.URL.fromContext(ctx)
1516                         there = there.clear("uri")
1517                         there = there.child("uri").child(uri)
1518                         return there, ()
1519                 if len(segments) == 1:
1520                     # /uri
1521                     if req.method == "PUT":
1522                         # either "PUT /uri" to create an unlinked file, or
1523                         # "PUT /uri?t=mkdir" to create an unlinked directory
1524                         t = get_arg(req, "t", "").strip()
1525                         if t == "":
1526                             mutable = bool(get_arg(req, "mutable", "").strip())
1527                             if mutable:
1528                                 return UnlinkedPUTSSKUploader(), ()
1529                             else:
1530                                 return UnlinkedPUTCHKUploader(), ()
1531                         if t == "mkdir":
1532                             return UnlinkedPUTCreateDirectory(), ()
1533                         errmsg = "/uri only accepts PUT and PUT?t=mkdir"
1534                         return WebError(http.BAD_REQUEST, errmsg), ()
1535
1536                     elif req.method == "POST":
1537                         # "POST /uri?t=upload&file=newfile" to upload an
1538                         # unlinked file or "POST /uri?t=mkdir" to create a
1539                         # new directory
1540                         t = get_arg(req, "t", "").strip()
1541                         if t in ("", "upload"):
1542                             mutable = bool(get_arg(req, "mutable", "").strip())
1543                             if mutable:
1544                                 return UnlinkedPOSTSSKUploader(), ()
1545                             else:
1546                                 return UnlinkedPOSTCHKUploader(client, req), ()
1547                         if t == "mkdir":
1548                             return UnlinkedPOSTCreateDirectory(), ()
1549                         errmsg = "/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, and POST?t=mkdir"
1550                         return WebError(http.BAD_REQUEST, errmsg), ()
1551                 if len(segments) < 2:
1552                     return rend.NotFound
1553                 uri = segments[1]
1554                 d = defer.maybeDeferred(client.create_node_from_uri, uri)
1555                 d.addCallback(lambda node: VDrive(node, uri))
1556                 d.addCallback(lambda vd: vd.locateChild(ctx, segments[2:]))
1557                 def _trap_KeyError(f):
1558                     f.trap(KeyError)
1559                     return rend.FourOhFour(), ()
1560                 d.addErrback(_trap_KeyError)
1561                 return d
1562             elif segments[0] == "xmlrpc":
1563                 raise NotImplementedError()
1564         return rend.Page.locateChild(self, ctx, segments)
1565
1566     child_webform_css = webform.defaultCSS
1567     child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
1568
1569     child_provisioning = provisioning.ProvisioningTool()
1570
1571     def data_version(self, ctx, data):
1572         return get_package_versions_string()
1573     def data_import_path(self, ctx, data):
1574         return str(allmydata)
1575     def data_my_nodeid(self, ctx, data):
1576         return idlib.nodeid_b2a(IClient(ctx).nodeid)
1577     def data_storage(self, ctx, data):
1578         client = IClient(ctx)
1579         try:
1580             ss = client.getServiceNamed("storage")
1581         except KeyError:
1582             return "Not running"
1583         allocated = ss.allocated_size()
1584         return "about %d bytes allocated" % allocated
1585
1586     def data_introducer_furl(self, ctx, data):
1587         return IClient(ctx).introducer_furl
1588     def data_connected_to_introducer(self, ctx, data):
1589         if IClient(ctx).connected_to_introducer():
1590             return "yes"
1591         return "no"
1592
1593     def data_helper_furl(self, ctx, data):
1594         try:
1595             uploader = IClient(ctx).getServiceNamed("uploader")
1596         except KeyError:
1597             return None
1598         furl, connected = uploader.get_helper_info()
1599         return furl
1600     def data_connected_to_helper(self, ctx, data):
1601         try:
1602             uploader = IClient(ctx).getServiceNamed("uploader")
1603         except KeyError:
1604             return "no" # we don't even have an Uploader
1605         furl, connected = uploader.get_helper_info()
1606         if connected:
1607             return "yes"
1608         return "no"
1609
1610     def data_known_storage_servers(self, ctx, data):
1611         ic = IClient(ctx).introducer_client
1612         servers = [c
1613                    for c in ic.get_all_connectors().values()
1614                    if c.service_name == "storage"]
1615         return len(servers)
1616
1617     def data_connected_storage_servers(self, ctx, data):
1618         ic = IClient(ctx).introducer_client
1619         return len(ic.get_all_connections_for("storage"))
1620
1621     def data_services(self, ctx, data):
1622         ic = IClient(ctx).introducer_client
1623         c = [ (service_name, nodeid, rsc)
1624               for (nodeid, service_name), rsc
1625               in ic.get_all_connectors().items() ]
1626         c.sort()
1627         return c
1628
1629     def render_service_row(self, ctx, data):
1630         (service_name, nodeid, rsc) = data
1631         ctx.fillSlots("peerid", "%s %s" % (idlib.nodeid_b2a(nodeid),
1632                                            rsc.nickname))
1633         if rsc.rref:
1634             rhost = rsc.remote_host
1635             if nodeid == IClient(ctx).nodeid:
1636                 rhost_s = "(loopback)"
1637             elif isinstance(rhost, address.IPv4Address):
1638                 rhost_s = "%s:%d" % (rhost.host, rhost.port)
1639             else:
1640                 rhost_s = str(rhost)
1641             connected = "Yes: to " + rhost_s
1642             since = rsc.last_connect_time
1643         else:
1644             connected = "No"
1645             since = rsc.last_loss_time
1646
1647         TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
1648         ctx.fillSlots("connected", connected)
1649         ctx.fillSlots("since", time.strftime(TIME_FORMAT, time.localtime(since)))
1650         ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
1651                                                  time.localtime(rsc.announcement_time)))
1652         ctx.fillSlots("version", rsc.version)
1653         ctx.fillSlots("service_name", rsc.service_name)
1654
1655         return ctx.tag
1656
1657     def render_download_form(self, ctx, data):
1658         # this is a form where users can download files by URI
1659         form = T.form(action="uri", method="get",
1660                       enctype="multipart/form-data")[
1661             T.fieldset[
1662             T.legend(class_="freeform-form-label")["Download a file"],
1663             "URI to download: ",
1664             T.input(type="text", name="uri"), " ",
1665             "Filename to download as: ",
1666             T.input(type="text", name="filename"), " ",
1667             T.input(type="submit", value="Download!"),
1668             ]]
1669         return T.div[form]
1670
1671     def render_view_form(self, ctx, data):
1672         # this is a form where users can download files by URI, or jump to a
1673         # named directory
1674         form = T.form(action="uri", method="get",
1675                       enctype="multipart/form-data")[
1676             T.fieldset[
1677             T.legend(class_="freeform-form-label")["View a file or directory"],
1678             "URI to view: ",
1679             T.input(type="text", name="uri"), " ",
1680             T.input(type="submit", value="View!"),
1681             ]]
1682         return T.div[form]
1683
1684     def render_upload_form(self, ctx, data):
1685         # this is a form where users can upload unlinked files
1686         form = T.form(action="uri", method="post",
1687                       enctype="multipart/form-data")[
1688             T.fieldset[
1689             T.legend(class_="freeform-form-label")["Upload a file"],
1690             "Choose a file: ",
1691             T.input(type="file", name="file", class_="freeform-input-file"),
1692             T.input(type="hidden", name="t", value="upload"),
1693             " Mutable?:", T.input(type="checkbox", name="mutable"),
1694             T.input(type="submit", value="Upload!"),
1695             ]]
1696         return T.div[form]
1697
1698     def render_mkdir_form(self, ctx, data):
1699         # this is a form where users can create new directories
1700         form = T.form(action="uri", method="post",
1701                       enctype="multipart/form-data")[
1702             T.fieldset[
1703             T.legend(class_="freeform-form-label")["Create a directory"],
1704             T.input(type="hidden", name="t", value="mkdir"),
1705             T.input(type="hidden", name="redirect_to_result", value="true"),
1706             T.input(type="submit", value="Create Directory!"),
1707             ]]
1708         return T.div[form]
1709
1710
1711 class LocalAccess:
1712     implements(ILocalAccess)
1713     def __init__(self):
1714         self.local_access = False
1715     def local_access_is_allowed(self):
1716         return self.local_access
1717
1718 class WebishServer(service.MultiService):
1719     name = "webish"
1720
1721     def __init__(self, webport, nodeurl_path=None):
1722         service.MultiService.__init__(self)
1723         self.webport = webport
1724         self.root = Root()
1725         self.site = site = appserver.NevowSite(self.root)
1726         self.site.requestFactory = MyRequest
1727         self.allow_local = LocalAccess()
1728         self.site.remember(self.allow_local, ILocalAccess)
1729         s = strports.service(webport, site)
1730         s.setServiceParent(self)
1731         self.listener = s # stash it so the tests can query for the portnum
1732         self._started = defer.Deferred()
1733         if nodeurl_path:
1734             self._started.addCallback(self._write_nodeurl_file, nodeurl_path)
1735
1736     def allow_local_access(self, enable=True):
1737         self.allow_local.local_access = enable
1738
1739     def startService(self):
1740         service.MultiService.startService(self)
1741         # to make various services available to render_* methods, we stash a
1742         # reference to the client on the NevowSite. This will be available by
1743         # adapting the 'context' argument to a special marker interface named
1744         # IClient.
1745         self.site.remember(self.parent, IClient)
1746         # I thought you could do the same with an existing interface, but
1747         # apparently 'ISite' does not exist
1748         #self.site._client = self.parent
1749         self._started.callback(None)
1750
1751     def _write_nodeurl_file(self, junk, nodeurl_path):
1752         # what is our webport?
1753         s = self.listener
1754         if isinstance(s, internet.TCPServer):
1755             base_url = "http://localhost:%d" % s._port.getHost().port
1756         elif isinstance(s, internet.SSLServer):
1757             base_url = "https://localhost:%d" % s._port.getHost().port
1758         else:
1759             base_url = None
1760         if base_url:
1761             f = open(nodeurl_path, 'wb')
1762             # this file is world-readable
1763             f.write(base_url + "\n")
1764             f.close()
1765