]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/webish.py
webish: add edge metadata to t=json output, including 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                                 'metadata': metadata,
660                                 })
661                 else:
662                     assert IDirectoryNode.providedBy(childnode), (childnode, children,)
663                     kiddata = ("dirnode",
664                                {'ro_uri': childnode.get_readonly_uri(),
665                                 'metadata': metadata,
666                                 })
667                     if not childnode.is_readonly():
668                         kiddata[1]['rw_uri'] = childnode.get_uri()
669                 kids[name] = kiddata
670             contents = { 'children': kids,
671                          'ro_uri': node.get_readonly_uri(),
672                          }
673             if not node.is_readonly():
674                 contents['rw_uri'] = node.get_uri()
675             data = ("dirnode", contents)
676             return simplejson.dumps(data, indent=1)
677         d.addCallback(_got)
678         return d
679
680 class DirectoryURI(DirectoryJSONMetadata):
681     def renderNode(self, node):
682         return node.get_uri()
683
684 class DirectoryReadonlyURI(DirectoryJSONMetadata):
685     def renderNode(self, node):
686         return node.get_readonly_uri()
687
688 class RenameForm(rend.Page):
689     addSlash = True
690     docFactory = getxmlfile("rename-form.xhtml")
691
692     def __init__(self, rootname, dirnode, dirpath):
693         self._rootname = rootname
694         self._dirnode = dirnode
695         self._dirpath = dirpath
696
697     def dirpath_as_string(self):
698         return "/" + "/".join(self._dirpath)
699
700     def render_title(self, ctx, data):
701         return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
702
703     def render_header(self, ctx, data):
704         parent_directories = ("<%s>" % self._rootname,) + self._dirpath
705         num_dirs = len(parent_directories)
706
707         header = [ "Rename in directory '",
708                    "<%s>/" % self._rootname,
709                    "/".join(self._dirpath),
710                    "':", ]
711
712         if self._dirnode.is_readonly():
713             header.append(" (readonly)")
714         return ctx.tag[header]
715
716     def render_when_done(self, ctx, data):
717         return T.input(type="hidden", name="when_done", value=url.here)
718
719     def render_get_name(self, ctx, data):
720         req = inevow.IRequest(ctx)
721         name = get_arg(req, "name", "")
722         ctx.tag.attributes['value'] = name
723         return ctx.tag
724
725 class POSTHandler(rend.Page):
726     def __init__(self, node, replace):
727         self._node = node
728         self._replace = replace
729
730     def _check_replacement(self, name):
731         if self._replace:
732             return defer.succeed(None)
733         d = self._node.has_child(name)
734         def _got(present):
735             if present:
736                 raise NoReplacementError("There was already a child by that "
737                                          "name, and you asked me to not "
738                                          "replace it.")
739             return None
740         d.addCallback(_got)
741         return d
742
743     def renderHTTP(self, ctx):
744         req = inevow.IRequest(ctx)
745
746         t = get_arg(req, "t")
747         assert t is not None
748
749         name = get_arg(req, "name", None)
750         if name and "/" in name:
751             req.setResponseCode(http.BAD_REQUEST)
752             req.setHeader("content-type", "text/plain")
753             return "name= may not contain a slash"
754         if name is not None:
755             name = name.strip()
756         # we allow the user to delete an empty-named file, but not to create
757         # them, since that's an easy and confusing mistake to make
758
759         when_done = get_arg(req, "when_done", None)
760         if not boolean_of_arg(get_arg(req, "replace", "true")):
761             self._replace = False
762
763         if t == "mkdir":
764             if not name:
765                 raise RuntimeError("mkdir requires a name")
766             d = self._check_replacement(name)
767             d.addCallback(lambda res: self._node.create_empty_directory(name))
768             d.addCallback(lambda res: "directory created")
769         elif t == "uri":
770             if not name:
771                 raise RuntimeError("set-uri requires a name")
772             newuri = get_arg(req, "uri")
773             assert newuri is not None
774             d = self._check_replacement(name)
775             d.addCallback(lambda res: self._node.set_uri(name, newuri))
776             d.addCallback(lambda res: newuri)
777         elif t == "delete":
778             if name is None:
779                 # apparently an <input type="hidden" name="name" value="">
780                 # won't show up in the resulting encoded form.. the 'name'
781                 # field is completely missing. So to allow deletion of an
782                 # empty file, we have to pretend that None means ''. The only
783                 # downide of this is a slightly confusing error message if
784                 # someone does a POST without a name= field. For our own HTML
785                 # thisn't a big deal, because we create the 'delete' POST
786                 # buttons ourselves.
787                 name = ''
788             d = self._node.delete(name)
789             d.addCallback(lambda res: "thing deleted")
790         elif t == "rename":
791             from_name = 'from_name' in req.fields and req.fields["from_name"].value
792             if from_name is not None:
793                 from_name = from_name.strip()
794             to_name = 'to_name' in req.fields and req.fields["to_name"].value
795             if to_name is not None:
796                 to_name = to_name.strip()
797             if not from_name or not to_name:
798                 raise RuntimeError("rename requires from_name and to_name")
799             if not IDirectoryNode.providedBy(self._node):
800                 raise RuntimeError("rename must only be called on directories")
801             for k,v in [ ('from_name', from_name), ('to_name', to_name) ]:
802                 if v and "/" in v:
803                     req.setResponseCode(http.BAD_REQUEST)
804                     req.setHeader("content-type", "text/plain")
805                     return "%s= may not contain a slash" % (k,)
806             d = self._check_replacement(to_name)
807             d.addCallback(lambda res: self._node.get(from_name))
808             def add_dest(child):
809                 uri = child.get_uri()
810                 # now actually do the rename
811                 return self._node.set_uri(to_name, uri)
812             d.addCallback(add_dest)
813             def rm_src(junk):
814                 return self._node.delete(from_name)
815             d.addCallback(rm_src)
816             d.addCallback(lambda res: "thing renamed")
817
818         elif t == "upload":
819             if "mutable" in req.fields:
820                 contents = req.fields["file"]
821                 name = name or contents.filename
822                 if name is not None:
823                     name = name.strip()
824                 if not name:
825                     raise RuntimeError("upload-mutable requires a name")
826                 # SDMF: files are small, and we can only upload data.
827                 contents.file.seek(0)
828                 data = contents.file.read()
829                 #uploadable = FileHandle(contents.file)
830                 d = self._check_replacement(name)
831                 d.addCallback(lambda res: self._node.has_child(name))
832                 def _checked(present):
833                     if present:
834                         # modify the existing one instead of creating a new
835                         # one
836                         d2 = self._node.get(name)
837                         def _got_newnode(newnode):
838                             d3 = newnode.replace(data)
839                             d3.addCallback(lambda res: newnode.get_uri())
840                             return d3
841                         d2.addCallback(_got_newnode)
842                     else:
843                         d2 = IClient(ctx).create_mutable_file(data)
844                         def _uploaded(newnode):
845                             d1 = self._node.set_node(name, newnode)
846                             d1.addCallback(lambda res: newnode.get_uri())
847                             return d1
848                         d2.addCallback(_uploaded)
849                     return d2
850                 d.addCallback(_checked)
851             else:
852                 contents = req.fields["file"]
853                 name = name or contents.filename
854                 if name is not None:
855                     name = name.strip()
856                 if not name:
857                     raise RuntimeError("upload requires a name")
858                 uploadable = FileHandle(contents.file)
859                 d = self._check_replacement(name)
860                 d.addCallback(lambda res: self._node.add_file(name, uploadable))
861                 def _done(newnode):
862                     return newnode.get_uri()
863                 d.addCallback(_done)
864
865         elif t == "overwrite":
866             contents = req.fields["file"]
867             # SDMF: files are small, and we can only upload data.
868             contents.file.seek(0)
869             data = contents.file.read()
870             # TODO: 'name' handling needs review
871             d = defer.succeed(self._node)
872             def _got_child_overwrite(child_node):
873                 child_node.replace(data)
874                 return child_node.get_uri()
875             d.addCallback(_got_child_overwrite)
876
877         elif t == "check":
878             d = self._node.get(name)
879             def _got_child_check(child_node):
880                 d2 = child_node.check()
881                 def _done(res):
882                     log.msg("checked %s, results %s" % (child_node, res))
883                     return str(res)
884                 d2.addCallback(_done)
885                 return d2
886             d.addCallback(_got_child_check)
887         else:
888             print "BAD t=%s" % t
889             return "BAD t=%s" % t
890         if when_done:
891             d.addCallback(lambda res: url.URL.fromString(when_done))
892         def _check_replacement(f):
893             # TODO: make this more human-friendly: maybe send them to the
894             # when_done page but with an extra query-arg that will display
895             # the error message in a big box at the top of the page. The
896             # directory page that when_done= usually points to accepts a
897             # result= argument.. use that.
898             f.trap(NoReplacementError)
899             req.setResponseCode(http.CONFLICT)
900             req.setHeader("content-type", "text/plain")
901             return str(f.value)
902         d.addErrback(_check_replacement)
903         return d
904
905 class DELETEHandler(rend.Page):
906     def __init__(self, node, name):
907         self._node = node
908         self._name = name
909
910     def renderHTTP(self, ctx):
911         req = inevow.IRequest(ctx)
912         d = self._node.delete(self._name)
913         def _done(res):
914             # what should this return??
915             return "%s deleted" % self._name
916         d.addCallback(_done)
917         def _trap_missing(f):
918             f.trap(KeyError)
919             req.setResponseCode(http.NOT_FOUND)
920             req.setHeader("content-type", "text/plain")
921             return "no such child %s" % self._name
922         d.addErrback(_trap_missing)
923         return d
924
925 class PUTHandler(rend.Page):
926     def __init__(self, node, path, t, localfile, localdir, replace):
927         self._node = node
928         self._path = path
929         self._t = t
930         self._localfile = localfile
931         self._localdir = localdir
932         self._replace = replace
933
934     def renderHTTP(self, ctx):
935         req = inevow.IRequest(ctx)
936         t = self._t
937         localfile = self._localfile
938         localdir = self._localdir
939
940         if t == "upload" and not (localfile or localdir):
941             req.setResponseCode(http.BAD_REQUEST)
942             req.setHeader("content-type", "text/plain")
943             return "t=upload requires localfile= or localdir="
944
945         # we must traverse the path, creating new directories as necessary
946         d = self._get_or_create_directories(self._node, self._path[:-1])
947         name = self._path[-1]
948         d.addCallback(self._check_replacement, name, self._replace)
949         if t == "upload":
950             if localfile:
951                 d.addCallback(self._upload_localfile, localfile, name)
952             else:
953                 # localdir
954                 # take the last step
955                 d.addCallback(self._get_or_create_directories, self._path[-1:])
956                 d.addCallback(self._upload_localdir, localdir)
957         elif t == "uri":
958             d.addCallback(self._attach_uri, req.content, name)
959         elif t == "mkdir":
960             d.addCallback(self._mkdir, name)
961         else:
962             d.addCallback(self._upload_file, req.content, name)
963
964         def _transform_error(f):
965             errors = {BlockingFileError: http.BAD_REQUEST,
966                       NoReplacementError: http.CONFLICT,
967                       NoLocalDirectoryError: http.BAD_REQUEST,
968                       }
969             for k,v in errors.items():
970                 if f.check(k):
971                     req.setResponseCode(v)
972                     req.setHeader("content-type", "text/plain")
973                     return str(f.value)
974             return f
975         d.addErrback(_transform_error)
976         return d
977
978     def _get_or_create_directories(self, node, path):
979         if not IDirectoryNode.providedBy(node):
980             # unfortunately it is too late to provide the name of the
981             # blocking directory in the error message.
982             raise BlockingFileError("cannot create directory because there "
983                                     "is a file in the way")
984         if not path:
985             return defer.succeed(node)
986         d = node.get(path[0])
987         def _maybe_create(f):
988             f.trap(KeyError)
989             return node.create_empty_directory(path[0])
990         d.addErrback(_maybe_create)
991         d.addCallback(self._get_or_create_directories, path[1:])
992         return d
993
994     def _check_replacement(self, node, name, replace):
995         if replace:
996             return node
997         d = node.has_child(name)
998         def _got(present):
999             if present:
1000                 raise NoReplacementError("There was already a child by that "
1001                                          "name, and you asked me to not "
1002                                          "replace it.")
1003             return node
1004         d.addCallback(_got)
1005         return d
1006
1007     def _mkdir(self, node, name):
1008         d = node.create_empty_directory(name)
1009         def _done(newnode):
1010             return newnode.get_uri()
1011         d.addCallback(_done)
1012         return d
1013
1014     def _upload_file(self, node, contents, name):
1015         uploadable = FileHandle(contents)
1016         d = node.add_file(name, uploadable)
1017         def _done(filenode):
1018             log.msg("webish upload complete")
1019             return filenode.get_uri()
1020         d.addCallback(_done)
1021         return d
1022
1023     def _upload_localfile(self, node, localfile, name):
1024         uploadable = FileName(localfile)
1025         d = node.add_file(name, uploadable)
1026         d.addCallback(lambda filenode: filenode.get_uri())
1027         return d
1028
1029     def _attach_uri(self, parentnode, contents, name):
1030         newuri = contents.read().strip()
1031         d = parentnode.set_uri(name, newuri)
1032         def _done(res):
1033             return newuri
1034         d.addCallback(_done)
1035         return d
1036
1037     def _upload_localdir(self, node, localdir):
1038         # build up a list of files to upload
1039         all_files = []
1040         all_dirs = []
1041         msg = "No files to upload! %s is empty" % localdir
1042         if not os.path.exists(localdir):
1043             msg = "%s doesn't exist!" % localdir
1044             raise NoLocalDirectoryError(msg)
1045         for root, dirs, files in os.walk(localdir):
1046             if root == localdir:
1047                 path = ()
1048             else:
1049                 relative_root = root[len(localdir)+1:]
1050                 path = tuple(relative_root.split(os.sep))
1051             for d in dirs:
1052                 all_dirs.append(path + (d,))
1053             for f in files:
1054                 all_files.append(path + (f,))
1055         d = defer.succeed(msg)
1056         for dir in all_dirs:
1057             if dir:
1058                 d.addCallback(self._makedir, node, dir)
1059         for f in all_files:
1060             d.addCallback(self._upload_one_file, node, localdir, f)
1061         return d
1062
1063     def _makedir(self, res, node, dir):
1064         d = defer.succeed(None)
1065         # get the parent. As long as os.walk gives us parents before
1066         # children, this ought to work
1067         d.addCallback(lambda res: node.get_child_at_path(dir[:-1]))
1068         # then create the child directory
1069         d.addCallback(lambda parent: parent.create_empty_directory(dir[-1]))
1070         return d
1071
1072     def _upload_one_file(self, res, node, localdir, f):
1073         # get the parent. We can be sure this exists because we already
1074         # went through and created all the directories we require.
1075         localfile = os.path.join(localdir, *f)
1076         d = node.get_child_at_path(f[:-1])
1077         d.addCallback(self._upload_localfile, localfile, f[-1])
1078         return d
1079
1080
1081 class Manifest(rend.Page):
1082     docFactory = getxmlfile("manifest.xhtml")
1083     def __init__(self, dirnode, dirpath):
1084         self._dirnode = dirnode
1085         self._dirpath = dirpath
1086
1087     def dirpath_as_string(self):
1088         return "/" + "/".join(self._dirpath)
1089
1090     def render_title(self, ctx):
1091         return T.title["Manifest of %s" % self.dirpath_as_string()]
1092
1093     def render_header(self, ctx):
1094         return T.p["Manifest of %s" % self.dirpath_as_string()]
1095
1096     def data_items(self, ctx, data):
1097         return self._dirnode.build_manifest()
1098
1099     def render_row(self, ctx, refresh_cap):
1100         ctx.fillSlots("refresh_capability", refresh_cap)
1101         return ctx.tag
1102
1103 class ChildError:
1104     implements(inevow.IResource)
1105     def renderHTTP(self, ctx):
1106         req = inevow.IRequest(ctx)
1107         req.setResponseCode(http.BAD_REQUEST)
1108         req.setHeader("content-type", "text/plain")
1109         return self.text
1110
1111 def child_error(text):
1112     ce = ChildError()
1113     ce.text = text
1114     return ce, ()
1115
1116 class VDrive(rend.Page):
1117
1118     def __init__(self, node, name):
1119         self.node = node
1120         self.name = name
1121
1122     def get_child_at_path(self, path):
1123         if path:
1124             return self.node.get_child_at_path(path)
1125         return defer.succeed(self.node)
1126
1127     def locateChild(self, ctx, segments):
1128         req = inevow.IRequest(ctx)
1129         method = req.method
1130         path = segments
1131
1132         t = get_arg(req, "t", "")
1133         localfile = get_arg(req, "localfile", None)
1134         if localfile is not None:
1135             if localfile != os.path.abspath(localfile):
1136                 return NeedAbsolutePathError(), ()
1137         localdir = get_arg(req, "localdir", None)
1138         if localdir is not None:
1139             if localdir != os.path.abspath(localdir):
1140                 return NeedAbsolutePathError(), ()
1141         if localfile or localdir:
1142             if not ILocalAccess(ctx).local_access_is_allowed():
1143                 return LocalAccessDisabledError(), ()
1144             if req.getHost().host != LOCALHOST:
1145                 return NeedLocalhostError(), ()
1146         # TODO: think about clobbering/revealing config files and node secrets
1147
1148         replace = boolean_of_arg(get_arg(req, "replace", "true"))
1149
1150         if method == "GET":
1151             # the node must exist, and our operation will be performed on the
1152             # node itself.
1153             d = self.get_child_at_path(path)
1154             def file_or_dir(node):
1155                 if (IFileNode.providedBy(node)
1156                     or IMutableFileNode.providedBy(node)):
1157                     filename = "unknown"
1158                     if path:
1159                         filename = path[-1]
1160                     filename = get_arg(req, "filename", filename)
1161                     if t == "download":
1162                         if localfile:
1163                             # write contents to a local file
1164                             return LocalFileDownloader(node, localfile), ()
1165                         # send contents as the result
1166                         return FileDownloader(node, filename), ()
1167                     elif t == "":
1168                         # send contents as the result
1169                         return FileDownloader(node, filename), ()
1170                     elif t == "json":
1171                         return FileJSONMetadata(node), ()
1172                     elif t == "uri":
1173                         return FileURI(node), ()
1174                     elif t == "readonly-uri":
1175                         return FileReadOnlyURI(node), ()
1176                     else:
1177                         return child_error("bad t=%s" % t)
1178                 elif IDirectoryNode.providedBy(node):
1179                     if t == "download":
1180                         if localdir:
1181                             # recursive download to a local directory
1182                             return LocalDirectoryDownloader(node, localdir), ()
1183                         return child_error("t=download requires localdir=")
1184                     elif t == "":
1185                         # send an HTML representation of the directory
1186                         return Directory(self.name, node, path), ()
1187                     elif t == "json":
1188                         return DirectoryJSONMetadata(node), ()
1189                     elif t == "uri":
1190                         return DirectoryURI(node), ()
1191                     elif t == "readonly-uri":
1192                         return DirectoryReadonlyURI(node), ()
1193                     elif t == "manifest":
1194                         return Manifest(node, path), ()
1195                     elif t == 'rename-form':
1196                         return RenameForm(self.name, node, path), ()
1197                     else:
1198                         return child_error("bad t=%s" % t)
1199                 else:
1200                     return child_error("unknown node type")
1201             d.addCallback(file_or_dir)
1202         elif method == "POST":
1203             # the node must exist, and our operation will be performed on the
1204             # node itself.
1205             d = self.get_child_at_path(path)
1206             def _got_POST(node):
1207                 return POSTHandler(node, replace), ()
1208             d.addCallback(_got_POST)
1209         elif method == "DELETE":
1210             # the node must exist, and our operation will be performed on its
1211             # parent node.
1212             assert path # you can't delete the root
1213             d = self.get_child_at_path(path[:-1])
1214             def _got_DELETE(node):
1215                 return DELETEHandler(node, path[-1]), ()
1216             d.addCallback(_got_DELETE)
1217         elif method in ("PUT",):
1218             # the node may or may not exist, and our operation may involve
1219             # all the ancestors of the node.
1220             return PUTHandler(self.node, path, t, localfile, localdir, replace), ()
1221         else:
1222             return rend.NotFound
1223         return d
1224
1225 class UnlinkedPUTCHKUploader(rend.Page):
1226     def renderHTTP(self, ctx):
1227         req = inevow.IRequest(ctx)
1228         assert req.method == "PUT"
1229         # "PUT /uri", to create an unlinked file. This is like PUT but
1230         # without the associated set_uri.
1231
1232         uploadable = FileHandle(req.content)
1233         d = IClient(ctx).upload(uploadable)
1234         d.addCallback(lambda results: results.uri)
1235         # that fires with the URI of the new file
1236         return d
1237
1238 class UnlinkedPUTSSKUploader(rend.Page):
1239     def renderHTTP(self, ctx):
1240         req = inevow.IRequest(ctx)
1241         assert req.method == "PUT"
1242         # SDMF: files are small, and we can only upload data
1243         contents = req.content
1244         contents.seek(0)
1245         data = contents.read()
1246         d = IClient(ctx).create_mutable_file(data)
1247         d.addCallback(lambda n: n.get_uri())
1248         return d
1249
1250 class UnlinkedPUTCreateDirectory(rend.Page):
1251     def renderHTTP(self, ctx):
1252         req = inevow.IRequest(ctx)
1253         assert req.method == "PUT"
1254         # "PUT /uri?t=mkdir", to create an unlinked directory.
1255         d = IClient(ctx).create_empty_dirnode()
1256         d.addCallback(lambda dirnode: dirnode.get_uri())
1257         # XXX add redirect_to_result
1258         return d
1259
1260
1261 class UnlinkedPOSTCHKUploader(rend.Page):
1262     """'POST /uri', to create an unlinked file."""
1263     docFactory = getxmlfile("unlinked-upload.xhtml")
1264
1265     def __init__(self, client, req):
1266         rend.Page.__init__(self)
1267         # we start the upload now, and distribute notification of its
1268         # completion to render_ methods with an ObserverList
1269         assert req.method == "POST"
1270         self._done = observer.OneShotObserverList()
1271         fileobj = req.fields["file"].file
1272         uploadable = FileHandle(fileobj)
1273         d = client.upload(uploadable)
1274         d.addBoth(self._done.fire)
1275
1276     def renderHTTP(self, ctx):
1277         req = inevow.IRequest(ctx)
1278         when_done = get_arg(req, "when_done", None)
1279         if when_done:
1280             # if when_done= is provided, return a redirect instead of our
1281             # usual upload-results page
1282             d = self._done.when_fired()
1283             d.addCallback(lambda res: url.URL.fromString(when_done))
1284             return d
1285         return rend.Page.renderHTTP(self, ctx)
1286
1287     def upload_results(self):
1288         return self._done.when_fired()
1289
1290     def data_done(self, ctx, data):
1291         d = self.upload_results()
1292         d.addCallback(lambda res: "done!")
1293         return d
1294
1295     def data_uri(self, ctx, data):
1296         d = self.upload_results()
1297         d.addCallback(lambda res: res.uri)
1298         return d
1299
1300     def render_download_link(self, ctx, data):
1301         d = self.upload_results()
1302         d.addCallback(lambda res: T.a(href="/uri/" + urllib.quote(res.uri))
1303                       ["/uri/" + res.uri])
1304         return d
1305
1306     def render_sharemap(self, ctx, data):
1307         d = self.upload_results()
1308         d.addCallback(lambda res: res.sharemap)
1309         def _render(sharemap):
1310             if sharemap is None:
1311                 return "None"
1312             l = T.ul()
1313             for shnum in sorted(sharemap.keys()):
1314                 l[T.li["%d -> %s" % (shnum, sharemap[shnum])]]
1315             return l
1316         d.addCallback(_render)
1317         return d
1318
1319     def render_servermap(self, ctx, data):
1320         d = self.upload_results()
1321         d.addCallback(lambda res: res.servermap)
1322         def _render(servermap):
1323             if servermap is None:
1324                 return "None"
1325             l = T.ul()
1326             for peerid in sorted(servermap.keys()):
1327                 peerid_s = idlib.shortnodeid_b2a(peerid)
1328                 shares_s = ",".join([str(shnum) for shnum in servermap[peerid]])
1329                 l[T.li["[%s] got shares: %s" % (peerid_s, shares_s)]]
1330             return l
1331         d.addCallback(_render)
1332         return d
1333
1334     def data_file_size(self, ctx, data):
1335         d = self.upload_results()
1336         d.addCallback(lambda res: res.file_size)
1337         return d
1338
1339     def render_time(self, ctx, data):
1340         # 1.23s, 790ms, 132us
1341         if data is None:
1342             return ""
1343         s = float(data)
1344         if s >= 1.0:
1345             return "%.2fs" % s
1346         if s >= 0.01:
1347             return "%dms" % (1000*s)
1348         if s >= 0.001:
1349             return "%.1fms" % (1000*s)
1350         return "%dus" % (1000000*s)
1351
1352     def render_rate(self, ctx, data):
1353         # 21.8kBps, 554.4kBps 4.37MBps
1354         if data is None:
1355             return ""
1356         r = float(data)
1357         if r > 1000000:
1358             return "%1.2fMBps" % (r/1000000)
1359         if r > 1000:
1360             return "%.1fkBps" % (r/1000)
1361         return "%dBps" % r
1362
1363     def _get_time(self, name):
1364         d = self.upload_results()
1365         d.addCallback(lambda res: res.timings.get(name))
1366         return d
1367
1368     def data_time_total(self, ctx, data):
1369         return self._get_time("total")
1370
1371     def data_time_storage_index(self, ctx, data):
1372         return self._get_time("storage_index")
1373
1374     def data_time_contacting_helper(self, ctx, data):
1375         return self._get_time("contacting_helper")
1376
1377     def data_time_existence_check(self, ctx, data):
1378         return self._get_time("existence_check")
1379
1380     def data_time_cumulative_fetch(self, ctx, data):
1381         return self._get_time("cumulative_fetch")
1382
1383     def data_time_helper_total(self, ctx, data):
1384         return self._get_time("helper_total")
1385
1386     def data_time_peer_selection(self, ctx, data):
1387         return self._get_time("peer_selection")
1388
1389     def data_time_total_encode_and_push(self, ctx, data):
1390         return self._get_time("total_encode_and_push")
1391
1392     def data_time_cumulative_encoding(self, ctx, data):
1393         return self._get_time("cumulative_encoding")
1394
1395     def data_time_cumulative_sending(self, ctx, data):
1396         return self._get_time("cumulative_sending")
1397
1398     def data_time_hashes_and_close(self, ctx, data):
1399         return self._get_time("hashes_and_close")
1400
1401     def _get_rate(self, name):
1402         d = self.upload_results()
1403         def _convert(r):
1404             file_size = r.file_size
1405             time = r.timings.get(name)
1406             if time is None:
1407                 return None
1408             try:
1409                 return 1.0 * file_size / time
1410             except ZeroDivisionError:
1411                 return None
1412         d.addCallback(_convert)
1413         return d
1414
1415     def data_rate_total(self, ctx, data):
1416         return self._get_rate("total")
1417
1418     def data_rate_storage_index(self, ctx, data):
1419         return self._get_rate("storage_index")
1420
1421     def data_rate_encode(self, ctx, data):
1422         return self._get_rate("cumulative_encoding")
1423
1424     def data_rate_push(self, ctx, data):
1425         return self._get_rate("cumulative_sending")
1426
1427     def data_rate_encode_and_push(self, ctx, data):
1428         d = self.upload_results()
1429         def _convert(r):
1430             file_size = r.file_size
1431             if file_size is None:
1432                 return None
1433             time1 = r.timings.get("cumulative_encoding")
1434             if time1 is None:
1435                 return None
1436             time2 = r.timings.get("cumulative_sending")
1437             if time2 is None:
1438                 return None
1439             try:
1440                 return 1.0 * file_size / (time1+time2)
1441             except ZeroDivisionError:
1442                 return None
1443         d.addCallback(_convert)
1444         return d
1445
1446     def data_rate_ciphertext_fetch(self, ctx, data):
1447         d = self.upload_results()
1448         def _convert(r):
1449             fetch_size = r.ciphertext_fetched
1450             if fetch_size is None:
1451                 return None
1452             time = r.timings.get("cumulative_fetch")
1453             if time is None:
1454                 return None
1455             try:
1456                 return 1.0 * fetch_size / time
1457             except ZeroDivisionError:
1458                 return None
1459         d.addCallback(_convert)
1460         return d
1461
1462 class UnlinkedPOSTSSKUploader(rend.Page):
1463     def renderHTTP(self, ctx):
1464         req = inevow.IRequest(ctx)
1465         assert req.method == "POST"
1466
1467         # "POST /uri", to create an unlinked file.
1468         # SDMF: files are small, and we can only upload data
1469         contents = req.fields["file"]
1470         contents.file.seek(0)
1471         data = contents.file.read()
1472         d = IClient(ctx).create_mutable_file(data)
1473         d.addCallback(lambda n: n.get_uri())
1474         return d
1475
1476 class UnlinkedPOSTCreateDirectory(rend.Page):
1477     def renderHTTP(self, ctx):
1478         req = inevow.IRequest(ctx)
1479         assert req.method == "POST"
1480
1481         # "POST /uri?t=mkdir", to create an unlinked directory.
1482         d = IClient(ctx).create_empty_dirnode()
1483         redirect = get_arg(req, "redirect_to_result", "false")
1484         if boolean_of_arg(redirect):
1485             def _then_redir(res):
1486                 new_url = "uri/" + urllib.quote(res.get_uri())
1487                 req.setResponseCode(http.SEE_OTHER) # 303
1488                 req.setHeader('location', new_url)
1489                 req.finish()
1490                 return ''
1491             d.addCallback(_then_redir)
1492         else:
1493             d.addCallback(lambda dirnode: dirnode.get_uri())
1494         return d
1495
1496
1497 class Root(rend.Page):
1498
1499     addSlash = True
1500     docFactory = getxmlfile("welcome.xhtml")
1501
1502     def locateChild(self, ctx, segments):
1503         client = IClient(ctx)
1504         req = inevow.IRequest(ctx)
1505
1506         segments = list(segments) # XXX HELP I AM YUCKY!
1507         while segments and not segments[-1]:
1508             segments.pop()
1509         if not segments:
1510             segments.append('')
1511         segments = tuple(segments)
1512         if segments:
1513             if segments[0] == "uri":
1514                 if len(segments) == 1 or segments[1] == '':
1515                     uri = get_arg(req, "uri", None)
1516                     if uri is not None:
1517                         there = url.URL.fromContext(ctx)
1518                         there = there.clear("uri")
1519                         there = there.child("uri").child(uri)
1520                         return there, ()
1521                 if len(segments) == 1:
1522                     # /uri
1523                     if req.method == "PUT":
1524                         # either "PUT /uri" to create an unlinked file, or
1525                         # "PUT /uri?t=mkdir" to create an unlinked directory
1526                         t = get_arg(req, "t", "").strip()
1527                         if t == "":
1528                             mutable = bool(get_arg(req, "mutable", "").strip())
1529                             if mutable:
1530                                 return UnlinkedPUTSSKUploader(), ()
1531                             else:
1532                                 return UnlinkedPUTCHKUploader(), ()
1533                         if t == "mkdir":
1534                             return UnlinkedPUTCreateDirectory(), ()
1535                         errmsg = "/uri only accepts PUT and PUT?t=mkdir"
1536                         return WebError(http.BAD_REQUEST, errmsg), ()
1537
1538                     elif req.method == "POST":
1539                         # "POST /uri?t=upload&file=newfile" to upload an
1540                         # unlinked file or "POST /uri?t=mkdir" to create a
1541                         # new directory
1542                         t = get_arg(req, "t", "").strip()
1543                         if t in ("", "upload"):
1544                             mutable = bool(get_arg(req, "mutable", "").strip())
1545                             if mutable:
1546                                 return UnlinkedPOSTSSKUploader(), ()
1547                             else:
1548                                 return UnlinkedPOSTCHKUploader(client, req), ()
1549                         if t == "mkdir":
1550                             return UnlinkedPOSTCreateDirectory(), ()
1551                         errmsg = "/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, and POST?t=mkdir"
1552                         return WebError(http.BAD_REQUEST, errmsg), ()
1553                 if len(segments) < 2:
1554                     return rend.NotFound
1555                 uri = segments[1]
1556                 d = defer.maybeDeferred(client.create_node_from_uri, uri)
1557                 d.addCallback(lambda node: VDrive(node, uri))
1558                 d.addCallback(lambda vd: vd.locateChild(ctx, segments[2:]))
1559                 def _trap_KeyError(f):
1560                     f.trap(KeyError)
1561                     return rend.FourOhFour(), ()
1562                 d.addErrback(_trap_KeyError)
1563                 return d
1564             elif segments[0] == "xmlrpc":
1565                 raise NotImplementedError()
1566         return rend.Page.locateChild(self, ctx, segments)
1567
1568     child_webform_css = webform.defaultCSS
1569     child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
1570
1571     child_provisioning = provisioning.ProvisioningTool()
1572
1573     def data_version(self, ctx, data):
1574         return get_package_versions_string()
1575     def data_import_path(self, ctx, data):
1576         return str(allmydata)
1577     def data_my_nodeid(self, ctx, data):
1578         return idlib.nodeid_b2a(IClient(ctx).nodeid)
1579     def data_storage(self, ctx, data):
1580         client = IClient(ctx)
1581         try:
1582             ss = client.getServiceNamed("storage")
1583         except KeyError:
1584             return "Not running"
1585         allocated = ss.allocated_size()
1586         return "about %d bytes allocated" % allocated
1587
1588     def data_introducer_furl(self, ctx, data):
1589         return IClient(ctx).introducer_furl
1590     def data_connected_to_introducer(self, ctx, data):
1591         if IClient(ctx).connected_to_introducer():
1592             return "yes"
1593         return "no"
1594
1595     def data_helper_furl(self, ctx, data):
1596         try:
1597             uploader = IClient(ctx).getServiceNamed("uploader")
1598         except KeyError:
1599             return None
1600         furl, connected = uploader.get_helper_info()
1601         return furl
1602     def data_connected_to_helper(self, ctx, data):
1603         try:
1604             uploader = IClient(ctx).getServiceNamed("uploader")
1605         except KeyError:
1606             return "no" # we don't even have an Uploader
1607         furl, connected = uploader.get_helper_info()
1608         if connected:
1609             return "yes"
1610         return "no"
1611
1612     def data_known_storage_servers(self, ctx, data):
1613         ic = IClient(ctx).introducer_client
1614         servers = [c
1615                    for c in ic.get_all_connectors().values()
1616                    if c.service_name == "storage"]
1617         return len(servers)
1618
1619     def data_connected_storage_servers(self, ctx, data):
1620         ic = IClient(ctx).introducer_client
1621         return len(ic.get_all_connections_for("storage"))
1622
1623     def data_services(self, ctx, data):
1624         ic = IClient(ctx).introducer_client
1625         c = [ (service_name, nodeid, rsc)
1626               for (nodeid, service_name), rsc
1627               in ic.get_all_connectors().items() ]
1628         c.sort()
1629         return c
1630
1631     def render_service_row(self, ctx, data):
1632         (service_name, nodeid, rsc) = data
1633         ctx.fillSlots("peerid", "%s %s" % (idlib.nodeid_b2a(nodeid),
1634                                            rsc.nickname))
1635         if rsc.rref:
1636             rhost = rsc.remote_host
1637             if nodeid == IClient(ctx).nodeid:
1638                 rhost_s = "(loopback)"
1639             elif isinstance(rhost, address.IPv4Address):
1640                 rhost_s = "%s:%d" % (rhost.host, rhost.port)
1641             else:
1642                 rhost_s = str(rhost)
1643             connected = "Yes: to " + rhost_s
1644             since = rsc.last_connect_time
1645         else:
1646             connected = "No"
1647             since = rsc.last_loss_time
1648
1649         TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
1650         ctx.fillSlots("connected", connected)
1651         ctx.fillSlots("since", time.strftime(TIME_FORMAT, time.localtime(since)))
1652         ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
1653                                                  time.localtime(rsc.announcement_time)))
1654         ctx.fillSlots("version", rsc.version)
1655         ctx.fillSlots("service_name", rsc.service_name)
1656
1657         return ctx.tag
1658
1659     def render_download_form(self, ctx, data):
1660         # this is a form where users can download files by URI
1661         form = T.form(action="uri", method="get",
1662                       enctype="multipart/form-data")[
1663             T.fieldset[
1664             T.legend(class_="freeform-form-label")["Download a file"],
1665             "URI to download: ",
1666             T.input(type="text", name="uri"), " ",
1667             "Filename to download as: ",
1668             T.input(type="text", name="filename"), " ",
1669             T.input(type="submit", value="Download!"),
1670             ]]
1671         return T.div[form]
1672
1673     def render_view_form(self, ctx, data):
1674         # this is a form where users can download files by URI, or jump to a
1675         # named directory
1676         form = T.form(action="uri", method="get",
1677                       enctype="multipart/form-data")[
1678             T.fieldset[
1679             T.legend(class_="freeform-form-label")["View a file or directory"],
1680             "URI to view: ",
1681             T.input(type="text", name="uri"), " ",
1682             T.input(type="submit", value="View!"),
1683             ]]
1684         return T.div[form]
1685
1686     def render_upload_form(self, ctx, data):
1687         # this is a form where users can upload unlinked files
1688         form = T.form(action="uri", method="post",
1689                       enctype="multipart/form-data")[
1690             T.fieldset[
1691             T.legend(class_="freeform-form-label")["Upload a file"],
1692             "Choose a file: ",
1693             T.input(type="file", name="file", class_="freeform-input-file"),
1694             T.input(type="hidden", name="t", value="upload"),
1695             " Mutable?:", T.input(type="checkbox", name="mutable"),
1696             T.input(type="submit", value="Upload!"),
1697             ]]
1698         return T.div[form]
1699
1700     def render_mkdir_form(self, ctx, data):
1701         # this is a form where users can create new directories
1702         form = T.form(action="uri", method="post",
1703                       enctype="multipart/form-data")[
1704             T.fieldset[
1705             T.legend(class_="freeform-form-label")["Create a directory"],
1706             T.input(type="hidden", name="t", value="mkdir"),
1707             T.input(type="hidden", name="redirect_to_result", value="true"),
1708             T.input(type="submit", value="Create Directory!"),
1709             ]]
1710         return T.div[form]
1711
1712
1713 class LocalAccess:
1714     implements(ILocalAccess)
1715     def __init__(self):
1716         self.local_access = False
1717     def local_access_is_allowed(self):
1718         return self.local_access
1719
1720 class WebishServer(service.MultiService):
1721     name = "webish"
1722
1723     def __init__(self, webport, nodeurl_path=None):
1724         service.MultiService.__init__(self)
1725         self.webport = webport
1726         self.root = Root()
1727         self.site = site = appserver.NevowSite(self.root)
1728         self.site.requestFactory = MyRequest
1729         self.allow_local = LocalAccess()
1730         self.site.remember(self.allow_local, ILocalAccess)
1731         s = strports.service(webport, site)
1732         s.setServiceParent(self)
1733         self.listener = s # stash it so the tests can query for the portnum
1734         self._started = defer.Deferred()
1735         if nodeurl_path:
1736             self._started.addCallback(self._write_nodeurl_file, nodeurl_path)
1737
1738     def allow_local_access(self, enable=True):
1739         self.allow_local.local_access = enable
1740
1741     def startService(self):
1742         service.MultiService.startService(self)
1743         # to make various services available to render_* methods, we stash a
1744         # reference to the client on the NevowSite. This will be available by
1745         # adapting the 'context' argument to a special marker interface named
1746         # IClient.
1747         self.site.remember(self.parent, IClient)
1748         # I thought you could do the same with an existing interface, but
1749         # apparently 'ISite' does not exist
1750         #self.site._client = self.parent
1751         self._started.callback(None)
1752
1753     def _write_nodeurl_file(self, junk, nodeurl_path):
1754         # what is our webport?
1755         s = self.listener
1756         if isinstance(s, internet.TCPServer):
1757             base_url = "http://localhost:%d" % s._port.getHost().port
1758         elif isinstance(s, internet.SSLServer):
1759             base_url = "https://localhost:%d" % s._port.getHost().port
1760         else:
1761             base_url = None
1762         if base_url:
1763             f = open(nodeurl_path, 'wb')
1764             # this file is world-readable
1765             f.write(base_url + "\n")
1766             f.close()
1767