Initial commit: tykkää.fi sivusto
- 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>
This commit is contained in:
110
server.py
Normal file
110
server.py
Normal file
@@ -0,0 +1,110 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user