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