|
| 1 | + |
| 2 | +import hashlib |
| 3 | +import pathlib |
| 4 | +import re |
| 5 | +from functools import cached_property |
| 6 | + |
| 7 | +import yaml |
| 8 | + |
| 9 | +import aiohttp |
| 10 | +from aiohttp import web |
| 11 | + |
| 12 | +import abstracts |
| 13 | + |
| 14 | +from aio.web import exceptions, interface |
| 15 | + |
| 16 | + |
| 17 | +@abstracts.implementer(interface.IRepositoryRequest) |
| 18 | +class ARepositoryRequest(metaclass=abstracts.Abstraction): |
| 19 | + |
| 20 | + def __init__(self, url, config, request): |
| 21 | + self._url = url |
| 22 | + self.config = config |
| 23 | + self.request = request |
| 24 | + |
| 25 | + @property |
| 26 | + def requested_repo(self): |
| 27 | + return f"{self.request.match_info['owner']}/{self.request.match_info['repo']}" |
| 28 | + |
| 29 | + @property |
| 30 | + def url(self) -> str: |
| 31 | + return f"https://{self._url}/{self.requested_repo}/{self.path}" |
| 32 | + |
| 33 | + @property |
| 34 | + def path(self): |
| 35 | + return self.matched["path"] |
| 36 | + |
| 37 | + @property |
| 38 | + def sha(self): |
| 39 | + return self.matched["sha"] |
| 40 | + |
| 41 | + @cached_property |
| 42 | + def matched(self) -> dict: |
| 43 | + for repo in self.config: |
| 44 | + if not re.match(repo, self.requested_repo): |
| 45 | + continue |
| 46 | + |
| 47 | + for path, sha in self.config[repo].items(): |
| 48 | + if path == self.request.match_info["extra"]: |
| 49 | + return dict(path=path, sha=sha) |
| 50 | + return {} |
| 51 | + |
| 52 | + @property # type: ignore |
| 53 | + @abstracts.interfacemethod |
| 54 | + def downloader_class(self): |
| 55 | + raise NotImplementedError |
| 56 | + |
| 57 | + async def fetch(self): |
| 58 | + content = await self.downloader_class(self.url, self.sha).download() |
| 59 | + response = web.Response(body=content) |
| 60 | + response.headers["cache-control"] = "max-age=31536000" |
| 61 | + return response |
| 62 | + |
| 63 | + def match(self): |
| 64 | + if not self.matched: |
| 65 | + raise exceptions.MatchError() |
| 66 | + return self |
| 67 | + |
| 68 | + |
| 69 | +@abstracts.implementer(interface.IRepositoryMirrors) |
| 70 | +class ARepositoryMirrors(metaclass=abstracts.Abstraction): |
| 71 | + |
| 72 | + def __init__(self, config_path): |
| 73 | + self.config_path = config_path |
| 74 | + |
| 75 | + @cached_property |
| 76 | + def config(self): |
| 77 | + return yaml.safe_load(pathlib.Path(self.config_path).read_text()) |
| 78 | + |
| 79 | + @property # type: ignore |
| 80 | + @abstracts.interfacemethod |
| 81 | + def request_class(self): |
| 82 | + raise NotImplementedError |
| 83 | + |
| 84 | + async def match(self, request): |
| 85 | + host = request.match_info['host'] |
| 86 | + if host not in self.config: |
| 87 | + raise exceptions.MatchError() |
| 88 | + upstream_request = self.request_class(host, self.config[host], request) |
| 89 | + return upstream_request.match() |
0 commit comments