DIY image hosting
I spent a couple of hours throwing together a quick Python HTTP server that I can use to run image hosting on this machine. It’s super ugly and not at all responsive to errors, so I have systemd just restart the script on failure. On the plus side, deployment is just a git clone
. The script is running behind Nginx so I don’t have to worry about SSL here. I’m using basic auth to prevent everyone else from uploading images. Any non-image requests get proxied to the script, while image requests get a cache header added and are served normally:
server {
listen 443 ssl http2;
listen [::]:443 http2;
server_name i.qasima.li;
root /var/www/i.qasima.li;
ssl_certificate /etc/letsencrypt/live/i.qasima.li/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/i.qasima.li/privkey.pem;
client_max_body_size 50m;
location / {
auth_basic "Image upload";
auth_basic_user_file /etc/nginx/i.qasima.li.htpasswd;
proxy_pass http://127.0.0.1:8000;
}
location ~* .*\.(jpe?g|png|gif|tiff?|webp)$ {
access_log off;
add_header Cache-Control "public, max-age=86400";
}
}
Since nginx is the one serving files, the script responds to all GET
s with an upload page. On POST
, we read out the uploaded image’s mime type so we know what to save it as. Then the script just copies the image to the resulting directory.
#!/usr/bin/env python3
from http.server import HTTPServer, BaseHTTPRequestHandler
from io import BytesIO
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
homepage = b"""
<!DOCTYPE html>
<html><head><meta name="viewport" content="width=device-width, initial-scale=1.0"></head>
<body style="padding: 10px;">
<form action="/" enctype="multipart/form-data" method="post">
<div style="margin:10px auto"><input type="file" accept="image/*" name="i" style="font-size: 16px;"></div>
<div><input type="submit" value="Upload" style="width: 100%;height: 60px;font-size: 23px;"></div>
</form>
</body></html>"""
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(self.homepage)
def do_POST(self):
content_type = self.headers["Content-Type"]
if not content_type.startswith("multipart/form-data"):
self.send_response(400)
self.end_headers()
response = BytesIO()
response.write(
bytes("Wrong content type " + content_type, encoding="utf-8")
)
self.wfile.write(response.getvalue())
return
content_length = int(self.headers["Content-Length"])
body = self.rfile.read(content_length)
lines = body.split(b"\r\n")
boundary = lines[0]
file_content_type = lines[2]
lead_up = "Content-Type: image/"
ftype = str(file_content_type[len(lead_up) :], encoding="utf-8")
import random, string
target_filename = (
"".join(
random.choice(string.ascii_lowercase + string.digits) for _ in range(5)
)
+ "."
+ ftype
)
end_boundary_start = body.rfind(boundary) - len(b"\r\n")
file_start = len(b"\r\n".join(lines[0:3])) + 2 * len(b"\r\n")
with open("/var/www/i.qasima.li/" + target_filename, "wb") as output:
output.write(body[file_start:end_boundary_start])
self.send_response(301)
self.send_header('Location', '/' + target_filename)
self.end_headers()
self.wfile.write(b'')
if __name__ == "__main__":
httpd = HTTPServer(("localhost", 8000), SimpleHTTPRequestHandler)
httpd.serve_forever()
The systemd unit file is as barebones as can be:
[Unit]
Description=Runs /var/www/img/main.py for i.qasima.li
After=network.target
[Service]
Type=simple
User=www-data
ExecStart=/usr/bin/python3 /var/www/img/main.py
Restart=on-failure
[Install]
WantedBy=multi-user.target
It’s super janky, but I think it’s all I’ll need for now. Cleanup can be done in shell:
#!/usr/bin/env sh
# delete >= 30d old files
find /var/www/i.qasima.li/ -mtime +30 -exec rm {} \;