- Julkaisualusta resepteille, neuloville, vinkeille - PHP-backend (api.php) palvelinpuolen datalle - Admin-paneeli salasanasuojauksella - Kuvaupload (upload.php) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
111 lines
3.4 KiB
Python
111 lines
3.4 KiB
Python
#!/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()
|