Skip to content

Commit

Permalink
dirserve: Python http.server decorated with timestamps and file sizes
Browse files Browse the repository at this point in the history
  • Loading branch information
Notgnoshi committed Nov 10, 2024
1 parent 22fd827 commit 50870b5
Showing 1 changed file with 136 additions and 0 deletions.
136 changes: 136 additions & 0 deletions stowdir/.local/bin/dirserve
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/usr/bin/env python3

import html
import http.server
import io
import os
import socket
import sys
import time
import urllib


def decoration(path) -> str:
mtime = time.ctime(os.path.getmtime(path))
mtime = html.escape(mtime, quote=False)
size = str(os.path.getsize(path)).rjust(16)
return f'<span style="white-space:pre;"><code>{mtime} {size} </code></span>'


# Copy pasta'd from Python 3.12.7, and modified to add timestamps and file sizes.
def list_directory_override(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().
"""
try:
list = os.listdir(path)
except OSError:
self.send_error(http.server.HTTPStatus.NOT_FOUND, "No permission to list directory")
return None
list.sort(key=lambda a: a.lower())
r = []
try:
displaypath = urllib.parse.unquote(self.path, errors="surrogatepass")
except UnicodeDecodeError:
displaypath = urllib.parse.unquote(self.path)
displaypath = html.escape(displaypath, quote=False)
enc = sys.getfilesystemencoding()
title = f"Directory listing for {displaypath}"
r.append("<!DOCTYPE HTML>")
r.append('<html lang="en">')
r.append("<head>")
r.append(f'<meta charset="{enc}">')
r.append(f"<title>{title}</title>\n</head>")
r.append(f"<body>\n<h1>{title}</h1>")
r.append("<hr>\n<ul>")
for name in list:
fullname = os.path.join(path, name)
displayname = linkname = name
# Append / for directories or @ for symbolic links
if os.path.isdir(fullname):
displayname = name + "/"
linkname = name + "/"
if os.path.islink(fullname):
displayname = name + "@"
# Note: a link to a directory displays with @ and links with /
r.append(
'<li>%s<a href="%s">%s</a></li>'
% (
decoration(fullname),
urllib.parse.quote(linkname, errors="surrogatepass"),
html.escape(displayname, quote=False),
)
)
r.append("</ul>\n<hr>\n</body>\n</html>\n")
encoded = "\n".join(r).encode(enc, "surrogateescape")
f = io.BytesIO()
f.write(encoded)
f.seek(0)
self.send_response(http.server.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


http.server.SimpleHTTPRequestHandler.list_directory = list_directory_override

# Unfortunately no way to just reach in and call this from the http.server module, so copy pasta it.
if __name__ == "__main__":
import argparse
import contextlib

parser = argparse.ArgumentParser()
parser.add_argument("--cgi", action="store_true", help="run as CGI server")
parser.add_argument(
"-b", "--bind", metavar="ADDRESS", help="bind to this address " "(default: all interfaces)"
)
parser.add_argument(
"-d",
"--directory",
default=os.getcwd(),
help="serve this directory " "(default: current directory)",
)
parser.add_argument(
"-p",
"--protocol",
metavar="VERSION",
default="HTTP/1.0",
help="conform to this HTTP version " "(default: %(default)s)",
)
parser.add_argument(
"port",
default=8000,
type=int,
nargs="?",
help="bind to this port " "(default: %(default)s)",
)
args = parser.parse_args()
if args.cgi:
handler_class = http.server.CGIHTTPRequestHandler
else:
handler_class = http.server.SimpleHTTPRequestHandler

# ensure dual-stack is not disabled; ref #38907
class DualStackServer(http.server.ThreadingHTTPServer):

def server_bind(self):
# suppress exception when protocol is IPv4
with contextlib.suppress(Exception):
self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
return super().server_bind()

def finish_request(self, request, client_address):
self.RequestHandlerClass(request, client_address, self, directory=args.directory)

http.server.test(
HandlerClass=handler_class,
ServerClass=DualStackServer,
port=args.port,
bind=args.bind,
protocol=args.protocol,
)

0 comments on commit 50870b5

Please sign in to comment.