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:
110
server.py
110
server.py
@@ -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.')
|
||||||
|
|||||||
Reference in New Issue
Block a user