Switch dev server from Python to PHP built-in server

The Python SimpleHTTPRequestHandler served api.php as a static file
instead of executing it, breaking registration, login and all API calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 11:50:31 +02:00
parent dcc1205244
commit 5d02a682b0

110
server.py
View File

@@ -1,110 +1,20 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
tykkää.fi dev server tykkää.fi dev server — launches PHP built-in server
- Serves static files (GET) so that api.php, upload.php and static files all work.
- Accepts image uploads (POST /upload) → saves to images/
""" """
import http.server, json, mimetypes, os, re, socketserver, time import os, subprocess, sys
from pathlib import Path from pathlib import Path
PORT = 3000 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__': if __name__ == '__main__':
os.chdir(Path(__file__).parent) os.chdir(Path(__file__).parent)
print(f'tykkää.fi running at http://localhost:{PORT}') print(f'tykkää.fi running at http://localhost:{PORT}')
with socketserver.TCPServer(('', PORT), Handler) as srv: try:
srv.serve_forever() subprocess.run(
['php', '-S', f'localhost:{PORT}'],
check=True,
)
except KeyboardInterrupt:
print('\nSammutettu.')