#!/usr/bin/env python3 """Private loopback demo server for the Grade 3 curriculum showcase.""" import argparse import json import mimetypes import os import re import sys import zipfile from http import HTTPStatus from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from urllib.parse import unquote, urlparse ROOT = Path(__file__).resolve().parent DEFAULT_DOWNLOADS = Path.home() / 'Downloads' / 'CoreKnowledge_Grades_1-5_EN_ES' sys.path.insert(0, str(ROOT / 'src')) from server_catalog import lookup_material, material_index class ShowcaseHandler(SimpleHTTPRequestHandler): def __init__(self, *args, downloads_dir=None, **kwargs): self.downloads_dir = Path(downloads_dir or DEFAULT_DOWNLOADS).resolve() super().__init__(*args, directory=str(ROOT), **kwargs) def catalog(self): return json.loads((ROOT / 'data' / 'grade3_catalog.json').read_text()) def send_json(self, data): body = json.dumps(data).encode('utf-8') self.send_response(HTTPStatus.OK) self.send_header('Content-Type', 'application/json; charset=utf-8') self.send_header('Content-Length', str(len(body))) self.send_header('Cache-Control', 'no-store') self.end_headers() self.wfile.write(body) def send_pdf(self, material_id): if not re.fullmatch(r'[0-9a-f]{20}', material_id): self.send_error(HTTPStatus.NOT_FOUND) return pair = lookup_material(material_index(self.catalog()), material_id) if pair is None: self.send_error(HTTPStatus.NOT_FOUND) return archive_name, internal_path = pair archive_path = (self.downloads_dir / archive_name).resolve() if archive_path.parent != self.downloads_dir: self.send_error(HTTPStatus.NOT_FOUND) return try: with zipfile.ZipFile(archive_path) as archive: content = archive.read(internal_path) except (FileNotFoundError, KeyError, zipfile.BadZipFile): self.send_error(HTTPStatus.NOT_FOUND) return filename = Path(internal_path).name self.send_response(HTTPStatus.OK) self.send_header('Content-Type', mimetypes.guess_type(filename)[0] or 'application/pdf') self.send_header('Content-Length', str(len(content))) self.send_header('Content-Disposition', 'inline; filename="%s"' % filename.replace('"', '')) self.send_header('Cache-Control', 'private, no-store') self.end_headers() self.wfile.write(content) def do_GET(self): path = urlparse(self.path).path if path == '/api/catalog': self.send_json(self.catalog()) return if path.startswith('/api/pdf/'): self.send_pdf(unquote(path[len('/api/pdf/'):])) return super().do_GET() def run(port, downloads_dir): def handler(*args, **kwargs): return ShowcaseHandler(*args, downloads_dir=downloads_dir, **kwargs) server = ThreadingHTTPServer(('127.0.0.1', port), handler) print('Serving Grade 3 showcase at http://127.0.0.1:%s' % port) server.serve_forever() if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--port', type=int, default=8899) parser.add_argument('--downloads-dir', type=Path, default=DEFAULT_DOWNLOADS) args = parser.parse_args() run(args.port, args.downloads_dir)