10 from html.parser import HTMLParser
11 from http import HTTPStatus
12 from http.client import HTTPMessage
13 from threading import Thread
14 from typing import Dict, Awaitable, NoReturn, cast, Any, Optional, List, Tuple, AsyncGenerator
15 from urllib.parse import unquote
16 from urllib.request import urlopen
28 def __init__(self) -> None:
29 self.port = int(os.getenv("PORT", "8000"))
30 # upstream file url, should end in slash
31 self.upstream = os.getenv("UPSTREAM", "http://beta.xonotic.org/autobuild-bsp/")
32 self.dlcache = os.getenv("DLCACHE", os.path.join(os.getcwd(), "dlcache"))
40 def __init__(self, config: Config) -> None:
42 os.makedirs(config.dlcache, exist_ok=True)
43 self._files: Dict[str, Awaitable[bool]] = {}
45 async def file_get(self, name: str) -> None:
46 url = self.config.upstream + name
47 future = self._files.get(url)
50 future = asyncio.get_event_loop().create_future()
51 self._files[url] = future
52 out = os.path.join(self.config.dlcache, name)
53 with open(out, "wb") as f:
54 async for buf in fetch(url):
56 future.set_result(True)
58 print("using existing")
61 async def file_wait(self, url: str) -> None:
62 await self._files[url]
64 async def ls(self) -> List[str]:
65 class Parser(HTMLParser):
68 def __init__(self) -> None:
70 self.paks: List[str] = []
72 def handle_starttag(self, tag: str, attrs: List[Tuple[str, str]]) -> None:
74 file: Optional[str] = next((unquote(v) for (k, v) in attrs if k == "href"), None)
75 if file and file.endswith(".pk3"):
76 self.paks.append(file)
80 async for buf in fetch(self.config.upstream):
83 parser.feed(buf.decode(UTF_8))
91 def router() -> Router:
92 return RouterCombinator(
97 class FileFetch(Router):
98 async def __call__(self, path: str, req: Request) -> Response:
99 await app.file_get(path)
104 class FileList(Router):
105 async def __call__(self, path: str, req: Request) -> Response:
107 "list": await app.ls(),
110 loop = asyncio.get_event_loop()
111 start_server(config, loop, router())
114 except KeyboardInterrupt:
124 Response = Optional[dict]
128 async def __call__(self, path: str, req: Request) -> Response:
132 class RouterCombinator(Router):
133 __slots__ = "_routers"
135 def __init__(self, **kwargs: Any) -> None:
136 self._routers: Dict[str, Router] = kwargs
138 async def __call__(self, path: str, req: Request) -> Response:
140 args: List[str] = path.split("/", 1)
142 rest = args[1] if len(args) > 1 else None
148 route = self._routers.get(name, None)
151 return await route(rest or "", req)
154 async def fetch(url: str) -> AsyncGenerator[bytes, None]:
155 res = cast(Any, urlopen(url))
156 msg: HTTPMessage = res.info()
158 length = msg.get("Content-length")
159 file_size = int(str(length)) if length else None
160 print(f"downloading {file_size or '???'} bytes...")
163 buf: bytes = res.read(IO_BLOCK)
167 print(f"downloaded {progress}/{file_size or '???'} bytes")
169 await asyncio.sleep(0)
172 def start_server(config: Config, loop: asyncio.AbstractEventLoop, router: Router) -> None:
173 async def on_message(req: http.server.BaseHTTPRequestHandler) -> None:
174 ret = await router(req.path, Request())
177 req.send_response(HTTPStatus.OK)
178 req.send_header("Content-Type", "application/json")
179 s = json.dumps(ret, indent=2).encode(UTF_8)
180 req.send_header("Content-Length", str(len(s)))
186 shutil.copyfileobj(f, req.wfile)
190 def serve(loop: asyncio.AbstractEventLoop) -> NoReturn:
191 class ThreadingHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
194 class RequestHandler(http.server.BaseHTTPRequestHandler):
195 def do_GET(self) -> None:
196 asyncio.run_coroutine_threadsafe(on_message(self), loop).result()
198 with ThreadingHTTPServer(("", config.port), RequestHandler) as httpd:
199 print("serving at port", config.port)
200 httpd.serve_forever()
202 assert False, "Unreachable"
204 server = Thread(target=serve, args=(loop,))
209 if __name__ == "__main__":