#!/usr/bin/env python3 """ tykkää.fi dev server - Serves static files (GET) - Accepts image uploads (POST /upload) → saves to images/ """ import http.server, json, mimetypes, os, re, socketserver, time from pathlib import Path PORT = 3000 IMAGES_DIR = Path('images') IMAGES_DIR.mkdir(exist_ok=True) ALLOWED_MIME = {'image/jpeg', 'image/png', 'image/gif', 'image/webp'} MAX_BYTES = 8 * 1024 * 1024 # 8 MB def parse_multipart(data: bytes, boundary: str): """Return list of (headers_dict, body_bytes) for each part.""" sep = ('--' + boundary).encode() parts = [] for chunk in data.split(sep): if not chunk or chunk in (b'\r\n', b'--\r\n', b'--'): continue chunk = chunk.lstrip(b'\r\n') if chunk.startswith(b'--'): continue if b'\r\n\r\n' not in chunk: continue hdr_raw, body = chunk.split(b'\r\n\r\n', 1) if body.endswith(b'\r\n'): body = body[:-2] headers = {} for line in hdr_raw.decode(errors='replace').split('\r\n'): if ':' in line: k, v = line.split(':', 1) headers[k.strip().lower()] = v.strip() parts.append((headers, body)) return parts class Handler(http.server.SimpleHTTPRequestHandler): def do_POST(self): if self.path != '/upload': self.send_error(404) return ct = self.headers.get('Content-Type', '') m = re.search(r'boundary=([^\s;]+)', ct) if not m: self.send_error(400, 'Missing boundary') return boundary = m.group(1).strip('"') length = int(self.headers.get('Content-Length', 0)) if length > MAX_BYTES + 4096: self.send_error(413, 'Request too large') return raw = self.rfile.read(length) parts = parse_multipart(raw, boundary) file_part = None for hdrs, body in parts: cd = hdrs.get('content-disposition', '') if 'name="file"' in cd: file_part = (hdrs, body) break if not file_part: self.send_error(400, 'No file part') return hdrs, data = file_part cd = hdrs.get('content-disposition', '') fn_match = re.search(r'filename="([^"]+)"', cd) filename = fn_match.group(1) if fn_match else 'upload.jpg' if len(data) > MAX_BYTES: self.send_error(413, 'File too large (max 8 MB)') return mime = mimetypes.guess_type(filename)[0] or '' if mime not in ALLOWED_MIME: self.send_error(415, 'Only images (jpeg/png/gif/webp) allowed') return ext = Path(filename).suffix.lower() or '.jpg' fname = f"{int(time.time() * 1000)}{ext}" (IMAGES_DIR / fname).write_bytes(data) self._json(200, {'url': f'images/{fname}'}) def _json(self, code, obj): body = json.dumps(obj).encode() self.send_response(code) self.send_header('Content-Type', 'application/json') self.send_header('Content-Length', str(len(body))) self.end_headers() self.wfile.write(body) def log_message(self, fmt, *args): pass # suppress request logs if __name__ == '__main__': os.chdir(Path(__file__).parent) print(f'tykkää.fi running at http://localhost:{PORT}') with socketserver.TCPServer(('', PORT), Handler) as srv: srv.serve_forever()