]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/webish.py
test_web.py: add coverage for directory listings that include mutable files
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / webish.py
1
2 from base64 import b32encode
3 import os.path
4 from twisted.application import service, strports, internet
5 from twisted.web import static, resource, server, html, http
6 from twisted.python import util, log
7 from twisted.internet import defer
8 from twisted.internet.interfaces import IConsumer
9 from nevow import inevow, rend, loaders, appserver, url, tags as T
10 from nevow.static import File as nevow_File # TODO: merge with static.File?
11 from allmydata.util import fileutil
12 import simplejson
13 from allmydata.interfaces import IDownloadTarget, IDirectoryNode, IFileNode, \
14      IMutableFileNode
15 from allmydata import upload, download
16 from allmydata import provisioning
17 from zope.interface import implements, Interface
18 import urllib
19 from formless import webform
20
21 def getxmlfile(name):
22     return loaders.xmlfile(util.sibpath(__file__, "web/%s" % name))
23
24 class IClient(Interface):
25     pass
26 class ILocalAccess(Interface):
27     def local_access_is_allowed():
28         """Return True if t=upload&localdir= is allowed, giving anyone who
29         can talk to the webserver control over the local (disk) filesystem."""
30
31
32 # we must override twisted.web.http.Request.requestReceived with a version
33 # that doesn't use cgi.parse_multipart() . Since we actually use Nevow, we
34 # override the nevow-specific subclass, nevow.appserver.NevowRequest . This
35 # is an exact copy of twisted.web.http.Request (from SVN HEAD on 10-Aug-2007)
36 # that modifies the way form arguments are parsed. Note that this sort of
37 # surgery may induce a dependency upon a particular version of twisted.web
38
39 parse_qs = http.parse_qs
40 class MyRequest(appserver.NevowRequest):
41     def requestReceived(self, command, path, version):
42         """Called by channel when all data has been received.
43
44         This method is not intended for users.
45         """
46         self.content.seek(0,0)
47         self.args = {}
48         self.stack = []
49
50         self.method, self.uri = command, path
51         self.clientproto = version
52         x = self.uri.split('?', 1)
53
54         if len(x) == 1:
55             self.path = self.uri
56         else:
57             self.path, argstring = x
58             self.args = parse_qs(argstring, 1)
59
60         # cache the client and server information, we'll need this later to be
61         # serialized and sent with the request so CGIs will work remotely
62         self.client = self.channel.transport.getPeer()
63         self.host = self.channel.transport.getHost()
64
65         # Argument processing.
66
67 ##      The original twisted.web.http.Request.requestReceived code parsed the
68 ##      content and added the form fields it found there to self.args . It
69 ##      did this with cgi.parse_multipart, which holds the arguments in RAM
70 ##      and is thus unsuitable for large file uploads. The Nevow subclass
71 ##      (nevow.appserver.NevowRequest) uses cgi.FieldStorage instead (putting
72 ##      the results in self.fields), which is much more memory-efficient.
73 ##      Since we know we're using Nevow, we can anticipate these arguments
74 ##      appearing in self.fields instead of self.args, and thus skip the
75 ##      parse-content-into-self.args step.
76
77 ##      args = self.args
78 ##      ctype = self.getHeader('content-type')
79 ##      if self.method == "POST" and ctype:
80 ##          mfd = 'multipart/form-data'
81 ##          key, pdict = cgi.parse_header(ctype)
82 ##          if key == 'application/x-www-form-urlencoded':
83 ##              args.update(parse_qs(self.content.read(), 1))
84 ##          elif key == mfd:
85 ##              try:
86 ##                  args.update(cgi.parse_multipart(self.content, pdict))
87 ##              except KeyError, e:
88 ##                  if e.args[0] == 'content-disposition':
89 ##                      # Parse_multipart can't cope with missing
90 ##                      # content-dispostion headers in multipart/form-data
91 ##                      # parts, so we catch the exception and tell the client
92 ##                      # it was a bad request.
93 ##                      self.channel.transport.write(
94 ##                              "HTTP/1.1 400 Bad Request\r\n\r\n")
95 ##                      self.channel.transport.loseConnection()
96 ##                      return
97 ##                  raise
98
99         self.process()
100
101 class Directory(rend.Page):
102     addSlash = True
103     docFactory = getxmlfile("directory.xhtml")
104
105     def __init__(self, rootname, dirnode, dirpath):
106         self._rootname = rootname
107         self._dirnode = dirnode
108         self._dirpath = dirpath
109
110     def dirpath_as_string(self):
111         return "/" + "/".join(self._dirpath)
112
113     def render_title(self, ctx, data):
114         return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
115
116     def render_header(self, ctx, data):
117         parent_directories = ("<%s>" % self._rootname,) + self._dirpath
118         num_dirs = len(parent_directories)
119
120         header = ["Directory '"]
121         for i,d in enumerate(parent_directories):
122             upness = num_dirs - i - 1
123             if upness:
124                 link = "/".join( ("..",) * upness )
125             else:
126                 link = "."
127             header.append(T.a(href=link)[d])
128             if upness != 0:
129                 header.append("/")
130         header.append("'")
131
132         if self._dirnode.is_readonly():
133             header.append(" (readonly)")
134         header.append(":")
135         return ctx.tag[header]
136
137     def render_welcome(self, ctx, data):
138         depth = len(self._dirpath) + 2
139         link = "/".join([".."] * depth)
140         return T.div[T.a(href=link)["Return to Welcome page"]]
141
142     def data_children(self, ctx, data):
143         d = self._dirnode.list()
144         d.addCallback(lambda dict: sorted(dict.items()))
145         return d
146
147     def render_row(self, ctx, data):
148         name, (target, metadata) = data
149
150         if self._dirnode.is_readonly():
151             delete = "-"
152             rename = "-"
153         else:
154             # this creates a button which will cause our child__delete method
155             # to be invoked, which deletes the file and then redirects the
156             # browser back to this directory
157             delete = T.form(action=url.here, method="post")[
158                 T.input(type='hidden', name='t', value='delete'),
159                 T.input(type='hidden', name='name', value=name),
160                 T.input(type='hidden', name='when_done', value=url.here),
161                 T.input(type='submit', value='del', name="del"),
162                 ]
163
164             rename = T.form(action=url.here, method="get")[
165                 T.input(type='hidden', name='t', value='rename-form'),
166                 T.input(type='hidden', name='name', value=name),
167                 T.input(type='hidden', name='when_done', value=url.here),
168                 T.input(type='submit', value='rename', name="rename"),
169                 ]
170
171         ctx.fillSlots("delete", delete)
172         ctx.fillSlots("rename", rename)
173         check = T.form(action=url.here, method="post")[
174             T.input(type='hidden', name='t', value='check'),
175             T.input(type='hidden', name='name', value=name),
176             T.input(type='hidden', name='when_done', value=url.here),
177             T.input(type='submit', value='check', name="check"),
178             ]
179         ctx.fillSlots("check", check)
180
181         # build the base of the uri_link link url
182         uri_link = "/uri/" + urllib.quote(target.get_uri().replace("/", "!"))
183
184         assert (IFileNode.providedBy(target)
185                 or IDirectoryNode.providedBy(target)
186                 or IMutableFileNode.providedBy(target)), target
187
188         if IMutableFileNode.providedBy(target):
189             # file
190
191             # add the filename to the uri_link url
192             uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
193
194             # to prevent javascript in displayed .html files from stealing a
195             # secret directory URI from the URL, send the browser to a URI-based
196             # page that doesn't know about the directory at all
197             #dlurl = urllib.quote(name)
198             dlurl = uri_link
199
200             ctx.fillSlots("filename",
201                           T.a(href=dlurl)[html.escape(name)])
202             ctx.fillSlots("type", "SSK")
203
204             ctx.fillSlots("size", "?")
205
206             text_plain_link = uri_link + "?filename=foo.txt"
207             text_plain_tag = T.a(href=text_plain_link)["text/plain"]
208
209         elif IFileNode.providedBy(target):
210             # file
211
212             # add the filename to the uri_link url
213             uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
214
215             # to prevent javascript in displayed .html files from stealing a
216             # secret directory URI from the URL, send the browser to a URI-based
217             # page that doesn't know about the directory at all
218             #dlurl = urllib.quote(name)
219             dlurl = uri_link
220
221             ctx.fillSlots("filename",
222                           T.a(href=dlurl)[html.escape(name)])
223             ctx.fillSlots("type", "FILE")
224
225             ctx.fillSlots("size", target.get_size())
226
227             text_plain_link = uri_link + "?filename=foo.txt"
228             text_plain_tag = T.a(href=text_plain_link)["text/plain"]
229
230         elif IDirectoryNode.providedBy(target):
231             # directory
232             subdir_url = urllib.quote(name)
233             ctx.fillSlots("filename",
234                           T.a(href=subdir_url)[html.escape(name)])
235             if target.is_readonly():
236                 dirtype = "DIR-RO"
237             else:
238                 dirtype = "DIR"
239             ctx.fillSlots("type", dirtype)
240             ctx.fillSlots("size", "-")
241             text_plain_tag = None
242
243         childdata = [T.a(href="%s?t=json" % name)["JSON"], ", ",
244                      T.a(href="%s?t=uri" % name)["URI"], ", ",
245                      T.a(href="%s?t=readonly-uri" % name)["readonly-URI"], ", ",
246                      T.a(href=uri_link)["URI-link"],
247                      ]
248         if text_plain_tag:
249             childdata.extend([", ", text_plain_tag])
250
251         ctx.fillSlots("data", childdata)
252
253         try:
254             checker = IClient(ctx).getServiceNamed("checker")
255         except KeyError:
256             checker = None
257         if checker:
258             d = defer.maybeDeferred(checker.checker_results_for,
259                                     target.get_verifier())
260             def _got(checker_results):
261                 recent_results = reversed(checker_results[-5:])
262                 if IFileNode.providedBy(target):
263                     results = ("[" +
264                                ", ".join(["%d/%d" % (found, needed)
265                                           for (when,
266                                                (needed, total, found, sharemap))
267                                           in recent_results]) +
268                                "]")
269                 elif IDirectoryNode.providedBy(target):
270                     results = ("[" +
271                                "".join([{True:"+",False:"-"}[res]
272                                         for (when, res) in recent_results]) +
273                                "]")
274                 else:
275                     results = "%d results" % len(checker_results)
276                 return results
277             d.addCallback(_got)
278             results = d
279         else:
280             results = "--"
281         # TODO: include a link to see more results, including timestamps
282         # TODO: use a sparkline
283         ctx.fillSlots("checker_results", results)
284
285         return ctx.tag
286
287     def render_forms(self, ctx, data):
288         if self._dirnode.is_readonly():
289             return T.div["No upload forms: directory is read-only"]
290         mkdir = T.form(action=".", method="post",
291                        enctype="multipart/form-data")[
292             T.fieldset[
293             T.input(type="hidden", name="t", value="mkdir"),
294             T.input(type="hidden", name="when_done", value=url.here),
295             T.legend(class_="freeform-form-label")["Create a new directory"],
296             "New directory name: ",
297             T.input(type="text", name="name"), " ",
298             T.input(type="submit", value="Create"),
299             ]]
300
301         upload = T.form(action=".", method="post",
302                         enctype="multipart/form-data")[
303             T.fieldset[
304             T.input(type="hidden", name="t", value="upload"),
305             T.input(type="hidden", name="when_done", value=url.here),
306             T.legend(class_="freeform-form-label")["Upload a file to this directory"],
307             "Choose a file to upload: ",
308             T.input(type="file", name="file", class_="freeform-input-file"),
309             " ",
310             T.input(type="submit", value="Upload"),
311             " Mutable?:",
312             T.input(type="checkbox", name="mutable"),
313             ]]
314
315         mount = T.form(action=".", method="post",
316                         enctype="multipart/form-data")[
317             T.fieldset[
318             T.input(type="hidden", name="t", value="uri"),
319             T.input(type="hidden", name="when_done", value=url.here),
320             T.legend(class_="freeform-form-label")["Attach a file or directory"
321                                                    " (by URI) to this"
322                                                    " directory"],
323             "New child name: ",
324             T.input(type="text", name="name"), " ",
325             "URI of new child: ",
326             T.input(type="text", name="uri"), " ",
327             T.input(type="submit", value="Attach"),
328             ]]
329         return [T.div(class_="freeform-form")[mkdir],
330                 T.div(class_="freeform-form")[upload],
331                 T.div(class_="freeform-form")[mount],
332                 ]
333
334     def render_overwrite(self, ctx, data):
335         name, target = data
336         if IMutableFileNode.providedBy(target) and not target.is_readonly():
337             overwrite = T.form(action=".", method="post",
338                                enctype="multipart/form-data")[
339                 T.fieldset[
340                 T.input(type="hidden", name="t", value="overwrite"),
341                 T.input(type='hidden', name='name', value=name),
342                 T.input(type='hidden', name='when_done', value=url.here),
343                 T.legend(class_="freeform-form-label")["Overwrite"],
344                 "Choose new file: ",
345                 T.input(type="file", name="file", class_="freeform-input-file"),
346                 " ",
347                 T.input(type="submit", value="Overwrite")
348                 ]]
349             return [T.div(class_="freeform-form")[overwrite],]
350         else:
351             return []
352
353     def render_results(self, ctx, data):
354         req = inevow.IRequest(ctx)
355         if "results" in req.args:
356             return req.args["results"]
357         else:
358             return ""
359
360 class WebDownloadTarget:
361     implements(IDownloadTarget, IConsumer)
362     def __init__(self, req, content_type, content_encoding):
363         self._req = req
364         self._content_type = content_type
365         self._content_encoding = content_encoding
366         self._opened = False
367         self._producer = None
368
369     def registerProducer(self, producer, streaming):
370         self._req.registerProducer(producer, streaming)
371     def unregisterProducer(self):
372         self._req.unregisterProducer()
373
374     def open(self, size):
375         self._opened = True
376         self._req.setHeader("content-type", self._content_type)
377         if self._content_encoding:
378             self._req.setHeader("content-encoding", self._content_encoding)
379         self._req.setHeader("content-length", str(size))
380
381     def write(self, data):
382         self._req.write(data)
383     def close(self):
384         self._req.finish()
385
386     def fail(self, why):
387         if self._opened:
388             # The content-type is already set, and the response code
389             # has already been sent, so we can't provide a clean error
390             # indication. We can emit text (which a browser might interpret
391             # as something else), and if we sent a Size header, they might
392             # notice that we've truncated the data. Keep the error message
393             # small to improve the chances of having our error response be
394             # shorter than the intended results.
395             #
396             # We don't have a lot of options, unfortunately.
397             self._req.write("problem during download\n")
398         else:
399             # We haven't written anything yet, so we can provide a sensible
400             # error message.
401             msg = str(why.type)
402             msg.replace("\n", "|")
403             self._req.setResponseCode(http.GONE, msg)
404             self._req.setHeader("content-type", "text/plain")
405             # TODO: HTML-formatted exception?
406             self._req.write(str(why))
407         self._req.finish()
408
409     def register_canceller(self, cb):
410         pass
411     def finish(self):
412         pass
413
414 class FileDownloader(resource.Resource):
415     def __init__(self, filenode, name):
416         assert (IFileNode.providedBy(filenode)
417                 or IMutableFileNode.providedBy(filenode))
418         self._filenode = filenode
419         self._name = name
420
421     def render(self, req):
422         gte = static.getTypeAndEncoding
423         type, encoding = gte(self._name,
424                              static.File.contentTypes,
425                              static.File.contentEncodings,
426                              defaultType="text/plain")
427
428         d = self._filenode.download(WebDownloadTarget(req, type, encoding))
429         # exceptions during download are handled by the WebDownloadTarget
430         d.addErrback(lambda why: None)
431         return server.NOT_DONE_YET
432
433 class BlockingFileError(Exception):
434     """We cannot auto-create a parent directory, because there is a file in
435     the way"""
436 class NoReplacementError(Exception):
437     """There was already a child by that name, and you asked me to not replace it"""
438
439 LOCALHOST = "127.0.0.1"
440
441 class NeedLocalhostError:
442     implements(inevow.IResource)
443
444     def renderHTTP(self, ctx):
445         req = inevow.IRequest(ctx)
446         req.setResponseCode(http.FORBIDDEN)
447         req.setHeader("content-type", "text/plain")
448         return "localfile= or localdir= requires a local connection"
449
450 class NeedAbsolutePathError:
451     implements(inevow.IResource)
452
453     def renderHTTP(self, ctx):
454         req = inevow.IRequest(ctx)
455         req.setResponseCode(http.FORBIDDEN)
456         req.setHeader("content-type", "text/plain")
457         return "localfile= or localdir= requires an absolute path"
458
459 class LocalAccessDisabledError:
460     implements(inevow.IResource)
461
462     def renderHTTP(self, ctx):
463         req = inevow.IRequest(ctx)
464         req.setResponseCode(http.FORBIDDEN)
465         req.setHeader("content-type", "text/plain")
466         return "local file access is disabled"
467
468
469 class LocalFileDownloader(resource.Resource):
470     def __init__(self, filenode, local_filename):
471         self._local_filename = local_filename
472         IFileNode(filenode)
473         self._filenode = filenode
474
475     def render(self, req):
476         target = download.FileName(self._local_filename)
477         d = self._filenode.download(target)
478         def _done(res):
479             req.write(self._filenode.get_uri())
480             req.finish()
481         d.addCallback(_done)
482         return server.NOT_DONE_YET
483
484
485 class FileJSONMetadata(rend.Page):
486     def __init__(self, filenode):
487         self._filenode = filenode
488
489     def renderHTTP(self, ctx):
490         req = inevow.IRequest(ctx)
491         req.setHeader("content-type", "text/plain")
492         return self.renderNode(self._filenode)
493
494     def renderNode(self, filenode):
495         file_uri = filenode.get_uri()
496         data = ("filenode",
497                 {'ro_uri': file_uri,
498                  'size': filenode.get_size(),
499                  })
500         return simplejson.dumps(data, indent=1)
501
502 class FileURI(FileJSONMetadata):
503     def renderNode(self, filenode):
504         file_uri = filenode.get_uri()
505         return file_uri
506
507 class FileReadOnlyURI(FileJSONMetadata):
508     def renderNode(self, filenode):
509         if filenode.is_readonly():
510             return filenode.get_uri()
511         else:
512             return filenode.get_readonly().get_uri()
513
514 class DirnodeWalkerMixin:
515     """Visit all nodes underneath (and including) the rootnode, one at a
516     time. For each one, call the visitor. The visitor will see the
517     IDirectoryNode before it sees any of the IFileNodes inside. If the
518     visitor returns a Deferred, I do not call the visitor again until it has
519     fired.
520     """
521
522 ##    def _walk_if_we_could_use_generators(self, rootnode, rootpath=()):
523 ##        # this is what we'd be doing if we didn't have the Deferreds and
524 ##        # thus could use generators
525 ##        yield rootpath, rootnode
526 ##        for childname, childnode in rootnode.list().items():
527 ##            childpath = rootpath + (childname,)
528 ##            if IFileNode.providedBy(childnode):
529 ##                yield childpath, childnode
530 ##            elif IDirectoryNode.providedBy(childnode):
531 ##                for res in self._walk_if_we_could_use_generators(childnode,
532 ##                                                                 childpath):
533 ##                    yield res
534
535     def walk(self, rootnode, visitor, rootpath=()):
536         d = rootnode.list()
537         def _listed(listing):
538             return listing.items()
539         d.addCallback(_listed)
540         d.addCallback(self._handle_items, visitor, rootpath)
541         return d
542
543     def _handle_items(self, items, visitor, rootpath):
544         if not items:
545             return
546         childname, (childnode, metadata) = items[0]
547         childpath = rootpath + (childname,)
548         d = defer.maybeDeferred(visitor, childpath, childnode, metadata)
549         if IDirectoryNode.providedBy(childnode):
550             d.addCallback(lambda res: self.walk(childnode, visitor, childpath))
551         d.addCallback(lambda res:
552                       self._handle_items(items[1:], visitor, rootpath))
553         return d
554
555 class LocalDirectoryDownloader(resource.Resource, DirnodeWalkerMixin):
556     def __init__(self, dirnode, localdir):
557         self._dirnode = dirnode
558         self._localdir = localdir
559
560     def _handle(self, path, node, metadata):
561         localfile = os.path.join(self._localdir, os.sep.join(path))
562         if IDirectoryNode.providedBy(node):
563             fileutil.make_dirs(localfile)
564         elif IFileNode.providedBy(node):
565             target = download.FileName(localfile)
566             return node.download(target)
567
568     def render(self, req):
569         d = self.walk(self._dirnode, self._handle)
570         def _done(res):
571             req.setHeader("content-type", "text/plain")
572             return "operation complete"
573         d.addCallback(_done)
574         return d
575
576 class DirectoryJSONMetadata(rend.Page):
577     def __init__(self, dirnode):
578         self._dirnode = dirnode
579
580     def renderHTTP(self, ctx):
581         req = inevow.IRequest(ctx)
582         req.setHeader("content-type", "text/plain")
583         return self.renderNode(self._dirnode)
584
585     def renderNode(self, node):
586         d = node.list()
587         def _got(children):
588             kids = {}
589             for name, (childnode, metadata) in children.iteritems():
590                 if IFileNode.providedBy(childnode):
591                     kiduri = childnode.get_uri()
592                     kiddata = ("filenode",
593                                {'ro_uri': kiduri,
594                                 'size': childnode.get_size(),
595                                 })
596                 else:
597                     assert IDirectoryNode.providedBy(childnode), (childnode, children,)
598                     kiddata = ("dirnode",
599                                {'ro_uri': childnode.get_readonly_uri(),
600                                 })
601                     if not childnode.is_readonly():
602                         kiddata[1]['rw_uri'] = childnode.get_uri()
603                 kids[name] = kiddata
604             contents = { 'children': kids,
605                          'ro_uri': node.get_readonly_uri(),
606                          }
607             if not node.is_readonly():
608                 contents['rw_uri'] = node.get_uri()
609             data = ("dirnode", contents)
610             return simplejson.dumps(data, indent=1)
611         d.addCallback(_got)
612         return d
613
614 class DirectoryURI(DirectoryJSONMetadata):
615     def renderNode(self, node):
616         return node.get_uri()
617
618 class DirectoryReadonlyURI(DirectoryJSONMetadata):
619     def renderNode(self, node):
620         return node.get_readonly_uri()
621
622 class RenameForm(rend.Page):
623     addSlash = True
624     docFactory = getxmlfile("rename-form.xhtml")
625
626     def __init__(self, rootname, dirnode, dirpath):
627         self._rootname = rootname
628         self._dirnode = dirnode
629         self._dirpath = dirpath
630
631     def dirpath_as_string(self):
632         return "/" + "/".join(self._dirpath)
633
634     def render_title(self, ctx, data):
635         return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
636
637     def render_header(self, ctx, data):
638         parent_directories = ("<%s>" % self._rootname,) + self._dirpath
639         num_dirs = len(parent_directories)
640
641         header = [ "Rename in directory '",
642                    "<%s>/" % self._rootname,
643                    "/".join(self._dirpath),
644                    "':", ]
645
646         if self._dirnode.is_readonly():
647             header.append(" (readonly)")
648         return ctx.tag[header]
649
650     def render_when_done(self, ctx, data):
651         return T.input(type="hidden", name="when_done", value=url.here)
652
653     def render_get_name(self, ctx, data):
654         req = inevow.IRequest(ctx)
655         if 'name' in req.args:
656             name = req.args['name'][0]
657         else:
658             name = ''
659         ctx.tag.attributes['value'] = name
660         return ctx.tag
661
662 class POSTHandler(rend.Page):
663     def __init__(self, node, replace):
664         self._node = node
665         self._replace = replace
666
667     def _check_replacement(self, name):
668         if self._replace:
669             return defer.succeed(None)
670         d = self._node.has_child(name)
671         def _got(present):
672             if present:
673                 raise NoReplacementError("There was already a child by that "
674                                          "name, and you asked me to not "
675                                          "replace it.")
676             return None
677         d.addCallback(_got)
678         return d
679
680     def renderHTTP(self, ctx):
681         req = inevow.IRequest(ctx)
682
683         if "t" in req.args:
684             t = req.args["t"][0]
685         else:
686             t = req.fields["t"].value
687
688         name = None
689         if "name" in req.args:
690             name = req.args["name"][0]
691         elif "name" in req.fields:
692             name = req.fields["name"].value
693         if name and "/" in name:
694             req.setResponseCode(http.BAD_REQUEST)
695             req.setHeader("content-type", "text/plain")
696             return "name= may not contain a slash"
697         if name is not None:
698             name = name.strip()
699         # we allow the user to delete an empty-named file, but not to create
700         # them, since that's an easy and confusing mistake to make
701
702         when_done = None
703         if "when_done" in req.args:
704             when_done = req.args["when_done"][0]
705         if "when_done" in req.fields:
706             when_done = req.fields["when_done"].value
707
708         if "replace" in req.fields:
709             if req.fields["replace"].value.lower() in ("false", "0"):
710                 self._replace = False
711
712         if t == "mkdir":
713             if not name:
714                 raise RuntimeError("mkdir requires a name")
715             d = self._check_replacement(name)
716             d.addCallback(lambda res: self._node.create_empty_directory(name))
717             def _done(res):
718                 return "directory created"
719             d.addCallback(_done)
720         elif t == "uri":
721             if not name:
722                 raise RuntimeError("set-uri requires a name")
723             if "uri" in req.args:
724                 newuri = req.args["uri"][0].strip()
725             else:
726                 newuri = req.fields["uri"].value.strip()
727             d = self._check_replacement(name)
728             d.addCallback(lambda res: self._node.set_uri(name, newuri))
729             def _done(res):
730                 return newuri
731             d.addCallback(_done)
732         elif t == "delete":
733             if name is None:
734                 # apparently an <input type="hidden" name="name" value="">
735                 # won't show up in the resulting encoded form.. the 'name'
736                 # field is completely missing. So to allow deletion of an
737                 # empty file, we have to pretend that None means ''. The only
738                 # downide of this is a slightly confusing error message if
739                 # someone does a POST without a name= field. For our own HTML
740                 # thisn't a big deal, because we create the 'delete' POST
741                 # buttons ourselves.
742                 name = ''
743             d = self._node.delete(name)
744             def _done(res):
745                 return "thing deleted"
746             d.addCallback(_done)
747         elif t == "rename":
748             from_name = 'from_name' in req.fields and req.fields["from_name"].value
749             if from_name is not None:
750                 from_name = from_name.strip()
751             to_name = 'to_name' in req.fields and req.fields["to_name"].value
752             if to_name is not None:
753                 to_name = to_name.strip()
754             if not from_name or not to_name:
755                 raise RuntimeError("rename requires from_name and to_name")
756             if not IDirectoryNode.providedBy(self._node):
757                 raise RuntimeError("rename must only be called on directories")
758             for k,v in [ ('from_name', from_name), ('to_name', to_name) ]:
759                 if v and "/" in v:
760                     req.setResponseCode(http.BAD_REQUEST)
761                     req.setHeader("content-type", "text/plain")
762                     return "%s= may not contain a slash" % (k,)
763             d = self._check_replacement(to_name)
764             d.addCallback(lambda res: self._node.get(from_name))
765             def add_dest(child):
766                 uri = child.get_uri()
767                 # now actually do the rename
768                 return self._node.set_uri(to_name, uri)
769             d.addCallback(add_dest)
770             def rm_src(junk):
771                 return self._node.delete(from_name)
772             d.addCallback(rm_src)
773             def _done(res):
774                 return "thing renamed"
775             d.addCallback(_done)
776
777         elif t == "upload":
778             if "mutable" in req.fields:
779                 contents = req.fields["file"]
780                 name = name or contents.filename
781                 if name is not None:
782                     name = name.strip()
783                 if not name:
784                     raise RuntimeError("upload-mutable requires a name")
785                 # SDMF: files are small, and we can only upload data.
786                 contents.file.seek(0)
787                 data = contents.file.read()
788                 uploadable = upload.FileHandle(contents.file)
789                 d = self._check_replacement(name)
790                 d.addCallback(lambda res: self._node.has_child(name))
791                 def _checked(present):
792                     if present:
793                         # modify the existing one instead of creating a new
794                         # one
795                         d2 = self._node.get(name)
796                         def _got_newnode(newnode):
797                             d3 = newnode.replace(data)
798                             d3.addCallback(lambda res: newnode.get_uri())
799                             return d3
800                         d2.addCallback(_got_newnode)
801                     else:
802                         d2 = IClient(ctx).create_mutable_file(data)
803                         def _uploaded(newnode):
804                             d1 = self._node.set_node(name, newnode)
805                             d1.addCallback(lambda res: newnode.get_uri())
806                             return d1
807                         d2.addCallback(_uploaded)
808                     return d2
809                 d.addCallback(_checked)
810             else:
811                 contents = req.fields["file"]
812                 name = name or contents.filename
813                 if name is not None:
814                     name = name.strip()
815                 if not name:
816                     raise RuntimeError("upload requires a name")
817                 uploadable = upload.FileHandle(contents.file)
818                 d = self._check_replacement(name)
819                 d.addCallback(lambda res: self._node.add_file(name, uploadable))
820                 def _done(newnode):
821                     return newnode.get_uri()
822                 d.addCallback(_done)
823
824         elif t == "overwrite":
825             contents = req.fields["file"]
826             # SDMF: files are small, and we can only upload data.
827             contents.file.seek(0)
828             data = contents.file.read()
829             # TODO: 'name' handling needs review
830             d = defer.succeed(self._node)
831             def _got_child(child_node):
832                 child_node.replace(data)
833                 return child_node.get_uri()
834             d.addCallback(_got_child)
835
836         elif t == "check":
837             d = self._node.get(name)
838             def _got_child(child_node):
839                 d2 = child_node.check()
840                 def _done(res):
841                     log.msg("checked %s, results %s" % (child_node, res))
842                     return str(res)
843                 d2.addCallback(_done)
844                 return d2
845             d.addCallback(_got_child)
846         else:
847             print "BAD t=%s" % t
848             return "BAD t=%s" % t
849         if when_done:
850             d.addCallback(lambda res: url.URL.fromString(when_done))
851         def _check_replacement(f):
852             # TODO: make this more human-friendly: maybe send them to the
853             # when_done page but with an extra query-arg that will display
854             # the error message in a big box at the top of the page. The
855             # directory page that when_done= usually points to accepts a
856             # result= argument.. use that.
857             f.trap(NoReplacementError)
858             req.setResponseCode(http.CONFLICT)
859             req.setHeader("content-type", "text/plain")
860             return str(f.value)
861         d.addErrback(_check_replacement)
862         return d
863
864 class DELETEHandler(rend.Page):
865     def __init__(self, node, name):
866         self._node = node
867         self._name = name
868
869     def renderHTTP(self, ctx):
870         req = inevow.IRequest(ctx)
871         d = self._node.delete(self._name)
872         def _done(res):
873             # what should this return??
874             return "%s deleted" % self._name
875         d.addCallback(_done)
876         def _trap_missing(f):
877             f.trap(KeyError)
878             req.setResponseCode(http.NOT_FOUND)
879             req.setHeader("content-type", "text/plain")
880             return "no such child %s" % self._name
881         d.addErrback(_trap_missing)
882         return d
883
884 class PUTHandler(rend.Page):
885     def __init__(self, node, path, t, localfile, localdir, replace):
886         self._node = node
887         self._path = path
888         self._t = t
889         self._localfile = localfile
890         self._localdir = localdir
891         self._replace = replace
892
893     def renderHTTP(self, ctx):
894         req = inevow.IRequest(ctx)
895         t = self._t
896         localfile = self._localfile
897         localdir = self._localdir
898
899         # we must traverse the path, creating new directories as necessary
900         d = self._get_or_create_directories(self._node, self._path[:-1])
901         name = self._path[-1]
902         d.addCallback(self._check_replacement, name, self._replace)
903         if t == "upload":
904             if localfile:
905                 d.addCallback(self._upload_localfile, localfile, name)
906             elif localdir:
907                 # take the last step
908                 d.addCallback(self._get_or_create_directories, self._path[-1:])
909                 d.addCallback(self._upload_localdir, localdir)
910             else:
911                 raise RuntimeError("t=upload requires localfile= or localdir=")
912         elif t == "uri":
913             d.addCallback(self._attach_uri, req.content, name)
914         elif t == "mkdir":
915             d.addCallback(self._mkdir, name)
916         else:
917             d.addCallback(self._upload_file, req.content, name)
918         def _check_blocking(f):
919             f.trap(BlockingFileError)
920             req.setResponseCode(http.BAD_REQUEST)
921             req.setHeader("content-type", "text/plain")
922             return str(f.value)
923         d.addErrback(_check_blocking)
924         def _check_replacement(f):
925             f.trap(NoReplacementError)
926             req.setResponseCode(http.CONFLICT)
927             req.setHeader("content-type", "text/plain")
928             return str(f.value)
929         d.addErrback(_check_replacement)
930         return d
931
932     def _get_or_create_directories(self, node, path):
933         if not IDirectoryNode.providedBy(node):
934             # unfortunately it is too late to provide the name of the
935             # blocking directory in the error message.
936             raise BlockingFileError("cannot create directory because there "
937                                     "is a file in the way")
938         if not path:
939             return defer.succeed(node)
940         d = node.get(path[0])
941         def _maybe_create(f):
942             f.trap(KeyError)
943             return node.create_empty_directory(path[0])
944         d.addErrback(_maybe_create)
945         d.addCallback(self._get_or_create_directories, path[1:])
946         return d
947
948     def _check_replacement(self, node, name, replace):
949         if replace:
950             return node
951         d = node.has_child(name)
952         def _got(present):
953             if present:
954                 raise NoReplacementError("There was already a child by that "
955                                          "name, and you asked me to not "
956                                          "replace it.")
957             return node
958         d.addCallback(_got)
959         return d
960
961     def _mkdir(self, node, name):
962         d = node.create_empty_directory(name)
963         def _done(newnode):
964             return newnode.get_uri()
965         d.addCallback(_done)
966         return d
967
968     def _upload_file(self, node, contents, name):
969         uploadable = upload.FileHandle(contents)
970         d = node.add_file(name, uploadable)
971         def _done(filenode):
972             log.msg("webish upload complete")
973             return filenode.get_uri()
974         d.addCallback(_done)
975         return d
976
977     def _upload_localfile(self, node, localfile, name):
978         uploadable = upload.FileName(localfile)
979         d = node.add_file(name, uploadable)
980         d.addCallback(lambda filenode: filenode.get_uri())
981         return d
982
983     def _attach_uri(self, parentnode, contents, name):
984         newuri = contents.read().strip()
985         d = parentnode.set_uri(name, newuri)
986         def _done(res):
987             return newuri
988         d.addCallback(_done)
989         return d
990
991     def _upload_localdir(self, node, localdir):
992         # build up a list of files to upload
993         all_files = []
994         all_dirs = []
995         msg = "No files to upload! %s is empty" % localdir
996         if not os.path.exists(localdir):
997             msg = "%s doesn't exist!" % localdir
998         for root, dirs, files in os.walk(localdir):
999             if root == localdir:
1000                 path = ()
1001             else:
1002                 relative_root = root[len(localdir)+1:]
1003                 path = tuple(relative_root.split(os.sep))
1004             for d in dirs:
1005                 all_dirs.append(path + (d,))
1006             for f in files:
1007                 all_files.append(path + (f,))
1008         d = defer.succeed(msg)
1009         for dir in all_dirs:
1010             if dir:
1011                 d.addCallback(self._makedir, node, dir)
1012         for f in all_files:
1013             d.addCallback(self._upload_one_file, node, localdir, f)
1014         return d
1015
1016     def _makedir(self, res, node, dir):
1017         d = defer.succeed(None)
1018         # get the parent. As long as os.walk gives us parents before
1019         # children, this ought to work
1020         d.addCallback(lambda res: node.get_child_at_path(dir[:-1]))
1021         # then create the child directory
1022         d.addCallback(lambda parent: parent.create_empty_directory(dir[-1]))
1023         return d
1024
1025     def _upload_one_file(self, res, node, localdir, f):
1026         # get the parent. We can be sure this exists because we already
1027         # went through and created all the directories we require.
1028         localfile = os.path.join(localdir, *f)
1029         d = node.get_child_at_path(f[:-1])
1030         d.addCallback(self._upload_localfile, localfile, f[-1])
1031         return d
1032
1033
1034 class Manifest(rend.Page):
1035     docFactory = getxmlfile("manifest.xhtml")
1036     def __init__(self, dirnode, dirpath):
1037         self._dirnode = dirnode
1038         self._dirpath = dirpath
1039
1040     def dirpath_as_string(self):
1041         return "/" + "/".join(self._dirpath)
1042
1043     def render_title(self, ctx):
1044         return T.title["Manifest of %s" % self.dirpath_as_string()]
1045
1046     def render_header(self, ctx):
1047         return T.p["Manifest of %s" % self.dirpath_as_string()]
1048
1049     def data_items(self, ctx, data):
1050         return self._dirnode.build_manifest()
1051
1052     def render_row(self, ctx, refresh_cap):
1053         ctx.fillSlots("refresh_capability", refresh_cap)
1054         return ctx.tag
1055
1056 class VDrive(rend.Page):
1057
1058     def __init__(self, node, name):
1059         self.node = node
1060         self.name = name
1061
1062     def get_child_at_path(self, path):
1063         if path:
1064             return self.node.get_child_at_path(path)
1065         return defer.succeed(self.node)
1066
1067     def locateChild(self, ctx, segments):
1068         req = inevow.IRequest(ctx)
1069         method = req.method
1070         path = segments
1071
1072         # when we're pointing at a directory (like /uri/$DIR_URI/my_pix),
1073         # Directory.addSlash causes a redirect to /uri/$DIR_URI/my_pix/,
1074         # which appears here as ['my_pix', '']. This is supposed to hit the
1075         # same Directory as ['my_pix'].
1076         if path and path[-1] == '':
1077             path = path[:-1]
1078
1079         t = ""
1080         if "t" in req.args:
1081             t = req.args["t"][0]
1082
1083         localfile = None
1084         if "localfile" in req.args:
1085             localfile = req.args["localfile"][0]
1086             if localfile != os.path.abspath(localfile):
1087                 return NeedAbsolutePathError(), ()
1088         localdir = None
1089         if "localdir" in req.args:
1090             localdir = req.args["localdir"][0]
1091             if localdir != os.path.abspath(localdir):
1092                 return NeedAbsolutePathError(), ()
1093         if localfile or localdir:
1094             if not ILocalAccess(ctx).local_access_is_allowed():
1095                 return LocalAccessDisabledError(), ()
1096             if req.getHost().host != LOCALHOST:
1097                 return NeedLocalhostError(), ()
1098         # TODO: think about clobbering/revealing config files and node secrets
1099
1100         replace = True
1101         if "replace" in req.args:
1102             if req.args["replace"][0].lower() in ("false", "0"):
1103                 replace = False
1104
1105         if method == "GET":
1106             # the node must exist, and our operation will be performed on the
1107             # node itself.
1108             d = self.get_child_at_path(path)
1109             def file_or_dir(node):
1110                 if (IFileNode.providedBy(node)
1111                     or IMutableFileNode.providedBy(node)):
1112                     filename = "unknown"
1113                     if path:
1114                         filename = path[-1]
1115                     if "filename" in req.args:
1116                         filename = req.args["filename"][0]
1117                     if t == "download":
1118                         if localfile:
1119                             # write contents to a local file
1120                             return LocalFileDownloader(node, localfile), ()
1121                         # send contents as the result
1122                         return FileDownloader(node, filename), ()
1123                     elif t == "":
1124                         # send contents as the result
1125                         return FileDownloader(node, filename), ()
1126                     elif t == "json":
1127                         return FileJSONMetadata(node), ()
1128                     elif t == "uri":
1129                         return FileURI(node), ()
1130                     elif t == "readonly-uri":
1131                         return FileReadOnlyURI(node), ()
1132                     else:
1133                         raise RuntimeError("bad t=%s" % t)
1134                 elif IDirectoryNode.providedBy(node):
1135                     if t == "download":
1136                         if localdir:
1137                             # recursive download to a local directory
1138                             return LocalDirectoryDownloader(node, localdir), ()
1139                         raise RuntimeError("t=download requires localdir=")
1140                     elif t == "":
1141                         # send an HTML representation of the directory
1142                         return Directory(self.name, node, path), ()
1143                     elif t == "json":
1144                         return DirectoryJSONMetadata(node), ()
1145                     elif t == "uri":
1146                         return DirectoryURI(node), ()
1147                     elif t == "readonly-uri":
1148                         return DirectoryReadonlyURI(node), ()
1149                     elif t == "manifest":
1150                         return Manifest(node, path), ()
1151                     elif t == 'rename-form':
1152                         return RenameForm(self.name, node, path), ()
1153                     else:
1154                         raise RuntimeError("bad t=%s" % t)
1155                 else:
1156                     raise RuntimeError("unknown node type")
1157             d.addCallback(file_or_dir)
1158         elif method == "POST":
1159             # the node must exist, and our operation will be performed on the
1160             # node itself.
1161             d = self.get_child_at_path(path)
1162             def _got(node):
1163                 return POSTHandler(node, replace), ()
1164             d.addCallback(_got)
1165         elif method == "DELETE":
1166             # the node must exist, and our operation will be performed on its
1167             # parent node.
1168             assert path # you can't delete the root
1169             d = self.get_child_at_path(path[:-1])
1170             def _got(node):
1171                 return DELETEHandler(node, path[-1]), ()
1172             d.addCallback(_got)
1173         elif method in ("PUT",):
1174             # the node may or may not exist, and our operation may involve
1175             # all the ancestors of the node.
1176             return PUTHandler(self.node, path, t, localfile, localdir, replace), ()
1177         else:
1178             return rend.NotFound
1179         def _trap_KeyError(f):
1180             f.trap(KeyError)
1181             return rend.FourOhFour(), ()
1182         d.addErrback(_trap_KeyError)
1183         return d
1184
1185 class URIPUTHandler(rend.Page):
1186     def renderHTTP(self, ctx):
1187         req = inevow.IRequest(ctx)
1188         assert req.method == "PUT"
1189
1190         t = ""
1191         if "t" in req.args:
1192             t = req.args["t"][0]
1193
1194         if t == "":
1195             # "PUT /uri", to create an unlinked file. This is like PUT but
1196             # without the associated set_uri.
1197             uploadable = upload.FileHandle(req.content)
1198             d = IClient(ctx).upload(uploadable)
1199             # that fires with the URI of the new file
1200             return d
1201
1202         if t == "mkdir":
1203             # "PUT /uri?t=mkdir", to create an unlinked directory.
1204             d = IClient(ctx).create_empty_dirnode()
1205             d.addCallback(lambda dirnode: dirnode.get_uri())
1206             return d
1207
1208         req.setResponseCode(http.BAD_REQUEST)
1209         req.setHeader("content-type", "text/plain")
1210         return "/uri only accepts PUT and PUT?t=mkdir"
1211
1212
1213 class Root(rend.Page):
1214
1215     addSlash = True
1216     docFactory = getxmlfile("welcome.xhtml")
1217
1218     def locateChild(self, ctx, segments):
1219         client = IClient(ctx)
1220         req = inevow.IRequest(ctx)
1221
1222         if segments[0] == "uri":
1223             if len(segments) == 1 or segments[1] == '':
1224                 if "uri" in req.args:
1225                     uri = req.args["uri"][0].replace("/", "!")
1226                     there = url.URL.fromContext(ctx)
1227                     there = there.clear("uri")
1228                     there = there.child("uri").child(uri)
1229                     return there, ()
1230             if len(segments) == 1 and req.method == "PUT":
1231                 # /uri
1232                 # either "PUT /uri" to create an unlinked file, or
1233                 # "PUT /uri?t=mkdir" to create an unlinked directory
1234                 return URIPUTHandler(), ()
1235             if len(segments) < 2:
1236                 return rend.NotFound
1237             uri = segments[1].replace("!", "/")
1238             d = defer.maybeDeferred(client.create_node_from_uri, uri)
1239             d.addCallback(lambda node: VDrive(node, "from-uri"))
1240             d.addCallback(lambda vd: vd.locateChild(ctx, segments[2:]))
1241             def _trap_KeyError(f):
1242                 f.trap(KeyError)
1243                 return rend.FourOhFour(), ()
1244             d.addErrback(_trap_KeyError)
1245             return d
1246         elif segments[0] == "xmlrpc":
1247             raise NotImplementedError()
1248         return rend.Page.locateChild(self, ctx, segments)
1249
1250     child_webform_css = webform.defaultCSS
1251     child_tahoe_css = nevow_File(util.sibpath(__file__, "web/tahoe.css"))
1252
1253     child_provisioning = provisioning.ProvisioningTool()
1254
1255     def data_version(self, ctx, data):
1256         v = IClient(ctx).get_versions()
1257         return "tahoe: %s, zfec: %s, foolscap: %s, twisted: %s" % \
1258                (v['allmydata'], v['zfec'], v['foolscap'], v['twisted'])
1259
1260     def data_my_nodeid(self, ctx, data):
1261         return b32encode(IClient(ctx).nodeid).lower()
1262     def data_introducer_furl(self, ctx, data):
1263         return IClient(ctx).introducer_furl
1264     def data_connected_to_introducer(self, ctx, data):
1265         if IClient(ctx).connected_to_introducer():
1266             return "yes"
1267         return "no"
1268     def data_num_peers(self, ctx, data):
1269         #client = inevow.ISite(ctx)._client
1270         client = IClient(ctx)
1271         return len(list(client.get_all_peerids()))
1272
1273     def data_peers(self, ctx, data):
1274         d = []
1275         client = IClient(ctx)
1276         for nodeid in sorted(client.get_all_peerids()):
1277             row = (b32encode(nodeid).lower(),)
1278             d.append(row)
1279         return d
1280
1281     def render_row(self, ctx, data):
1282         (nodeid_a,) = data
1283         ctx.fillSlots("peerid", nodeid_a)
1284         return ctx.tag
1285
1286     def render_private_vdrive(self, ctx, data):
1287         basedir = IClient(ctx).basedir
1288         start_html = os.path.abspath(os.path.join(basedir, "start.html"))
1289         if os.path.exists(start_html):
1290             return T.p["To view your personal private non-shared filestore, ",
1291                        "use this browser to open the following file from ",
1292                        "your local filesystem:",
1293                        T.pre[start_html],
1294                        ]
1295         return T.p["personal vdrive not available."]
1296
1297     # this is a form where users can download files by URI
1298
1299     def render_download_form(self, ctx, data):
1300         form = T.form(action="uri", method="get",
1301                       enctype="multipart/form-data")[
1302             T.fieldset[
1303             T.legend(class_="freeform-form-label")["Download a file"],
1304             "URI of file to download: ",
1305             T.input(type="text", name="uri"), " ",
1306             "Filename to download as: ",
1307             T.input(type="text", name="filename"), " ",
1308             T.input(type="submit", value="Download"),
1309             ]]
1310         return T.div[form]
1311
1312
1313 class LocalAccess:
1314     implements(ILocalAccess)
1315     def __init__(self):
1316         self.local_access = False
1317     def local_access_is_allowed(self):
1318         return self.local_access
1319
1320 class WebishServer(service.MultiService):
1321     name = "webish"
1322
1323     def __init__(self, webport):
1324         service.MultiService.__init__(self)
1325         self.webport = webport
1326         self.root = Root()
1327         self.site = site = appserver.NevowSite(self.root)
1328         self.site.requestFactory = MyRequest
1329         self.allow_local = LocalAccess()
1330         self.site.remember(self.allow_local, ILocalAccess)
1331         s = strports.service(webport, site)
1332         s.setServiceParent(self)
1333         self.listener = s # stash it so the tests can query for the portnum
1334         self._started = defer.Deferred()
1335
1336     def allow_local_access(self, enable=True):
1337         self.allow_local.local_access = enable
1338
1339     def startService(self):
1340         service.MultiService.startService(self)
1341         # to make various services available to render_* methods, we stash a
1342         # reference to the client on the NevowSite. This will be available by
1343         # adapting the 'context' argument to a special marker interface named
1344         # IClient.
1345         self.site.remember(self.parent, IClient)
1346         # I thought you could do the same with an existing interface, but
1347         # apparently 'ISite' does not exist
1348         #self.site._client = self.parent
1349         self._started.callback(None)
1350
1351     def create_start_html(self, private_uri, startfile, nodeurl_file):
1352         """
1353         Returns a deferred that eventually fires once the start.html page has
1354         been created.
1355         """
1356         self._started.addCallback(self._create_start_html, private_uri, startfile, nodeurl_file)
1357         return self._started
1358
1359     def _create_start_html(self, dummy, private_uri, startfile, nodeurl_file):
1360         f = open(startfile, "w")
1361         os.chmod(startfile, 0600)
1362         template = open(util.sibpath(__file__, "web/start.html"), "r").read()
1363         # what is our webport?
1364         s = self.listener
1365         if isinstance(s, internet.TCPServer):
1366             base_url = "http://localhost:%d" % s._port.getHost().port
1367         elif isinstance(s, internet.SSLServer):
1368             base_url = "https://localhost:%d" % s._port.getHost().port
1369         else:
1370             base_url = "UNKNOWN"  # this will break the href
1371             # TODO: emit a start.html that explains that we don't know
1372             # how to create a suitable URL
1373         if private_uri:
1374             private_uri = private_uri.replace("/","!")
1375             link_to_private_uri = "View <a href=\"%s/uri/%s\">your personal private non-shared filestore</a>." % (base_url, private_uri)
1376             fields = {"link_to_private_uri": link_to_private_uri,
1377                       "base_url": base_url,
1378                       }
1379         else:
1380             fields = {"link_to_private_uri": "",
1381                       "base_url": base_url,
1382                       }
1383         f.write(template % fields)
1384         f.close()
1385
1386         f = open(nodeurl_file, "w")
1387         # this file is world-readable
1388         f.write(base_url + "\n")
1389         f.close()