mirror of
https://github.com/NoCLin/LightMirrors
synced 2025-07-27 12:30:31 +08:00
initial commit
This commit is contained in:
commit
79c1cdde2f
13
.env.example
Normal file
13
.env.example
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
# for caddy
|
||||
CLOUDFLARE_DNS_API_TOKEN=xxxx
|
||||
|
||||
# for caddy and mirrors
|
||||
BASE_DOMAIN=local.homeinfra.org
|
||||
|
||||
# for aria2 and mirrors
|
||||
RPC_SECRET=changeit
|
||||
|
||||
# for all, if you want set proxy for all services
|
||||
# aria2 need the lowercase one, FYI https://aria2.github.io/manual/en/html/aria2c.html#environment
|
||||
# all_proxy=
|
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
data/*
|
||||
!data/cache/.gitkeep
|
||||
caddy/caddy_data
|
||||
.idea
|
||||
.env
|
||||
node_modules
|
63
README.md
Normal file
63
README.md
Normal file
@ -0,0 +1,63 @@
|
||||
<div style="text-align: center">
|
||||
|
||||
# LightMirrors
|
||||
|
||||
LightMirrors是一个开源的缓存镜像站服务,用于加速软件包下载和镜像拉取。
|
||||
目前支持DockerHub、PyPI、PyTorch、NPM等镜像缓存服务。
|
||||
|
||||
|
||||
<a href='https://github.com/NoCLin/LightMirrors/'><img src='https://img.shields.io/badge/Light-Mirrors-green'></a>
|
||||
<a href='https://github.com/homeinfra-org/infra'><img src='https://img.shields.io/static/v1?label=Home&message=Infra&color=orange'></a>
|
||||
[](https://github.com/NoCLin/LightMirrors)
|
||||
[](https://github.com/NoCLin/LightMirrors)
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- docker + docker-compose.
|
||||
- 一个域名,设置 `*.local.homeinfra.org` 的A记录指向您的服务器.
|
||||
- 代理服务器(如有必要).
|
||||
- 一个Cloudflare账户(非强制,也可以使用其他DNS服务,请自行修改Caddy)
|
||||
|
||||
### Deployment
|
||||
|
||||
修改 `.env` 文件,设置下列参数:
|
||||
|
||||
- `BASE_DOMAIN`: 基础域名,如 `local.homeinfra.org`,镜像站将会使用 `*.local.homeinfra.org` 的子域名。
|
||||
- `CLOUDFLARE_DNS_API_TOKEN`,Cloudflare的API Token,用于管理DNS申请HTTPS证书。
|
||||
- `RPC_SECRET`:Aria2的RPC密钥。
|
||||
- `all_proxy`:代理服务器地址,如有必要。
|
||||
|
||||
```bash
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
## Design
|
||||
|
||||
LightMirrors依赖于三个组件:
|
||||
|
||||
- aria2 + Aria2Ng : 下载器与管理UI。
|
||||
- mirrors: 镜像HTTP服务器。
|
||||
- caddy: HTTP网关。
|
||||
|
||||
## Test
|
||||
|
||||
> 假设我们的域名为 local.homeinfra.org
|
||||
|
||||
| subdomain | source | test command |
|
||||
|-----------|---------------------------------|-----------------------------------------------------------------|
|
||||
| pypi | https://pypi.org | pip3 download -i https://pypi.local.homeinfra.org/simple jinja2 |
|
||||
| torch | https://download.pytorch.org | pip3 download -i https://pypi.local.homeinfra.org/whl/ torch |
|
||||
| dockerhub | https://registry-1.docker.io/v2 | docker pull docker.local.homeinfra.org/alpine |
|
||||
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#NoCLin/LightMirrors&Date)
|
||||
|
3
ROADMAP.md
Normal file
3
ROADMAP.md
Normal file
@ -0,0 +1,3 @@
|
||||
- Test
|
||||
- Publish to ghcr and dockerhub
|
||||
- Configurable upstreams
|
13
aria2/Dockerfile
Normal file
13
aria2/Dockerfile
Normal file
@ -0,0 +1,13 @@
|
||||
FROM alpine:3.19.1
|
||||
|
||||
# Optimization for China
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
|
||||
|
||||
RUN apk update && apk add --no-cache aria2
|
||||
|
||||
ADD entrypoint.sh /
|
||||
ADD aria2.conf /
|
||||
|
||||
EXPOSE 6800
|
||||
|
||||
CMD ["/entrypoint.sh"]
|
54
aria2/aria2.conf
Normal file
54
aria2/aria2.conf
Normal file
@ -0,0 +1,54 @@
|
||||
dir=/app/cache
|
||||
disk-cache=64M
|
||||
file-allocation=none
|
||||
no-file-allocation-limit=64M
|
||||
continue=true
|
||||
always-resume=false
|
||||
max-resume-failure-tries=0
|
||||
remote-time=true
|
||||
input-file=/data/aria2.session
|
||||
save-session=/data/aria2.session
|
||||
save-session-interval=1
|
||||
auto-save-interval=20
|
||||
|
||||
# remove .aria2 when download is done
|
||||
# we leverage this to detech if download is done
|
||||
force-save=false
|
||||
|
||||
max-file-not-found=3
|
||||
max-tries=0
|
||||
retry-wait=10
|
||||
connect-timeout=10
|
||||
timeout=10
|
||||
max-concurrent-downloads=5
|
||||
max-connection-per-server=16
|
||||
split=64
|
||||
min-split-size=4M
|
||||
piece-length=1M
|
||||
allow-piece-length-change=true
|
||||
lowest-speed-limit=0
|
||||
max-overall-download-limit=0
|
||||
max-download-limit=0
|
||||
disable-ipv6=true
|
||||
http-accept-gzip=true
|
||||
reuse-uri=false
|
||||
no-netrc=true
|
||||
allow-overwrite=true
|
||||
auto-file-renaming=false
|
||||
content-disposition-default-utf8=true
|
||||
|
||||
enable-rpc=true
|
||||
rpc-allow-origin-all=false
|
||||
rpc-listen-all=true
|
||||
rpc-listen-port=6800
|
||||
rpc-max-request-size=10M
|
||||
|
||||
log=-
|
||||
log-level=warn
|
||||
|
||||
console-log-level=notice
|
||||
|
||||
quiet=false
|
||||
|
||||
summary-interval=0
|
||||
|
4
aria2/entrypoint.sh
Executable file
4
aria2/entrypoint.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
touch /data/aria2.session
|
||||
|
||||
aria2c --conf-path=/aria2.conf --rpc-secret $RPC_SECRET
|
15
caddy/Caddyfile
Normal file
15
caddy/Caddyfile
Normal file
@ -0,0 +1,15 @@
|
||||
*.{$BASE_DOMAIN} {
|
||||
|
||||
tls {
|
||||
dns cloudflare {env.CLOUDFLARE_DNS_API_TOKEN}
|
||||
}
|
||||
|
||||
reverse_proxy http://lightmirrors:8080
|
||||
|
||||
@aria2 host aria2.{$BASE_DOMAIN}
|
||||
handle @aria2 {
|
||||
root * /wwwroot
|
||||
file_server
|
||||
reverse_proxy /jsonrpc http://aria2:6800
|
||||
}
|
||||
}
|
23
caddy/Dockerfile
Normal file
23
caddy/Dockerfile
Normal file
@ -0,0 +1,23 @@
|
||||
FROM caddy:2.7.6-builder-alpine AS builder
|
||||
|
||||
# Optimization for China
|
||||
RUN go env -w GOPROXY=https://goproxy.cn,direct
|
||||
|
||||
RUN xcaddy build \
|
||||
--with github.com/caddy-dns/cloudflare
|
||||
|
||||
FROM caddy:2.7.6-alpine
|
||||
|
||||
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
|
||||
|
||||
RUN mkdir -p /wwwroot
|
||||
WORKDIR /wwwroot
|
||||
|
||||
# Optimization for China as the project is aimed at Chinese users
|
||||
ADD https://hub.gitmirror.com/https://github.com/mayswind/AriaNg/releases/download/1.3.7/AriaNg-1.3.7.zip /wwwroot/
|
||||
|
||||
RUN unzip AriaNg-1.3.7.zip && rm AriaNg-1.3.7.zip
|
||||
|
||||
ENTRYPOINT ["caddy"]
|
||||
|
||||
CMD ["run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
|
44
docker-compose.yml
Normal file
44
docker-compose.yml
Normal file
@ -0,0 +1,44 @@
|
||||
services:
|
||||
lightmirrors:
|
||||
image: lightmirrors/mirrors
|
||||
build: ./mirrors
|
||||
volumes:
|
||||
- ./mirrors:/app
|
||||
- ./data/cache:/app/cache
|
||||
env_file:
|
||||
- .env
|
||||
networks:
|
||||
- app
|
||||
restart: unless-stopped
|
||||
# for linux
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
caddy:
|
||||
image: lightmirrors/caddy
|
||||
build: ./caddy
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./caddy/Caddyfile:/etc/caddy/Caddyfile
|
||||
- ./caddy/caddy_data:/data/caddy
|
||||
env_file:
|
||||
- .env
|
||||
networks:
|
||||
- app
|
||||
restart: unless-stopped
|
||||
aria2:
|
||||
image: lightmirrors/aria2
|
||||
build: ./aria2
|
||||
volumes:
|
||||
- ./aria2/aria2.conf:/aria2.conf
|
||||
- ./data/cache:/app/cache
|
||||
- ./data/aria2:/data/
|
||||
networks:
|
||||
- app
|
||||
env_file:
|
||||
- .env
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
app:
|
||||
driver: bridge
|
13
mirrors/Dockerfile
Normal file
13
mirrors/Dockerfile
Normal file
@ -0,0 +1,13 @@
|
||||
FROM python:3.11-alpine
|
||||
|
||||
ADD requirements.txt /app/requirements.txt
|
||||
|
||||
RUN pip install -r /app/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["python", "server.py"]
|
||||
|
||||
|
67
mirrors/aria2_api.py
Normal file
67
mirrors/aria2_api.py
Normal file
@ -0,0 +1,67 @@
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
import httpx
|
||||
|
||||
from config import RPC_SECRET, ARIA2_RPC_URL
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def send_request(method, params=None):
|
||||
payload = {
|
||||
'jsonrpc': '2.0',
|
||||
'id': uuid.uuid4().hex,
|
||||
'method': method,
|
||||
'params': [f'token:{RPC_SECRET}'] + (params or [])
|
||||
}
|
||||
|
||||
# specify the internal API call don't use proxy
|
||||
async with httpx.AsyncClient(mounts={
|
||||
"all://": httpx.AsyncHTTPTransport()
|
||||
}) as client:
|
||||
response = await client.post(ARIA2_RPC_URL, json=payload)
|
||||
logger.info(f"aria2 request: {method} {params} -> {response.status_code} {response.text}")
|
||||
try:
|
||||
return response.json()
|
||||
except json.JSONDecodeError as e:
|
||||
logger.warning(f"aria2 response: {response.status_code} {response.text}")
|
||||
raise e
|
||||
|
||||
|
||||
async def add_download(url, save_dir='/app/cache'):
|
||||
method = 'aria2.addUri'
|
||||
params = [[url],
|
||||
{'dir': save_dir,
|
||||
'header': []
|
||||
}]
|
||||
response = await send_request(method, params)
|
||||
return response['result']
|
||||
|
||||
|
||||
async def pause_download(gid):
|
||||
method = 'aria2.pause'
|
||||
params = [gid]
|
||||
response = await send_request(method, params)
|
||||
return response['result']
|
||||
|
||||
|
||||
async def resume_download(gid):
|
||||
method = 'aria2.unpause'
|
||||
params = [gid]
|
||||
response = await send_request(method, params)
|
||||
return response['result']
|
||||
|
||||
|
||||
async def get_status(gid):
|
||||
method = 'aria2.tellStatus'
|
||||
params = [gid]
|
||||
response = await send_request(method, params)
|
||||
return response['result']
|
||||
|
||||
|
||||
async def list_downloads():
|
||||
method = 'aria2.tellActive'
|
||||
response = await send_request(method)
|
||||
return response['result']
|
11
mirrors/config.py
Normal file
11
mirrors/config.py
Normal file
@ -0,0 +1,11 @@
|
||||
import os
|
||||
|
||||
ARIA2_RPC_URL = os.environ.get("ARIA2_RPC_URL", 'http://aria2:6800/jsonrpc')
|
||||
RPC_SECRET = os.environ.get("RPC_SECRET", '')
|
||||
BASE_DOMAIN = os.environ.get("BASE_DOMAIN", '127.0.0.1.nip.io')
|
||||
|
||||
PROXY = os.environ.get("PROXY", None)
|
||||
CACHE_DIR = os.environ.get("CACHE_DIR", "/app/cache/")
|
||||
|
||||
EXTERNAL_HOST_ARIA2 = f"aria2." + BASE_DOMAIN
|
||||
EXTERNAL_URL_ARIA2 = f"https://" + EXTERNAL_HOST_ARIA2
|
344
mirrors/poetry.lock
generated
Normal file
344
mirrors/poetry.lock
generated
Normal file
@ -0,0 +1,344 @@
|
||||
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.6.0"
|
||||
description = "Reusable constraint types to use with typing.Annotated"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"},
|
||||
{file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.2.0"
|
||||
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"},
|
||||
{file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""}
|
||||
idna = ">=2.8"
|
||||
sniffio = ">=1.1"
|
||||
typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
|
||||
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
|
||||
trio = ["trio (>=0.23)"]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2024.2.2"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
|
||||
{file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.7"
|
||||
description = "Composable command line interface toolkit"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
||||
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.2.0"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"},
|
||||
{file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.109.0"
|
||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "fastapi-0.109.0-py3-none-any.whl", hash = "sha256:8c77515984cd8e8cfeb58364f8cc7a28f0692088475e2614f7bf03275eba9093"},
|
||||
{file = "fastapi-0.109.0.tar.gz", hash = "sha256:b978095b9ee01a5cf49b19f4bc1ac9b8ca83aa076e770ef8fd9af09a2b88d191"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
|
||||
starlette = ">=0.35.0,<0.36.0"
|
||||
typing-extensions = ">=4.8.0"
|
||||
|
||||
[package.extras]
|
||||
all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.14.0"
|
||||
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
|
||||
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.2"
|
||||
description = "A minimal low-level HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"},
|
||||
{file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = "*"
|
||||
h11 = ">=0.13,<0.15"
|
||||
|
||||
[package.extras]
|
||||
asyncio = ["anyio (>=4.0,<5.0)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
trio = ["trio (>=0.22.0,<0.23.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.26.0"
|
||||
description = "The next generation HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"},
|
||||
{file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = "*"
|
||||
certifi = "*"
|
||||
httpcore = "==1.*"
|
||||
idna = "*"
|
||||
sniffio = "*"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli", "brotlicffi"]
|
||||
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.6"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
|
||||
{file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.6.0"
|
||||
description = "Data validation using Python type hints"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pydantic-2.6.0-py3-none-any.whl", hash = "sha256:1440966574e1b5b99cf75a13bec7b20e3512e8a61b894ae252f56275e2c465ae"},
|
||||
{file = "pydantic-2.6.0.tar.gz", hash = "sha256:ae887bd94eb404b09d86e4d12f93893bdca79d766e738528c6fa1c849f3c6bcf"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
annotated-types = ">=0.4.0"
|
||||
pydantic-core = "2.16.1"
|
||||
typing-extensions = ">=4.6.1"
|
||||
|
||||
[package.extras]
|
||||
email = ["email-validator (>=2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.16.1"
|
||||
description = ""
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pydantic_core-2.16.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:300616102fb71241ff477a2cbbc847321dbec49428434a2f17f37528721c4948"},
|
||||
{file = "pydantic_core-2.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5511f962dd1b9b553e9534c3b9c6a4b0c9ded3d8c2be96e61d56f933feef9e1f"},
|
||||
{file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98f0edee7ee9cc7f9221af2e1b95bd02810e1c7a6d115cfd82698803d385b28f"},
|
||||
{file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9795f56aa6b2296f05ac79d8a424e94056730c0b860a62b0fdcfe6340b658cc8"},
|
||||
{file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c45f62e4107ebd05166717ac58f6feb44471ed450d07fecd90e5f69d9bf03c48"},
|
||||
{file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:462d599299c5971f03c676e2b63aa80fec5ebc572d89ce766cd11ca8bcb56f3f"},
|
||||
{file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ebaa4bf6386a3b22eec518da7d679c8363fb7fb70cf6972161e5542f470798"},
|
||||
{file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:99f9a50b56713a598d33bc23a9912224fc5d7f9f292444e6664236ae471ddf17"},
|
||||
{file = "pydantic_core-2.16.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8ec364e280db4235389b5e1e6ee924723c693cbc98e9d28dc1767041ff9bc388"},
|
||||
{file = "pydantic_core-2.16.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:653a5dfd00f601a0ed6654a8b877b18d65ac32c9d9997456e0ab240807be6cf7"},
|
||||
{file = "pydantic_core-2.16.1-cp310-none-win32.whl", hash = "sha256:1661c668c1bb67b7cec96914329d9ab66755911d093bb9063c4c8914188af6d4"},
|
||||
{file = "pydantic_core-2.16.1-cp310-none-win_amd64.whl", hash = "sha256:561be4e3e952c2f9056fba5267b99be4ec2afadc27261505d4992c50b33c513c"},
|
||||
{file = "pydantic_core-2.16.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:102569d371fadc40d8f8598a59379c37ec60164315884467052830b28cc4e9da"},
|
||||
{file = "pydantic_core-2.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:735dceec50fa907a3c314b84ed609dec54b76a814aa14eb90da31d1d36873a5e"},
|
||||
{file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e83ebbf020be727d6e0991c1b192a5c2e7113eb66e3def0cd0c62f9f266247e4"},
|
||||
{file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:30a8259569fbeec49cfac7fda3ec8123486ef1b729225222f0d41d5f840b476f"},
|
||||
{file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:920c4897e55e2881db6a6da151198e5001552c3777cd42b8a4c2f72eedc2ee91"},
|
||||
{file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5247a3d74355f8b1d780d0f3b32a23dd9f6d3ff43ef2037c6dcd249f35ecf4c"},
|
||||
{file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5bea8012df5bb6dda1e67d0563ac50b7f64a5d5858348b5c8cb5043811c19d"},
|
||||
{file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ed3025a8a7e5a59817b7494686d449ebfbe301f3e757b852c8d0d1961d6be864"},
|
||||
{file = "pydantic_core-2.16.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06f0d5a1d9e1b7932477c172cc720b3b23c18762ed7a8efa8398298a59d177c7"},
|
||||
{file = "pydantic_core-2.16.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:150ba5c86f502c040b822777e2e519b5625b47813bd05f9273a8ed169c97d9ae"},
|
||||
{file = "pydantic_core-2.16.1-cp311-none-win32.whl", hash = "sha256:d6cbdf12ef967a6aa401cf5cdf47850559e59eedad10e781471c960583f25aa1"},
|
||||
{file = "pydantic_core-2.16.1-cp311-none-win_amd64.whl", hash = "sha256:afa01d25769af33a8dac0d905d5c7bb2d73c7c3d5161b2dd6f8b5b5eea6a3c4c"},
|
||||
{file = "pydantic_core-2.16.1-cp311-none-win_arm64.whl", hash = "sha256:1a2fe7b00a49b51047334d84aafd7e39f80b7675cad0083678c58983662da89b"},
|
||||
{file = "pydantic_core-2.16.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f478ec204772a5c8218e30eb813ca43e34005dff2eafa03931b3d8caef87d51"},
|
||||
{file = "pydantic_core-2.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1936ef138bed2165dd8573aa65e3095ef7c2b6247faccd0e15186aabdda7f66"},
|
||||
{file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99d3a433ef5dc3021c9534a58a3686c88363c591974c16c54a01af7efd741f13"},
|
||||
{file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd88f40f2294440d3f3c6308e50d96a0d3d0973d6f1a5732875d10f569acef49"},
|
||||
{file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fac641bbfa43d5a1bed99d28aa1fded1984d31c670a95aac1bf1d36ac6ce137"},
|
||||
{file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72bf9308a82b75039b8c8edd2be2924c352eda5da14a920551a8b65d5ee89253"},
|
||||
{file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb4363e6c9fc87365c2bc777a1f585a22f2f56642501885ffc7942138499bf54"},
|
||||
{file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:20f724a023042588d0f4396bbbcf4cffd0ddd0ad3ed4f0d8e6d4ac4264bae81e"},
|
||||
{file = "pydantic_core-2.16.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fb4370b15111905bf8b5ba2129b926af9470f014cb0493a67d23e9d7a48348e8"},
|
||||
{file = "pydantic_core-2.16.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23632132f1fd608034f1a56cc3e484be00854db845b3a4a508834be5a6435a6f"},
|
||||
{file = "pydantic_core-2.16.1-cp312-none-win32.whl", hash = "sha256:b9f3e0bffad6e238f7acc20c393c1ed8fab4371e3b3bc311020dfa6020d99212"},
|
||||
{file = "pydantic_core-2.16.1-cp312-none-win_amd64.whl", hash = "sha256:a0b4cfe408cd84c53bab7d83e4209458de676a6ec5e9c623ae914ce1cb79b96f"},
|
||||
{file = "pydantic_core-2.16.1-cp312-none-win_arm64.whl", hash = "sha256:d195add190abccefc70ad0f9a0141ad7da53e16183048380e688b466702195dd"},
|
||||
{file = "pydantic_core-2.16.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:502c062a18d84452858f8aea1e520e12a4d5228fc3621ea5061409d666ea1706"},
|
||||
{file = "pydantic_core-2.16.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8c032ccee90b37b44e05948b449a2d6baed7e614df3d3f47fe432c952c21b60"},
|
||||
{file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:920f4633bee43d7a2818e1a1a788906df5a17b7ab6fe411220ed92b42940f818"},
|
||||
{file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f5d37ff01edcbace53a402e80793640c25798fb7208f105d87a25e6fcc9ea06"},
|
||||
{file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:399166f24c33a0c5759ecc4801f040dbc87d412c1a6d6292b2349b4c505effc9"},
|
||||
{file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac89ccc39cd1d556cc72d6752f252dc869dde41c7c936e86beac5eb555041b66"},
|
||||
{file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73802194f10c394c2bedce7a135ba1d8ba6cff23adf4217612bfc5cf060de34c"},
|
||||
{file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8fa00fa24ffd8c31fac081bf7be7eb495be6d248db127f8776575a746fa55c95"},
|
||||
{file = "pydantic_core-2.16.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:601d3e42452cd4f2891c13fa8c70366d71851c1593ed42f57bf37f40f7dca3c8"},
|
||||
{file = "pydantic_core-2.16.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07982b82d121ed3fc1c51faf6e8f57ff09b1325d2efccaa257dd8c0dd937acca"},
|
||||
{file = "pydantic_core-2.16.1-cp38-none-win32.whl", hash = "sha256:d0bf6f93a55d3fa7a079d811b29100b019784e2ee6bc06b0bb839538272a5610"},
|
||||
{file = "pydantic_core-2.16.1-cp38-none-win_amd64.whl", hash = "sha256:fbec2af0ebafa57eb82c18c304b37c86a8abddf7022955d1742b3d5471a6339e"},
|
||||
{file = "pydantic_core-2.16.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a497be217818c318d93f07e14502ef93d44e6a20c72b04c530611e45e54c2196"},
|
||||
{file = "pydantic_core-2.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:694a5e9f1f2c124a17ff2d0be613fd53ba0c26de588eb4bdab8bca855e550d95"},
|
||||
{file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d4dfc66abea3ec6d9f83e837a8f8a7d9d3a76d25c9911735c76d6745950e62c"},
|
||||
{file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8655f55fe68c4685673265a650ef71beb2d31871c049c8b80262026f23605ee3"},
|
||||
{file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21e3298486c4ea4e4d5cc6fb69e06fb02a4e22089304308817035ac006a7f506"},
|
||||
{file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71b4a48a7427f14679f0015b13c712863d28bb1ab700bd11776a5368135c7d60"},
|
||||
{file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dca874e35bb60ce4f9f6665bfbfad050dd7573596608aeb9e098621ac331dc"},
|
||||
{file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa496cd45cda0165d597e9d6f01e36c33c9508f75cf03c0a650018c5048f578e"},
|
||||
{file = "pydantic_core-2.16.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5317c04349472e683803da262c781c42c5628a9be73f4750ac7d13040efb5d2d"},
|
||||
{file = "pydantic_core-2.16.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:42c29d54ed4501a30cd71015bf982fa95e4a60117b44e1a200290ce687d3e640"},
|
||||
{file = "pydantic_core-2.16.1-cp39-none-win32.whl", hash = "sha256:ba07646f35e4e49376c9831130039d1b478fbfa1215ae62ad62d2ee63cf9c18f"},
|
||||
{file = "pydantic_core-2.16.1-cp39-none-win_amd64.whl", hash = "sha256:2133b0e412a47868a358713287ff9f9a328879da547dc88be67481cdac529118"},
|
||||
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d25ef0c33f22649b7a088035fd65ac1ce6464fa2876578df1adad9472f918a76"},
|
||||
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:99c095457eea8550c9fa9a7a992e842aeae1429dab6b6b378710f62bfb70b394"},
|
||||
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b49c604ace7a7aa8af31196abbf8f2193be605db6739ed905ecaf62af31ccae0"},
|
||||
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c56da23034fe66221f2208c813d8aa509eea34d97328ce2add56e219c3a9f41c"},
|
||||
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cebf8d56fee3b08ad40d332a807ecccd4153d3f1ba8231e111d9759f02edfd05"},
|
||||
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:1ae8048cba95f382dba56766525abca438328455e35c283bb202964f41a780b0"},
|
||||
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:780daad9e35b18d10d7219d24bfb30148ca2afc309928e1d4d53de86822593dc"},
|
||||
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c94b5537bf6ce66e4d7830c6993152940a188600f6ae044435287753044a8fe2"},
|
||||
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:adf28099d061a25fbcc6531febb7a091e027605385de9fe14dd6a97319d614cf"},
|
||||
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:644904600c15816a1f9a1bafa6aab0d21db2788abcdf4e2a77951280473f33e1"},
|
||||
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87bce04f09f0552b66fca0c4e10da78d17cb0e71c205864bab4e9595122cb9d9"},
|
||||
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:877045a7969ace04d59516d5d6a7dee13106822f99a5d8df5e6822941f7bedc8"},
|
||||
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9c46e556ee266ed3fb7b7a882b53df3c76b45e872fdab8d9cf49ae5e91147fd7"},
|
||||
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4eebbd049008eb800f519578e944b8dc8e0f7d59a5abb5924cc2d4ed3a1834ff"},
|
||||
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c0be58529d43d38ae849a91932391eb93275a06b93b79a8ab828b012e916a206"},
|
||||
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b1fc07896fc1851558f532dffc8987e526b682ec73140886c831d773cef44b76"},
|
||||
{file = "pydantic_core-2.16.1.tar.gz", hash = "sha256:daff04257b49ab7f4b3f73f98283d3dbb1a65bf3500d55c7beac3c66c310fe34"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.0"
|
||||
description = "Sniff out which async library your code is running under"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
|
||||
{file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.35.1"
|
||||
description = "The little ASGI library that shines."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "starlette-0.35.1-py3-none-any.whl", hash = "sha256:50bbbda9baa098e361f398fda0928062abbaf1f54f4fadcbe17c092a01eb9a25"},
|
||||
{file = "starlette-0.35.1.tar.gz", hash = "sha256:3e2639dac3520e4f58734ed22553f950d3f3cb1001cd2eaac4d57e8cdc5f66bc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = ">=3.4.0,<5"
|
||||
typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
|
||||
|
||||
[package.extras]
|
||||
full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.9.0"
|
||||
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"},
|
||||
{file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.27.0.post1"
|
||||
description = "The lightning-fast ASGI server."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "uvicorn-0.27.0.post1-py3-none-any.whl", hash = "sha256:4b85ba02b8a20429b9b205d015cbeb788a12da527f731811b643fd739ef90d5f"},
|
||||
{file = "uvicorn-0.27.0.post1.tar.gz", hash = "sha256:54898fcd80c13ff1cd28bf77b04ec9dbd8ff60c5259b499b4b12bb0917f22907"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=7.0"
|
||||
h11 = ">=0.8"
|
||||
typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.9"
|
||||
content-hash = "3f85c224110f9432dab85aaffc90d271b06475dfb7ab97e89d8817d2afeab223"
|
107
mirrors/proxy/cached.py
Normal file
107
mirrors/proxy/cached.py
Normal file
@ -0,0 +1,107 @@
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import typing
|
||||
from asyncio import sleep
|
||||
from enum import Enum
|
||||
from urllib.parse import urlparse, quote
|
||||
|
||||
import httpx
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
from starlette.status import HTTP_500_INTERNAL_SERVER_ERROR, HTTP_504_GATEWAY_TIMEOUT
|
||||
|
||||
from aria2_api import add_download
|
||||
|
||||
from config import CACHE_DIR, EXTERNAL_URL_ARIA2, PROXY
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_cache_file_and_folder(url: str) -> typing.Tuple[str, str]:
|
||||
parsed_url = urlparse(url)
|
||||
base_dir = pathlib.Path(CACHE_DIR)
|
||||
assert parsed_url.path[0] == "/"
|
||||
assert parsed_url.path[-1] != "/"
|
||||
cache_file = (base_dir / parsed_url.hostname / parsed_url.path[1:]).resolve()
|
||||
assert cache_file.is_relative_to(base_dir)
|
||||
|
||||
return str(cache_file), os.path.dirname(cache_file)
|
||||
|
||||
|
||||
class DownloadingStatus(Enum):
|
||||
DOWNLOADING = 1
|
||||
DOWNLOADED = 2
|
||||
NOT_FOUND = 3
|
||||
|
||||
|
||||
def lookup_cache(url: str) -> DownloadingStatus:
|
||||
cache_file, _ = get_cache_file_and_folder(url)
|
||||
|
||||
cache_file_aria2 = f"{cache_file}.aria2"
|
||||
if os.path.exists(cache_file_aria2):
|
||||
return DownloadingStatus.DOWNLOADING
|
||||
|
||||
if os.path.exists(cache_file):
|
||||
assert not os.path.isdir(cache_file)
|
||||
return DownloadingStatus.DOWNLOADED
|
||||
return DownloadingStatus.NOT_FOUND
|
||||
|
||||
|
||||
def make_cached_response(url):
|
||||
cache_file, _ = get_cache_file_and_folder(url)
|
||||
|
||||
assert os.path.exists(cache_file)
|
||||
assert not os.path.isdir(cache_file)
|
||||
with open(cache_file, "rb") as f:
|
||||
content = f.read()
|
||||
return Response(content=content, status_code=200)
|
||||
|
||||
|
||||
async def get_url_content_length(url):
|
||||
async with httpx.AsyncClient(proxy=PROXY, verify=False) as client:
|
||||
head_response = await client.head(url)
|
||||
content_len = (head_response.headers.get("content-length", None))
|
||||
return content_len
|
||||
|
||||
|
||||
async def try_get_cache(
|
||||
request: Request,
|
||||
target_url: str,
|
||||
download_wait_time: int = 60,
|
||||
post_process: typing.Callable[[Request, Response], Response] = None,
|
||||
) -> typing.Optional[Response]:
|
||||
cache_status = lookup_cache(target_url)
|
||||
if cache_status == DownloadingStatus.DOWNLOADED:
|
||||
resp = make_cached_response(target_url)
|
||||
if post_process:
|
||||
resp = post_process(request, resp)
|
||||
return resp
|
||||
|
||||
if cache_status == DownloadingStatus.DOWNLOADING:
|
||||
logger.info(f"Download is not finished, return 503 for {target_url}")
|
||||
return Response(content=f"This file is downloading, view it at {EXTERNAL_URL_ARIA2}",
|
||||
status_code=HTTP_504_GATEWAY_TIMEOUT)
|
||||
|
||||
assert cache_status == DownloadingStatus.NOT_FOUND
|
||||
|
||||
cache_file, cache_file_dir = get_cache_file_and_folder(target_url)
|
||||
print("prepare to download", target_url, cache_file, cache_file_dir)
|
||||
|
||||
processed_url = quote(target_url, safe='/:?=&')
|
||||
|
||||
try:
|
||||
await add_download(processed_url, save_dir=cache_file_dir)
|
||||
except Exception as e:
|
||||
logger.error(f"Download error, return 503500 for {target_url}", exc_info=e)
|
||||
return Response(content=f"Failed to add download: {e}", status_code=HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
# wait for download finished
|
||||
for _ in range(download_wait_time):
|
||||
await sleep(1)
|
||||
cache_status = lookup_cache(target_url)
|
||||
if cache_status == DownloadingStatus.DOWNLOADED:
|
||||
return make_cached_response(target_url)
|
||||
logger.info(f"Download is not finished, return 503 for {target_url}")
|
||||
return Response(content=f"This file is downloading, view it at {EXTERNAL_URL_ARIA2}",
|
||||
status_code=HTTP_504_GATEWAY_TIMEOUT)
|
47
mirrors/proxy/direct.py
Normal file
47
mirrors/proxy/direct.py
Normal file
@ -0,0 +1,47 @@
|
||||
import typing
|
||||
|
||||
import httpx
|
||||
import starlette.requests
|
||||
import starlette.responses
|
||||
|
||||
from config import PROXY
|
||||
|
||||
|
||||
async def direct_proxy(
|
||||
request: starlette.requests.Request,
|
||||
target_url: str,
|
||||
pre_process: typing.Callable[[starlette.requests.Request, httpx.Request], httpx.Request] = None,
|
||||
post_process: typing.Callable[[starlette.requests.Request, httpx.Response], httpx.Response] = None,
|
||||
) -> typing.Optional[starlette.responses.Response]:
|
||||
async with httpx.AsyncClient(proxy=PROXY, verify=False) as client:
|
||||
|
||||
headers = request.headers.mutablecopy()
|
||||
for key in headers.keys():
|
||||
if key not in ["user-agent", "accept"]:
|
||||
del headers[key]
|
||||
|
||||
httpx_req = client.build_request(request.method, target_url, headers=headers, )
|
||||
|
||||
if pre_process:
|
||||
httpx_req = pre_process(request, httpx_req)
|
||||
upstream_response = await client.send(httpx_req)
|
||||
|
||||
# TODO: move to post_process
|
||||
if upstream_response.status_code == 307:
|
||||
location = upstream_response.headers["location"]
|
||||
print("catch redirect", location)
|
||||
|
||||
headers = upstream_response.headers
|
||||
cl = headers.pop("content-length", None)
|
||||
ce = headers.pop("content-encoding", None)
|
||||
# print(target_url, cl, ce)
|
||||
content = upstream_response.content
|
||||
response = starlette.responses.Response(
|
||||
headers=headers,
|
||||
content=content,
|
||||
status_code=upstream_response.status_code)
|
||||
|
||||
if post_process:
|
||||
response = post_process(request, response)
|
||||
|
||||
return response
|
17
mirrors/pyproject.toml
Normal file
17
mirrors/pyproject.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[tool.poetry]
|
||||
name = "light-mirrors"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["'Anonymous' <'<>'>"]
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9"
|
||||
fastapi = "^0.109.0"
|
||||
uvicorn = "^0.27.0.post1"
|
||||
httpx = "^0.26.0"
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
128
mirrors/requirements.txt
Normal file
128
mirrors/requirements.txt
Normal file
@ -0,0 +1,128 @@
|
||||
annotated-types==0.6.0 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43 \
|
||||
--hash=sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d
|
||||
anyio==4.2.0 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee \
|
||||
--hash=sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f
|
||||
certifi==2024.2.2 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f \
|
||||
--hash=sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1
|
||||
click==8.1.7 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
|
||||
--hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
|
||||
colorama==0.4.6 ; python_version >= "3.9" and python_version < "4.0" and platform_system == "Windows" \
|
||||
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
|
||||
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
|
||||
exceptiongroup==1.2.0 ; python_version >= "3.9" and python_version < "3.11" \
|
||||
--hash=sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14 \
|
||||
--hash=sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68
|
||||
fastapi==0.109.0 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:8c77515984cd8e8cfeb58364f8cc7a28f0692088475e2614f7bf03275eba9093 \
|
||||
--hash=sha256:b978095b9ee01a5cf49b19f4bc1ac9b8ca83aa076e770ef8fd9af09a2b88d191
|
||||
h11==0.14.0 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \
|
||||
--hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761
|
||||
httpcore==1.0.2 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7 \
|
||||
--hash=sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535
|
||||
httpx==0.26.0 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf \
|
||||
--hash=sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd
|
||||
idna==3.6 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \
|
||||
--hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f
|
||||
pydantic-core==2.16.1 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:06f0d5a1d9e1b7932477c172cc720b3b23c18762ed7a8efa8398298a59d177c7 \
|
||||
--hash=sha256:07982b82d121ed3fc1c51faf6e8f57ff09b1325d2efccaa257dd8c0dd937acca \
|
||||
--hash=sha256:0f478ec204772a5c8218e30eb813ca43e34005dff2eafa03931b3d8caef87d51 \
|
||||
--hash=sha256:102569d371fadc40d8f8598a59379c37ec60164315884467052830b28cc4e9da \
|
||||
--hash=sha256:10dca874e35bb60ce4f9f6665bfbfad050dd7573596608aeb9e098621ac331dc \
|
||||
--hash=sha256:150ba5c86f502c040b822777e2e519b5625b47813bd05f9273a8ed169c97d9ae \
|
||||
--hash=sha256:1661c668c1bb67b7cec96914329d9ab66755911d093bb9063c4c8914188af6d4 \
|
||||
--hash=sha256:1a2fe7b00a49b51047334d84aafd7e39f80b7675cad0083678c58983662da89b \
|
||||
--hash=sha256:1ae8048cba95f382dba56766525abca438328455e35c283bb202964f41a780b0 \
|
||||
--hash=sha256:20f724a023042588d0f4396bbbcf4cffd0ddd0ad3ed4f0d8e6d4ac4264bae81e \
|
||||
--hash=sha256:2133b0e412a47868a358713287ff9f9a328879da547dc88be67481cdac529118 \
|
||||
--hash=sha256:21e3298486c4ea4e4d5cc6fb69e06fb02a4e22089304308817035ac006a7f506 \
|
||||
--hash=sha256:21ebaa4bf6386a3b22eec518da7d679c8363fb7fb70cf6972161e5542f470798 \
|
||||
--hash=sha256:23632132f1fd608034f1a56cc3e484be00854db845b3a4a508834be5a6435a6f \
|
||||
--hash=sha256:2d5bea8012df5bb6dda1e67d0563ac50b7f64a5d5858348b5c8cb5043811c19d \
|
||||
--hash=sha256:300616102fb71241ff477a2cbbc847321dbec49428434a2f17f37528721c4948 \
|
||||
--hash=sha256:30a8259569fbeec49cfac7fda3ec8123486ef1b729225222f0d41d5f840b476f \
|
||||
--hash=sha256:399166f24c33a0c5759ecc4801f040dbc87d412c1a6d6292b2349b4c505effc9 \
|
||||
--hash=sha256:3fac641bbfa43d5a1bed99d28aa1fded1984d31c670a95aac1bf1d36ac6ce137 \
|
||||
--hash=sha256:42c29d54ed4501a30cd71015bf982fa95e4a60117b44e1a200290ce687d3e640 \
|
||||
--hash=sha256:462d599299c5971f03c676e2b63aa80fec5ebc572d89ce766cd11ca8bcb56f3f \
|
||||
--hash=sha256:4eebbd049008eb800f519578e944b8dc8e0f7d59a5abb5924cc2d4ed3a1834ff \
|
||||
--hash=sha256:502c062a18d84452858f8aea1e520e12a4d5228fc3621ea5061409d666ea1706 \
|
||||
--hash=sha256:5317c04349472e683803da262c781c42c5628a9be73f4750ac7d13040efb5d2d \
|
||||
--hash=sha256:5511f962dd1b9b553e9534c3b9c6a4b0c9ded3d8c2be96e61d56f933feef9e1f \
|
||||
--hash=sha256:561be4e3e952c2f9056fba5267b99be4ec2afadc27261505d4992c50b33c513c \
|
||||
--hash=sha256:601d3e42452cd4f2891c13fa8c70366d71851c1593ed42f57bf37f40f7dca3c8 \
|
||||
--hash=sha256:644904600c15816a1f9a1bafa6aab0d21db2788abcdf4e2a77951280473f33e1 \
|
||||
--hash=sha256:653a5dfd00f601a0ed6654a8b877b18d65ac32c9d9997456e0ab240807be6cf7 \
|
||||
--hash=sha256:694a5e9f1f2c124a17ff2d0be613fd53ba0c26de588eb4bdab8bca855e550d95 \
|
||||
--hash=sha256:71b4a48a7427f14679f0015b13c712863d28bb1ab700bd11776a5368135c7d60 \
|
||||
--hash=sha256:72bf9308a82b75039b8c8edd2be2924c352eda5da14a920551a8b65d5ee89253 \
|
||||
--hash=sha256:735dceec50fa907a3c314b84ed609dec54b76a814aa14eb90da31d1d36873a5e \
|
||||
--hash=sha256:73802194f10c394c2bedce7a135ba1d8ba6cff23adf4217612bfc5cf060de34c \
|
||||
--hash=sha256:780daad9e35b18d10d7219d24bfb30148ca2afc309928e1d4d53de86822593dc \
|
||||
--hash=sha256:8655f55fe68c4685673265a650ef71beb2d31871c049c8b80262026f23605ee3 \
|
||||
--hash=sha256:877045a7969ace04d59516d5d6a7dee13106822f99a5d8df5e6822941f7bedc8 \
|
||||
--hash=sha256:87bce04f09f0552b66fca0c4e10da78d17cb0e71c205864bab4e9595122cb9d9 \
|
||||
--hash=sha256:8d4dfc66abea3ec6d9f83e837a8f8a7d9d3a76d25c9911735c76d6745950e62c \
|
||||
--hash=sha256:8ec364e280db4235389b5e1e6ee924723c693cbc98e9d28dc1767041ff9bc388 \
|
||||
--hash=sha256:8fa00fa24ffd8c31fac081bf7be7eb495be6d248db127f8776575a746fa55c95 \
|
||||
--hash=sha256:920c4897e55e2881db6a6da151198e5001552c3777cd42b8a4c2f72eedc2ee91 \
|
||||
--hash=sha256:920f4633bee43d7a2818e1a1a788906df5a17b7ab6fe411220ed92b42940f818 \
|
||||
--hash=sha256:9795f56aa6b2296f05ac79d8a424e94056730c0b860a62b0fdcfe6340b658cc8 \
|
||||
--hash=sha256:98f0edee7ee9cc7f9221af2e1b95bd02810e1c7a6d115cfd82698803d385b28f \
|
||||
--hash=sha256:99c095457eea8550c9fa9a7a992e842aeae1429dab6b6b378710f62bfb70b394 \
|
||||
--hash=sha256:99d3a433ef5dc3021c9534a58a3686c88363c591974c16c54a01af7efd741f13 \
|
||||
--hash=sha256:99f9a50b56713a598d33bc23a9912224fc5d7f9f292444e6664236ae471ddf17 \
|
||||
--hash=sha256:9c46e556ee266ed3fb7b7a882b53df3c76b45e872fdab8d9cf49ae5e91147fd7 \
|
||||
--hash=sha256:9f5d37ff01edcbace53a402e80793640c25798fb7208f105d87a25e6fcc9ea06 \
|
||||
--hash=sha256:a0b4cfe408cd84c53bab7d83e4209458de676a6ec5e9c623ae914ce1cb79b96f \
|
||||
--hash=sha256:a497be217818c318d93f07e14502ef93d44e6a20c72b04c530611e45e54c2196 \
|
||||
--hash=sha256:ac89ccc39cd1d556cc72d6752f252dc869dde41c7c936e86beac5eb555041b66 \
|
||||
--hash=sha256:adf28099d061a25fbcc6531febb7a091e027605385de9fe14dd6a97319d614cf \
|
||||
--hash=sha256:afa01d25769af33a8dac0d905d5c7bb2d73c7c3d5161b2dd6f8b5b5eea6a3c4c \
|
||||
--hash=sha256:b1fc07896fc1851558f532dffc8987e526b682ec73140886c831d773cef44b76 \
|
||||
--hash=sha256:b49c604ace7a7aa8af31196abbf8f2193be605db6739ed905ecaf62af31ccae0 \
|
||||
--hash=sha256:b9f3e0bffad6e238f7acc20c393c1ed8fab4371e3b3bc311020dfa6020d99212 \
|
||||
--hash=sha256:ba07646f35e4e49376c9831130039d1b478fbfa1215ae62ad62d2ee63cf9c18f \
|
||||
--hash=sha256:bd88f40f2294440d3f3c6308e50d96a0d3d0973d6f1a5732875d10f569acef49 \
|
||||
--hash=sha256:c0be58529d43d38ae849a91932391eb93275a06b93b79a8ab828b012e916a206 \
|
||||
--hash=sha256:c45f62e4107ebd05166717ac58f6feb44471ed450d07fecd90e5f69d9bf03c48 \
|
||||
--hash=sha256:c56da23034fe66221f2208c813d8aa509eea34d97328ce2add56e219c3a9f41c \
|
||||
--hash=sha256:c94b5537bf6ce66e4d7830c6993152940a188600f6ae044435287753044a8fe2 \
|
||||
--hash=sha256:cebf8d56fee3b08ad40d332a807ecccd4153d3f1ba8231e111d9759f02edfd05 \
|
||||
--hash=sha256:d0bf6f93a55d3fa7a079d811b29100b019784e2ee6bc06b0bb839538272a5610 \
|
||||
--hash=sha256:d195add190abccefc70ad0f9a0141ad7da53e16183048380e688b466702195dd \
|
||||
--hash=sha256:d25ef0c33f22649b7a088035fd65ac1ce6464fa2876578df1adad9472f918a76 \
|
||||
--hash=sha256:d6cbdf12ef967a6aa401cf5cdf47850559e59eedad10e781471c960583f25aa1 \
|
||||
--hash=sha256:d8c032ccee90b37b44e05948b449a2d6baed7e614df3d3f47fe432c952c21b60 \
|
||||
--hash=sha256:daff04257b49ab7f4b3f73f98283d3dbb1a65bf3500d55c7beac3c66c310fe34 \
|
||||
--hash=sha256:e83ebbf020be727d6e0991c1b192a5c2e7113eb66e3def0cd0c62f9f266247e4 \
|
||||
--hash=sha256:ed3025a8a7e5a59817b7494686d449ebfbe301f3e757b852c8d0d1961d6be864 \
|
||||
--hash=sha256:f1936ef138bed2165dd8573aa65e3095ef7c2b6247faccd0e15186aabdda7f66 \
|
||||
--hash=sha256:f5247a3d74355f8b1d780d0f3b32a23dd9f6d3ff43ef2037c6dcd249f35ecf4c \
|
||||
--hash=sha256:fa496cd45cda0165d597e9d6f01e36c33c9508f75cf03c0a650018c5048f578e \
|
||||
--hash=sha256:fb4363e6c9fc87365c2bc777a1f585a22f2f56642501885ffc7942138499bf54 \
|
||||
--hash=sha256:fb4370b15111905bf8b5ba2129b926af9470f014cb0493a67d23e9d7a48348e8 \
|
||||
--hash=sha256:fbec2af0ebafa57eb82c18c304b37c86a8abddf7022955d1742b3d5471a6339e
|
||||
pydantic==2.6.0 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:1440966574e1b5b99cf75a13bec7b20e3512e8a61b894ae252f56275e2c465ae \
|
||||
--hash=sha256:ae887bd94eb404b09d86e4d12f93893bdca79d766e738528c6fa1c849f3c6bcf
|
||||
sniffio==1.3.0 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101 \
|
||||
--hash=sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384
|
||||
starlette==0.35.1 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:3e2639dac3520e4f58734ed22553f950d3f3cb1001cd2eaac4d57e8cdc5f66bc \
|
||||
--hash=sha256:50bbbda9baa098e361f398fda0928062abbaf1f54f4fadcbe17c092a01eb9a25
|
||||
typing-extensions==4.9.0 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783 \
|
||||
--hash=sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd
|
||||
uvicorn==0.27.0.post1 ; python_version >= "3.9" and python_version < "4.0" \
|
||||
--hash=sha256:4b85ba02b8a20429b9b205d015cbeb788a12da527f731811b643fd739ef90d5f \
|
||||
--hash=sha256:54898fcd80c13ff1cd28bf77b04ec9dbd8ff60c5259b499b4b12bb0917f22907
|
58
mirrors/server.py
Normal file
58
mirrors/server.py
Normal file
@ -0,0 +1,58 @@
|
||||
import base64
|
||||
import signal
|
||||
import urllib.parse
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from starlette.requests import Request
|
||||
|
||||
from config import BASE_DOMAIN, RPC_SECRET, EXTERNAL_URL_ARIA2, EXTERNAL_HOST_ARIA2
|
||||
from sites.docker import docker
|
||||
from sites.npm import npm
|
||||
from sites.pypi import pypi
|
||||
from sites.torch import torch
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def capture_request(request: Request, call_next: callable):
|
||||
hostname = request.url.hostname
|
||||
if not hostname.endswith(f".{BASE_DOMAIN}"):
|
||||
return await call_next(request)
|
||||
|
||||
if hostname.startswith("pypi."):
|
||||
return await pypi(request)
|
||||
if hostname.startswith("torch."):
|
||||
return await torch(request)
|
||||
if hostname.startswith("docker."):
|
||||
return await docker(request)
|
||||
if hostname.startswith("npm."):
|
||||
return await npm(request)
|
||||
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
port = 8080
|
||||
print(f"Server started at https://*.{BASE_DOMAIN})")
|
||||
|
||||
for dn in ["pypi", "torch", "docker", "npm"]:
|
||||
print(f" - https://{dn}.{BASE_DOMAIN}")
|
||||
|
||||
aria2_secret = base64.b64encode(RPC_SECRET.encode()).decode()
|
||||
|
||||
params = {
|
||||
'protocol': 'https',
|
||||
'host': EXTERNAL_HOST_ARIA2,
|
||||
'port': '443',
|
||||
'interface': 'jsonrpc',
|
||||
'secret': aria2_secret
|
||||
}
|
||||
|
||||
query_string = urllib.parse.urlencode(params)
|
||||
aria2_url_with_auth = EXTERNAL_URL_ARIA2 + "/#!/settings/rpc/set?" + query_string
|
||||
|
||||
print(f"Download manager (Aria2) at {aria2_url_with_auth}")
|
||||
uvicorn.run(app="server:app", host="0.0.0.0", port=port, reload=True, proxy_headers=True, forwarded_allow_ips="*")
|
100
mirrors/sites/docker.py
Normal file
100
mirrors/sites/docker.py
Normal file
@ -0,0 +1,100 @@
|
||||
import base64
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
|
||||
import httpx
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
from proxy.direct import direct_proxy
|
||||
|
||||
BASE_URL = "https://registry-1.docker.io"
|
||||
|
||||
cached_token = {
|
||||
|
||||
}
|
||||
|
||||
# https://github.com/opencontainers/distribution-spec/blob/main/spec.md
|
||||
name_regex = "[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*"
|
||||
reference_regex = "[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}"
|
||||
|
||||
|
||||
def try_extract_image_name(path):
|
||||
pattern = rf"^/v2/(.*)/([a-zA-Z]+)/(.*)$"
|
||||
match = re.search(pattern, path)
|
||||
|
||||
if match:
|
||||
assert len(match.groups()) == 3
|
||||
name, operation, reference = match.groups()
|
||||
assert re.match(name_regex, name)
|
||||
assert re.match(reference_regex, reference)
|
||||
assert operation in ["manifests", "blobs"]
|
||||
return name, operation, reference
|
||||
|
||||
return None, None, None
|
||||
|
||||
|
||||
def get_docker_token(name):
|
||||
cached = cached_token.get(name, {})
|
||||
exp = cached.get("exp", 0)
|
||||
|
||||
if exp > time.time():
|
||||
return cached.get("token", 0)
|
||||
|
||||
url = "https://auth.docker.io/token"
|
||||
params = {
|
||||
"scope": f"repository:{name}:pull",
|
||||
"service": "registry.docker.io",
|
||||
}
|
||||
|
||||
response = httpx.get(url, params=params, verify=False)
|
||||
response.raise_for_status()
|
||||
|
||||
token_data = response.json()
|
||||
token = token_data["token"]
|
||||
payload = (token.split(".")[1])
|
||||
padding = len(payload) % 4
|
||||
payload += "=" * padding
|
||||
|
||||
payload = json.loads(base64.b64decode(payload))
|
||||
assert payload["iss"] == "auth.docker.io"
|
||||
assert len(payload["access"]) > 0
|
||||
|
||||
cached_token[name] = {
|
||||
"exp": payload["exp"],
|
||||
"token": token
|
||||
}
|
||||
|
||||
return token
|
||||
|
||||
|
||||
async def docker(request: Request):
|
||||
path = request.url.path
|
||||
print("[request]", request.method, request.url)
|
||||
if not path.startswith("/v2/"):
|
||||
return Response(content="Not Found", status_code=404)
|
||||
|
||||
if path == "/v2/":
|
||||
return Response(content="OK")
|
||||
# return await direct_proxy(request, BASE_URL + '/v2/')
|
||||
|
||||
name, operation, reference = try_extract_image_name(path)
|
||||
|
||||
if not name:
|
||||
return Response(content='404 Not Found', status_code=404)
|
||||
|
||||
# support docker pull xxx which name without library
|
||||
if '/' not in name:
|
||||
name = f"library/{name}"
|
||||
|
||||
target_url = BASE_URL + f"/v2/{name}/{operation}/{reference}"
|
||||
|
||||
print('[PARSED]', path, name, operation, reference, target_url)
|
||||
|
||||
def inject_token(req, httpx_req):
|
||||
docker_token = get_docker_token(f"{name}")
|
||||
httpx_req.headers["Authorization"] = f"Bearer {docker_token}"
|
||||
return httpx_req
|
||||
|
||||
return await direct_proxy(request, target_url, pre_process=inject_token)
|
11
mirrors/sites/npm.py
Normal file
11
mirrors/sites/npm.py
Normal file
@ -0,0 +1,11 @@
|
||||
from starlette.requests import Request
|
||||
|
||||
from proxy.direct import direct_proxy
|
||||
|
||||
BASE_URL = "https://registry.npmjs.org/"
|
||||
|
||||
|
||||
async def npm(request: Request):
|
||||
path = request.url.path
|
||||
|
||||
return await direct_proxy(request, BASE_URL + path)
|
44
mirrors/sites/pypi.py
Normal file
44
mirrors/sites/pypi.py
Normal file
@ -0,0 +1,44 @@
|
||||
import re
|
||||
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
from proxy.direct import direct_proxy
|
||||
from proxy.cached import try_get_cache
|
||||
|
||||
pypi_file_base_url = "https://files.pythonhosted.org"
|
||||
pypi_base_url = "https://pypi.org"
|
||||
|
||||
|
||||
def pypi_replace(request: Request, response: Response) -> Response:
|
||||
is_detail_page = re.search(r"/simple/([^/]+)/", request.url.path) is not None
|
||||
if not is_detail_page:
|
||||
return response
|
||||
|
||||
if is_detail_page:
|
||||
mirror_url = f"{request.url.scheme}://{request.url.netloc}"
|
||||
content = response.body
|
||||
content = content.replace(pypi_file_base_url.encode(), mirror_url.encode())
|
||||
response.body = content
|
||||
del response.headers["content-length"]
|
||||
del response.headers["content-encoding"]
|
||||
return response
|
||||
|
||||
|
||||
async def pypi(request: Request) -> Response:
|
||||
# TODO: a debug flag to show origin url
|
||||
path = request.url.path
|
||||
if path == "/simple":
|
||||
path = "/simple/"
|
||||
|
||||
if path.startswith("/simple/"):
|
||||
target_url = pypi_base_url + path
|
||||
elif path.startswith("/packages/"):
|
||||
target_url = pypi_file_base_url + path
|
||||
else:
|
||||
return Response(content="Not Found", status_code=404)
|
||||
|
||||
if path.endswith(".whl") or path.endswith(".tar.gz"):
|
||||
return await try_get_cache(request, target_url)
|
||||
|
||||
return await direct_proxy(request, target_url, post_process=pypi_replace)
|
24
mirrors/sites/torch.py
Normal file
24
mirrors/sites/torch.py
Normal file
@ -0,0 +1,24 @@
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
from proxy.cached import try_get_cache
|
||||
from proxy.direct import direct_proxy
|
||||
|
||||
BASE_URL = "https://download.pytorch.org"
|
||||
|
||||
|
||||
async def torch(request: Request):
|
||||
path = request.url.path
|
||||
|
||||
if not path.startswith("/whl/"):
|
||||
return Response(content="Not Found", status_code=404)
|
||||
|
||||
if path == "/whl":
|
||||
path = "/whl/"
|
||||
|
||||
target_url = BASE_URL + path
|
||||
|
||||
if path.endswith(".whl") or path.endswith(".tar.gz"):
|
||||
return await try_get_cache(request, target_url)
|
||||
|
||||
return await direct_proxy(request, target_url, )
|
Loading…
x
Reference in New Issue
Block a user