-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
dirserve: Python http.server decorated with timestamps and file sizes
- Loading branch information
Showing
1 changed file
with
136 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) |