|
#!/usr/bin/env python3
|
|
import html
|
|
from http import HTTPStatus
|
|
from http.server import HTTPServer, SimpleHTTPRequestHandler, test
|
|
import io
|
|
from pathlib import Path
|
|
import sys
|
|
import urllib.parse
|
|
|
|
class CORSRequestHandler (SimpleHTTPRequestHandler):
|
|
def end_headers(self):
|
|
self.send_header('Access-Control-Allow-Origin', '*')
|
|
SimpleHTTPRequestHandler.end_headers(self)
|
|
|
|
def list_directory(self, path):
|
|
"""Helper to produce a directory listing (absent index.html).
|
|
|
|
Return value is either a file object, or None (indicating an
|
|
error). In either case, the headers are sent, making the
|
|
interface the same as for send_head().
|
|
|
|
"""
|
|
path = Path(path)
|
|
try:
|
|
entries = list(path.iterdir())
|
|
except OSError:
|
|
self.send_error(
|
|
HTTPStatus.NOT_FOUND,
|
|
"No permission to list directory")
|
|
return None
|
|
entries.sort(key=lambda a: (not a.is_dir(), a.name.lower()))
|
|
r = []
|
|
try:
|
|
displaypath = urllib.parse.unquote(self.path,
|
|
errors='surrogatepass')
|
|
except UnicodeDecodeError:
|
|
displaypath = urllib.parse.unquote(self.path)
|
|
escape = lambda x: html.escape(x, quote=False)
|
|
root = Path(self.directory)
|
|
displaypath = '/'.join(reversed([escape(path.name)]+[f'''<a href="{'/'.join(['..']*(i+1))}">{escape(d.name)}</a>''' if d.is_relative_to(root) else escape(d.name) for i,d in enumerate(path.parents)]))
|
|
enc = sys.getfilesystemencoding()
|
|
title = f''
|
|
out = f'''<!DOCTYPE HTML>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="{enc}">
|
|
<title>Directory listing for {displaypath}</title>
|
|
<style>
|
|
''' + '''
|
|
body {
|
|
font-family: sans-serif;
|
|
font-size: 16px;
|
|
}
|
|
|
|
ul {
|
|
display: grid;
|
|
grid-auto-flow: column;
|
|
grid-template-columns: repeat(3,1fr);
|
|
list-style: none;
|
|
gap: 0 1em;
|
|
}
|
|
|
|
li {
|
|
font-family: monospace;
|
|
line-height: 1.5;
|
|
grid-column: 3;
|
|
|
|
&.dir {
|
|
font-weight: bold;
|
|
grid-column: 2;
|
|
|
|
&.parent {
|
|
margin-bottom: 1em;
|
|
grid-column: 1;
|
|
}
|
|
}
|
|
|
|
& + li.file {
|
|
margin-top: 1em;
|
|
}
|
|
}
|
|
''' + f'''
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Static server at <code>{displaypath}</code></h1>
|
|
<ul>
|
|
'''
|
|
if path.parent.is_relative_to(self.directory):
|
|
out += f'''<li class="dir parent"><a href="..">← {path.parent.name}</a></li>'''
|
|
for f in entries:
|
|
name = f.name
|
|
displayname = linkname = f.name
|
|
# Append / for directories or @ for symbolic links
|
|
if f.is_dir():
|
|
displayname = name + "/"
|
|
linkname = name + "/"
|
|
if f.is_symlink():
|
|
displayname = name + "@"
|
|
# Note: a link to a directory displays with @ and links with /
|
|
url = urllib.parse.quote(linkname, errors='surrogatepass')
|
|
text = html.escape(displayname, quote=False)
|
|
css_class = 'dir' if f.is_dir() else 'file'
|
|
out += f'<li class="{css_class}"><a href="{url}">{text}</a></li>'
|
|
|
|
out += f'''
|
|
</ul>
|
|
</body>
|
|
</html>
|
|
'''
|
|
encoded = out.encode(enc, 'surrogateescape')
|
|
f = io.BytesIO()
|
|
f.write(encoded)
|
|
f.seek(0)
|
|
self.send_response(HTTPStatus.OK)
|
|
self.send_header("Content-type", "text/html; charset=%s" % enc)
|
|
self.send_header("Content-Length", str(len(encoded)))
|
|
self.end_headers()
|
|
return f
|
|
|
|
if __name__ == '__main__':
|
|
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8000
|
|
print(f"Using port {port}")
|
|
test(CORSRequestHandler, HTTPServer, port=port)
|