Baseline: pr1 HYC下载站 v2.3 before security/functional fixes

This commit is contained in:
HYC Fixer
2026-08-30 12:12:58 +08:00
commit a8e773839b
77 changed files with 38568 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
.venv/
venv/
*.egg-info/
dist/
build/
# IDE
.idea/
.vscode/
*.swp
*.swo
# 项目特定
.claude/
.spec-workflow/
.zcf/
*.log
*.bak
debug.txt
api_keys.json
auth_sessions.json
auth_token.txt
sync_state.json
data/
logs/
downloads/
cache/
# OS
.DS_Store
Thumbs.db
+16
View File
@@ -0,0 +1,16 @@
{
"extends": [
"development"
],
"hints": {
"meta-viewport": "off",
"compat-api/css": [
"default",
{
"ignore": [
"backdrop-filter"
]
}
]
}
}
+328
View File
@@ -0,0 +1,328 @@
# Docker 部署指南
本项目提供多种 Docker 部署方式,支持多种架构和配置。
## 目录
- [快速开始](#快速开始)
- [Docker 镜像](#docker-镜像)
- [Dockerfile 说明](#dockerfile-说明)
- [Docker Compose](#docker-compose)
- [Kubernetes 部署](#kubernetes-部署)
## 快速开始
### 使用 Docker Hub 镜像(推荐)
```bash
# 拉取最新镜像
docker pull hx100cv/hyc-download:latest
# 运行容器
docker run -d \
--name hyc-download \
-p 8080:8080 \
-v /data:/data \
-v /downloads:/downloads \
hx100cv/hyc-download:latest
```
### 使用 GitHub Container Registry
```bash
# 拉取镜像
docker pull ghcr.io/hexikx/mirror_server:latest
# 运行容器
docker run -d \
--name hyc-download \
-p 8080:8080 \
-v /data:/data \
-v /downloads:/downloads \
ghcr.io/hexikx/mirror_server:latest
```
## Docker 镜像
### 多架构支持
项目自动构建以下架构的镜像:
| 架构 | 镜像标签 |
|------|----------|
| Linux AMD64 | `hx100cv/hyc-download:amd64-latest` |
| Linux ARM64 | `hx100cv/hyc-download:arm64v8-latest` |
| Linux ARMv7 | `hx100cv/hyc-download:arm32v7-latest` |
### 拉取特定架构
```bash
# AMD64
docker pull hx100cv/hyc-download:amd64-latest
# ARM64
docker pull hx100cv/hyc-download:arm64v8-latest
# ARMv7
docker pull hx100cv/hyc-download:arm32v7-latest
```
### 使用 Manifest(自动选择架构)
```bash
# 自动选择对应架构的镜像
docker pull hx100cv/hyc-download:latest
```
## Dockerfile 说明
### Dockerfile(推荐)
完整功能镜像,包含所有依赖。
```dockerfile
FROM hx100cv/hyc-download:latest
```
### Dockerfile.lite
轻量级镜像,适合资源受限的设备。
```dockerfile
FROM hx100cv/hyc-download:lite
```
### Dockerfile.multiarch
用于构建多架构镜像的 Dockerfile。
```bash
# 构建多架构镜像
docker buildx build -f docker/Dockerfile.multiarch \
--platform linux/amd64,linux/arm64,linux/arm/v7 \
-t hx100cv/hyc-download:multi \
--push .
```
## Docker Compose
### 基础用法
```bash
# 启动服务
docker-compose up -d
# 查看日志
docker-compose logs -f
# 停止服务
docker-compose down
```
### 配置文件
项目提供以下 Docker Compose 配置文件:
| 文件 | 说明 |
|------|------|
| `docker-compose.yml` | 完整配置 |
| `docker-compose.lite.yml` | 轻量配置 |
| `docker-compose.raspberry.yml` | Raspberry Pi 配置 |
```bash
# 使用完整配置
docker-compose -f docker-compose.yml up -d
# 使用轻量配置
docker-compose -f docker-compose.lite.yml up -d
# 使用 Raspberry Pi 配置
docker-compose -f docker-compose.raspberry.yml up -d
```
### 带 Nginx 反向代理
```bash
docker-compose --profile with-nginx up -d
```
## Kubernetes 部署
### 部署文件
项目提供以下 K8s 配置文件:
| 文件 | 说明 |
|------|------|
| `k8s/deployment.yaml` | Deployment 配置 |
| `k8s/service.yaml` | Service 配置 |
| `k8s/helm/` | Helm Chart |
### 使用 Helm 部署
```bash
# 添加 Helm 仓库
helm repo add hyc-download https://hexikx.github.io/mirror_server
# 更新 Helm 仓库
helm repo update
# 部署
helm install my-hyc-download hyc-download/hyc-download
```
### 手动部署
```bash
# 应用配置
kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml
# 查看状态
kubectl get pods
kubectl get services
```
## 数据持久化
### 挂载卷
```bash
# 数据目录
-v /path/to/data:/data
# 下载目录
-v /path/to/downloads:/downloads
# 配置目录
-v /path/to/config:/app/config
```
### 使用命名卷
```yaml
volumes:
- data:/data
- downloads:/downloads
```
## 环境变量
### 支持的环境变量
| 变量名 | 说明 | 默认值 |
|--------|------|--------|
| `HYC_HOST` | 监听地址 | `0.0.0.0` |
| `HYC_PORT` | 监听端口 | `8080` |
| `HYC_BASE_DIR` | 文件存储目录 | `/downloads` |
| `HYC_AUTH_TYPE` | 认证类型 | `none` |
| `HYC_AUTH_USER` | 认证用户名 | `admin` |
| `HYC_AUTH_PASS` | 认证密码 | `password` |
| `HYC_ENABLE_MONITOR` | 启用监控 | `true` |
| `HYC_ENABLE_MIRRORS` | 启用镜像加速 | `true` |
| `HYC_RATE_LIMIT` | 下载限速 | `0` |
### 示例
```bash
docker run -d \
--name hyc-download \
-p 8080:8080 \
-e HYC_AUTH_TYPE=basic \
-e HYC_AUTH_USER=admin \
-e HYC_AUTH_PASS=secret \
hx100cv/hyc-download:latest
```
## 健康检查
容器内置健康检查:
```yaml
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/api/v1/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
```
## 日志
### 查看日志
```bash
# 实时日志
docker logs -f hyc-download
# 最近 100 行
docker logs --tail 100 hyc-download
```
### 日志级别
```bash
# 详细输出
docker run -e HYC_VERBOSE=1 hx100cv/hyc-download:latest
# 静默模式
docker run -e HYC_QUIET=1 hx100cv/hyc-download:latest
```
## 更新升级
### 拉取新版本
```bash
# 拉取最新镜像
docker pull hx100cv/hyc-download:latest
# 停止旧容器
docker-compose down
# 启动新容器
docker-compose up -d
```
### 使用 Watchtower 自动更新
```bash
# 运行 Watchtower
docker run -d \
--name watchtower \
-v /var/run/docker.sock:/var/run/docker.sock \
containrrr/watchtower \
hyc-download
```
## 故障排查
### 容器无法启动
```bash
# 查看详细错误
docker logs hyc-download
# 进入容器排查
docker exec -it hyc-download /bin/bash
```
### 端口冲突
```bash
# 检查端口占用
netstat -tlnp | grep 8080
# 修改映射端口
docker run -p 8888:8080 hx100cv/hyc-download:latest
```
### 权限问题
```bash
# 修改目录权限
chmod 777 /data /downloads
# 或使用用户运行
docker run -u 1000:1000 hx100cv/hyc-download:latest
```
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 HYC Download Station
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+723
View File
@@ -0,0 +1,723 @@
# HYC下载站 v2.3
模块化重构的Python镜像文件服务器,支持API版本化、实时通信、系统监控、数据库集成等高级功能。
## 项目结构
```
mirror_server/
├── main.py # 主入口文件
├── requirements.txt # 依赖列表
├── settings.json # 主配置文件
├── auth_token.txt # 认证令牌文件
├── api/ # API模块
│ ├── __init__.py
│ ├── router.py # API路由器
│ ├── v1.py # API v1(基础功能)
│ ├── v2.py # API v2(增强功能)
│ ├── admin.py # 管理接口
│ ├── ws_handler.py # WebSocket处理
│ ├── sse_handler.py # SSE事件流
│ └── docs/ # API文档
├── core/ # 核心模块
│ ├── __init__.py
│ ├── config.py # 配置管理
│ ├── config_hotreload.py # 配置热重载
│ ├── utils.py # 工具函数
│ ├── server.py # 服务器核心
│ ├── database.py # 数据库管理
│ ├── mirror_sync.py # 镜像同步管理
│ ├── sync_scheduler.py # 同步调度器
│ ├── sync_engine.py # 同步引擎
│ ├── cache_manager.py # 缓存管理
│ ├── cache_prewarm.py # 缓存预热
│ ├── monitor.py # 系统监控
│ ├── health_check.py # 健康检查
│ ├── alerts.py # 告警系统
│ ├── security.py # 安全模块
│ ├── api_auth.py # API认证
│ ├── api_docs.py # API文档
│ ├── optimization.py # 性能优化
│ ├── scheduler.py # 任务调度
│ ├── graceful_restart.py # 优雅重启
│ └── prometheus.py # Prometheus监控
├── handlers/ # HTTP请求处理器
│ ├── __init__.py
│ └── http_handler.py # HTTP请求处理
├── mirrors/ # 镜像加速源模块
│ ├── __init__.py
│ ├── http.py # HTTP/HTTPS镜像
│ ├── docker.py # Docker Hub镜像
│ ├── apt.py # APT镜像
│ ├── yum.py # YUM镜像
│ ├── pypi.py # PyPI镜像
│ ├── npm.py # npm镜像
│ └── go.py # Go镜像
├── scripts/ # 辅助脚本
├── data/ # 数据目录
├── docker/ # Docker配置
└── k8s/ # Kubernetes配置
```
## 安装
```bash
pip install -r requirements.txt
```
### 依赖
- psutil - 系统监控
- python-multipart - 多部分表单数据处理
- paramiko - SFTP支持(可选)
## 使用
### 基本使用
```bash
# 启动服务器(默认端口8080)
python main.py
# 指定端口和目录
python main.py -p 8080 -d ./downloads
# 启用HTTPS
python main.py --ssl-cert cert.pem --ssl-key key.pem
# 使用配置文件
python main.py --config config.json
# 指定API版本 (默认: v2)
python main.py --api-version v2
# 使用settings.json配置文件
python main.py --settings settings.json
# 检查系统兼容性
python main.py --check-compat
```
### 认证
```bash
# 无认证
python main.py --auth-type none
# 基本认证
python main.py --auth-type basic --auth-user admin --auth-pass password
# 令牌认证
python main.py --auth-type token --auth-token your_token_here
```
### 功能开关
```bash
# 禁用监控/同步/加速源
python main.py --enable-monitor --enable-sync --enable-mirrors
# 禁用WebSocket/SSE (适合低端设备)
python main.py --disable-ws --disable-sse
# 禁用文件哈希计算
python main.py --disable-hash
# 启用下载统计
python main.py --enable-stats --show-hash
# 启用目录浏览
python main.py --directory-listing true
```
### 镜像加速源
```bash
# 启用/禁用所有加速源功能
python main.py --enable-mirrors # 启用
python main.py --enable-mirrors=false # 禁用
# 各个镜像的启用/禁用通过 settings.json 配置
# 在 mirrors.xxx.enabled 中设置,例如:
# "docker": { "enabled": true, ... }
# "pypi": { "enabled": true, ... }
```
### 镜像同步配置
镜像同步支持多种同步类型,通过 `settings.json` 中的 `sync_sources` 配置:
```json
{
"mirrors": {
"my-http-mirror": {
"type": "http",
"url": "https://example.com/mirror/",
"target": "downloads/mirror",
"enabled": true,
"auto_sync": true,
"schedule": { "enabled": true, "type": "cron", "cron": "0 4 * * *" }
},
"my-ftp-mirror": {
"type": "ftp",
"host": "ftp.example.com",
"port": 21,
"username": "anonymous",
"password": "[email protected]",
"remote_path": "/pub",
"target": "downloads/ftp",
"enabled": true
},
"my-sftp-mirror": {
"type": "sftp",
"host": "sftp.example.com",
"port": 22,
"username": "syncuser",
"password": "password",
"private_key": "/path/to/id_rsa",
"remote_path": "/mirror",
"target": "downloads/sftp"
},
"my-git-repo": {
"type": "git",
"url": "https://github.com/example/repo.git",
"branch": "main",
"depth": 1,
"target": "downloads/git"
},
"my-s3-mirror": {
"type": "s3",
"endpoint": "https://s3.amazonaws.com",
"bucket": "my-bucket",
"access_key": "AKIAIOSFODNN7EXAMPLE",
"secret_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"region": "us-east-1",
"prefix": "mirrors/",
"target": "downloads/s3"
},
"my-oss-mirror": {
"type": "oss",
"bucket": "my-bucket",
"access_key": "your-access-key",
"secret_key": "your-secret-key",
"region": "cn-hangzhou",
"target": "downloads/oss"
},
"my-cos-mirror": {
"type": "cos",
"bucket": "my-bucket",
"access_key": "your-access-key",
"secret_key": "your-secret-key",
"region": "ap-guangzhou",
"target": "downloads/cos"
},
"my-rsync-mirror": {
"type": "rsync",
"source": "rsync://rsync.example.com/module/path",
"target": "downloads/rsync",
"exclude": ["*.tmp", "*.log"]
},
"my-local-mirror": {
"type": "local",
"path": "/mnt/external drive/mirrors",
"target": "downloads/local"
}
}
}
```
**支持的同步类型:**
| 类型 | 说明 | 必需配置 |
|------|------|----------|
| `http` / `https` | HTTP/HTTPS同步 | `url`, `target` |
| `ftp` | FTP同步 | `host`, `target` |
| `sftp` | SFTP同步 | `host`, `username`, `target` |
| `rsync` | Rsync同步 | `source`, `target` |
| `git` | Git仓库克隆/更新 | `url`, `target` |
| `s3` | AWS S3 / MinIO | `bucket`, `access_key`, `secret_key`, `endpoint` |
| `oss` | 阿里云OSS | `bucket`, `access_key`, `secret_key` |
| `cos` | 腾讯云COS | `bucket`, `access_key`, `secret_key` |
| `webdav` | WebDAV同步 | `url`, `username`, `password` |
| `local` | 本地目录同步 | `path`, `target` |
### 下载限速
```bash
# 全局限速 (字节/秒)
python main.py --rate-limit 10485760 # 10MB/s
```
### 低端设备优化
```bash
# 使用预设配置
python main.py --preset ultra_low # 超低功耗
python main.py --preset low # 低配置
python main.py --preset medium # 中等
python main.py --preset high # 高性能
python main.py --preset auto # 自动检测
# 自定义参数
python main.py --memory-limit 256M \
--workers 2 \
--chunk-size 64K \
--buffer-size 128K
```
### 调试选项
```bash
# 启用所有调试
python main.py --debug
# 启用特定调试
python main.py --debug-http
python main.py --debug-auth
python main.py --debug-api
python main.py --debug-v2
python main.py --debug-error
python main.py --debug-download
# 输出到调试文件
python main.py --debug-log /var/log/hyc-debug.log
# 指定调试类型
python main.py --debug-types http auth api v2 error
# 详细输出
python main.py -v
python main.py -vv
python main.py -q # 静默模式
```
### 启动参数完整列表
```
服务器配置:
--host HOST 监听地址 (默认: 0.0.0.0)
-p, --port PORT 监听端口 (默认: 8080)
-d, --base-dir DIR 文件存储目录 (默认: ./downloads)
--server-name NAME 服务器名称 (默认: HYC下载站)
HTTPS配置:
--ssl-cert FILE SSL证书文件路径
--ssl-key FILE SSL私钥文件路径
认证配置:
--auth-type TYPE 认证类型: none/basic/token (默认: none)
--auth-user USER 基本认证用户名 (默认: admin)
--auth-pass PASS 基本认证密码
--auth-token TOKEN 令牌认证密钥
功能配置:
--directory-listing BOOL 启用目录浏览 (默认: True)
--enable-stats BOOL 启用下载统计 (默认: True)
--show-hash 显示文件哈希值
--ignore-hidden 忽略隐藏文件 (默认: True)
--max-upload-size SIZE 最大上传文件大小 (默认: 1G)
--api-version VERSION API版本: v1/v2 (默认: v2)
实时通信:
--enable-ws 启用WebSocket (默认: True)
--enable-sse 启用SSE (默认: True)
系统监控:
--enable-monitor 启用系统监控 (默认: True)
--monitor-interval SEC 监控采集间隔(秒) (默认: 5)
同步配置:
--enable-sync 启用镜像同步 (默认: True)
--sync-config FILE 同步配置文件路径
镜像加速源:
--enable-mirrors 启用下载加速源 (默认: True)
# 各个镜像通过 settings.json 配置 (mirrors.xxx.enabled)
下载限速:
--rate-limit BYTES 全局下载限速 (默认: 0=不限速)
低端设备优化:
--preset PRESET 设备预设: ultra_low/low/medium/high/auto (默认: auto)
--memory-limit SIZE 内存限制 (例如: 256M, 512M, 1G)
--workers NUM 工作进程数 (0=自动)
--chunk-size SIZE 文件传输块大小 (默认: 128K)
--buffer-size SIZE 缓冲区大小 (默认: 256K)
--disable-ws 禁用WebSocket
--disable-sse 禁用SSE
--disable-hash 禁用文件哈希计算
--check-compat 检查系统兼容性后退出
日志配置:
--access-log FILE 访问日志文件路径
-v, --verbose 详细输出 (可叠加: -vvv)
-q, --quiet 静默模式
调试选项:
-D, --debug 启用所有调试输出
--debug-log FILE 调试日志文件路径
--debug-http 调试HTTP请求
--debug-auth 调试认证检查
--debug-api 调试API路由
--debug-v2 调试V2 API
--debug-error 调试错误堆栈
--debug-download 调试下载记录
--debug-types TYPES 指定调试类型列表
配置文件:
--settings FILE 默认配置文件路径 (默认: settings.json)
--config FILE 覆盖配置文件路径 (JSON格式)
```
## API版本
### API v1 - 基础功能
- 文件管理:上传、下载、删除、搜索
- 目录浏览
- 基础统计
- 同步管理(HTTP/HTTPS、FTP、SFTP、本地)
#### 示例API端点
```
GET /api/v1/files # 列出文件
GET /api/v1/file/{path} # 获取文件信息
DELETE /api/v1/file/{path} # 删除文件
POST /api/v1/upload # 上传文件
GET /api/v1/search?q={term} # 搜索文件
GET /api/v1/stats # 获取统计
GET /api/v1/health # 健康检查
POST /api/v1/sync/start # 开始同步
```
### API v2 - 增强功能
继承v1所有功能,额外提供:
- 增强搜索(模糊匹配、正则表达式、内容搜索)
- 按标签和日期范围搜索
- 文件元数据管理
- 文件版本控制
- 缩略图生成
- 实时服务器监控
- Webhook支持
- 历史数据分析
#### 示例API端点
```
GET /api/v2/search/enhanced?q={term} # 增强搜索
GET /api/v2/search/by-tag?tag={tag} # 按标签搜索
GET /api/v2/search/by-date?start={date} # 按日期搜索
GET /api/v2/stats/detailed # 详细统计
GET /api/v2/stats/trending # 热门文件
GET /api/v2/file/{path}/metadata # 获取文件元数据
PUT /api/v2/file/{path}/metadata # 更新文件元数据
GET /api/v2/file/{path}/versions # 获取文件版本
POST /api/v2/file/{path}/versions # 创建文件版本
GET /api/v2/file/{path}/thumbnail?w=200&h=200 # 获取缩略图
GET /api/v2/monitor/realtime # 实时监控
GET /api/v2/webhooks # 列出webhooks
POST /api/v2/webhooks # 创建webhook
```
## 配置
### 配置文件示例
参考 `config.example.json`
### 命令行参数
```
服务器配置:
--host 监听地址 (默认: 0.0.0.0)
-p, --port 监听端口 (默认: 8080)
-d, --base-dir 文件存储目录 (默认: ./downloads)
--server-name 服务器名称
HTTPS配置:
--ssl-cert SSL证书文件路径
--ssl-key SSL私钥文件路径
认证配置:
--auth-type 认证类型: none, basic, token
--auth-user 基本认证用户名
--auth-pass 基本认证密码
--auth-token 令牌认证密钥
功能配置:
--no-directory-listing 禁用目录浏览
--no-stats 禁用下载统计
--show-hash 显示文件哈希值
--max-upload-size 最大上传文件大小 (默认: 1G)
--sync-config 同步配置文件路径
--api-version API版本: v1, v2 (默认: v1)
日志配置:
--access-log 访问日志文件路径
-v, --verbose 详细输出
-q, --quiet 静默模式
配置:
--config 配置文件路径 (JSON格式)
```
## 模块说明
### core/ - 核心模块
- **config.py**: 配置管理和验证,支持多级配置合并
- **config_hotreload.py**: 配置文件热重载,无需重启生效
- **utils.py**: 通用工具函数(文件大小格式化、哈希计算等)
- **server.py**: 服务器核心类,基于aiohttp
- **database.py**: 数据库管理,支持SQLite/MySQL/PostgreSQL
- **mirror_sync.py**: 镜像同步管理器(支持HTTP/HTTPS、FTP、SFTP、本地、Rsync、Git、S3/OSS/COS、WebDAV)
- **sync_scheduler.py**: 同步调度器,定时执行同步任务
- **sync_engine.py**: 同步引擎,核心同步逻辑
- **cache_manager.py**: 缓存管理,智能缓存策略
- **cache_prewarm.py**: 缓存预热,启动时预加载热门资源
- **monitor.py**: 系统监控,CPU/内存/磁盘/网络实时监控
- **health_check.py**: 健康检查,系统组件状态检测
- **alerts.py**: 告警系统,异常情况自动告警
- **security.py**: 安全模块,防护和审计
- **api_auth.py**: API认证,Token/Basic认证
- **api_docs.py**: API文档自动生成
- **optimization.py**: 性能优化,自动检测设备配置
- **scheduler.py**: 通用任务调度器
- **graceful_restart.py**: 优雅重启,服务无缝更新
- **prometheus.py**: Prometheus监控指标导出
### api/ - API模块
- **router.py**: API路由器,支持版本化
- **v1.py**: API v1实现(基础功能)
- **v2.py**: API v2实现(增强功能,继承v1)
- **admin.py**: 管理接口,Webhooks管理
- **ws_handler.py**: WebSocket处理,实时双向通信
- **sse_handler.py**: SSE事件流,单向实时推送
- **docs/**: API文档 (index.html, api.md)
### handlers/ - HTTP请求处理器
- **http_handler.py**: HTTP请求处理,包括文件服务、目录浏览等
## 特性
- ✅ 模块化设计,易于扩展
- ✅ API版本化支持(v1/v2)
- ✅ 镜像同步(HTTP/HTTPS、FTP、SFTP、Rsync、Git、S3/OSS/COS、WebDAV、本地目录)
- ✅ 文件上传下载(支持断点续传)
- ✅ 目录浏览(镜像站风格)
- ✅ 认证支持(无、基本认证、令牌认证)
- ✅ HTTPS支持
- ✅ 下载统计
- ✅ 文件搜索(基础/增强/正则)
- ✅ 批量操作
- ✅ 压缩/解压缩
- ✅ 增强搜索(v2)
- ✅ 文件元数据管理(v2)
- ✅ 服务器监控(v2)
- ✅ Webhook支持(v2)
- ✅ 实时通信(WebSocket + SSE)
- ✅ 数据库支持(SQLite/MySQL/PostgreSQL)
- ✅ 缓存系统(智能/手动/全同步)
- ✅ 配置热重载
- ✅ 优雅重启
- ✅ Prometheus监控
- ✅ 告警系统
- ✅ 设备自动优化
- ✅ 多镜像加速源(Docker/APT/YUM/PyPI/npm/Go)
- ✅ Web管理界面
## 开发
### 添加新的API版本
1. 在 `api/` 目录下创建 `v3.py`
2. 继承 `APIv2` 或 `APIv1`
3. 实现所需方法
4. 在 `api/router.py` 中注册新版本
### 添加新的同步类型
在 `core/mirror_sync.py` 的 `MirrorSyncManager` 类中:
1. 添加 `_sync_{type}` 方法
2. 实现同步逻辑
3. 在 `_sync_worker` 中添加路由
## 构建和部署
### 使用 PyInstaller 打包成二进制文件
#### 构建当前平台
项目提供了 `build.py` 脚本,可以将项目打包成当前平台的二进制可执行文件。
```bash
# 安装 PyInstaller
pip install pyinstaller
# 查看支持的目标平台
python build.py --list
# 构建当前平台
python build.py
# 清理构建缓存
python build.py --clean
```
**支持的平台(仅当前平台):**
- Linux: amd64, arm64, 386
- Windows: amd64, 386
- macOS: amd64, arm64
构建完成后,二进制文件和发布包会输出到 `dist/` 目录。
#### 跨平台构建(GitHub Actions)
使用 GitHub Actions 可以方便地构建多平台版本(Linux ARM、AMD64 等):
```bash
# 1. 初始化 git 仓库
git init
git add .
git commit -m "Add build workflow"
# 2. 添加远程仓库(替换为你的仓库地址)
git remote add origin https://github.com/your-username/your-repo.git
git branch -M main
git push -u origin main
```
推送完成后:
1. 访问 GitHub 仓库
2. 点击 **Actions** 标签
3. 选择 **"Build Linux ARM Binaries"**
4. 点击 **"Run workflow"**
5. 等待 5-10 分钟后下载构建产物
**优势:**
- ✅ 完全免费
- ✅ 无需本地配置
- ✅ 支持多平台构建
- ✅ 自动化持续集成
详细说明请参考 [BUILD.md](../BUILD.md)
#### 快速开始
**Linux/macOS:**
```bash
# 给脚本添加执行权限
chmod +x docker-deploy.sh
# 构建并运行
./docker-deploy.sh build
./docker-deploy.sh run
# 使用 docker-compose
./docker-deploy.sh compose
```
**Windows:**
```batch
docker-deploy.bat build
docker-deploy.bat run
```
### HTTPS 支持
#### 使用 Nginx 反向代理
1. 创建 SSL 证书目录并放置证书
```bash
mkdir -p ssl
# 将 cert.pem 和 key.pem 放入 ssl/ 目录
```
2. 使用带 Nginx 的 docker-compose 启动
```bash
docker-compose --profile with-nginx up -d
```
#### 直接启用 HTTPS
```bash
python main.py --ssl-cert cert.pem --ssl-key key.pem
```
详细说明请参考 [DOCKER.md](DOCKER.md)
## CI/CD 自动构建
项目使用 GitHub Actions 自动构建 Docker 镜像和可执行文件。
### GitHub Actions 工作流
| 工作流文件 | 触发条件 | 功能 |
|------------|----------|------|
| `.github/workflows/releases.yml` | 打 Tag (`v*`) | 构建并发布二进制可执行文件到 GitHub Releases |
| `.github/workflows/docker-multiarch.yml` | 打 Tag (`v*`) | 构建并推送多架构 Docker 镜像到 Docker Hub |
| `.github/workflows/ci-cd.yml` | push main / PR | 运行测试 |
### 发布新版本
```bash
# 1. 推送所有代码
git push origin main
# 2. 创建版本标签
git tag v1.0.0
# 3. 推送到远程
git push origin v1.0.0
```
推送 Tag 后,GitHub Actions 会自动:
- ✅ 构建 4 个架构的二进制文件(Linux AMD64/ARM64/ARMv7 + Windows AMD64)
- ✅ 构建 3 个架构的 Docker 镜像(AMD64/ARM64/ARMv7)
- ✅ 上传到 GitHub Releases
- ✅ 推送到 Docker Hub 和 GitHub Container Registry
### GitHub Secrets 配置
需要配置以下 Secrets 才能正常工作:
| Secret 名称 | 值 | 说明 |
|-------------|-----|------|
| `DOCKERHUB_USERNAME` | Docker Hub 用户名 | |
| `DOCKERHUB_TOKEN` | Docker Hub Access Token | |
### Docker Hub 镜像
构建完成后,镜像会自动推送到:
- **Docker Hub**: `hx100cv/hyc-download:latest`
- **GitHub Container Registry**: `ghcr.io/hexikx/mirror_server:latest`
### GitHub Releases
每个版本会生成以下文件:
| 文件名 | 说明 |
|--------|------|
| `hyc-download-linux-amd64` | Linux x64 可执行文件 |
| `hyc-download-linux-arm64` | Linux ARM64 可执行文件 |
| `hyc-download-linux-armv7` | Linux ARMv7 可执行文件 |
| `hyc-download-windows-amd64.exe` | Windows x64 可执行文件 |
## Stargazers over time
[![Stargazers over time](https://starchart.cc/HEXIkx/mirror_server.svg?variant=adaptive)](https://starchart.cc/HEXIkx/mirror_server)
## Contributors
[![Contributors](https://contrib.rocks/image?repo=HEXIkx/mirror_server)](https://github.com/HEXIkx/mirror_server/graphs/contributors)
## License
[The MIT License (MIT)](https://github.com/HEXIkx/mirror_server/blob/main/LICENSE)
+6
View File
@@ -0,0 +1,6 @@
# API模块初始化
from .router import APIRouter
from .v1 import APIv1
from .v2 import APIv2
__all__ = ['APIRouter', 'APIv1', 'APIv2']
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
管理员API处理器
提供认证相关API(无 keys 管理)
"""
import json
import time
from core.api_auth import APIAuthManager, require_auth
class AdminAPI:
"""管理员API处理器"""
def __init__(self, config: dict):
self.config = config
self.auth_manager = APIAuthManager(config)
def handle_request(self, handler, method, path, query_params):
"""处理管理员API请求"""
# 解析路径
parts = path.strip('/').split('/')
# 根路径 - 列出API
if len(parts) == 0 or parts[0] == '':
self._api_overview(handler)
return
action = parts[0]
if action == 'auth':
self._handle_auth(handler, method, query_params)
elif action == 'sessions':
self._handle_sessions(handler, method, parts[1:] if len(parts) > 1 else [])
elif action == 'stats':
self._handle_stats(handler)
else:
handler.send_json_response({
"error": f"Unknown admin action: {action}",
"available_actions": ["auth", "sessions", "stats"]
}, 404)
def _api_overview(self, handler):
"""API概览"""
handler.send_json_response({
"name": "HYC Admin API",
"version": "1.0",
"description": "管理员认证API",
"endpoints": {
"GET /api/v2/admin/sessions": "列出活跃会话",
"DELETE /api/v2/admin/sessions/{session_id}": "销毁会话",
"POST /api/v2/admin/auth/verify": "验证认证状态",
"GET /api/v2/admin/stats": "获取认证统计"
},
"authentication": {
"methods": [
"Authorization: Bearer <token>",
"X-API-Key: <token>",
"Cookie: hyc_auth=<session>",
"?key=<token>"
]
}
})
def _handle_auth(self, handler, method, query_params):
"""处理认证相关"""
# POST /api/v2/admin/auth/verify - 验证当前认证状态
if method == 'POST':
auth_result = handler.auth_result if hasattr(handler, 'auth_result') else {}
if auth_result.get('authenticated'):
handler.send_json_response({
"authenticated": True,
"level": auth_result.get('level'),
"key_id": auth_result.get('key_id'),
"name": auth_result.get('name'),
"permissions": auth_result.get('permissions', [])
})
else:
handler.send_json_response({
"authenticated": False
}, 401)
else:
handler.send_json_response({"error": "Invalid method"}, 405)
def _handle_sessions(self, handler, method, parts):
"""处理会话管理"""
if method == 'GET':
# 列出活跃会话
sessions = []
for session_id, session in self.auth_manager.sessions.items():
if time.time() < session.expires_at:
sessions.append({
"session_id": session.session_id,
"user_id": session.user_id,
"level": session.level,
"created_at": session.created_at,
"expires_at": session.expires_at,
"last_activity": session.last_activity
})
handler.send_json_response({
"sessions": sessions,
"count": len(sessions)
})
elif method == 'DELETE' and len(parts) >= 1 and parts[0]:
session_id = parts[0]
success = self.auth_manager.destroy_session(session_id)
if success:
handler.send_json_response({
"success": True,
"message": f"Session {session_id} destroyed"
})
else:
handler.send_json_response({"error": "Session not found"}, 404)
else:
handler.send_json_response({"error": "Invalid request"}, 400)
def _handle_stats(self, handler):
"""获取认证统计"""
stats = self.auth_manager.get_stats()
handler.send_json_response(stats)
+282
View File
@@ -0,0 +1,282 @@
# 低端设备优化指南
## 支持的架构
| 架构 | 状态 | 说明 |
|------|------|------|
| X86_64/AMD64 | ✓ 完全支持 | 主流 PC/服务器 |
| X86_32/i386/i686 | ✓ 支持 | 老旧 PC 设备 |
| ARM64/AArch64 | ✓ 完全支持 | 树莓派4/5, Jetson |
| ARMv7/armhf | ✓ 支持 | 树莓派3/2, Orange Pi |
| ARMv6 | ⚠ 实验性 | 树莓派Zero |
| MIPS | ⚠ 实验性 | 路由器等 |
## 设备预设
### ultra_low (极低端设备)
- **内存**: < 256MB RAM
- **示例**: 树莓派 Zero, 老旧路由器
- **配置**:
```bash
python main.py --preset ultra_low
```
- **自动设置**:
- Workers: 1
- 最大缓存: 50MB
- 块大小: 32KB
- 禁用: WebSocket, SSE, 哈希计算
### low (低端设备)
- **内存**: 256MB - 512MB RAM
- **示例**: 树莓派 2, 老旧 VPS
- **配置**:
```bash
python main.py --preset low
```
- **自动设置**:
- Workers: 1
- 最大缓存: 100MB
- 块大小: 64KB
- 启用所有功能
### medium (中等设备)
- **内存**: 512MB - 1GB RAM
- **示例**: 树莓派 4 (1GB), 低配 VPS
- **配置**:
```bash
python main.py --preset medium
```
- **自动设置**:
- Workers: 2
- 最大缓存: 256MB
- 块大小: 128KB
### high (高端设备)
- **内存**: 1GB+ RAM
- **示例**: 树莓派 4 (4GB/8GB), 家用服务器
- **配置**:
```bash
python main.py --preset high
```
- **自动设置**:
- Workers: 4
- 最大缓存: 512MB
- 块大小: 256KB
### auto (自动检测)
- 根据系统资源自动选择预设
- **配置**:
```bash
python main.py --preset auto # 默认
```
## 手动配置
### 内存限制
```bash
# 设置 256MB 内存限制
python main.py --memory-limit 256M
# 设置 512MB 内存限制
python main.py --memory-limit 512M
```
### 工作进程
```bash
# 单进程 (低端设备)
python main.py --workers 1
# 双进程
python main.py --workers 2
```
### 传输优化
```bash
# 小块传输 (节省内存)
python main.py --chunk-size 32K --buffer-size 64K
```
### 禁用可选功能
```bash
# 禁用 WebSocket 和 SSE (节省内存)
python main.py --disable-ws --disable-sse
# 禁用哈希计算 (节省 CPU)
python main.py --disable-hash
```
## 树莓派部署
### 方式一: 使用预编译镜像
```bash
# 拉取 ARM64 镜像
docker pull hx100cv/hyc-download-station:v2.3-arm64
# 运行
docker run -d \
--name hyc-server \
-p 8080:8080 \
-v ./data:/data \
-v ./downloads:/downloads \
hyc-download-station:v2.3-arm64
```
### 方式二: 使用 Docker Compose
```bash
# 树莓派专用配置
docker-compose -f docker-compose.raspberry.yml up -d
```
### 方式三: 轻量级配置
```bash
# 适用于 512MB RAM 的树莓派
docker-compose -f docker-compose.lite.yml up -d
```
## 系统兼容性检查
```bash
# 检查系统兼容性
python main.py --check-compat
# 或使用脚本
./scripts/check-compat.sh
```
输出示例:
```
========================================
HYC下载站 v2.3 - 兼容性检查
========================================
系统信息:
- 架构: aarch64
- 系统: Linux
✓ ARM64 (64位) - 完全支持
✓ Python 3.11 - 支持
内存检查:
- 总内存: 4096MB
✓ 内存 1GB+ - 使用 high 预设
...
推荐启动命令:
python main.py --preset high
```
## Docker 多架构构建
### 环境准备
```bash
# 设置 QEMU 仿真 (x86_64 上构建 ARM)
./scripts/setup-qemu.sh
```
### 构建镜像
```bash
# 构建所有架构
./scripts/build-multiarch.sh v2.3 hyc-download-station
# 或手动构建
docker buildx build \
--platform linux/amd64,linux/arm64,linux/arm/v7 \
--tag hx100cv/hyc-download-station:v2.3 \
--file docker/Dockerfile.multiarch \
--push .
```
### 手动构建特定架构
```bash
# ARMv7
docker build \
--platform linux/arm/v7 \
--tag hx100cv/hyc-download-station:v2.3-armv7 \
--file docker/Dockerfile.lite \
--push .
# ARM64
docker build \
--platform linux/arm64 \
--tag hx100cv/hyc-download-station:v2.3-arm64 \
--file docker/Dockerfile.lite \
--push .
# i386 (32位)
docker build \
--platform linux/386 \
--tag hx100cv/hyc-download-station:v2.3-i386 \
--file docker/Dockerfile.lite \
--push .
```
## 性能调优建议
### 树莓派 4 (4GB)
```bash
# 推荐配置
python main.py \
--preset high \
--memory-limit 1G \
--workers 2
```
### 树莓派 3
```bash
# 推荐配置
python main.py \
--preset medium \
--memory-limit 512M \
--workers 2
```
### 树莓派 2/Zero
```bash
# 推荐配置
python main.py \
--preset low \
--memory-limit 256M \
--workers 1 \
--chunk-size 16K \
--disable-ws \
--disable-sse
```
## 内存使用监控
启动后可以通过 API 查看内存使用:
```bash
# 查看内存状态
curl http://localhost:8080/api/v1/monitor
# 或在 Web 界面查看
# 访问 http://localhost:8080/api/ui/
```
## 故障排除
### 内存不足
```
症状: OOM (Out of Memory) 错误
解决:
1. 使用 --preset ultra_low
2. 减小 --memory-limit
3. 增加 swap 空间
```
### 构建失败
```
症状: 无法导入模块
解决:
1. 重新安装依赖: pip install -r requirements.txt
2. 检查 Python 版本: python3 --version
```
### ARM 镜像运行失败
```
症状: Illegal instruction
解决:
1. 确保使用正确的架构镜像
2. 检查 QEMU 设置
```
+2055
View File
File diff suppressed because it is too large Load Diff
+1066
View File
File diff suppressed because it is too large Load Diff
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""API路由模块"""
import re
from urllib.parse import urlparse, parse_qs
from .v1 import APIv1
from .v2 import APIv2
from .admin import AdminAPI
from core.api_auth import APIAuthManager
class APIRouter:
"""API路由器 - 支持版本化API"""
def __init__(self, config):
self.config = config
self.api_versions = {
'v1': APIv1(config),
'v2': APIv2(config)
}
self.default_version = config.get('api_version', 'v1')
# 初始化认证管理器(使用共享实例)
if not config.get('_auth_manager'):
config['_auth_manager'] = APIAuthManager(config)
self.auth_manager = config['_auth_manager']
# 初始化Admin API
self.admin_api = AdminAPI(config)
def handle_request(self, handler, method, path, query):
"""处理API请求"""
# 注入认证管理器到handler
handler.auth_manager = self.auth_manager
# 解析路径,提取API版本
# 格式: /api/v1/... 或 /api/v2/... 或 /api/...
# 移除 /api/ 前缀
if path.startswith('api/'):
api_path = path[4:]
else:
api_path = path
# 解析版本号
parts = api_path.split('/')
if parts[0] in ['v1', 'v2']:
api_version = parts[0]
api_action = '/'.join(parts[1:]) if len(parts) > 1 else ''
else:
# 检查是否是直接访问的 admin API (不带版本前缀)
if api_path.startswith('admin/'):
admin_action = api_path[6:] # 移除 'admin/'
try:
self.admin_api.handle_request(handler, method, admin_action, {})
except Exception as e:
handler.send_json_response({
"error": f"Admin API处理错误: {str(e)}",
"path": admin_action
}, 500)
return
else:
handler.send_error(400, "未指定API请求版本/指定版本错误")
return
# 解析查询参数
parsed_query = parse_qs(query)
# 检查是否是 admin API (带版本前缀,如 /api/v2/admin/stats)
# 注意:auth/verify 应该交给 APIv2 处理,而不是 admin_api
if api_version in ['v1', 'v2'] and api_action.startswith('admin/') and not api_action.startswith('admin/auth'):
admin_action = api_action[6:] # 移除 'admin/'
try:
self.admin_api.handle_request(handler, method, admin_action, parsed_query)
except Exception as e:
handler.send_json_response({
"error": f"Admin API处理错误: {str(e)}",
"path": admin_action
}, 500)
return
# 获取对应的API处理器
api_handler = self.api_versions.get(api_version)
if not api_handler:
handler.send_json_response({
"error": f"不支持的API版本: {api_version}",
"supported_versions": list(self.api_versions.keys())
}, 400)
return
# 调用对应的API处理器
try:
# 调试模式输出路由信息 (debug-api)
if handler._is_debug_enabled('api'):
msg = f"\n=== DEBUG API Router ===\n Version: {api_version}\n Action: {api_action}\n Method: {method}\n Query: {parsed_query}"
handler._debug_log('api', msg, '\033[35m')
api_handler.handle_request(handler, method, api_action, parsed_query)
except Exception as e:
if handler._is_debug_enabled('error'):
import traceback
tb_str = traceback.format_exc()
msg = f"\n=== DEBUG API ERROR ===\n{tb_str}"
handler._debug_log('error', msg, '\033[31m')
handler.send_json_response({
"error": f"API处理错误: {str(e)}",
"version": api_version,
"path": api_action
}, 500)
+286
View File
@@ -0,0 +1,286 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
SSE (Server-Sent Events) 处理器模块
提供单向事件推送功能
"""
import json
import threading
import time
import uuid
from typing import Dict, Set, Optional
from dataclasses import dataclass, field
@dataclass
class SSEClient:
"""SSE客户端"""
client_id: str
handler: any # HTTP请求处理器
topics: Set[str] = field(default_factory=set)
connected_at: float = field(default_factory=time.time)
last_activity: float = field(default_factory=time.time)
running: bool = False
class SSEHandler:
"""SSE事件处理器"""
# 预定义事件类型
EVENT_STATS = 'stats'
EVENT_MONITOR = 'monitor'
EVENT_SYNC = 'sync'
EVENT_DOWNLOAD = 'download'
EVENT_SERVER = 'server'
EVENT_ERROR = 'error'
EVENT_PING = 'ping'
def __init__(self, config: dict = None):
self.config = config or {}
self.clients: Dict[str, SSEClient] = {}
self.lock = threading.Lock()
# 心跳配置
self.heartbeat_interval = 30 # 秒
self.retry_interval = 3000 # 毫秒(客户端重连间隔)
# 消息缓冲区
self.message_buffer_size = 100
# 统计
self.stats = {
'total_connections': 0,
'total_events_sent': 0,
'total_bytes_sent': 0
}
def handle_connection(self, handler, topics: list = None) -> Optional[str]:
"""
处理新的SSE连接
返回客户端ID
"""
client_id = self._generate_client_id()
# 设置SSE响应头
handler.send_response(200)
handler.send_header('Content-Type', 'text/event-stream')
handler.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
handler.send_header('Connection', 'keep-alive')
handler.send_header('Access-Control-Allow-Origin', '*')
handler.send_header('X-Accel-Buffering', 'no') # 禁用Nginx缓冲
handler.end_headers()
# 创建客户端
client = SSEClient(
client_id=client_id,
handler=handler,
topics=set(topics) if topics else {self.EVENT_STATS, self.EVENT_SYNC}
)
with self.lock:
self.clients[client_id] = client
self.stats['total_connections'] += 1
# 启动心跳
client.running = True
self._start_heartbeat(client_id)
# 发送初始连接事件
self._send_event(client, self.EVENT_SERVER, {
'type': 'connected',
'client_id': client_id,
'timestamp': time.time(),
'topics': list(client.topics)
})
return client_id
def close_connection(self, client_id: str):
"""关闭连接"""
with self.lock:
client = self.clients.pop(client_id, None)
if client:
client.running = False
def subscribe(self, client_id: str, *topics: str):
"""订阅主题"""
with self.lock:
client = self.clients.get(client_id)
if client:
client.topics.update(topics)
def unsubscribe(self, client_id: str, *topics: str):
"""取消订阅"""
with self.lock:
client = self.clients.get(client_id)
if client:
for topic in topics:
client.topics.discard(topic)
def is_subscribed(self, client_id: str, event_type: str) -> bool:
"""检查是否订阅了事件类型"""
with self.lock:
client = self.clients.get(client_id)
if not client:
return False
return event_type in client.topics or '*' in client.topics
def send_event(self, client_id: str, event_type: str, data: dict):
"""发送事件到指定客户端"""
with self.lock:
client = self.clients.get(client_id)
if not client or not client.running:
return False
if event_type not in client.topics and '*' not in client.topics:
return False
return self._send_event(client, event_type, data)
def broadcast(self, event_type: str, data: dict, topic: str = None):
"""广播事件到所有客户端"""
with self.lock:
sent_count = 0
dead_clients = []
for client_id, client in self.clients.items():
if not client.running:
dead_clients.append(client_id)
continue
# 检查主题匹配
if topic and event_type != topic:
continue
if self._send_event(client, event_type, data):
sent_count += 1
# 清理死掉的客户端
for client_id in dead_clients:
self.clients.pop(client_id, None)
return sent_count
def broadcast_to_topic(self, topic: str, event_type: str, data: dict):
"""广播到订阅特定主题的客户端"""
self.broadcast(event_type, data, topic)
def send_monitor_update(self, stats: dict):
"""发送监控更新"""
self.broadcast(self.EVENT_MONITOR, stats)
def send_sync_update(self, sync_data: dict):
"""发送同步更新"""
self.broadcast(self.EVENT_SYNC, sync_data)
def send_download_update(self, download_data: dict):
"""发送下载更新"""
self.broadcast(self.EVENT_DOWNLOAD, download_data)
def send_server_event(self, event_data: dict):
"""发送服务器事件"""
self.broadcast(self.EVENT_SERVER, event_data)
def get_client_count(self) -> int:
"""获取客户端数量"""
with self.lock:
return len(self.clients)
def get_stats(self) -> dict:
"""获取统计信息"""
with self.lock:
return {
**self.stats,
'connected_clients': len(self.clients),
'topics': list(set(
t for client in self.clients.values()
for t in client.topics
))
}
def _send_event(self, client: SSEClient, event_type: str, data: dict) -> bool:
"""发送单个事件"""
try:
event_data = {
'event': event_type,
'timestamp': time.time(),
'data': data
}
# SSE格式
message = f"event: {event_type}\n"
message += f"id: {uuid.uuid4().hex[:16]}\n"
message += f"retry: {self.retry_interval}\n"
message += "data: " + json.dumps(event_data, ensure_ascii=False) + "\n\n"
# 发送
client.handler.wfile.write(message.encode('utf-8'))
client.handler.wfile.flush()
# 统计
self.stats['total_events_sent'] += len(message)
client.last_activity = time.time()
return True
except Exception as e:
print(f"SSE发送失败 {client.client_id}: {e}")
client.running = False
return False
def _start_heartbeat(self, client_id: str):
"""启动心跳"""
def heartbeat():
while True:
time.sleep(self.heartbeat_interval)
with self.lock:
client = self.clients.get(client_id)
if not client or not client.running:
return
# 发送心跳
self.send_event(client_id, self.EVENT_PING, {
'timestamp': time.time()
})
thread = threading.Thread(target=heartbeat, daemon=True)
thread.start()
def _generate_client_id(self) -> str:
"""生成客户端ID"""
return f"sse_{uuid.uuid4().hex[:12]}"
def cleanup(self, max_idle_time: float = 300.0):
"""清理空闲连接"""
now = time.time()
idle_clients = []
with self.lock:
for client_id, client in self.clients.items():
if now - client.last_activity > max_idle_time:
idle_clients.append(client_id)
for client_id in idle_clients:
self.send_event(client_id, self.EVENT_ERROR, {
'type': 'timeout',
'message': 'Connection timed out'
})
self.close_connection(client_id)
def get_client_info(self, client_id: str) -> Optional[dict]:
"""获取客户端信息"""
with self.lock:
client = self.clients.get(client_id)
if not client:
return None
return {
'client_id': client.client_id,
'topics': list(client.topics),
'connected_at': client.connected_at,
'last_activity': client.last_activity,
'running': client.running
}
+5949
View File
File diff suppressed because it is too large Load Diff
+2285
View File
File diff suppressed because it is too large Load Diff
+4414
View File
File diff suppressed because it is too large Load Diff
+403
View File
@@ -0,0 +1,403 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
WebSocket处理器模块
提供双向实时通信功能
"""
import json
import threading
import time
import uuid
from datetime import datetime
from typing import Dict, Set, Optional, Callable
from dataclasses import dataclass, field
@dataclass
class WebSocketClient:
"""WebSocket客户端"""
client_id: str
connection: any # WebSocket连接对象
topics: Set[str] = field(default_factory=set)
connected_at: float = field(default_factory=time.time)
last_activity: float = field(default_factory=time.time)
metadata: dict = field(default_factory=dict)
class WebSocketManager:
"""WebSocket连接管理器"""
# 预定义主题
TOPIC_MONITOR_CPU = 'monitor:cpu'
TOPIC_MONITOR_MEMORY = 'monitor:memory'
TOPIC_MONITOR_DISK = 'monitor:disk'
TOPIC_MONITOR_NETWORK = 'monitor:network'
TOPIC_MONITOR_ALL = 'monitor:*'
TOPIC_SYNC_PROGRESS = 'sync:progress'
TOPIC_SYNC_STATUS = 'sync:status'
TOPIC_SYNC_ALL = 'sync:*'
TOPIC_DOWNLOAD_PROGRESS = 'download:progress'
TOPIC_DOWNLOAD_STATUS = 'download:status'
TOPIC_DOWNLOAD_ALL = 'download:*'
TOPIC_SERVER_STATUS = 'server:status'
TOPIC_ALL = '*'
def __init__(self, config: dict = None):
self.config = config or {}
self.clients: Dict[str, WebSocketClient] = {}
self.lock = threading.Lock()
self._running = False
# 消息队列(用于批量发送)
self.message_queues: Dict[str, list] = {}
# 心跳间隔(秒)
self.heartbeat_interval = 30
# 统计
self.stats = {
'total_connections': 0,
'total_messages_sent': 0,
'total_messages_received': 0
}
def register_client(self, client_id: str, connection, metadata: dict = None) -> WebSocketClient:
"""注册新客户端"""
with self.lock:
client = WebSocketClient(
client_id=client_id,
connection=connection,
topics={self.TOPIC_ALL}, # 默认订阅所有
metadata=metadata or {}
)
self.clients[client_id] = client
self.stats['total_connections'] += 1
# 启动心跳
self._start_heartbeat(client_id)
return client
def unregister_client(self, client_id: str):
"""注销客户端"""
with self.lock:
client = self.clients.pop(client_id, None)
if client:
self.message_queues.pop(client_id, None)
def subscribe(self, client_id: str, *topics: str):
"""订阅主题"""
with self.lock:
client = self.clients.get(client_id)
if client:
client.topics.update(topics)
def unsubscribe(self, client_id: str, *topics: str):
"""取消订阅"""
with self.lock:
client = self.clients.get(client_id)
if client:
for topic in topics:
client.topics.discard(topic)
def is_subscribed(self, client_id: str, topic: str) -> bool:
"""检查客户端是否订阅了主题"""
with self.lock:
client = self.clients.get(client_id)
if not client:
return False
if self.TOPIC_ALL in client.topics:
return True
# 检查精确匹配或通配符匹配
if topic in client.topics:
return True
# 检查通配符匹配
topic_parts = topic.split(':')
for subscribed_topic in client.topics:
if subscribed_topic.endswith('*'):
prefix = subscribed_topic.rstrip('*').rstrip(':')
if topic.startswith(prefix):
return True
return False
def send_to_client(self, client_id: str, event_type: str, data: dict, callback: Callable = None):
"""发送消息到指定客户端"""
with self.lock:
client = self.clients.get(client_id)
if not client:
return False
message = self._format_message(event_type, data)
try:
if hasattr(client.connection, 'send'):
client.connection.send(message)
self.stats['total_messages_sent'] += 1
client.last_activity = time.time()
if callback:
callback(client_id, True)
return True
else:
# 放入消息队列
if client_id not in self.message_queues:
self.message_queues[client_id] = []
self.message_queues[client_id].append(message)
if callback:
callback(client_id, True)
return True
except Exception as e:
print(f"WebSocket发送失败 {client_id}: {e}")
if callback:
callback(client_id, False)
return False
def broadcast(self, event_type: str, data: dict, topic: str = None):
"""广播消息到所有客户端"""
with self.lock:
sent_count = 0
failed_clients = []
for client_id, client in self.clients.items():
# 检查是否匹配主题
if topic and not self.is_subscribed(client_id, topic):
continue
message = self._format_message(event_type, data)
try:
if hasattr(client.connection, 'send'):
client.connection.send(message)
sent_count += 1
else:
if client_id not in self.message_queues:
self.message_queues[client_id] = []
self.message_queues[client_id].append(message)
sent_count += 1
client.last_activity = time.time()
except Exception as e:
print(f"广播到 {client_id} 失败: {e}")
failed_clients.append(client_id)
self.stats['total_messages_sent'] += sent_count
# 清理失败的客户端
for client_id in failed_clients:
self.unregister_client(client_id)
return sent_count
def broadcast_to_topic(self, topic: str, event_type: str, data: dict):
"""广播到订阅特定主题的客户端"""
with self.lock:
sent_count = 0
for client_id, client in self.clients.items():
if self.is_subscribed(client_id, topic):
message = self._format_message(event_type, data)
try:
if hasattr(client.connection, 'send'):
client.connection.send(message)
sent_count += 1
else:
if client_id not in self.message_queues:
self.message_queues[client_id] = []
self.message_queues[client_id].append(message)
sent_count += 1
except Exception as e:
print(f"发送到 {client_id} 失败: {e}")
return sent_count
def broadcast_monitor_update(self, stats: dict):
"""广播监控更新"""
# 提取关键指标
cpu_percent = stats.get('cpu', {}).get('percent', 0)
memory_percent = stats.get('memory', {}).get('percent', 0)
disk_percent = stats.get('disk', {}).get('percent', 0)
# 按阈值过滤
if cpu_percent > 0 or memory_percent > 0 or disk_percent > 0:
self.broadcast('monitor:stats', stats, 'monitor:*')
def broadcast_sync_progress(self, task_id: str, progress: dict):
"""广播同步进度"""
self.broadcast('sync:progress', progress, 'sync:*')
def broadcast_download_progress(self, download_id: str, progress: dict):
"""广播下载进度"""
self.broadcast('download:progress', progress, 'download:*')
def get_client_count(self) -> int:
"""获取客户端数量"""
with self.lock:
return len(self.clients)
def get_client_topics(self, client_id: str) -> Set[str]:
"""获取客户端订阅的主题"""
with self.lock:
client = self.clients.get(client_id)
return client.topics.copy() if client else set()
def get_all_topics(self) -> Set[str]:
"""获取所有被订阅的主题"""
with self.lock:
topics = set()
for client in self.clients.values():
topics.update(client.topics)
return topics
def get_stats(self) -> dict:
"""获取统计信息"""
with self.lock:
return {
**self.stats,
'connected_clients': len(self.clients),
'active_topics': len(self.get_all_topics()),
'queues_queued': sum(len(q) for q in self.message_queues.values())
}
def _format_message(self, event_type: str, data: dict) -> str:
"""格式化消息"""
return json.dumps({
'type': event_type,
'timestamp': time.time(),
'data': data
}, ensure_ascii=False)
def _start_heartbeat(self, client_id: str):
"""启动心跳"""
def heartbeat():
while client_id in self.clients:
try:
# 发送心跳
self.send_to_client(
client_id,
'ping',
{'timestamp': time.time()}
)
except Exception:
break
time.sleep(self.heartbeat_interval)
thread = threading.Thread(target=heartbeat, daemon=True)
thread.start()
def generate_client_id(self) -> str:
"""生成客户端ID"""
return f"ws_{uuid.uuid4().hex[:12]}"
def process_messages(self, client_id: str, messages: list):
"""处理客户端消息"""
for message in messages:
self._handle_message(client_id, message)
def _handle_message(self, client_id: str, message: str):
"""处理客户端消息"""
self.stats['total_messages_received'] += 1
try:
data = json.loads(message)
event_type = data.get('type')
payload = data.get('data', {})
if event_type == 'subscribe':
# 订阅主题
topics = payload.get('topics', [])
self.subscribe(client_id, *topics)
elif event_type == 'unsubscribe':
# 取消订阅
topics = payload.get('topics', [])
self.unsubscribe(client_id, *topics)
elif event_type == 'ping':
# 心跳响应
self.send_to_client(client_id, 'pong', {'timestamp': time.time()})
elif event_type == 'status':
# 请求状态 - 返回服务器和客户端状态
response_data = {'status': 'ok'}
if payload.get('monitor'):
# 获取实时监控数据
try:
if handler.monitor:
response_data['monitor'] = handler.monitor.get_realtime_stats()
else:
# 回退到直接使用 psutil
import psutil
response_data['monitor'] = {
'timestamp': datetime.now().isoformat(),
'cpu': {
'percent': psutil.cpu_percent(interval=0.1),
'count': psutil.cpu_count()
},
'memory': psutil.virtual_memory()._asdict(),
'disk': psutil.disk_usage(handler.config.get('base_dir', './downloads'))._asdict()
}
except Exception as e:
response_data['monitor'] = {'error': str(e)}
if payload.get('ws'):
# 获取 WebSocket 统计
response_data['ws'] = self.get_stats()
if payload.get('client'):
# 获取当前客户端信息
client = self.clients.get(client_id)
if client:
response_data['client'] = {
'client_id': client.client_id,
'connected_at': client.connected_at,
'last_activity': client.last_activity,
'topics': list(client.topics),
'metadata': client.metadata
}
self.send_to_client(client_id, 'status:response', response_data)
elif event_type == 'sync':
# 同步相关操作
sync_action = payload.get('action', 'status')
if sync_action == 'status':
# 获取同步状态
response_data = {'action': 'status'}
try:
from core.sync_scheduler import SyncScheduler
scheduler = SyncScheduler()
response_data['sync_status'] = scheduler.get_status() if hasattr(scheduler, 'get_status') else {'message': 'sync scheduler running'}
except Exception as e:
response_data['sync_status'] = {'error': str(e)}
self.send_to_client(client_id, 'sync:response', response_data)
elif sync_action == 'list':
# 获取同步任务列表
response_data = {'action': 'list'}
self.send_to_client(client_id, 'sync:response', response_data)
elif sync_action == 'trigger':
# 触发手动同步
response_data = {'action': 'trigger', 'status': 'pending'}
self.send_to_client(client_id, 'sync:response', response_data)
except json.JSONDecodeError:
pass
except Exception as e:
print(f"处理WebSocket消息失败: {e}")
+15
View File
@@ -0,0 +1,15 @@
# 核心模块初始化
from .config import ConfigManager
from .mirror_sync import MirrorSyncManager
from .server import MirrorServer
from .utils import format_file_size, get_file_hash, parse_size, sanitize_filename
__all__ = [
'ConfigManager',
'MirrorSyncManager',
'MirrorServer',
'format_file_size',
'get_file_hash',
'parse_size',
'sanitize_filename'
]
+591
View File
@@ -0,0 +1,591 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
告警模块
支持邮件告警、Webhook 告警、告警规则引擎
"""
import os
import sys
import json
import time
import threading
import logging
import smtplib
import requests
from datetime import datetime
from typing import Dict, List, Optional, Callable
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from enum import Enum
logger = logging.getLogger(__name__)
class AlertSeverity(Enum):
"""告警级别"""
INFO = "info"
WARNING = "warning"
ERROR = "error"
CRITICAL = "critical"
class AlertType(Enum):
"""告警类型"""
DISK_HIGH = "disk_high"
DISK_CRITICAL = "disk_critical"
SYNC_FAILED = "sync_failed"
SOURCE_UNHEALTHY = "source_unhealthy"
CACHE_FULL = "cache_full"
SERVICE_DOWN = "service_down"
CUSTOM = "custom"
class Alert:
"""告警对象"""
def __init__(
self,
alert_type: str,
severity: AlertSeverity,
title: str,
message: str,
details: Dict = None,
source: str = None
):
self.id = f"{int(time.time())}_{threading.get_ident()}"
self.type = alert_type
self.severity = severity
self.title = title
self.message = message
self.details = details or {}
self.source = source
self.timestamp = datetime.now()
self.sent = False
self.acknowledged = False
def to_dict(self) -> Dict:
"""转换为字典"""
return {
'id': self.id,
'type': self.type,
'severity': self.severity.value,
'title': self.title,
'message': self.message,
'details': self.details,
'source': self.source,
'timestamp': self.timestamp.isoformat(),
'sent': self.sent,
'acknowledged': self.acknowledged
}
class EmailAlerter:
"""邮件告警器"""
def __init__(self, config: Dict = None):
"""
初始化邮件告警器
Args:
config: 邮件配置
"""
self.config = config or {}
self.enabled = self.config.get('enabled', False)
self.smtp_host = self.config.get('smtp_host', 'localhost')
self.smtp_port = self.config.get('smtp_port', 587)
self.smtp_user = self.config.get('smtp_user', '')
self.smtp_password = self.config.get('smtp_password', '')
self.from_address = self.config.get('from_address', 'hyc-mirror@localhost')
self.to_addresses = self.config.get('to_addresses', [])
self.use_tls = self.config.get('use_tls', True)
# 连接池
self._connection: Optional[smtplib.SMTP] = None
self._last_connect_time: Optional[datetime] = None
self._connection_timeout = 30
def _get_connection(self) -> smtplib.SMTP:
"""获取 SMTP 连接"""
if self._connection:
# 检查连接是否仍然有效
try:
self._connection.noop()
return self._connection
except Exception:
try:
self._connection.quit()
except Exception:
pass
self._connection = None
# 创建新连接
try:
self._connection = smtplib.SMTP(self.smtp_host, self.smtp_port, timeout=self._connection_timeout)
if self.use_tls:
self._connection.starttls()
if self.smtp_user and self.smtp_password:
self._connection.login(self.smtp_user, self.smtp_password)
self._last_connect_time = datetime.now()
return self._connection
except Exception as e:
logger.error(f"Failed to connect to SMTP server: {e}")
raise
def send(self, alert: Alert) -> bool:
"""
发送告警邮件
Args:
alert: 告警对象
Returns:
是否发送成功
"""
if not self.enabled:
logger.debug("Email alerts disabled")
return False
if not self.to_addresses:
logger.warning("No recipients configured for email alerts")
return False
try:
msg = MIMEMultipart('alternative')
msg['Subject'] = f"[{alert.severity.value.upper()}] {alert.title}"
msg['From'] = self.from_address
msg['To'] = ', '.join(self.to_addresses)
# HTML 格式
html_content = self._format_html(alert)
msg.attach(MIMEText(html_content, 'html', 'utf-8'))
# 纯文本格式
text_content = self._format_text(alert)
msg.attach(MIMEText(text_content, 'plain', 'utf-8'))
# 发送邮件
server = self._get_connection()
server.send_message(msg)
logger.info(f"Alert email sent: {alert.title}")
return True
except Exception as e:
logger.error(f"Failed to send alert email: {e}")
return False
def _format_html(self, alert: Alert) -> str:
"""格式化 HTML 内容"""
severity_colors = {
'info': '#2196F3',
'warning': '#FF9800',
'error': '#F44336',
'critical': '#9C27B0'
}
color = severity_colors.get(alert.severity.value, '#666666')
details_html = ''
if alert.details:
details_html = '<h3>Details</h3><table>'
for key, value in alert.details.items():
details_html += f'<tr><td><b>{key}:</b></td><td>{value}</td></tr>'
details_html += '</table>'
return f"""
<html>
<head>
<style>
body {{ font-family: Arial, sans-serif; margin: 20px; }}
.header {{ background-color: {color}; color: white; padding: 10px; }}
.title {{ font-size: 24px; margin: 20px 0; }}
.message {{ background-color: #f5f5f5; padding: 15px; border-radius: 5px; margin: 20px 0; }}
.details {{ margin-top: 20px; }}
.footer {{ margin-top: 30px; font-size: 12px; color: #666; }}
</style>
</head>
<body>
<div class="header">
<h1>HYC Mirror Alert</h1>
</div>
<div class="title">[{alert.severity.value.upper()}] {alert.title}</div>
<div class="message">
<p><b>Message:</b> {alert.message}</p>
<p><b>Time:</b> {alert.timestamp.strftime('%Y-%m-%d %H:%M:%S')}</p>
<p><b>Type:</b> {alert.type}</p>
{details_html}
</div>
<div class="footer">
<p>This is an automated alert from HYC Mirror Server</p>
</div>
</body>
</html>
"""
def _format_text(self, alert: Alert) -> str:
"""格式化纯文本内容"""
details_text = ''
if alert.details:
details_text = '\nDetails:\n'
for key, value in alert.details.items():
details_text += f" {key}: {value}\n"
return f"""
HYC Mirror Alert
================
Severity: {alert.severity.value.upper()}
Title: {alert.title}
Message: {alert.message}
Time: {alert.timestamp.strftime('%Y-%m-%d %H:%M:%S')}
Type: {alert.type}
{details_text}
---
This is an automated alert from HYC Mirror Server
"""
def test_connection(self) -> Dict:
"""测试 SMTP 连接"""
try:
server = self._get_connection()
return {
'success': True,
'message': 'SMTP connection successful'
}
except Exception as e:
return {
'success': False,
'message': f'SMTP connection failed: {str(e)}'
}
def close(self):
"""关闭连接"""
if self._connection:
try:
self._connection.quit()
except Exception:
pass
self._connection = None
class WebhookAlerter:
"""Webhook 告警器"""
def __init__(self, config: Dict = None):
"""
初始化 Webhook 告警器
Args:
config: Webhook 配置
"""
self.config = config or {}
self.enabled = self.config.get('enabled', False)
self.webhook_url = self.config.get('webhook_url', '')
def send(self, alert: Alert) -> bool:
"""
发送告警到 Webhook
Args:
alert: 告警对象
Returns:
是否发送成功
"""
if not self.enabled:
logger.debug("Webhook alerts disabled")
return False
if not self.webhook_url:
logger.warning("No webhook URL configured")
return False
try:
payload = {
'event': 'alert',
'alert': alert.to_dict(),
'timestamp': datetime.now().isoformat()
}
headers = {
'Content-Type': 'application/json',
'User-Agent': 'HYC-Mirror-Alerts/1.0'
}
response = requests.post(
self.webhook_url,
json=payload,
headers=headers,
timeout=30
)
if response.status_code < 400:
logger.info(f"Alert webhook sent: {alert.title}")
return True
else:
logger.error(f"Webhook returned error: {response.status_code}")
return False
except Exception as e:
logger.error(f"Failed to send webhook alert: {e}")
return False
class AlertManager:
"""告警管理器"""
def __init__(self, config: Dict = None):
"""
初始化告警管理器
Args:
config: 告警配置
"""
self.config = config or {}
self.enabled = self.config.get('enabled', False)
# 初始化告警器
self.email_alerter = EmailAlerter(self.config.get('email', {}))
self.webhook_alerter = WebhookAlerter(self.config.get('webhook', {}))
# 告警规则
self.rules = self.config.get('rules', {})
# 告警历史
self._alerts: List[Alert] = []
self._alerts_lock = threading.Lock()
self._max_history = 100
# 回调函数
self._on_alert: Optional[Callable] = None
self._on_ack: Optional[Callable] = None
# 告警冷却(防止重复告警)
self._alert_cooldowns: Dict[str, float] = {}
self._default_cooldown = 300 # 5 分钟
def set_alert_callback(self, callback: Callable):
"""设置告警回调"""
self._on_alert = callback
def set_ack_callback(self, callback: Callable):
"""设置确认回调"""
self._on_ack = callback
def check_rule(self, rule_name: str, data: Dict) -> Optional[Alert]:
"""
检查规则并生成告警
Args:
rule_name: 规则名称
data: 检查数据
Returns:
告警对象或 None
"""
if not self.enabled:
return None
rule = self.rules.get(rule_name, {})
if not rule.get('enabled', False):
return None
severity = AlertSeverity(rule.get('severity', 'warning'))
threshold = rule.get('threshold')
# 磁盘空间检查
if rule_name == 'disk_high' and threshold:
disk_percent = data.get('disk_percent', 0)
if disk_percent >= threshold:
return Alert(
alert_type=AlertType.DISK_HIGH.value,
severity=severity,
title=f"Disk usage is high: {disk_percent}%",
message=f"Disk usage has reached {disk_percent}%, which is above the {threshold}% threshold.",
details={'disk_percent': disk_percent, 'threshold': threshold},
source='monitor'
)
if rule_name == 'disk_critical' and threshold:
disk_percent = data.get('disk_percent', 0)
if disk_percent >= threshold:
return Alert(
alert_type=AlertType.DISK_CRITICAL.value,
severity=severity,
title=f"Disk usage is critical: {disk_percent}%",
message=f"Disk usage has reached {disk_percent}%, which is above the {critical_threshold}% threshold. Immediate action required!",
details={'disk_percent': disk_percent, 'threshold': threshold},
source='monitor'
)
# 同步失败检查
if rule_name == 'sync_failed':
sync_result = data.get('sync_result')
if sync_result and not sync_result.get('success', True):
return Alert(
alert_type=AlertType.SYNC_FAILED.value,
severity=severity,
title=f"Sync failed: {sync_result.get('source', 'unknown')}",
message=sync_result.get('error', 'Unknown sync error'),
details=sync_result,
source='sync'
)
# 源不健康检查
if rule_name == 'source_unhealthy':
unhealthy_sources = data.get('unhealthy_sources', [])
if unhealthy_sources:
return Alert(
alert_type=AlertType.SOURCE_UNHEALTHY.value,
severity=severity,
title=f"Unhealthy mirror sources detected: {len(unhealthy_sources)}",
message=f"The following mirror sources are unhealthy: {', '.join(unhealthy_sources)}",
details={'unhealthy_sources': unhealthy_sources},
source='health_check'
)
return None
def trigger_alert(self, alert: Alert) -> bool:
"""
触发告警
Args:
alert: 告警对象
Returns:
是否发送成功
"""
if not self.enabled:
return False
# 检查冷却时间
cooldown_key = f"{alert.type}:{alert.source or 'unknown'}"
last_alert = self._alert_cooldowns.get(cooldown_key, 0)
if time.time() - last_alert < self._default_cooldown:
logger.debug(f"Alert {alert.type} in cooldown, skipping")
return False
# 发送告警
email_sent = self.email_alerter.send(alert)
webhook_sent = self.webhook_alerter.send(alert)
alert.sent = email_sent or webhook_sent
# 记录告警
with self._alerts_lock:
self._alerts.append(alert)
if len(self._alerts) > self._max_history:
self._alerts = self._alerts[-self._max_history:]
# 更新冷却时间
self._alert_cooldowns[cooldown_key] = time.time()
# 触发回调
if self._on_alert and alert.sent:
try:
self._on_alert(alert)
except Exception as e:
logger.error(f"Alert callback failed: {e}")
return alert.sent
def acknowledge_alert(self, alert_id: str) -> bool:
"""
确认告警
Args:
alert_id: 告警 ID
Returns:
是否成功
"""
with self._alerts_lock:
for alert in self._alerts:
if alert.id == alert_id:
alert.acknowledged = True
if self._on_ack:
try:
self._on_ack(alert)
except Exception as e:
logger.error(f"Ack callback failed: {e}")
return True
return False
def get_alerts(
self,
acknowledged: bool = None,
severity: str = None,
limit: int = 50
) -> List[Dict]:
"""
获取告警列表
Args:
acknowledged: 过滤已确认状态
severity: 过滤级别
limit: 返回数量限制
Returns:
告警列表
"""
with self._alerts_lock:
alerts = [a.to_dict() for a in self._alerts]
# 过滤
if acknowledged is not None:
alerts = [a for a in alerts if a['acknowledged'] == acknowledged]
if severity:
alerts = [a for a in alerts if a['severity'] == severity]
# 返回最近的告警
return alerts[-limit:]
def get_stats(self) -> Dict:
"""获取告警统计"""
with self._alerts_lock:
total = len(self._alerts)
unack = sum(1 for a in self._alerts if not a['acknowledged'])
by_severity = {}
for a in self._alerts:
by_severity[a['severity']] = by_severity.get(a['severity'], 0) + 1
return {
'total_alerts': total,
'unacknowledged': unack,
'by_severity': by_severity,
'email_enabled': self.email_alerter.enabled,
'webhook_enabled': self.webhook_alerter.enabled,
'rules_enabled': sum(1 for r in self.rules.values() if r.get('enabled', False))
}
def clear_history(self) -> bool:
"""清除告警历史"""
with self._alerts_lock:
self._alerts = []
return True
def test_email(self, to_address: str) -> Dict:
"""测试邮件发送"""
test_alert = Alert(
alert_type=AlertType.CUSTOM.value,
severity=AlertSeverity.INFO,
title="Test Alert",
message="This is a test alert from HYC Mirror Server",
details={'test': True}
)
# 临时添加收件人
original_recipients = self.email_alerter.to_addresses
self.email_alerter.to_addresses = [to_address]
success = self.email_alerter.send(test_alert)
# 恢复收件人
self.email_alerter.to_addresses = original_recipients
return {
'success': success,
'message': 'Test email sent successfully' if success else 'Failed to send test email'
}
+587
View File
@@ -0,0 +1,587 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
API认证模块
使用数据库进行认证,支持:
- none: 无认证
- basic: Basic Auth(用户名密码)
- token: Token 认证(登录生成的token)
"""
import os
import sys
import json
import hashlib
import time
import secrets
from typing import Dict, List, Optional
from dataclasses import dataclass
from functools import wraps
@dataclass
class AuthSession:
"""认证会话"""
session_id: str
user_id: str
level: str
created_at: float
expires_at: float
last_activity: float
permissions: List[str]
class APIAuthManager:
"""API认证管理器"""
def __init__(self, config: dict = None):
self.config = config or {}
self.sessions: Dict[str, AuthSession] = {}
# 确定基础目录(用于保存会话文件)
base_dir = config.get('base_dir', '.') if config else '.'
# 检测是否是 PyInstaller 打包环境
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
base_dir = os.path.dirname(os.path.abspath(sys.executable))
elif base_dir == '.':
base_dir = os.getcwd()
# 会话文件路径
sessions_filename = config.get('auth_sessions_file', 'auth_sessions.json') if config else 'auth_sessions.json'
self.sessions_file = os.path.join(base_dir, sessions_filename)
# 会话超时时间
self.session_timeout = config.get('auth_session_timeout', 3600) if config else 3600
# Cookie名称
self.cookie_name = 'hyc_auth'
self.cookie_max_age = config.get('auth_cookie_max_age', 86400) if config else 86400
# IP 白名单
self.ip_whitelist = config.get('ip_whitelist', []) if config else []
self.ip_whitelist_enabled = config.get('ip_whitelist_enabled', False) if config else False
# 加载已保存的会话
self._load_sessions()
@property
def db(self):
"""动态获取数据库实例"""
return self.config.get('_db_instance')
def _get_client_ip(self, handler) -> str:
"""从 handler 获取客户端 IP"""
try:
forwarded = handler.headers.get('X-Forwarded-For')
if forwarded:
return forwarded.split(',')[0].strip()
return handler.client_address[0]
except Exception:
return None
def _check_ip_whitelist(self, ip: str) -> bool:
"""检查 IP 是否在白名单中"""
if not self.ip_whitelist_enabled or not self.ip_whitelist:
return True
if not ip:
return False
import ipaddress
for pattern in self.ip_whitelist:
try:
if '/' in pattern:
network = ipaddress.ip_network(pattern, strict=False)
if ipaddress.ip_address(ip) in network:
return True
elif pattern == ip:
return True
except ValueError:
continue
return False
# === Token 验证 ===
def validate_token(self, token: str, client_ip: str = None) -> Optional[dict]:
"""验证 token(从数据库)"""
if not token:
return None
# 优先从数据库验证
if self.db:
user = self.db.get_user_by_token(token)
if user:
# 检查 token 是否过期
if user.get('token_expires_at') and time.time() > user['token_expires_at']:
return {"valid": False, "reason": "Token已过期"}
# 检查用户是否启用
if not user.get('enabled', True):
return {"valid": False, "reason": "用户已被禁用"}
return {
"valid": True,
"key_id": f"user_{user['id']}",
"name": user['username'],
"level": user.get('role', 'admin'),
"permissions": ["*"],
"user_id": user['id'],
"username": user['username']
}
return {"valid": False, "reason": "无效的Token"}
# === Basic Auth 验证 ===
def validate_basic_auth(self, username: str, password: str, client_ip: str = None) -> dict:
"""验证 Basic Auth 用户名密码(从数据库)"""
# IP 白名单检查
if not self._check_ip_whitelist(client_ip):
if self.db:
self.db.add_login_log(username, client_ip, 'failed', 'IP不在白名单')
return {"valid": False, "reason": "IP不在白名单内"}
auth_type = self.config.get('auth_type', 'none')
# 如果认证类型为 none,任何用户都可以通过
if auth_type == 'none':
return {
"valid": True,
"user_id": 0,
"username": username or 'anonymous',
"level": "admin",
"key_id": "anonymous",
"name": f"Anonymous - {username or 'anonymous'}",
"permissions": ["*"]
}
# 从数据库验证
if self.db:
result = self.db.verify_user(username, password)
if result.get('valid'):
if self.db:
self.db.add_login_log(username, client_ip, 'success', '数据库验证')
return {
"valid": True,
"user_id": result.get('user_id'),
"username": username,
"level": result.get('role', 'admin'),
"key_id": f"user_{result.get('user_id')}",
"name": f"User - {username}",
"permissions": ["*"]
}
else:
if self.db:
self.db.add_login_log(username, client_ip, 'failed', result.get('reason', '验证失败'))
return {"valid": False, "reason": result.get('reason', '用户名或密码错误')}
return {"valid": False, "reason": "数据库不可用"}
# === Cookie 验证 ===
def validate_cookie(self, cookie_value: str, client_ip: str = None) -> Optional[dict]:
"""验证认证Cookie"""
if not cookie_value:
return None
parts = cookie_value.split('.')
if len(parts) != 3:
return None
session_id, timestamp, signature = parts
session = self.sessions.get(session_id)
if not session:
return None
if time.time() > session.expires_at:
del self.sessions[session_id]
return None
expected_sig = self._generate_signature(session_id, timestamp, session.user_id)
if signature != expected_sig:
return None
session.last_activity = time.time()
return {
"valid": True,
"session_id": session_id,
"user_id": session.user_id,
"level": session.level,
"permissions": session.permissions
}
def validate_session_id(self, session_id: str) -> Optional[dict]:
"""验证会话ID"""
if not session_id:
return None
session = self.sessions.get(session_id)
if not session:
return None
if time.time() > session.expires_at:
del self.sessions[session_id]
return None
session.last_activity = time.time()
return {
"valid": True,
"session_id": session_id,
"user_id": session.user_id,
"level": session.level,
"permissions": session.permissions
}
# === 请求验证 ===
def validate_request(self, handler, required_level: str = "admin") -> dict:
"""
验证请求的认证状态
支持的认证方式:
1. Authorization: Bearer <token>
2. Authorization: Basic <credentials>
3. X-API-Key: <token>
4. Cookie: hyc_auth=<session>
5. ?key=<token>
"""
import base64
auth_header = handler.headers.get('Authorization')
api_key = handler.headers.get('X-API-Key')
cookie = handler.headers.get('Cookie', '')
client_ip = handler.client_address[0] if hasattr(handler, 'client_address') else None
# 提取cookie值
cookie_value = None
for c in cookie.split(';'):
c = c.strip()
if c.startswith(f'{self.cookie_name}='):
cookie_value = c[len(self.cookie_name)+1:]
break
# 获取查询参数中的key
parsed_path = handler.path.split('?')
query_key = None
if len(parsed_path) > 1:
from urllib.parse import parse_qs
query = parse_qs(parsed_path[1])
query_key = query.get('key', [None])[0]
# 1. Bearer Token
if auth_header and auth_header.startswith('Bearer '):
token = auth_header[7:]
result = self.validate_token(token, client_ip)
if result and result.get('valid'):
return {"authenticated": True, "method": "bearer", **result}
# 2. Basic Auth
if auth_header and auth_header.startswith('Basic '):
try:
credentials = base64.b64decode(auth_header[6:]).decode('utf-8')
if ':' in credentials:
username, password = credentials.split(':', 1)
result = self.validate_basic_auth(username, password, client_ip)
if result and result.get('valid'):
return {"authenticated": True, "method": "basic", **result}
except Exception:
pass
# 3. API Key Header
if api_key:
result = self.validate_token(api_key, client_ip)
if result and result.get('valid'):
return {"authenticated": True, "method": "api_key", **result}
# 4. Cookie
if cookie_value:
result = self.validate_cookie(cookie_value, client_ip)
if result and result.get('valid'):
return {"authenticated": True, "method": "cookie", **result}
# 5. Query Parameter
if query_key:
result = self.validate_token(query_key, client_ip)
if result and result.get('valid'):
return {"authenticated": True, "method": "query", **result}
# 未认证
return {
"authenticated": False,
"error": "Authentication required",
"required_level": required_level
}
def check_permission(self, auth_result: dict, permission: str) -> bool:
"""检查是否有权限访问特定API"""
if not auth_result.get('authenticated'):
return False
permissions = auth_result.get('permissions', [])
if '*' in permissions:
return True
if permission in permissions:
return True
for p in permissions:
if p.endswith('*'):
prefix = p.rstrip('*')
if permission.startswith(prefix):
return True
return False
# === 会话管理 ===
def create_session(self, user_id: str, level: str,
permissions: List[str] = None) -> dict:
"""创建认证会话"""
session_id = secrets.token_hex(32)
timestamp = time.time()
session = AuthSession(
session_id=session_id,
user_id=user_id,
level=level,
created_at=timestamp,
expires_at=timestamp + self.session_timeout,
last_activity=timestamp,
permissions=permissions or ['*']
)
self.sessions[session_id] = session
self._save_sessions()
signature = self._generate_signature(session_id, timestamp, user_id)
cookie_value = f"{session_id}.{timestamp}.{signature}"
return {
"session_id": session_id,
"cookie_name": self.cookie_name,
"cookie_value": cookie_value,
"cookie_max_age": self.cookie_max_age,
"expires": timestamp + self.session_timeout
}
def destroy_session(self, session_id: str) -> bool:
"""销毁会话"""
if session_id in self.sessions:
del self.sessions[session_id]
self._save_sessions()
return True
return False
# === 内部方法 ===
def _generate_signature(self, session_id: str, timestamp: str, user_id: str) -> str:
"""生成签名"""
secret = self.config.get('auth_secret', 'default_secret_change_me')
data = f"{session_id}.{timestamp}.{user_id}.{secret}"
return hashlib.sha256(data.encode()).hexdigest()[:32]
def _load_sessions(self):
"""加载会话"""
if os.path.exists(self.sessions_file):
try:
with open(self.sessions_file, 'r', encoding='utf-8') as f:
data = json.load(f)
now = time.time()
for item in data:
if item.get('expires_at') and now > item['expires_at']:
continue
session = AuthSession(
session_id=item['session_id'],
user_id=item['user_id'],
level=item['level'],
created_at=item['created_at'],
expires_at=item['expires_at'],
last_activity=item['last_activity'],
permissions=item.get('permissions', ['*'])
)
self.sessions[session.session_id] = session
except Exception as e:
print(f"加载会话失败: {e}")
def _save_sessions(self):
"""保存会话"""
data = []
for session in self.sessions.values():
data.append({
"session_id": session.session_id,
"user_id": session.user_id,
"level": session.level,
"created_at": session.created_at,
"expires_at": session.expires_at,
"last_activity": session.last_activity,
"permissions": session.permissions
})
try:
with open(self.sessions_file, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
except Exception:
pass
def get_stats(self) -> dict:
"""获取认证统计"""
active_sessions = sum(
1 for s in self.sessions.values()
if time.time() < s.expires_at
)
return {
"active_sessions": active_sessions,
"session_timeout": self.session_timeout
}
# === API认证装饰器 ===
def require_auth(required_level: str = "admin", permission: str = None):
"""
API认证装饰器
使用方式:
@require_auth()
def api_endpoint(self, handler):
...
@require_auth(permission="sync:start")
def api_sync_start(self, handler):
...
"""
def decorator(func):
@wraps(func)
def wrapper(self, handler, *args, **kwargs):
# 检查是否需要认证
if required_level == "none":
return func(self, handler, *args, **kwargs)
config = getattr(handler, 'config', {})
auth_type = config.get('auth_type', 'none')
# 如果auth_type为none,跳过认证
if auth_type == "none":
handler.auth_result = {
"authenticated": True,
"level": "admin",
"user_id": "anonymous",
"permissions": ["*"]
}
return func(self, handler, *args, **kwargs)
auth_manager = getattr(handler, 'auth_manager', None)
if not auth_manager:
handler.send_json_response({
"error": "认证系统未初始化",
"code": "AUTH_NOT_INITIALIZED"
}, 500)
return
auth_result = auth_manager.validate_request(handler, required_level)
if not auth_result.get('authenticated'):
handler.send_response(401)
handler.send_header('WWW-Authenticate', 'Bearer realm="HYC API"')
handler.send_header('Access-Control-Allow-Origin', '*')
handler.send_json_response({
"error": "未认证或认证已过期",
"code": "UNAUTHORIZED",
"required_level": required_level,
"auth_methods": [
"Authorization: Bearer <token>",
"X-API-Key: <token>",
f"Cookie: {auth_manager.cookie_name}=<session>",
"?key=<token>"
]
})
return
if permission:
if not auth_manager.check_permission(auth_result, permission):
handler.send_json_response({
"error": "权限不足",
"code": "FORBIDDEN",
"required_permission": permission
}, 403)
return
handler.auth_result = auth_result
return func(self, handler, *args, **kwargs)
return wrapper
return decorator
# === 需要认证的API端点定义 ===
ADMIN_API_ENDPOINTS = {
# 同步管理
'POST:/api/v2/sync/*': 'sync:manage',
'POST:/api/v2/sync/*/start': 'sync:start',
'POST:/api/v2/sync/*/stop': 'sync:stop',
'DELETE:/api/v2/sync/*': 'sync:manage',
# 缓存管理
'POST:/api/v2/cache/clean': 'cache:manage',
'DELETE:/api/v2/cache/*': 'cache:manage',
# Webhook管理
'POST:/api/v2/webhooks': 'webhook:create',
'PUT:/api/v2/webhooks/*': 'webhook:update',
'DELETE:/api/v2/webhooks/*': 'webhook:delete',
'POST:/api/v2/webhooks/*/trigger': 'webhook:trigger',
# 服务器配置
'PUT:/api/v2/config': 'config:manage',
'POST:/api/v2/server/reload': 'server:reload',
# 文件管理(高危操作)
'DELETE:/api/v2/files/*': 'files:delete',
'PUT:/api/v2/files/*/rename': 'files:rename',
# 用户管理
'POST:/api/v2/users': 'users:create',
'DELETE:/api/v2/users/*': 'users:delete',
'PUT:/api/v2/users/*': 'users:update',
# === API v1 文件操作认证 ===
'DELETE:/api/v1/file/*': 'files:delete',
'PUT:/api/v1/mkdir': 'files:create',
'POST:/api/v1/upload': 'files:upload',
'POST:/api/v1/batch': 'files:batch',
'POST:/api/v1/archive': 'files:archive',
}
def check_endpoint_auth(method: str, path: str, auth_manager: APIAuthManager) -> dict:
"""检查端点是否需要认证"""
key = f"{method}:{path}"
if key in ADMIN_API_ENDPOINTS:
return {
"required": True,
"permission": ADMIN_API_ENDPOINTS[key]
}
for pattern, permission in ADMIN_API_ENDPOINTS.items():
if '*' in pattern:
pat_method, pat_path = pattern.split(':', 1)
if method == pat_method or pat_method == '*':
if pat_path.endswith('*'):
prefix = pat_path.rstrip('*').rstrip('/')
if path.startswith(prefix):
return {
"required": True,
"permission": permission
}
return {
"required": False,
"permission": None
}
+816
View File
@@ -0,0 +1,816 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
API 文档生成器
自动生成 OpenAPI/Swagger 格式的 API 文档
"""
import os
import sys
import json
import inspect
from datetime import datetime
from typing import Dict, List, Any, Optional
from dataclasses import dataclass, field
@dataclass
class APIDoc:
"""API 文档信息"""
title: str = "HYC下载站 API"
version: str = "2.2.0"
description: str = "HYC镜像下载站 REST API 文档"
servers: List[Dict] = field(default_factory=list)
tags: List[Dict] = field(default_factory=list)
paths: Dict = field(default_factory=dict)
components: Dict = field(default_factory=dict)
class APIDocGenerator:
"""API 文档生成器"""
def __init__(self, config: Dict = None):
self.config = config or {}
self.api_doc = APIDoc()
# 初始化组件
self._init_components()
def _init_components(self):
"""初始化文档组件"""
self.api_doc.components = {
'securitySchemes': {
'BearerAuth': {
'type': 'http',
'scheme': 'bearer',
'bearerFormat': 'JWT',
'description': 'JWT token 认证'
},
'ApiKeyAuth': {
'type': 'apiKey',
'in': 'header',
'name': 'X-API-Key',
'description': 'API Key 认证'
}
},
'schemas': {
'Error': {
'type': 'object',
'properties': {
'error': {'type': 'string', 'description': '错误信息'},
'code': {'type': 'string', 'description': '错误代码'},
'message': {'type': 'string', 'description': '详细描述'}
}
},
'Success': {
'type': 'object',
'properties': {
'success': {'type': 'boolean'},
'message': {'type': 'string'},
'data': {'type': 'object'}
}
},
'HealthStatus': {
'type': 'object',
'properties': {
'status': {'type': 'string', 'enum': ['healthy', 'degraded', 'unhealthy']},
'components': {'type': 'object'},
'timestamp': {'type': 'string', 'format': 'date-time'}
}
}
}
}
def generate(self) -> Dict:
"""生成完整的 API 文档"""
doc = {
'openapi': '3.0.3',
'info': {
'title': self.api_doc.title,
'version': self.api_doc.version,
'description': self.api_doc.description,
'contact': {
'name': 'HYC Mirror Support',
'email': '[email protected]'
},
'license': {
'name': 'MIT',
'url': 'https://opensource.org/licenses/MIT'
}
},
'servers': self.api_doc.servers,
'tags': self.api_doc.tags,
'paths': self.api_doc.paths,
'components': self.api_doc.components
}
return doc
def add_server(self, url: str, description: str = ''):
"""添加服务器"""
self.api_doc.servers.append({
'url': url,
'description': description
})
def add_tag(self, name: str, description: str = ''):
"""添加标签"""
self.api_doc.tags.append({
'name': name,
'description': description
})
def add_endpoint(
self,
method: str,
path: str,
summary: str,
description: str = '',
tags: List[str] = None,
parameters: List[Dict] = None,
requestBody: Dict = None,
responses: Dict = None,
security: List[Dict] = None,
deprecated: bool = False
):
"""
添加 API 端点
Args:
method: HTTP 方法 (GET, POST, PUT, DELETE, PATCH)
path: API 路径
summary: 简要描述
description: 详细描述
tags: 标签列表
parameters: 参数列表
requestBody: 请求体
responses: 响应定义
security: 安全要求
deprecated: 是否废弃
"""
if parameters is None:
parameters = []
if responses is None:
responses = self._default_responses()
if security is None:
security = []
# 转换路径参数
path_params = self._extract_path_params(path)
for param in path_params:
parameters.append({
'name': param,
'in': 'path',
'required': True,
'schema': {'type': 'string'},
'description': f'Path parameter: {param}'
})
# 转换查询参数
query_params = self._extract_query_params(path)
for param in query_params:
parameters.append({
'name': param,
'in': 'query',
'required': False,
'schema': {'type': 'string'},
'description': f'Query parameter: {param}'
})
# 构建路径
clean_path = path.format(**{p: f'{{{p}}}' for p in path_params})
if clean_path not in self.api_doc.paths:
self.api_doc.paths[clean_path] = {}
endpoint = {
'summary': summary,
'description': description,
'tags': tags or [],
'parameters': parameters,
'responses': responses,
'deprecated': deprecated
}
if security:
endpoint['security'] = security
if requestBody:
endpoint['requestBody'] = requestBody
self.api_doc.paths[clean_path][method.lower()] = endpoint
def _default_responses(self) -> Dict:
"""获取默认响应"""
return {
'200': {
'description': 'Successful response',
'content': {
'application/json': {
'schema': {'type': 'object'}
}
}
},
'400': {
'description': 'Bad request',
'content': {
'application/json': {
'schema': {'$ref': '#/components/schemas/Error'}
}
}
},
'401': {
'description': 'Unauthorized',
'content': {
'application/json': {
'schema': {'$ref': '#/components/schemas/Error'}
}
}
},
'403': {
'description': 'Forbidden',
'content': {
'application/json': {
'schema': {'$ref': '#/components/schemas/Error'}
}
}
},
'404': {
'description': 'Not found',
'content': {
'application/json': {
'schema': {'$ref': '#/components/schemas/Error'}
}
}
},
'500': {
'description': 'Internal server error',
'content': {
'application/json': {
'schema': {'$ref': '#/components/schemas/Error'}
}
}
}
}
def _extract_path_params(self, path: str) -> List[str]:
"""提取路径参数"""
import re
return re.findall(r'\{(\w+)\}', path)
def _extract_query_params(self, path: str) -> List[str]:
"""提取查询参数"""
import re
return re.findall(r':(\w+)', path)
def save(self, filepath: str, format: str = 'json'):
"""
保存 API 文档
Args:
filepath: 保存路径
format: 格式 (json, yaml)
"""
doc = self.generate()
if format == 'yaml':
try:
import yaml
with open(filepath, 'w', encoding='utf-8') as f:
yaml.dump(doc, f, default_flow_style=False, allow_unicode=True)
except ImportError:
# 回退为 JSON
format = 'json'
if format == 'json':
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(doc, f, ensure_ascii=False, indent=2)
return filepath
class APIEndpointRegistry:
"""API 端点注册表"""
def __init__(self):
self.endpoints: List[Dict] = []
def register(self, method: str, path: str, handler_name: str, description: str = ''):
"""注册端点"""
self.endpoints.append({
'method': method.upper(),
'path': path,
'handler': handler_name,
'description': description
})
def get_all(self) -> List[Dict]:
"""获取所有端点"""
return self.endpoints
def generate_docs(self) -> Dict:
"""生成文档"""
generator = APIDocGenerator()
for ep in self.endpoints:
generator.add_endpoint(
method=ep['method'],
path=ep['path'],
summary=ep['description'],
description=ep['description']
)
return generator.generate()
def generate_api_docs(config: Dict = None) -> Dict:
"""
生成完整的 API 文档
Args:
config: 服务器配置
Returns:
OpenAPI 格式的文档
"""
generator = APIDocGenerator(config)
# 设置服务器信息
host = config.get('host', 'localhost')
port = config.get('port', 8080)
protocol = 'https' if config.get('ssl_cert') else 'http'
generator.add_server(f'{protocol}://{host}:{port}', 'Production server')
# 添加标签
generator.add_tag('Server', '服务器信息')
generator.add_tag('Monitoring', '监控与指标')
generator.add_tag('Mirrors', '镜像源管理')
generator.add_tag('Sync', '同步管理')
generator.add_tag('Cache', '缓存管理')
generator.add_tag('Health', '健康检查')
generator.add_tag('Alerts', '告警管理')
generator.add_tag('Webhooks', 'Webhook管理')
generator.add_tag('Configuration', '配置管理')
generator.add_tag('Authentication', '认证管理')
# ========== 服务器信息端点 ==========
generator.add_endpoint(
'GET', '/api/v2/server/info',
summary='获取服务器信息',
description='返回服务器的详细信息,包括版本、运行时间、配置等',
tags=['Server']
)
# ========== 监控端点 ==========
generator.add_endpoint(
'GET', '/api/v2/monitor/realtime',
summary='获取实时监控数据',
description='返回服务器的实时监控数据,包括 CPU、内存、磁盘使用情况',
tags=['Monitoring']
)
generator.add_endpoint(
'GET', '/api/v2/monitor/history',
summary='获取历史监控数据',
description='返回指定时间段内的历史监控数据',
tags=['Monitoring'],
parameters=[{
'name': 'period',
'in': 'query',
'schema': {'type': 'string'},
'description': '时间周期: 1h, 6h, 24h, 7d, 30d'
}]
)
generator.add_endpoint(
'GET', '/api/v2/metrics',
summary='获取 Prometheus 指标',
description='返回 Prometheus 格式的监控指标',
tags=['Monitoring']
)
# ========== 镜像源端点 ==========
generator.add_endpoint(
'GET', '/api/v2/mirrors',
summary='列出所有镜像加速源',
description='返回所有可用的镜像加速源列表',
tags=['Mirrors']
)
generator.add_endpoint(
'GET', '/api/v2/mirrors/:name',
summary='获取镜像源详情',
description='返回指定镜像源的详细信息',
tags=['Mirrors']
)
generator.add_endpoint(
'POST', '/api/v2/mirrors',
summary='添加自定义镜像源',
description='添加新的自定义镜像加速源',
tags=['Mirrors'],
requestBody={
'required': True,
'content': {
'application/json': {
'schema': {
'type': 'object',
'properties': {
'name': {'type': 'string'},
'type': {'type': 'string'},
'url': {'type': 'string'},
'enabled': {'type': 'boolean'}
},
'required': ['name', 'url']
}
}
}
}
)
generator.add_endpoint(
'DELETE', '/api/v2/mirrors/:name',
summary='删除自定义镜像源',
description='删除指定的自定义镜像加速源',
tags=['Mirrors']
)
generator.add_endpoint(
'POST', '/api/v2/mirrors/:name/refresh',
summary='刷新镜像源缓存',
description='刷新指定镜像源的缓存数据',
tags=['Mirrors']
)
# ========== 同步管理端点 ==========
generator.add_endpoint(
'GET', '/api/v2/sync/sources',
summary='获取同步源列表',
description='返回所有配置的同步源',
tags=['Sync']
)
generator.add_endpoint(
'POST', '/api/v2/sync/sources',
summary='添加同步源',
description='添加新的同步源配置',
tags=['Sync'],
requestBody={
'required': True,
'content': {
'application/json': {
'schema': {
'type': 'object',
'properties': {
'name': {'type': 'string'},
'type': {'type': 'string'},
'url': {'type': 'string'},
'schedule': {'type': 'string'}
}
}
}
}
}
)
generator.add_endpoint(
'POST', '/api/v2/sync/:source_name/start',
summary='启动同步任务',
description='启动指定源的同步任务',
tags=['Sync']
)
generator.add_endpoint(
'POST', '/api/v2/sync/:source_name/stop',
summary='停止同步任务',
description='停止指定源的同步任务',
tags=['Sync']
)
generator.add_endpoint(
'GET', '/api/v2/sync/:source_name/status',
summary='获取同步状态',
description='返回指定同步源的当前状态',
tags=['Sync']
)
# ========== 缓存管理端点 ==========
generator.add_endpoint(
'GET', '/api/v2/cache/stats',
summary='获取缓存统计',
description='返回缓存的使用统计信息',
tags=['Cache']
)
generator.add_endpoint(
'GET', '/api/v2/cache/usage',
summary='获取缓存使用详情',
description='返回缓存的详细使用情况',
tags=['Cache']
)
generator.add_endpoint(
'POST', '/api/v2/cache/clean',
summary='清理缓存',
description='清理指定或全部缓存',
tags=['Cache'],
requestBody={
'content': {
'application/json': {
'schema': {
'type': 'object',
'properties': {
'source': {'type': 'string'}
}
}
}
}
}
)
# ========== 缓存预热端点 ==========
generator.add_endpoint(
'GET', '/api/v2/cache/prewarm',
summary='获取预热状态',
description='返回缓存预热的当前状态',
tags=['Cache']
)
generator.add_endpoint(
'POST', '/api/v2/cache/prewarm',
summary='执行缓存预热',
description='手动执行缓存预热任务',
tags=['Cache']
)
generator.add_endpoint(
'GET', '/api/v2/cache/prewarm/items',
summary='获取预热项目列表',
description='返回待预热的项目列表',
tags=['Cache']
)
generator.add_endpoint(
'POST', '/api/v2/cache/prewarm/clear',
summary='清空预热队列',
description='清空待预热的项目队列',
tags=['Cache']
)
# ========== 健康检查端点 ==========
generator.add_endpoint(
'GET', '/api/v2/health',
summary='获取健康状态',
description='返回服务器的整体健康状态',
tags=['Health']
)
generator.add_endpoint(
'GET', '/api/v2/health/sources',
summary='获取镜像源健康状态',
description='返回所有镜像源的健康检查结果',
tags=['Health']
)
generator.add_endpoint(
'GET', '/api/v2/health/check/:source_name',
summary='检查指定源健康',
description='手动触发指定镜像源的健康检查',
tags=['Health']
)
generator.add_endpoint(
'GET', '/api/v2/health/failover',
summary='获取故障切换状态',
description='返回故障切换系统的当前状态',
tags=['Health']
)
generator.add_endpoint(
'POST', '/api/v2/health/failover/:mirror_type',
summary='触发故障切换',
description='手动触发指定镜像类型的故障切换',
tags=['Health']
)
# ========== 告警管理端点 ==========
generator.add_endpoint(
'GET', '/api/v2/alerts',
summary='获取告警列表',
description='返回当前告警列表',
tags=['Alerts'],
parameters=[{
'name': 'limit',
'in': 'query',
'schema': {'type': 'integer'},
'description': '返回数量限制'
}]
)
generator.add_endpoint(
'POST', '/api/v2/alerts/:alert_id/acknowledge',
summary='确认告警',
description='确认指定告警',
tags=['Alerts']
)
generator.add_endpoint(
'POST', '/api/v2/alerts/clear',
summary='清除告警历史',
description='清除所有告警历史记录',
tags=['Alerts']
)
generator.add_endpoint(
'POST', '/api/v2/alerts/test',
summary='测试告警发送',
description='发送测试告警以验证配置',
tags=['Alerts']
)
generator.add_endpoint(
'GET', '/api/v2/alerts/config',
summary='获取告警配置',
description='返回当前的告警配置',
tags=['Alerts']
)
generator.add_endpoint(
'PUT', '/api/v2/alerts/config',
summary='更新告警配置',
description='更新告警配置(邮件、Webhook 等)',
tags=['Alerts']
)
# ========== Webhook 端点 ==========
generator.add_endpoint(
'GET', '/api/v2/webhooks',
summary='列出所有 Webhook',
description='返回所有配置的 Webhook',
tags=['Webhooks']
)
generator.add_endpoint(
'POST', '/api/v2/webhooks',
summary='创建 Webhook',
description='创建新的 Webhook 配置',
tags=['Webhooks']
)
generator.add_endpoint(
'GET', '/api/v2/webhooks/:webhook_id',
summary='获取 Webhook 详情',
description='返回指定 Webhook 的详细信息',
tags=['Webhooks']
)
generator.add_endpoint(
'PUT', '/api/v2/webhooks/:webhook_id',
summary='更新 Webhook',
description='更新指定 Webhook 的配置',
tags=['Webhooks']
)
generator.add_endpoint(
'DELETE', '/api/v2/webhooks/:webhook_id',
summary='删除 Webhook',
description='删除指定的 Webhook',
tags=['Webhooks']
)
generator.add_endpoint(
'POST', '/api/v2/webhooks/:webhook_id/test',
summary='测试 Webhook',
description='发送测试请求到指定的 Webhook',
tags=['Webhooks']
)
generator.add_endpoint(
'GET', '/api/v2/webhooks/:webhook_id/deliveries',
summary='获取 Webhook 交付历史',
description='返回指定 Webhook 的交付历史记录',
tags=['Webhooks']
)
generator.add_endpoint(
'GET', '/api/v2/webhooks/:webhook_id/stats',
summary='获取 Webhook 统计',
description='返回指定 Webhook 的交付统计信息',
tags=['Webhooks']
)
# ========== 配置管理端点 ==========
generator.add_endpoint(
'GET', '/api/v2/config',
summary='获取配置',
description='返回当前的服务器配置',
tags=['Configuration']
)
generator.add_endpoint(
'PUT', '/api/v2/config',
summary='保存配置',
description='保存配置到 settings.json',
tags=['Configuration']
)
generator.add_endpoint(
'POST', '/api/v2/config/reload',
summary='重新加载配置',
description='重新加载配置文件(热更新)',
tags=['Configuration']
)
generator.add_endpoint(
'GET', '/api/v2/config/changes',
summary='获取配置变更历史',
description='返回配置变更的历史记录',
tags=['Configuration']
)
# ========== 重启管理端点 ==========
generator.add_endpoint(
'GET', '/api/v2/server/restart',
summary='获取重启状态',
description='返回服务器重启管理的当前状态',
tags=['Server']
)
generator.add_endpoint(
'POST', '/api/v2/server/restart',
summary='准备重启',
description='准备执行服务器重启',
tags=['Server']
)
generator.add_endpoint(
'POST', '/api/v2/server/restart/confirm',
summary='确认执行重启',
description='确认并执行服务器重启',
tags=['Server']
)
generator.add_endpoint(
'POST', '/api/v2/server/restart/immediate',
summary='立即重启',
description='立即重启服务器(不等待请求完成)',
tags=['Server']
)
generator.add_endpoint(
'GET', '/api/v2/server/restart/pending',
summary='获取待处理请求',
description='返回当前待处理的请求列表',
tags=['Server']
)
generator.add_endpoint(
'GET', '/api/v2/server/restart/history',
summary='获取重启历史',
description='返回服务器重启的历史记录',
tags=['Server']
)
# ========== 认证端点 ==========
generator.add_endpoint(
'POST', '/api/v2/admin/auth/verify',
summary='验证认证状态',
description='验证当前请求的认证状态',
tags=['Authentication']
)
return generator.generate()
def save_api_docs(config: Dict, filepath: str = None, format: str = 'json'):
"""
生成并保存 API 文档
Args:
config: 服务器配置
filepath: 保存路径
format: 输出格式 (json, yaml)
"""
doc = generate_api_docs(config)
if filepath is None:
# 默认保存到项目根目录
script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
filepath = os.path.join(script_dir, 'docs', 'api-docs.json')
# 确保目录存在
os.makedirs(os.path.dirname(filepath), exist_ok=True)
if format == 'yaml':
try:
import yaml
with open(filepath, 'w', encoding='utf-8') as f:
yaml.dump(doc, f, default_flow_style=False, allow_unicode=True)
except ImportError:
format = 'json'
if format == 'json':
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(doc, f, ensure_ascii=False, indent=2)
return filepath
+677
View File
@@ -0,0 +1,677 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
缓存预热模块
预热常用镜像缓存提高命中率
"""
import os
import sys
import json
import time
import threading
import logging
import requests
from datetime import datetime
from typing import Dict, List, Optional, Callable
from dataclasses import dataclass, field
from enum import Enum
from concurrent.futures import ThreadPoolExecutor, as_completed
logger = logging.getLogger(__name__)
class PrewarmPriority(Enum):
"""预热优先级"""
CRITICAL = "critical" # 关键,必须预热
HIGH = "high" # 高优先级
MEDIUM = "medium" # 中优先级
LOW = "low" # 低优先级
class ItemStatus(Enum):
"""项目状态"""
PENDING = "pending"
IN_PROGRESS = "in_progress"
SUCCESS = "success"
FAILED = "failed"
SKIPPED = "skipped"
@dataclass
class PrewarmItem:
"""预热项目"""
id: str
mirror_type: str
item_name: str
url: str
priority: str
status: str = field(default=ItemStatus.PENDING.value)
attempts: int = 0
max_attempts: int = 3
response_time_ms: float = 0
error_message: str = None
prewarmed_at: float = None
size_bytes: int = 0
def to_dict(self) -> Dict:
return {
'id': self.id,
'mirror_type': self.mirror_type,
'item_name': self.item_name,
'url': self.url,
'priority': self.priority,
'status': self.status,
'attempts': self.attempts,
'response_time_ms': self.response_time_ms,
'error_message': self.error_message,
'prewarmed_at': self.prewarmed_at,
'size_bytes': self.size_bytes
}
@dataclass
class PrewarmTarget:
"""预热目标配置"""
mirror_type: str
priority: str
limit: int
items: List[str] = field(default_factory=list) # 指定的预热项目列表
tags: List[str] = field(default_factory=list) # 按标签筛选
class CachePrewarmer:
"""缓存预热管理器"""
def __init__(self, config: Dict = None):
"""
初始化缓存预热管理器
Args:
config: 预热配置
"""
self.config = config or {}
self.enabled = self.config.get('enabled', False)
# 预热目标
self.targets = self._parse_targets(self.config.get('targets', []))
# 调度配置
self.schedule = self.config.get('schedule', '0 3 * * *') # 默认每天凌晨3点
self.batch_size = self.config.get('batch_size', 10)
# 状态管理
self._items: Dict[str, PrewarmItem] = {}
self._items_lock = threading.Lock()
self._is_running = False
self._run_lock = threading.Lock()
# 历史记录
self._history: List[Dict] = []
self._history_lock = threading.Lock()
# 回调函数
self._on_start: Optional[Callable] = None
self._on_complete: Optional[Callable] = None
self._on_item_complete: Optional[Callable] = None
self._on_error: Optional[Callable] = None
# HTTP 会话
self._session = requests.Session()
self._session.headers.update({
'User-Agent': 'HYC-Mirror-Prewarmer/1.0'
})
# 超时设置
self._request_timeout = self.config.get('request_timeout', 30)
# 常用镜像包列表
self._popular_items = self._load_popular_items()
def _parse_targets(self, targets_config: List[Dict]) -> List[PrewarmTarget]:
"""解析预热目标配置"""
targets = []
for t in targets_config:
targets.append(PrewarmTarget(
mirror_type=t.get('mirror_type', 'docker'),
priority=t.get('priority', 'medium'),
limit=t.get('limit', 100),
items=t.get('items', []),
tags=t.get('tags', [])
))
return targets
def _load_popular_items(self) -> Dict[str, List[str]]:
"""加载常用镜像包列表"""
return {
'docker': [
'library/alpine:latest',
'library/ubuntu:latest',
'library/debian:latest',
'library/centos:latest',
'library/nginx:latest',
'library/python:3.9',
'library/python:3.10',
'library/node:18',
'library/node:20',
'library/go:1.20',
'library/redis:alpine',
'library/mysql:8',
'library/postgres:15',
],
'pip': [
'requests',
'numpy',
'pandas',
'flask',
'django',
'scipy',
'scikit-learn',
'torch',
'tensorflow',
'celery',
'pytest',
'black',
],
'npm': [
'react',
'vue',
'angular',
'lodash',
'express',
'axios',
'typescript',
'webpack',
'vite',
'eslint',
],
'apt': [
'ubuntu-desktop',
'ubuntu-standard',
'nginx',
'python3-pip',
'nodejs',
'docker.io',
],
'yum': [
'epel-release',
'nginx',
'docker-ce',
'python3-pip',
'nodejs',
],
'go': [
'golang.org/x/tools',
'github.com/gin-gonic/gin',
'github.com/beego/beego',
'github.com/gorilla/mux',
],
}
def _get_base_url(self, mirror_type: str) -> str:
"""获取镜像源基础 URL"""
mirrors = self.config.get('mirrors', {})
if isinstance(mirrors, dict):
mirror_config = mirrors.get(mirror_type, {})
if isinstance(mirror_config, dict):
sources = mirror_config.get('sources', [])
if sources:
source_config = mirror_config.get('sources_config', {}).get(sources[0], {})
return source_config.get('url', '')
return ''
def _generate_url(self, mirror_type: str, item_name: str) -> str:
"""生成预热 URL"""
base_url = self._get_base_url(mirror_type)
if not base_url:
return ''
if mirror_type == 'docker':
return f"{base_url}/v2/{item_name}/manifests/latest"
elif mirror_type == 'pip':
return f"{base_url}/simple/{item_name}/"
elif mirror_type == 'npm':
return f"{base_url}/{item_name}"
elif mirror_type == 'apt':
return f"{base_url}/dists/{item_name}/InRelease"
elif mirror_type == 'yum':
return f"{base_url}/repodata/repomd.xml"
elif mirror_type == 'go':
return f"{base_url}/{item_name}?go-get=1"
else:
return f"{base_url}/{item_name}"
def _create_item(
self,
mirror_type: str,
item_name: str,
priority: str = 'medium'
) -> PrewarmItem:
"""创建预热项目"""
url = self._generate_url(mirror_type, item_name)
return PrewarmItem(
id=f"{mirror_type}_{item_name}_{int(time.time())}",
mirror_type=mirror_type,
item_name=item_name,
url=url,
priority=priority
)
def add_item(self, item: PrewarmItem):
"""添加预热项目"""
with self._items_lock:
self._items[item.id] = item
def add_items_batch(self, mirror_type: str, items: List[str], priority: str = 'medium'):
"""批量添加预热项目"""
for item_name in items:
item = self._create_item(mirror_type, item_name, priority)
self.add_item(item)
def _prewarm_item(self, item: PrewarmItem) -> PrewarmItem:
"""
预热单个项目
Args:
item: 预热项目
Returns:
更新后的项目
"""
item.attempts += 1
item.status = ItemStatus.IN_PROGRESS.value
try:
start_time = time.time()
response = self._session.get(
item.url,
timeout=self._request_timeout,
allow_redirects=True
)
response.raise_for_status()
item.response_time_ms = round((time.time() - start_time) * 1000, 2)
item.size_bytes = len(response.content)
item.status = ItemStatus.SUCCESS.value
item.prewarmed_at = time.time()
logger.debug(f"Prewarmed {item.mirror_type}/{item.item_name}: {item.response_time_ms}ms")
except requests.exceptions.Timeout:
item.error_message = f"Timeout after {self._request_timeout}s"
if item.attempts < item.max_attempts:
item.status = ItemStatus.PENDING.value
else:
item.status = ItemStatus.FAILED.value
except requests.exceptions.HTTPError as e:
item.error_message = f"HTTP Error: {e.response.status_code}"
if item.attempts < item.max_attempts:
item.status = ItemStatus.PENDING.value
else:
item.status = ItemStatus.FAILED.value
except Exception as e:
item.error_message = str(e)
item.status = ItemStatus.FAILED.value
return item
def run(self, targets: List[PrewarmTarget] = None) -> Dict:
"""
执行预热
Args:
targets: 预热目标列表,None 表示使用配置的默认目标
Returns:
预热结果
"""
with self._run_lock:
if self._is_running:
return {
'success': False,
'error': 'Prewarm already running'
}
self._is_running = True
start_time = time.time()
result = {
'success': True,
'total_items': 0,
'success_count': 0,
'failed_count': 0,
'skipped_count': 0,
'elapsed_seconds': 0,
'errors': []
}
try:
# 通知开始
if self._on_start:
try:
self._on_start()
except Exception as e:
logger.error(f"Prewarm start callback failed: {e}")
# 确定要预热的目标
if targets is None:
targets = self.targets
# 如果没有指定目标,使用流行项目
if not targets:
for mirror_type, items in self._popular_items.items():
targets.append(PrewarmTarget(
mirror_type=mirror_type,
priority='medium',
limit=len(items),
items=items
))
# 添加项目到队列
total_added = 0
for target in targets:
if target.items:
# 使用指定的预热项目
for item_name in target.items[:target.limit]:
item = self._create_item(target.mirror_type, item_name, target.priority)
self.add_item(item)
total_added += 1
else:
# 使用流行项目列表
popular = self._popular_items.get(target.mirror_type, [])
for item_name in popular[:target.limit]:
item = self._create_item(target.mirror_type, item_name, target.priority)
self.add_item(item)
total_added += 1
result['total_items'] = total_added
# 按优先级排序
priority_order = {
'critical': 0,
'high': 1,
'medium': 2,
'low': 3
}
with self._items_lock:
sorted_items = sorted(
self._items.values(),
key=lambda x: (priority_order.get(x.priority, 99), x.id)
)
# 分批执行
completed = 0
with ThreadPoolExecutor(max_workers=self.batch_size) as executor:
futures = {
executor.submit(self._prewarm_item, item): item
for item in sorted_items
}
for future in as_completed(futures):
item = futures[future]
try:
updated_item = future.result()
# 更新项目状态
with self._items_lock:
self._items[item.id] = updated_item
# 统计
if updated_item.status == ItemStatus.SUCCESS.value:
result['success_count'] += 1
elif updated_item.status == ItemStatus.FAILED.value:
result['failed_count'] += 1
result['errors'].append({
'item': updated_item.item_name,
'error': updated_item.error_message
})
else:
result['skipped_count'] += 1
completed += 1
# 回调
if self._on_item_complete:
try:
self._on_item_complete(updated_item)
except Exception as e:
logger.error(f"Item complete callback failed: {e}")
except Exception as e:
logger.error(f"Prewarm execution error: {e}")
result['failed_count'] += 1
result['errors'].append({
'item': item.item_name,
'error': str(e)
})
result['elapsed_seconds'] = round(time.time() - start_time, 2)
# 记录历史
self._add_to_history(result)
# 通知完成
if self._on_complete:
try:
self._on_complete(result)
except Exception as e:
logger.error(f"Prewarm complete callback failed: {e}")
except Exception as e:
logger.error(f"Prewarm failed: {e}")
result['success'] = False
result['errors'].append({'error': str(e)})
finally:
self._is_running = False
return result
def _add_to_history(self, result: Dict):
"""添加历史记录"""
record = {
'timestamp': datetime.now().isoformat(),
'success': result.get('success', False),
'total_items': result.get('total_items', 0),
'success_count': result.get('success_count', 0),
'failed_count': result.get('failed_count', 0),
'elapsed_seconds': result.get('elapsed_seconds', 0)
}
with self._history_lock:
self._history.append(record)
# 只保留最近 50 条记录
self._history = self._history[-50:]
def get_status(self) -> Dict:
"""获取预热状态"""
with self._items_lock:
items = list(self._items.values())
total = len(items)
success = sum(1 for i in items if i.status == ItemStatus.SUCCESS.value)
failed = sum(1 for i in items if i.status == ItemStatus.FAILED.value)
in_progress = sum(1 for i in items if i.status == ItemStatus.IN_PROGRESS.value)
pending = sum(1 for i in items if i.status == ItemStatus.PENDING.value)
return {
'enabled': self.enabled,
'is_running': self._is_running,
'total_items': total,
'success_count': success,
'failed_count': failed,
'in_progress_count': in_progress,
'pending_count': pending,
'success_rate': (success / total * 100) if total > 0 else 0,
'targets_count': len(self.targets)
}
def get_items(
self,
status: str = None,
mirror_type: str = None,
limit: int = 50
) -> List[Dict]:
"""获取预热项目列表"""
with self._items_lock:
items = [i.to_dict() for i in self._items.values()]
# 过滤
if status:
items = [i for i in items if i['status'] == status]
if mirror_type:
items = [i for i in items if i['mirror_type'] == mirror_type]
return items[-limit:]
def get_history(self, limit: int = 20) -> List[Dict]:
"""获取预热历史"""
with self._history_lock:
return list(self._history[-limit:])
def get_stats(self) -> Dict:
"""获取统计信息"""
status = self.get_status()
history = self.get_history(10)
avg_duration = 0
if history:
durations = [h['elapsed_seconds'] for h in history if 'elapsed_seconds' in h]
if durations:
avg_duration = sum(durations) / len(durations)
return {
'enabled': self.enabled,
'is_running': self._is_running,
'total_prewarmed': status['success_count'],
'total_failed': status['failed_count'],
'success_rate': status['success_rate'],
'avg_duration_seconds': round(avg_duration, 2),
'recent_runs': len(history),
'targets': [
{
'mirror_type': t.mirror_type,
'priority': t.priority,
'limit': t.limit
}
for t in self.targets
]
}
def clear_items(self, status: str = None):
"""清除预热项目"""
with self._items_lock:
if status:
self._items = {
k: v for k, v in self._items.items()
if v.status != status
}
else:
self._items = {}
def set_start_callback(self, callback: Callable):
"""设置开始回调"""
self._on_start = callback
def set_complete_callback(self, callback: Callable):
"""设置完成回调"""
self._on_complete = callback
def set_item_complete_callback(self, callback: Callable):
"""设置项目完成回调"""
self._on_item_complete = callback
def set_error_callback(self, callback: Callable):
"""设置错误回调"""
self._on_error = callback
def get_popular_items(self, mirror_type: str) -> List[str]:
"""获取指定镜像类型的流行项目列表"""
return self._popular_items.get(mirror_type, [])
def add_popular_items_to_queue(
self,
mirror_type: str,
limit: int = None,
priority: str = 'medium'
):
"""
添加流行项目到预热队列
Args:
mirror_type: 镜像类型
limit: 数量限制
priority: 优先级
"""
popular = self._popular_items.get(mirror_type, [])
if limit:
popular = popular[:limit]
self.add_items_batch(mirror_type, popular, priority)
class PrewarmScheduler:
"""预热调度器"""
def __init__(self, config: Dict = None):
self.config = config or {}
self._scheduler = None
self._running = False
def start(self, prewarmer: CachePrewarmer):
"""启动调度器"""
if self._running:
return
schedule = self.config.get('schedule', '0 3 * * *')
logger.info(f"Starting prewarm scheduler with schedule: {schedule}")
# 使用简单的时间间隔检查
self._running = True
self._scheduler_thread = threading.Thread(
target=self._run_scheduler,
args=(prewarmer,),
daemon=True
)
self._scheduler_thread.start()
def _run_scheduler(self, prewarmer: CachePrewarmer):
"""运行调度器"""
import croniter
schedule = self.config.get('schedule', '0 3 * * *')
try:
cron = croniter.croniter(schedule, datetime.now())
next_run = cron.get_next(datetime)
except Exception as e:
logger.error(f"Invalid cron schedule: {e}")
return
while self._running:
now = datetime.now()
if now >= next_run:
logger.info("Running scheduled prewarm")
try:
prewarmer.run()
except Exception as e:
logger.error(f"Scheduled prewarm failed: {e}")
cron = croniter.croniter(schedule, datetime.now())
next_run = cron.get_next(datetime)
# 检查间隔
time.sleep(60)
def stop(self):
"""停止调度器"""
self._running = False
+350
View File
@@ -0,0 +1,350 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""配置管理模块"""
import os
import sys
import json
import hashlib
import time
from typing import Dict, Any, Optional
from .utils import parse_size
def get_resource_path(relative_path: str) -> str:
"""获取打包后的资源路径"""
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
# 打包后的路径
return os.path.join(sys._MEIPASS, relative_path)
return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), relative_path)
# 打包模式
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
# 外部目录:与 exe 同级
external_path = os.path.join(os.path.dirname(sys.executable), relative_path)
if os.path.exists(external_path):
return external_path
# 打包后的资源路径(_MEIPASS)
bundled_path = os.path.join(sys._MEIPASS, relative_path)
if os.path.exists(bundled_path):
return bundled_path
return external_path
# 开发模式
return os.path.join(project_root, relative_path)
def deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
"""
深度合并配置
- 如果 override 中有的键,会完全替换 base 中的值(除非都是 dict)
- 如果都是 dict,则递归合并
- 不会修改原参数
Args:
base: 默认配置(基础配置)
override: 要合并的配置(优先级更高)
Returns:
合并后的配置
"""
result = base.copy()
for key, value in override.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
# 两者都是字典,递归合并
result[key] = deep_merge(result[key], value)
else:
# 直接覆盖
result[key] = value
return result
def load_json_config(file_path: str) -> Optional[Dict[str, Any]]:
"""加载 JSON 配置文件"""
if not os.path.exists(file_path):
return None
try:
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
except json.JSONDecodeError as e:
print(f"警告: 配置文件 {file_path} JSON 格式错误: {e}")
return None
except Exception as e:
print(f"警告: 无法读取配置文件 {file_path}: {e}")
return None
class ConfigManager:
"""配置管理器"""
def __init__(self, config: Dict[str, Any] = None, settings_path: str = None):
"""
初始化配置管理器
Args:
config: 传入的配置(会覆盖默认配置)
settings_path: 默认配置文件路径(可以是位置参数或关键字参数)
"""
# 支持 settings_path 作为位置参数
if isinstance(config, str):
settings_path = config
config = None
elif settings_path is None:
# 使用打包后的资源路径
settings_path = get_resource_path('settings.json')
elif settings_path:
settings_path = settings_path
self._settings_path = settings_path
# 加载默认配置
self.default_config = self._load_default_config()
# 合并传入的配置
if config:
self.config = self._validate_config(deep_merge(self.default_config, config))
else:
self.config = self._validate_config(self.default_config.copy())
def _load_default_config(self) -> Dict[str, Any]:
"""加载默认配置文件"""
default_config = load_json_config(self._settings_path)
if default_config is None:
# 如果找不到默认配置,使用内联最小配置
default_config = {
'server_name': 'HYC下载站',
'host': '0.0.0.0',
'port': 8080,
'base_dir': './downloads',
'api_version': 'v2',
'directory_listing': True,
'enable_stats': True,
'auth_type': 'none',
'max_upload_size': 1024 * 1024 * 1024,
'timeout': 30,
'verbose': 0,
'enable_range': True,
'ignore_hidden': True,
'show_hash': False,
'calculate_hash': False,
'max_search_results': 100,
'enable_ws': True,
'enable_sse': True,
'enable_monitor': True,
'monitor_interval': 5,
'enable_sync': True,
'enable_mirrors': True,
'database': {
'enabled': True,
'type': 'sqlite',
'sqlite': {'path': './data/hyc.db'}
},
'mirrors': {
'docker': {'enabled': True},
'apt': {'enabled': True},
'yum': {'enabled': True},
'pypi': {'enabled': True},
'npm': {'enabled': True},
'go': {'enabled': True}
},
'sync_sources': {},
'webhooks': {'enabled': False, 'storage': 'webhooks.json'},
'auth_sessions_file': 'auth_sessions.json',
'auth_session_timeout': 3600,
'auth_cookie_max_age': 86400
}
print(f"警告: 未找到默认配置文件 ({self._settings_path}),使用内联默认配置")
return default_config
@classmethod
def from_settings(cls, custom_config: Dict[str, Any] = None, settings_path: str = None) -> 'ConfigManager':
"""
从默认配置创建配置管理器
Args:
custom_config: 自定义配置,会覆盖默认配置
settings_path: 默认配置文件路径
Returns:
ConfigManager 实例
"""
return cls(config=custom_config, settings_path=settings_path)
def _validate_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""验证和修复配置"""
# 确保必要配置存在
required = ['base_dir', 'host', 'port']
for key in required:
if key not in config:
raise ValueError(f"缺少必要配置: {key}")
# 修复路径配置
config['base_dir'] = os.path.abspath(config['base_dir'])
# 设置默认值(不在 _validate_config 中处理,由默认配置提供)
# 验证认证配置
auth_type = config.get('auth_type', 'none')
if auth_type == 'basic':
if 'auth_user' not in config:
config['auth_user'] = 'admin'
if 'auth_pass' not in config:
config['auth_pass'] = 'admin123'
elif auth_type == 'token':
# 每次运行都重新生成标准的 token
import secrets
config['auth_token'] = secrets.token_hex(32)
# 验证上传大小配置
if 'max_upload_size' in config:
try:
if isinstance(config['max_upload_size'], str):
config['max_upload_size'] = parse_size(config['max_upload_size'])
except ValueError as e:
print(f"警告: 无效的上传大小配置: {e}")
config['max_upload_size'] = 1024 * 1024 * 1024
# 验证端口范围
if 'port' in config:
port = config['port']
if not (1 <= port <= 65535):
raise ValueError(f"无效的端口号: {port}")
# 验证并创建必要目录
base_dir = config['base_dir']
try:
if not os.path.exists(base_dir):
os.makedirs(base_dir, exist_ok=True)
# 测试写入权限
test_file = os.path.join(base_dir, '.write_test')
with open(test_file, 'w') as f:
f.write('test')
os.remove(test_file)
except Exception as e:
raise ValueError(f"基础目录无法访问: {e}")
# 获取项目根目录(脚本所在目录)
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 在项目根目录创建必要的数据目录
necessary_dirs = [
os.path.join(project_root, 'data'),
os.path.join(project_root, 'logs'),
]
for dir_path in necessary_dirs:
if not os.path.exists(dir_path):
try:
os.makedirs(dir_path, exist_ok=True)
except Exception as e:
print(f"警告: 无法创建目录 {dir_path}: {e}")
# 更新配置指向项目根目录
config['data_dir'] = os.path.join(project_root, 'data')
config['logs_dir'] = os.path.join(project_root, 'logs')
return config
def get(self, key: str, default: Any = None) -> Any:
"""获取配置项"""
return self.config.get(key, default)
def update(self, updates: Dict[str, Any]):
"""更新配置"""
self.config.update(updates)
self.config = self._validate_config(self.config)
def get_full_config(self) -> Dict[str, Any]:
"""获取完整配置字典"""
return self.config.copy()
def to_dict(self) -> Dict[str, Any]:
"""返回配置字典(不包含敏感信息)"""
safe_config = {
"server_name": self.config.get("server_name", "Mirror Server"),
"version": "2.2",
"base_dir": self.config['base_dir'],
"directory_listing": self.config.get('directory_listing', True),
"max_upload_size": self.config.get('max_upload_size'),
"enable_stats": self.config.get('enable_stats', True),
"auth_type": self.config.get('auth_type', 'none'),
"sort_by": self.config.get('sort_by', 'name'),
"sort_reverse": self.config.get('sort_reverse', False),
"ignore_hidden": self.config.get('ignore_hidden', True),
"enable_range": self.config.get('enable_range', True),
"show_hash": self.config.get('show_hash', False),
"calculate_hash": self.config.get('calculate_hash', False),
"max_search_results": self.config.get('max_search_results', 100),
"api_version": self.config.get('api_version', 'v1'),
"verbose": self.config.get('verbose', 0)
}
return safe_config
def load_config_file(config_path: str) -> Dict[str, Any]:
"""加载配置文件"""
if not os.path.exists(config_path):
return {}
try:
with open(config_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"错误: 无法加载配置文件 {config_path}: {e}")
return {}
def load_settings_with_override(settings_path: str, override_path: str = None) -> Dict[str, Any]:
"""
加载默认配置并合并覆盖配置
Args:
settings_path: 默认配置文件路径
override_path: 覆盖配置文件路径(可选)
Returns:
合并后的完整配置
"""
# 加载默认配置
default_config = load_json_config(settings_path) or {}
# 加载覆盖配置
override_config = {}
if override_path:
override_config = load_json_config(override_path) or {}
# 深度合并
return deep_merge(default_config, override_config)
def save_config_file(config_path: str, config: Dict[str, Any]) -> bool:
"""
保存配置文件
Args:
config_path: 保存路径
config: 配置字典
Returns:
是否保存成功
"""
try:
# 创建目录
os.makedirs(os.path.dirname(config_path), exist_ok=True)
with open(config_path, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False, indent=4)
return True
except Exception as e:
print(f"错误: 无法保存配置文件 {config_path}: {e}")
return False
+340
View File
@@ -0,0 +1,340 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
配置热更新模块
支持不重启服务的情况下重新加载配置
"""
import os
import sys
import json
import time
import threading
import logging
from typing import Dict, Any, Optional, Callable
from datetime import datetime
logger = logging.getLogger(__name__)
class ConfigHotReloader:
"""配置热重载管理器"""
def __init__(self, config_path: str, callback: Callable = None):
"""
初始化热重载管理器
Args:
config_path: 配置文件路径
callback: 配置变更时的回调函数,接收 (config, change_type) 参数
"""
self.config_path = config_path
self.callback = callback
self._config: Dict[str, Any] = {}
self._last_modified: float = 0
self._last_load_time: Optional[datetime] = None
self._lock = threading.RLock()
# 配置变更历史
self._change_history: list = []
# 监听器
self._listeners: Dict[str, list] = {
'on_change': [],
'on_error': []
}
# 加载初始配置
self.reload()
def reload(self, silent: bool = False) -> bool:
"""
重新加载配置
Args:
silent: 静默模式,不触发变更通知
Returns:
是否加载成功
"""
try:
if not os.path.exists(self.config_path):
if not silent:
logger.warning(f"配置文件不存在: {self.config_path}")
return False
# 获取文件修改时间
current_mtime = os.path.getmtime(self.config_path)
# 检查是否有变化
if current_mtime == self._last_modified and not silent:
return True
# 加载配置
with open(self.config_path, 'r', encoding='utf-8') as f:
new_config = json.load(f)
# 计算变更
changes = self._compute_changes(self._config, new_config)
with self._lock:
old_config = self._config.copy()
self._config = new_config
self._last_modified = current_mtime
self._last_load_time = datetime.now()
# 记录变更
if changes:
change_record = {
'timestamp': self._last_load_time.isoformat(),
'changes': changes,
'old_config_keys': list(old_config.keys()),
'new_config_keys': list(new_config.keys())
}
self._change_history.append(change_record)
# 保持历史记录在合理范围内
if len(self._change_history) > 100:
self._change_history = self._change_history[-50:]
if not silent and changes:
self._notify_change(changes)
logger.info(f"配置已重新加载: {self.config_path}")
return True
except json.JSONDecodeError as e:
error_msg = f"配置 JSON 格式错误: {e}"
logger.error(error_msg)
self._notify_error(error_msg)
return False
except Exception as e:
error_msg = f"加载配置失败: {e}"
logger.error(error_msg)
self._notify_error(error_msg)
return False
def _compute_changes(self, old: Dict, new: Dict) -> Dict:
"""计算配置变更"""
changes = {
'added': [],
'removed': [],
'modified': []
}
old_keys = set(old.keys())
new_keys = set(new.keys())
# 新增的键
for key in new_keys - old_keys:
changes['added'].append(key)
# 移除的键
for key in old_keys - new_keys:
changes['removed'].append(key)
# 修改的键
for key in old_keys & new_keys:
if old[key] != new[key]:
# 检查是否是嵌套字典
if isinstance(old[key], dict) and isinstance(new[key], dict):
nested = self._compute_nested_changes(old[key], new[key], f"{key}.")
if nested['added'] or nested['removed'] or nested['modified']:
changes['modified'].append({
'key': key,
'type': 'nested',
'changes': nested
})
else:
changes['modified'].append({
'key': key,
'type': 'value',
'old_value': old[key],
'new_value': new[key]
})
return changes
def _compute_nested_changes(self, old: Dict, new: Dict, prefix: str = "") -> Dict:
"""计算嵌套字典的变更"""
changes = {
'added': [],
'removed': [],
'modified': []
}
old_keys = set(old.keys())
new_keys = set(new.keys())
for key in new_keys - old_keys:
changes['added'].append(f"{prefix}{key}")
for key in old_keys - new_keys:
changes['removed'].append(f"{prefix}{key}")
for key in old_keys & new_keys:
if old[key] != new[key]:
changes['modified'].append(f"{prefix}{key}")
return changes
def _notify_change(self, changes: Dict):
"""通知配置变更"""
for listener in self._listeners['on_change']:
try:
if callable(listener):
listener(self._config, changes)
except Exception as e:
logger.error(f"配置变更监听器执行失败: {e}")
if self.callback:
try:
self.callback(self._config, changes)
except Exception as e:
logger.error(f"配置回调函数执行失败: {e}")
def _notify_error(self, error: str):
"""通知错误"""
for listener in self._listeners['on_error']:
try:
if callable(listener):
listener(error)
except Exception as e:
logger.error(f"错误监听器执行失败: {e}")
def add_change_listener(self, callback: Callable):
"""添加配置变更监听器"""
self._listeners['on_change'].append(callback)
def add_error_listener(self, callback: Callable):
"""添加错误监听器"""
self._listeners['on_error'].append(callback)
def get(self, key: str, default: Any = None) -> Any:
"""获取配置值"""
with self._lock:
return self._config.get(key, default)
def get_all(self) -> Dict:
"""获取完整配置"""
with self._lock:
return self._config.copy()
def set(self, key: str, value: Any, save: bool = True) -> bool:
"""设置配置值(仅内存中)"""
with self._lock:
self._config[key] = value
if save:
return self.save()
return True
def save(self, path: str = None) -> bool:
"""保存配置到文件"""
save_path = path or self.config_path
try:
with open(save_path, 'w', encoding='utf-8') as f:
json.dump(self._config, f, ensure_ascii=False, indent=4)
self._last_modified = os.path.getmtime(save_path)
return True
except Exception as e:
logger.error(f"保存配置失败: {e}")
return False
def get_change_history(self, limit: int = 10) -> list:
"""获取配置变更历史"""
return self._change_history[-limit:]
def watch(self, interval: float = 5.0):
"""
启动后台监控线程
Args:
interval: 检查间隔(秒)
"""
def _watch_loop():
while True:
try:
self.reload()
except Exception as e:
logger.error(f"配置监控错误: {e}")
time.sleep(interval)
thread = threading.Thread(target=_watch_loop, daemon=True)
thread.start()
logger.info(f"配置热监控已启动,间隔: {interval}秒")
class ConfigManager:
"""配置管理器 - 支持热更新"""
def __init__(self, config: Dict[str, Any] = None):
self.config = config or {}
self._hot_reloader: Optional[ConfigHotReloader] = None
def load_from_file(self, path: str, enable_watch: bool = False) -> bool:
"""从文件加载配置"""
if not os.path.exists(path):
return False
try:
with open(path, 'r', encoding='utf-8') as f:
self.config = json.load(f)
if enable_watch:
self._hot_reloader = ConfigHotReloader(path)
self._hot_reloader.watch()
return True
except Exception as e:
logger.error(f"加载配置失败: {e}")
return False
def hot_reload(self, path: str = None) -> bool:
"""触发热重载"""
if self._hot_reloader:
return self._hot_reloader.reload()
return False
def get(self, key: str, default: Any = None) -> Any:
"""获取配置值"""
return self.config.get(key, default)
def set(self, key: str, value: Any, persist: bool = False, path: str = None) -> bool:
"""设置配置值"""
keys = key.split('.')
current = self.config
for k in keys[:-1]:
if k not in current:
current[k] = {}
current = current[k]
current[keys[-1]] = value
if persist:
if self._hot_reloader:
return self._hot_reloader.save(path)
elif path:
try:
with open(path, 'w', encoding='utf-8') as f:
json.dump(self.config, f, ensure_ascii=False, indent=4)
return True
except Exception as e:
logger.error(f"保存配置失败: {e}")
return False
return True
def add_change_listener(self, callback: Callable):
"""添加变更监听器"""
if self._hot_reloader:
self._hot_reloader.add_change_listener(callback)
def get_all(self) -> Dict:
"""获取完整配置"""
return self.config.copy()
+1545
View File
File diff suppressed because it is too large Load Diff
+603
View File
@@ -0,0 +1,603 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
平滑重启模块
支持优雅停止、零停机重启、滚动更新
"""
import os
import sys
import signal
import time
import threading
import logging
import subprocess
from typing import Dict, List, Optional, Callable
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
logger = logging.getLogger(__name__)
class RestartStrategy(Enum):
"""重启策略"""
GRACEFUL = "graceful" # 优雅停止,等待请求完成
ROLLING = "rolling" # 滚动更新,零停机
IMMEDIATE = "immediate" # 立即重启
class ServerState(Enum):
"""服务器状态"""
RUNNING = "running"
STOPPING = "stopping"
STOPPED = "stopped"
STARTING = "starting"
RESTARTING = "restarting"
@dataclass
class PendingRequest:
"""待处理的请求"""
request_id: str
start_time: float
endpoint: str
method: str
client_address: tuple
class GracefulRestartManager:
"""平滑重启管理器"""
def __init__(self, config: Dict = None):
"""
初始化平滑重启管理器
Args:
config: 重启配置
"""
self.config = config or {}
self.state = ServerState.STOPPED
# 等待超时时间
self.graceful_timeout = self.config.get('graceful_timeout', 30) # 秒
self.shutdown_timeout = self.config.get('shutdown_timeout', 10) # 秒
# 待处理请求追踪
self._pending_requests: Dict[str, PendingRequest] = {}
self._requests_lock = threading.Lock()
# 请求计数器
self._request_counter = 0
self._counter_lock = threading.Lock()
# 回调函数
self._on_prepare_restart: Optional[Callable] = None
self._on_start_restart: Optional[Callable] = None
self._on_complete_restart: Optional[Callable] = None
self._on_restart_failed: Optional[Callable] = None
# 状态锁
self._state_lock = threading.Lock()
# 重启历史
self._restart_history: List[Dict] = []
self._history_lock = threading.Lock()
# 信号处理器
self._setup_signal_handlers()
def _setup_signal_handlers(self):
"""设置信号处理器"""
signal.signal(signal.SIGTERM, self._handle_signal)
signal.signal(signal.SIGINT, self._handle_signal)
signal.signal(signal.SIGHUP, self._handle_signal)
def _handle_signal(self, signum, frame):
"""处理退出信号"""
signal_names = {
signal.SIGTERM: 'SIGTERM',
signal.SIGINT: 'SIGINT',
signal.SIGHUP: 'SIGHUP'
}
sig_name = signal_names.get(signum, str(signum))
logger.info(f"收到信号 {sig_name},准备优雅关闭")
# 设置状态为停止中
self.state = ServerState.STOPPING
def register_request(self, endpoint: str, method: str, client: tuple) -> str:
"""
注册一个待处理的请求
Args:
endpoint: 请求端点
method: 请求方法
client: 客户端地址
Returns:
请求 ID
"""
with self._counter_lock:
self._request_counter += 1
request_id = f"req_{self._request_counter}_{int(time.time())}"
request = PendingRequest(
request_id=request_id,
start_time=time.time(),
endpoint=endpoint,
method=method,
client_address=client
)
with self._requests_lock:
self._pending_requests[request_id] = request
return request_id
def complete_request(self, request_id: str):
"""标记请求完成"""
with self._requests_lock:
self._pending_requests.pop(request_id, None)
def get_pending_count(self) -> int:
"""获取待处理请求数量"""
with self._requests_lock:
return len(self._pending_requests)
def get_pending_requests(self) -> List[Dict]:
"""获取待处理请求详情"""
with self._requests_lock:
return [
{
'request_id': r.request_id,
'endpoint': r.endpoint,
'method': r.method,
'duration': round(time.time() - r.start_time, 2),
'client': str(r.client_address[0]) if r.client_address else 'unknown'
}
for r in self._pending_requests.values()
]
def is_safe_to_restart(self) -> bool:
"""检查是否安全重启(没有待处理请求)"""
return self.get_pending_count() == 0
def wait_for_requests(
self,
timeout: int = None,
progress_callback: Callable[[int, int], None] = None
) -> bool:
"""
等待所有请求完成
Args:
timeout: 超时时间(秒)
progress_callback: 进度回调函数(remaining, total)
Returns:
是否在超时前完成所有请求
"""
timeout = timeout or self.graceful_timeout
start_time = time.time()
while time.time() - start_time < timeout:
pending = self.get_pending_count()
if progress_callback:
progress_callback(pending, 0)
if pending == 0:
return True
time.sleep(0.5)
return False
def set_prepare_restart_callback(self, callback: Callable):
"""设置准备重启回调"""
self._on_prepare_restart = callback
def set_start_restart_callback(self, callback: Callable):
"""设置开始重启回调"""
self._on_start_restart = callback
def set_complete_restart_callback(self, callback: Callable):
"""设置完成重启回调"""
self._on_complete_restart = callback
def set_restart_failed_callback(self, callback: Callable):
"""设置重启失败回调"""
self._on_restart_failed = callback
def prepare_restart(self) -> Dict:
"""
准备重启(通知各模块准备)
Returns:
准备结果
"""
with self._state_lock:
self.state = ServerState.STOPPING
result = {
'success': True,
'pending_requests': self.get_pending_count(),
'message': ''
}
# 通知回调
if self._on_prepare_restart:
try:
self._on_prepare_restart()
except Exception as e:
logger.error(f"Prepare restart callback failed: {e}")
result['success'] = False
result['message'] = str(e)
return result
def perform_restart(
self,
strategy: RestartStrategy = RestartStrategy.GRACEFUL,
new_config: Dict = None,
script_path: str = None
) -> Dict:
"""
执行重启
Args:
strategy: 重启策略
new_config: 新配置(用于配置热更新)
script_path: 服务器脚本路径
Returns:
重启结果
"""
with self._state_lock:
if self.state == ServerState.RESTARTING:
return {
'success': False,
'error': 'Restart already in progress'
}
self.state = ServerState.RESTARTING
start_time = time.time()
result = {
'success': False,
'strategy': strategy.value,
'elapsed_seconds': 0,
'message': ''
}
try:
# 准备阶段
prepare_result = self.prepare_restart()
if not prepare_result['success']:
result['message'] = f"Prepare failed: {prepare_result['message']}"
self.state = ServerState.RUNNING
return result
# 根据策略执行重启
if strategy == RestartStrategy.GRACEFUL:
result = self._graceful_restart(prepare_result)
elif strategy == RestartStrategy.ROLLING:
result = self._rolling_restart(script_path, new_config)
elif strategy == RestartStrategy.IMMEDIATE:
result = self._immediate_restart(script_path)
result['elapsed_seconds'] = round(time.time() - start_time, 2)
# 记录重启历史
self._add_to_history(result)
except Exception as e:
logger.error(f"Restart failed: {e}")
result['success'] = False
result['message'] = str(e)
self.state = ServerState.RUNNING
if self._on_restart_failed:
try:
self._on_restart_failed(str(e))
except Exception:
pass
return result
def _graceful_restart(self, prepare_result: Dict) -> Dict:
"""优雅重启(等待请求完成)"""
result = {
'success': True,
'strategy': 'graceful',
'pending_requests': prepare_result['pending_requests'],
'message': ''
}
# 等待请求完成
pending = prepare_result['pending_requests']
if pending > 0:
logger.info(f"等待 {pending} 个请求完成,超时 {self.graceful_timeout} 秒")
def progress_callback(remaining, total):
if remaining % 5 == 0:
logger.info(f"还有 {remaining} 个请求待处理")
success = self.wait_for_requests(
timeout=self.graceful_timeout,
progress_callback=progress_callback
)
if not success:
remaining = self.get_pending_count()
result['success'] = False
result['message'] = f"{remaining} 个请求未在 {self.graceful_timeout} 秒内完成"
logger.warning(result['message'])
self.state = ServerState.RUNNING
return result
result['message'] = '所有请求已完成'
logger.info('所有请求已完成')
# 通知开始重启
if self._on_start_restart:
try:
self._on_start_restart()
except Exception as e:
logger.error(f"Start restart callback failed: {e}")
result['success'] = True
result['message'] = 'Graceful restart prepared'
self.state = ServerState.STOPPED
return result
def _rolling_restart(self, script_path: str, new_config: Dict = None) -> Dict:
"""滚动重启(零停机)"""
result = {
'success': True,
'strategy': 'rolling',
'message': ''
}
if not script_path:
script_path = os.path.join(os.path.dirname(__file__), '..', 'main.py')
script_path = os.path.abspath(script_path)
# 检查是否有新配置需要应用
if new_config:
result['config_updated'] = True
logger.info("配置将在重启后应用")
else:
result['config_updated'] = False
# 通知开始重启
if self._on_start_restart:
try:
self._on_start_restart()
except Exception as e:
logger.error(f"Start restart callback failed: {e}")
# 启动新进程(在同一端口,但使用不同进程ID)
# 注意:实际实现需要在负载均衡器层面处理
logger.info("滚动重启:建议在负载均衡器层面处理零停机")
self.state = ServerState.STOPPED
result['message'] = 'Rolling restart prepared - use load balancer for zero-downtime'
return result
def _immediate_restart(self, script_path: str = None) -> Dict:
"""立即重启"""
result = {
'success': True,
'strategy': 'immediate',
'message': ''
}
if not script_path:
script_path = os.path.join(os.path.dirname(__file__), '..', 'main.py')
script_path = os.path.abspath(script_path)
# 通知开始重启
if self._on_start_restart:
try:
self._on_start_restart()
except Exception as e:
logger.error(f"Start restart callback failed: {e}")
# 发送重启信号给主进程
logger.info("立即重启服务器")
# 在子进程中重启
try:
# 启动新进程
cmd = [sys.executable, script_path]
env = os.environ.copy()
env['HYC_RESTARTED'] = '1'
subprocess.Popen(cmd, env=env)
self.state = ServerState.STOPPED
result['message'] = 'Immediate restart initiated'
except Exception as e:
result['success'] = False
result['message'] = f"Failed to restart: {str(e)}"
self.state = ServerState.RUNNING
return result
def _add_to_history(self, result: Dict):
"""添加重启历史记录"""
record = {
'timestamp': datetime.now().isoformat(),
'strategy': result.get('strategy', 'unknown'),
'success': result.get('success', False),
'elapsed_seconds': result.get('elapsed_seconds', 0),
'message': result.get('message', '')
}
with self._history_lock:
self._restart_history.append(record)
# 只保留最近 20 条记录
self._restart_history = self._restart_history[-20:]
def get_restart_history(self) -> List[Dict]:
"""获取重启历史"""
with self._history_lock:
return list(self._restart_history)
def get_stats(self) -> Dict:
"""获取统计信息"""
return {
'state': self.state.value,
'pending_requests': self.get_pending_count(),
'graceful_timeout': self.graceful_timeout,
'shutdown_timeout': self.shutdown_timeout,
'restart_count': len(self._restart_history),
'recent_restarts': self.get_restart_history()[-5:]
}
def update_config(self, new_config: Dict):
"""更新配置(热更新)"""
if 'graceful_timeout' in new_config:
self.graceful_timeout = new_config['graceful_timeout']
if 'shutdown_timeout' in new_config:
self.shutdown_timeout = new_config['shutdown_timeout']
logger.info(f"重启配置已更新: timeout={self.graceful_timeout}s")
class ServerHealthChecker:
"""服务器健康检查器"""
def __init__(self, host: str = 'localhost', port: int = 8080):
self.host = host
self.port = port
self._last_check = None
self._is_healthy = False
def check(self) -> Dict:
"""
检查服务器健康状态
Returns:
健康检查结果
"""
import socket
result = {
'healthy': False,
'latency_ms': 0,
'error': None,
'timestamp': datetime.now().isoformat()
}
try:
start_time = time.time()
# 尝试连接
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
sock.connect((self.host, self.port))
sock.close()
result['latency_ms'] = round((time.time() - start_time) * 1000, 2)
result['healthy'] = True
except Exception as e:
result['error'] = str(e)
self._last_check = result
return result
def get_latency(self) -> float:
"""获取延迟(毫秒)"""
if self._last_check:
return self._last_check.get('latency_ms', 0)
return 0
class RollingRestartManager:
"""滚动重启管理器(支持多实例)"""
def __init__(self, config: Dict = None):
self.config = config or {}
self.instances: Dict[str, Dict] = {} # instance_id -> info
self.current_instance_id = None
self._lock = threading.Lock()
def register_instance(self, instance_id: str, info: Dict):
"""注册实例"""
with self._lock:
self.instances[instance_id] = {
**info,
'registered_at': datetime.now().isoformat(),
'status': 'active'
}
def unregister_instance(self, instance_id: str):
"""注销实例"""
with self._lock:
if instance_id in self.instances:
self.instances[instance_id]['status'] = 'draining'
def get_active_instances(self) -> List[str]:
"""获取活跃实例列表"""
with self._lock:
return [
i for i, info in self.instances.items()
if info['status'] == 'active'
]
def perform_rolling_restart(
self,
instance_id: str,
restart_func: Callable
) -> Dict:
"""
对单个实例执行滚动重启
Args:
instance_id: 实例 ID
restart_func: 重启函数
Returns:
重启结果
"""
with self._lock:
if instance_id not in self.instances:
return {
'success': False,
'error': f'Instance {instance_id} not found'
}
# 标记为排水中
self.instances[instance_id]['status'] = 'draining'
# 等待连接耗尽
time.sleep(5)
# 执行重启
try:
restart_func(instance_id)
self.instances[instance_id]['status'] = 'active'
self.instances[instance_id]['restarted_at'] = datetime.now().isoformat()
return {
'success': True,
'instance_id': instance_id
}
except Exception as e:
self.instances[instance_id]['status'] = 'error'
return {
'success': False,
'instance_id': instance_id,
'error': str(e)
}
+426
View File
@@ -0,0 +1,426 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
镜像源健康检查模块
自动检测上游镜像源的可用性,支持故障切换
"""
import os
import sys
import json
import time
import threading
import logging
import urllib.request
import urllib.error
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Callable
from enum import Enum
from dataclasses import dataclass, field
logger = logging.getLogger(__name__)
class HealthStatus(Enum):
"""健康状态"""
UNKNOWN = "unknown"
HEALTHY = "healthy"
DEGRADED = "degraded"
UNHEALTHY = "unhealthy"
@dataclass
class HealthCheckResult:
"""健康检查结果"""
source_name: str
status: HealthStatus
response_time: float # 毫秒
http_status: Optional[int] = None
error_message: Optional[str] = None
last_check: Optional[datetime] = None
consecutive_failures: int = 0
total_checks: int = 0
success_rate: float = 100.0
details: Dict = field(default_factory=dict)
class HealthChecker:
"""健康检查器"""
def __init__(self, config: dict = None):
"""
初始化健康检查器
Args:
config: 健康检查配置
"""
self.config = config or {}
self.default_timeout = self.config.get('timeout', 10) # 秒
self.default_interval = self.config.get('interval', 60) # 秒
self.max_retries = self.config.get('max_retries', 3)
self.failure_threshold = self.config.get('failure_threshold', 3) # 连续失败次数阈值
# 检查结果存储
self._results: Dict[str, HealthCheckResult] = {}
self._lock = threading.Lock()
# 状态变化回调
self._on_status_change: Optional[Callable] = None
def set_status_change_callback(self, callback: Callable):
"""设置状态变化回调"""
self._on_status_change = callback
def check_source(self, source_name: str, source_config: dict) -> HealthCheckResult:
"""
检查单个镜像源的健康状态
Args:
source_name: 镜像源名称
source_config: 镜像源配置
Returns:
HealthCheckResult: 检查结果
"""
url = source_config.get('url', '')
if not url:
return HealthCheckResult(
source_name=source_name,
status=HealthStatus.UNHEALTHY,
response_time=0,
error_message="No URL configured",
last_check=datetime.now()
)
start_time = time.time()
http_status = None
error_message = None
details = {}
try:
# 发送 HEAD 请求(更轻量)
req = urllib.request.Request(
url.rstrip('/') + '/',
method='HEAD',
headers={
'User-Agent': 'HYC-Mirror-HealthCheck/1.0',
'Accept': '*/*'
}
)
with urllib.request.urlopen(req, timeout=self.default_timeout) as response:
http_status = response.status
details['headers'] = dict(response.headers)
except urllib.error.HTTPError as e:
http_status = e.code
error_message = f"HTTP {e.code}"
except urllib.error.URLError as e:
error_message = f"Connection error: {str(e.reason)}"
except Exception as e:
error_message = str(e)
response_time = (time.time() - start_time) * 1000 # 转换为毫秒
# 判断健康状态
if error_message:
if http_status and 400 <= http_status < 500:
status = HealthStatus.DEGRADED # 客户端错误,可能暂时
else:
status = HealthStatus.UNHEALTHY
elif http_status and 200 <= http_status < 400:
status = HealthStatus.HEALTHY
elif http_status:
status = HealthStatus.DEGRADED
else:
status = HealthStatus.UNHEALTHY
# 计算统计数据
with self._lock:
if source_name not in self._results:
self._results[source_name] = HealthCheckResult(
source_name=source_name,
status=status,
response_time=response_time,
last_check=datetime.now(),
consecutive_failures=0,
total_checks=1
)
else:
old_result = self._results[source_name]
consecutive_failures = old_result.consecutive_failures + (1 if status == HealthStatus.UNHEALTHY else 0)
total_checks = old_result.total_checks + 1
success_rate = ((total_checks - consecutive_failures) / total_checks) * 100
self._results[source_name] = HealthCheckResult(
source_name=source_name,
status=status,
response_time=response_time,
http_status=http_status,
error_message=error_message,
last_check=datetime.now(),
consecutive_failures=consecutive_failures,
total_checks=total_checks,
success_rate=success_rate,
details=details
)
# 检查状态变化,触发回调
if self._on_status_change and source_name in self._results:
old_status = self._results[source_name].status
if old_status != status:
try:
self._on_status_change(source_name, old_status, status)
except Exception as e:
logger.error(f"状态变化回调执行失败: {e}")
return self._results[source_name]
def get_all_results(self) -> List[HealthCheckResult]:
"""获取所有检查结果"""
with self._lock:
return list(self._results.values())
def get_result(self, source_name: str) -> Optional[HealthCheckResult]:
"""获取指定源的结果"""
with self._lock:
return self._results.get(source_name)
def is_healthy(self, source_name: str) -> bool:
"""检查源是否健康"""
result = self.get_result(source_name)
if result is None:
return True # 未检查过的默认健康
return result.status == HealthStatus.HEALTHY
def get_unhealthy_sources(self) -> List[str]:
"""获取不健康的源列表"""
with self._lock:
return [name for name, result in self._results.items()
if result.status == HealthStatus.UNHEALTHY]
def get_stats(self) -> dict:
"""获取健康检查统计"""
with self._lock:
total = len(self._results)
healthy = sum(1 for r in self._results.values() if r.status == HealthStatus.HEALTHY)
degraded = sum(1 for r in self._results.values() if r.status == HealthStatus.DEGRADED)
unhealthy = sum(1 for r in self._results.values() if r.status == HealthStatus.UNHEALTHY)
avg_response_time = 0
if total > 0:
avg_response_time = sum(r.response_time for r in self._results.values()) / total
return {
'total_sources': total,
'healthy': healthy,
'degraded': degraded,
'unhealthy': unhealthy,
'avg_response_time_ms': round(avg_response_time, 2),
'timestamp': datetime.now().isoformat()
}
class MirrorFailoverManager:
"""镜像源故障切换管理器"""
def __init__(self, config: dict = None):
"""
初始化故障切换管理器
Args:
config: 配置,包含镜像源列表
"""
self.config = config or {}
self.mirrors: Dict[str, Dict] = self.config.get('mirrors', {})
# 启用故障切换
self.failover_enabled = self.config.get('failover_enabled', True)
self.failover_threshold = self.config.get('failover_threshold', 3) # 连续失败次数
# 健康检查器
self.health_checker = HealthChecker(self.config.get('health_check', {}))
# 当前活跃源
self._active_source: Dict[str, str] = {} # mirror_type -> source_name
self._source_priority: Dict[str, List[str]] = {} # mirror_type -> [优先列表]
# 故障切换历史
self._failover_history: List[dict] = []
# 回调
self._on_failover: Optional[Callable] = None
def set_failover_callback(self, callback: Callable):
"""设置故障切换回调"""
self._on_failover = callback
def initialize(self):
"""初始化,确定各镜像类型的首选源"""
for mirror_type, mirror_config in self.mirrors.items():
if not isinstance(mirror_config, dict):
continue
sources = mirror_config.get('sources', [])
if sources:
# 使用配置的优先列表
self._source_priority[mirror_type] = sources
else:
# 使用内置的默认优先列表
self._source_priority[mirror_type] = self._get_default_priority(mirror_type)
# 选择首选源
if self._source_priority[mirror_type]:
self._active_source[mirror_type] = self._source_priority[mirror_type][0]
def _get_default_priority(self, mirror_type: str) -> List[str]:
"""获取默认的镜像源优先列表"""
priorities = {
'docker': ['docker.io', 'docker.mirrors.aliyun.com', 'dockerhub.azk8s.cn'],
'apt': ['archive.ubuntu.com', 'mirrors.aliyun.com', 'security.ubuntu.com'],
'yum': ['mirror.centos.org', 'mirrors.aliyun.com'],
'pypi': ['pypi.org', 'pypi.mirrors.aliyun.com'],
'npm': ['registry.npmjs.org', 'registry.npmmirror.com'],
'go': ['proxy.golang.org', 'goproxy.cn']
}
return priorities.get(mirror_type, [])
def check_all(self) -> Dict[str, HealthCheckResult]:
"""检查所有镜像源"""
results = {}
for mirror_type, mirror_config in self.mirrors.items():
if not isinstance(mirror_config, dict):
continue
sources = mirror_config.get('sources', [])
for source_name in sources:
if source_name not in results:
result = self.health_checker.check_source(source_name, {'url': self._get_source_url(mirror_type, source_name)})
results[source_name] = result
return results
def _get_source_url(self, mirror_type: str, source_name: str) -> str:
"""获取源 URL"""
# 从配置中获取
sources_config = self.mirrors.get(mirror_type, {}).get('sources_config', {})
if source_name in sources_config:
return sources_config[source_name].get('url', '')
# 从 URL 模板生成
url_template = self.mirrors.get(mirror_type, {}).get('url_template', '')
if url_template and '{mirror}' in url_template:
return url_template.replace('{mirror}', source_name)
return ''
def get_active_source(self, mirror_type: str) -> Optional[str]:
"""获取当前活跃的镜像源"""
return self._active_source.get(mirror_type)
def get_source_for_request(self, mirror_type: str, original_url: str) -> str:
"""
根据故障切换策略获取请求的源 URL
Args:
mirror_type: 镜像类型
original_url: 原始 URL
Returns:
str: 实际请求的 URL
"""
if not self.failover_enabled:
return original_url
active_source = self._active_source.get(mirror_type)
if not active_source:
return original_url
source_url = self._get_source_url(mirror_type, active_source)
if not source_url:
return original_url
# 替换 URL 中的主机部分
try:
from urllib.parse import urlparse
parsed = urlparse(original_url)
# 构建新 URL
new_url = f"{parsed.scheme}://{source_url}{parsed.path}"
if parsed.query:
new_url += f"?{parsed.query}"
return new_url
except Exception:
return original_url
def perform_failover(self, mirror_type: str) -> bool:
"""
对指定镜像类型执行故障切换
Args:
mirror_type: 镜像类型
Returns:
bool: 是否成功切换
"""
priority_list = self._source_priority.get(mirror_type, [])
if not priority_list:
return False
current_index = 0
if mirror_type in self._active_source:
try:
current_index = priority_list.index(self._active_source[mirror_type])
except ValueError:
pass
# 查找下一个健康的源
old_source = self._active_source.get(mirror_type)
for i in range(current_index + 1, len(priority_list)):
source_name = priority_list[i]
result = self.health_checker.get_result(source_name)
if result and result.status == HealthStatus.HEALTHY:
self._active_source[mirror_type] = source_name
# 记录故障切换
failover_record = {
'timestamp': datetime.now().isoformat(),
'mirror_type': mirror_type,
'old_source': old_source,
'new_source': source_name,
'reason': 'Health check failed'
}
self._failover_history.append(failover_record)
# 保持历史记录在合理范围内
if len(self._failover_history) > 100:
self._failover_history = self._failover_history[-50:]
# 触发回调
if self._on_failover:
try:
self._on_failover(mirror_type, old_source, source_name)
except Exception as e:
logger.error(f"故障切换回调执行失败: {e}")
return True
return False
def get_failover_history(self, limit: int = 10) -> List[dict]:
"""获取故障切换历史"""
return self._failover_history[-limit:]
def get_health_summary(self) -> dict:
"""获取健康状态摘要"""
return {
'failover_enabled': self.failover_enabled,
'health': self.health_checker.get_stats(),
'active_sources': self._active_source.copy(),
'failover_history_count': len(self._failover_history)
}
+1258
View File
File diff suppressed because it is too large Load Diff
+376
View File
@@ -0,0 +1,376 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
系统监控模块 - 提供实时系统监控功能
支持CPU、内存、磁盘、网络等指标的实时采集和历史记录
"""
import os
import json
import time
import threading
from datetime import datetime
from typing import Dict, List, Optional, Any
class SystemMonitor:
"""系统监控器 - 采集和提供系统运行指标"""
def __init__(self, config: dict):
self.config = config
self.base_dir = config.get('base_dir', './downloads')
# 历史数据配置
self.history_file = config.get('monitor_history_file', 'monitor_history.json')
self.history_hours = config.get('monitor_history_hours', 168) # 默认7天
self.history = []
self.history_lock = threading.Lock()
# SSE客户端管理
self.sse_clients = {}
self.sse_lock = threading.Lock()
# 监控配置
self.collection_interval = config.get('monitor_interval', 5) # 采集间隔(秒)
self.enabled = True
# 负载历史(用于计算趋势)
self.load_history = []
# 加载历史数据
self._load_history()
def get_realtime_stats(self) -> dict:
"""获取实时系统状态"""
import psutil
import traceback
stats = {
"timestamp": datetime.now().isoformat(),
"errors": []
}
try:
# CPU信息
cpu_percent = psutil.cpu_percent(interval=0.1)
cpu_count = psutil.cpu_count()
cpu_freq = psutil.cpu_freq()
stats["cpu"] = {
"percent": cpu_percent,
"count": cpu_count,
"freq_current": round(cpu_freq.current, 0) if cpu_freq else None,
"freq_max": round(cpu_freq.max, 0) if cpu_freq else None,
"freq_min": round(cpu_freq.min, 0) if cpu_freq else None,
"per_core": psutil.cpu_percent(interval=None, percpu=True)
}
except Exception as e:
stats["cpu"] = {"error": str(e)}
stats["errors"].append(f"CPU: {str(e)}")
try:
# 内存信息
memory = psutil.virtual_memory()
swap = psutil.swap_memory()
stats["memory"] = {
"total": memory.total,
"available": memory.available,
"used": memory.used,
"percent": memory.percent,
"swap_total": swap.total,
"swap_used": swap.used,
"swap_percent": swap.percent
}
except Exception as e:
stats["memory"] = {"error": str(e)}
stats["errors"].append(f"内存: {str(e)}")
try:
# 负载平均值(可能在 Termux 中不可用)
load_avg = os.getloadavg() if hasattr(os, 'getloadavg') else [0, 0, 0]
if "cpu" in stats:
stats["cpu"]["load_avg_1m"] = round(load_avg[0], 2)
stats["cpu"]["load_avg_5m"] = round(load_avg[1], 2)
stats["cpu"]["load_avg_15m"] = round(load_avg[2], 2)
except Exception:
pass
try:
# 磁盘信息
disk_usage = psutil.disk_usage(self.base_dir)
disk_io = psutil.disk_io_counters()
stats["disk"] = {
"total": disk_usage.total,
"used": disk_usage.used,
"free": disk_usage.free,
"percent": disk_usage.percent,
"read_bytes": disk_io.read_bytes if disk_io else 0,
"write_bytes": disk_io.write_bytes if disk_io else 0,
"read_count": disk_io.read_count if disk_io else 0,
"write_count": disk_io.write_count if disk_io else 0
}
except Exception as e:
stats["disk"] = {"error": str(e)}
stats["errors"].append(f"磁盘: {str(e)}")
try:
# 网络信息(可能在受限环境中失败)
net_io = psutil.net_io_counters()
connections = psutil.net_connections()
stats["network"] = {
"bytes_sent": net_io.bytes_sent,
"bytes_recv": net_io.bytes_recv,
"packets_sent": net_io.packets_sent,
"packets_recv": net_io.packets_recv,
"connections_count": len(connections),
"connections_established": len([c for c in connections if c.status == 'ESTABLISHED'])
}
except PermissionError as e:
stats["network"] = {
"note": "权限不足,无法访问网络信息",
"error": str(e)
}
except Exception as e:
stats["network"] = {"error": str(e)}
stats["errors"].append(f"网络: {str(e)}")
try:
# 进程信息
process = psutil.Process()
proc_mem = process.memory_info()
proc_cpu = process.cpu_percent(interval=0.1)
stats["process"] = {
"memory_rss": proc_mem.rss,
"memory_vms": proc_mem.vms,
"cpu_percent": proc_cpu,
"thread_count": process.num_threads(),
"open_files": process.num_fds() if hasattr(process, 'num_fds') else 0
}
except Exception as e:
stats["process"] = {"error": str(e)}
stats["errors"].append(f"进程: {str(e)}")
# 计算运行时间
try:
uptime = time.time() - self.config.get('start_time', time.time())
stats["uptime"] = round(uptime, 2)
except Exception:
stats["uptime"] = None
return stats
def get_monitor_history(self, hours: int = 24) -> dict:
"""获取历史监控数据"""
cutoff_time = time.time() - (hours * 3600)
with self.history_lock:
filtered_history = [
point for point in self.history
if point.get('timestamp_unix', 0) >= cutoff_time
]
return {
"hours": hours,
"total_points": len(filtered_history),
"data": filtered_history
}
def get_stats_summary(self) -> dict:
"""获取统计摘要"""
history = self.get_monitor_history(24).get('data', [])
if not history:
return {
"status": "no_data",
"message": "暂无监控数据"
}
# 计算各项指标的平均值和最大值
cpu_values = [p.get('cpu', {}).get('percent', 0) for p in history]
memory_values = [p.get('memory', {}).get('percent', 0) for p in history]
disk_values = [p.get('disk', {}).get('percent', 0) for p in history]
return {
"status": "ok",
"period_hours": 24,
"cpu": {
"avg": round(sum(cpu_values) / len(cpu_values), 1) if cpu_values else 0,
"max": max(cpu_values) if cpu_values else 0,
"min": min(cpu_values) if cpu_values else 0
},
"memory": {
"avg": round(sum(memory_values) / len(memory_values), 1) if memory_values else 0,
"max": max(memory_values) if memory_values else 0,
"min": min(memory_values) if memory_values else 0
},
"disk": {
"avg": round(sum(disk_values) / len(disk_values), 1) if disk_values else 0,
"max": max(disk_values) if disk_values else 0,
"min": min(disk_values) if disk_values else 0
},
"total_downloads": history[-1].get('downloads', {}).get('total', 0) if history else 0,
"total_connections": sum(p.get('network', {}).get('connections_count', 0) for p in history)
}
def start_monitoring(self, callback=None):
"""启动监控循环(后台线程)"""
def monitor_loop():
while self.enabled:
try:
stats = self.get_realtime_stats()
# 添加时间戳
stats['timestamp_unix'] = time.time()
# 保存历史
self._add_history_point(stats)
# SSE广播
if callback:
callback(stats)
self._broadcast_sse('stats', stats)
except Exception as e:
print(f"监控采集错误: {e}")
time.sleep(self.collection_interval)
thread = threading.Thread(target=monitor_loop, daemon=True)
thread.start()
return thread
def stop_monitoring(self):
"""停止监控"""
self.enabled = False
def register_sse_client(self, client_id: str, topics: List[str] = None) -> None:
"""注册SSE客户端"""
with self.sse_lock:
self.sse_clients[client_id] = {
'topics': set(topics) if topics else {'*'},
'last_ping': time.time()
}
def unregister_sse_client(self, client_id: str) -> None:
"""注销SSE客户端"""
with self.sse_lock:
self.sse_clients.pop(client_id, None)
def get_sse_clients_count(self) -> int:
"""获取SSE客户端数量"""
with self.sse_lock:
return len(self.sse_clients)
def broadcast_event(self, event_type: str, data: dict) -> None:
"""广播事件到所有SSE客户端"""
self._broadcast_sse(event_type, data)
def _broadcast_sse(self, event_type: str, data: dict) -> None:
"""SSE广播(内部方法)"""
message = f"event: {event_type}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n"
with self.sse_lock:
disconnected = []
for client_id, client in self.sse_clients.items():
try:
if client['topics'] == {'*'} or event_type in client['topics']:
# 实际发送需要在request handler中处理
# 这里只记录消息
pass
except Exception:
disconnected.append(client_id)
# 清理断开的客户端
for client_id in disconnected:
self.sse_clients.pop(client_id, None)
def _add_history_point(self, stats: dict) -> None:
"""添加历史数据点"""
# 精简数据以减少存储
point = {
"timestamp": stats.get('timestamp'),
"timestamp_unix": stats.get('timestamp_unix'),
"cpu_percent": stats.get('cpu', {}).get('percent', 0),
"memory_percent": stats.get('memory', {}).get('percent', 0),
"disk_percent": stats.get('disk', {}).get('percent', 0),
"network_bytes_sent": stats.get('network', {}).get('bytes_sent', 0),
"network_bytes_recv": stats.get('network', {}).get('bytes_recv', 0),
"connections_count": stats.get('network', {}).get('connections_count', 0),
"load_avg_1m": stats.get('cpu', {}).get('load_avg_1m', 0)
}
with self.history_lock:
self.history.append(point)
# 清理过期数据
cutoff_time = time.time() - (self.history_hours * 3600)
self.history = [
p for p in self.history
if p.get('timestamp_unix', 0) >= cutoff_time
]
# 保存到文件
self._save_history()
def _save_history(self) -> None:
"""保存历史数据到文件"""
try:
with open(self.history_file, 'w', encoding='utf-8') as f:
json.dump(self.history, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"保存监控历史失败: {e}")
def _load_history(self) -> None:
"""从文件加载历史数据"""
try:
if os.path.exists(self.history_file):
with open(self.history_file, 'r', encoding='utf-8') as f:
data = json.load(f)
if isinstance(data, list):
self.history = data[-10000:] # 限制历史数量
except Exception as e:
print(f"加载监控历史失败: {e}")
self.history = []
def get_health_status(self) -> dict:
"""获取健康状态"""
stats = self.get_realtime_stats()
if 'error' in stats:
return {
"status": "unhealthy",
"error": stats['error']
}
# 检查各项指标
warnings = []
cpu_percent = stats.get('cpu', {}).get('percent', 0)
if cpu_percent > 90:
warnings.append(f"CPU使用率过高: {cpu_percent}%")
elif cpu_percent > 70:
warnings.append(f"CPU使用率较高: {cpu_percent}%")
memory_percent = stats.get('memory', {}).get('percent', 0)
if memory_percent > 90:
warnings.append(f"内存使用率过高: {memory_percent}%")
elif memory_percent > 80:
warnings.append(f"内存使用率较高: {memory_percent}%")
disk_percent = stats.get('disk', {}).get('percent', 0)
if disk_percent > 90:
warnings.append(f"磁盘使用率过高: {disk_percent}%")
elif disk_percent > 80:
warnings.append(f"磁盘使用率较高: {disk_percent}%")
if warnings:
return {
"status": "degraded",
"warnings": warnings,
"timestamp": stats.get('timestamp')
}
return {
"status": "healthy",
"timestamp": stats.get('timestamp')
}
+722
View File
@@ -0,0 +1,722 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
内存优化模块
专为低端设备优化 (2CPU/1G 内存等)
"""
import os
import sys
import gc
import time
import threading
from typing import Dict, List, Optional, Callable
from dataclasses import dataclass
from contextlib import contextmanager
from functools import wraps
# 跨平台兼容处理:resource 模块仅在 Unix/Linux 上可用
try:
import resource
_HAS_RESOURCE = True
except ImportError:
_HAS_RESOURCE = False
resource = None
# 内存限制配置
DEFAULT_MEMORY_LIMIT = 512 * 1024 * 1024 # 512MB
LOW_MEMORY_LIMIT = 256 * 1024 * 1024 # 256MB
VERY_LOW_MEMORY_LIMIT = 128 * 1024 * 1024 # 128MB
class MemoryManager:
"""内存管理器 - 支持定时垃圾回收"""
def __init__(self, config: dict = None):
self.config = config or {}
self.enabled = self.config.get('enabled', True)
self.memory_limit = self.config.get('memory_limit', DEFAULT_MEMORY_LIMIT)
self.soft_limit = self.memory_limit * 0.8 # 80% 时触发警告
self.check_interval = self.config.get('check_interval', 10) # 秒
# 定时垃圾回收配置
self.gc_interval = self.config.get('gc_interval', 300) # 默认 5 分钟
self.enable_scheduled_gc = self.config.get('enable_scheduled_gc', True)
# 缓存清理回调
self.cache_cleaners: List[Callable] = []
# 回调函数
self.on_memory_warning: Optional[Callable] = None
self.on_memory_critical: Optional[Callable] = None
self._running = False
self._monitor_thread = None
self._gc_thread = None
def start(self):
"""启动内存监控"""
if not self.enabled:
return
self._running = True
# 启动内存监控线程
self._monitor_thread = threading.Thread(target=self._monitor_loop, daemon=True)
self._monitor_thread.start()
# 启动定时垃圾回收线程
if self.enable_scheduled_gc:
self._gc_thread = threading.Thread(target=self._gc_loop, daemon=True)
self._gc_thread.start()
print(f"[内存管理] 定时GC: {self.gc_interval}秒")
# 设置内存限制
self.set_memory_limit(self.memory_limit)
print(f"[内存管理] 已启动, 限制: {self.memory_limit // 1024 // 1024}MB")
def stop(self):
"""停止内存监控"""
self._running = False
if self._monitor_thread:
self._monitor_thread.join(timeout=2)
if self._gc_thread:
self._gc_thread.join(timeout=2)
def register_cache_cleaner(self, cleaner: Callable):
"""注册缓存清理回调函数"""
self.cache_cleaners.append(cleaner)
def set_memory_limit(self, limit: int):
"""设置内存限制 (Linux/Unix)"""
if not _HAS_RESOURCE or resource is None:
# Windows 平台不支持内存限制,跳过
print(f"[内存管理] 跳过内存限制设置 (Windows平台不支持)")
return
try:
# 软限制
resource.setrlimit(resource.RLIMIT_AS, (limit, limit))
print(f"[内存管理] 已设置内存限制: {limit // 1024 // 1024}MB")
except Exception as e:
print(f"[内存管理] 设置内存限制失败: {e}")
def get_memory_usage(self) -> dict:
"""获取内存使用情况"""
try:
# 进程内存
import psutil
process = psutil.Process(os.getpid())
mem_info = process.memory_info()
# 系统内存
sys_mem = psutil.virtual_memory()
return {
'process_rss': mem_info.rss,
'process_vms': mem_info.vms,
'process_percent': process.memory_percent(),
'system_total': sys_mem.total,
'system_available': sys_mem.available,
'system_percent': sys_mem.percent,
'process_rss_mb': mem_info.rss / 1024 / 1024,
'system_available_mb': sys_mem.available / 1024 / 1024
}
except Exception as e:
# 备用方法:使用 resource (Unix) 或返回估计值 (Windows)
if _HAS_RESOURCE and resource is not None:
try:
return {
'process_rss': resource.getrusage(resource.RUSAGE_SELF).ru_maxrss * 1024,
'process_rss_mb': resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
}
except Exception:
pass
# Windows 无 psutil 的极端情况
return {
'process_rss': 0,
'process_rss_mb': 0,
'error': str(e)
}
def _monitor_loop(self):
"""监控循环"""
while self._running:
try:
usage = self.get_memory_usage()
# 检查是否达到软限制
if usage['process_rss'] >= self.soft_limit:
if self.on_memory_warning:
self.on_memory_warning(usage)
self._aggressive_cleanup()
# 检查是否达到硬限制
if usage['process_rss'] >= self.memory_limit:
if self.on_memory_critical:
self.on_memory_critical(usage)
self._emergency_cleanup()
time.sleep(self.check_interval)
except Exception:
pass
def _gc_loop(self):
"""定时垃圾回收循环"""
while self._running:
try:
# 执行垃圾回收
self._scheduled_gc()
# 清理注册过的缓存
for cleaner in self.cache_cleaners:
try:
cleaner()
except Exception:
pass
except Exception:
pass
time.sleep(self.gc_interval)
def _scheduled_gc(self):
"""定时 GC 执行"""
# 标准 GC
collected = gc.collect()
# 清理 Python 内部缓存
if hasattr(sys, 'exc_clear'):
sys.exc_clear()
def _aggressive_cleanup(self):
"""激进清理"""
# 强制垃圾回收
gc.collect()
# 清理 Python 缓存
if hasattr(gc, 'set_threshold'):
gc.set_threshold(500, 10, 5)
# 尝试释放内存
try:
import psutil
process = psutil.Process(os.getpid())
process.memory_info().rss # 刷新
except Exception:
pass
def _emergency_cleanup(self):
"""紧急清理"""
print("[内存管理] ⚠️ 达到内存限制,尝试紧急清理...")
# 完全垃圾回收
gc.collect()
gc.collect()
gc.collect()
# 清理所有缓存
if hasattr(gc, 'garbage'):
del gc.garbage[:]
# 触发警告
print("[内存管理] ⚠️ 内存仍过高,考虑重启服务")
def get_status(self) -> dict:
"""获取状态"""
usage = self.get_memory_usage()
return {
'enabled': self.enabled,
'memory_limit_mb': self.memory_limit // 1024 // 1024,
'current_mb': usage['process_rss_mb'],
'available_mb': usage.get('system_available_mb', 0),
'percent': (usage['process_rss'] / self.memory_limit * 100) if self.memory_limit else 0
}
# ==================== 低内存配置 ====================
class LowMemoryConfig:
"""低端设备配置"""
# 可禁用的功能列表
FEATURES = {
'ws': 'enable_ws', # WebSocket
'sse': 'enable_sse', # Server-Sent Events
'hash_calc': 'calculate_hash', # 文件哈希计算
'stats': 'enable_stats', # 统计功能
'monitor': 'enable_monitor', # 系统监控
'sync': 'enable_sync', # 同步功能
'mirrors': 'enable_mirrors', # 加速源
}
# 预设配置
PRESETS = {
'ultra_low': {
'description': '极低端设备 (<256MB RAM)',
'workers': 1,
'max_cache_size': 32 * 1024 * 1024, # 32MB
'chunk_size': 16 * 1024, # 16KB
'buffer_size': 32 * 1024, # 32KB
'db_pool_size': 1,
'max_connections': 3,
'monitor_interval': 60,
'gc_interval': 180, # 3分钟
'timeout': 15,
'disable_optional_features': ['ws', 'sse', 'hash_calc', 'stats', 'monitor']
},
'low': {
'description': '低端设备 (256-512MB RAM)',
'workers': 1,
'max_cache_size': 64 * 1024 * 1024, # 64MB
'chunk_size': 32 * 1024, # 32KB
'buffer_size': 64 * 1024, # 64KB
'db_pool_size': 1,
'max_connections': 5,
'monitor_interval': 30,
'gc_interval': 300, # 5分钟
'timeout': 20,
'disable_optional_features': ['ws', 'sse', 'hash_calc', 'stats']
},
'medium': {
'description': '中等设备 (512MB-1GB RAM)',
'workers': 2,
'max_cache_size': 128 * 1024 * 1024, # 128MB
'chunk_size': 64 * 1024, # 64KB
'buffer_size': 128 * 1024, # 128KB
'db_pool_size': 2,
'max_connections': 15,
'monitor_interval': 15,
'gc_interval': 600, # 10分钟
'timeout': 30,
'disable_optional_features': []
},
'high': {
'description': '高端设备 (1GB+ RAM)',
'workers': 4,
'max_cache_size': 256 * 1024 * 1024, # 256MB
'chunk_size': 128 * 1024, # 128KB
'buffer_size': 256 * 1024, # 256KB
'db_pool_size': 4,
'max_connections': 50,
'monitor_interval': 5,
'gc_interval': 900, # 15分钟
'timeout': 30,
'disable_optional_features': []
},
'performance': {
'description': '高性能设备 (4GB+ RAM)',
'workers': 8,
'max_cache_size': 1024 * 1024 * 1024, # 1GB
'chunk_size': 256 * 1024, # 256KB
'buffer_size': 512 * 1024, # 512KB
'db_pool_size': 8,
'max_connections': 200,
'monitor_interval': 3,
'gc_interval': 1800, # 30分钟
'timeout': 60,
'disable_optional_features': []
}
}
def __init__(self, preset: str = 'auto', custom_config: dict = None):
"""
初始化配置
Args:
preset: 预设 ('ultra_low', 'low', 'medium', 'high', 'auto')
custom_config: 自定义配置
"""
if preset == 'auto':
preset = self._detect_preset()
self.preset = preset
self.config = self.PRESETS.get(preset, self.PRESETS['low']).copy()
if custom_config:
self.config.update(custom_config)
def _detect_preset(self) -> str:
"""自动检测设备配置
检测逻辑:
1. 获取总内存和可用内存
2. 计算可用内存占比
3. 结合总内存和可用内存占比综合判断
"""
try:
import psutil
mem = psutil.virtual_memory()
total_ram = mem.total
available_ram = mem.available
percent_used = mem.percent # 已使用百分比
# 转换为MB
total_mb = total_ram / (1024 * 1024)
# 根据总内存和可用内存占比综合判断
if total_mb < 200:
# 低于 200MB 总内存
return 'ultra_low'
elif total_mb < 400:
# 200MB - 400MB
return 'ultra_low'
elif total_mb < 700:
# 400MB - 700MB
if percent_used > 80:
return 'ultra_low'
return 'low'
elif total_mb < 1200:
# 700MB - 1.2GB
if percent_used > 70:
return 'ultra_low'
elif percent_used > 50:
return 'low'
return 'medium'
elif total_mb < 2500:
# 1.2GB - 2.5GB
if percent_used > 70:
return 'low'
elif percent_used > 40:
return 'medium'
return 'high'
elif total_mb < 5000:
# 2.5GB - 5GB
if percent_used > 60:
return 'medium'
return 'high'
else:
# 5GB+
return 'performance'
except ImportError:
# 如果没有 psutil,使用保守的 low 配置
return 'low'
except Exception:
return 'low'
def get_device_info(self) -> dict:
"""获取设备详细信息用于显示"""
try:
import psutil
mem = psutil.virtual_memory()
cpu_count = psutil.cpu_count(logical=True) or 1
return {
'total_ram_mb': mem.total / (1024 * 1024),
'available_ram_mb': mem.available / (1024 * 1024),
'percent_used': mem.percent,
'cpu_count': cpu_count,
'preset': self.preset
}
except Exception:
return {
'total_ram_mb': 0,
'available_ram_mb': 0,
'percent_used': 0,
'cpu_count': 1,
'preset': self.preset
}
def apply_to_config(self, base_config: dict) -> dict:
"""应用配置到基础配置"""
config = base_config.copy()
# 应用通用设置
config['workers'] = self.config.get('workers', 1)
config['max_cache_size'] = self.config.get('max_cache_size', 64 * 1024 * 1024)
config['chunk_size'] = self.config.get('chunk_size', 64 * 1024)
config['buffer_size'] = self.config.get('buffer_size', 128 * 1024)
config['timeout'] = self.config.get('timeout', 30)
# 数据库池
if 'database' not in config:
config['database'] = {}
config['database']['db_pool_size'] = self.config.get('db_pool_size', 2)
# 定时 GC 配置
config['gc_interval'] = self.config.get('gc_interval', 300)
# 禁用可选功能
for feature in self.config.get('disable_optional_features', []):
feature_key = self.FEATURES.get(feature, feature)
if feature_key.startswith('enable_') or feature_key == 'calculate_hash':
config[feature_key] = False
return config
def get_config(self) -> dict:
"""获取配置"""
return self.config.copy()
def get_status(self) -> dict:
"""获取状态"""
return {
'preset': self.preset,
'description': self.config['description'],
'settings': self.config
}
# ==================== 流式处理优化 ====================
class StreamingOptimizer:
"""流式处理优化器"""
def __init__(self, config: dict = None):
self.config = config or {}
self.chunk_size = self.config.get('chunk_size', 128 * 1024)
self.buffer_size = self.config.get('buffer_size', 256 * 1024)
# 内存池
self._chunk_pool = None
self._use_memory_pool = self.config.get('use_memory_pool', True)
def get_optimized_chunk_size(self, file_size: int) -> int:
"""根据文件大小获取优化的块大小"""
if file_size < 1024 * 1024: # < 1MB
return 16 * 1024 # 16KB
elif file_size < 10 * 1024 * 1024: # < 10MB
return 32 * 1024 # 32KB
elif file_size < 100 * 1024 * 1024: # < 100MB
return 64 * 1024 # 64KB
else:
return self.chunk_size
@contextmanager
def memory_efficient_file_read(self, file_path: str, chunk_size: int = None):
"""
内存高效的文件读取
Usage:
with optimizer.memory_efficient_file_read('/path/to/file') as f:
for chunk in f:
process(chunk)
"""
chunk_size = chunk_size or self.chunk_size
file_size = os.path.getsize(file_path)
chunk_size = self.get_optimized_chunk_size(file_size)
file = open(file_path, 'rb')
try:
yield file
finally:
file.close()
@contextmanager
def memory_efficient_file_write(self, file_path: str, chunk_size: int = None):
"""内存高效的文件写入"""
chunk_size = chunk_size or self.chunk_size
file = open(file_path, 'wb')
try:
yield file
finally:
file.close()
# ==================== 架构检测 ====================
class ArchitectureDetector:
"""架构检测器"""
@staticmethod
def get_architecture() -> dict:
"""
获取架构信息
Returns:
dict: 包含架构信息的字典
"""
info = {
'platform': sys.platform,
'architecture': 'unknown',
'machine': 'unknown',
'processor': 'unknown',
'python_version': sys.version,
'byte_order': sys.byteorder
}
# 机器类型
info['machine'] = os.uname().machine if hasattr(os, 'uname') else 'unknown'
# 检测 32位/64位
if info['machine'] in ['x86_64', 'amd64', 'aarch64', 'arm64']:
info['architecture'] = '64bit'
elif info['machine'] in ['i386', 'i686', 'armv7l', 'armv6l']:
info['architecture'] = '32bit'
elif info['machine'] in ['armv8l', 'aarch32']:
info['architecture'] = '32bit' # 32位ARM
# ARM 变体
if info['machine'].startswith('arm'):
if info['machine'] in ['armv7l', 'armv7hl']:
info['arm_variant'] = 'armv7'
elif info['machine'].startswith('armv8'):
info['arm_variant'] = 'armv8'
elif info['machine'].startswith('armv6'):
info['arm_variant'] = 'armv6'
else:
info['arm_variant'] = 'unknown'
# x86 变体
if info['machine'] in ['i386', 'i686']:
info['x86_variant'] = 'i386'
elif info['machine'] == 'x86_64':
info['x86_variant'] = 'x86_64'
return info
@staticmethod
def is_low_end_device() -> bool:
"""检测是否为低端设备"""
try:
import psutil
mem = psutil.virtual_memory()
return mem.total < 1024 * 1024 * 1024 # < 1GB
except Exception:
return False
@staticmethod
def get_recommended_config() -> dict:
"""获取推荐的配置"""
arch = ArchitectureDetector.get_architecture()
if arch['architecture'] == '32bit':
return {
'max_workers': 2,
'max_cache_size': 100 * 1024 * 1024, # 100MB
'enable_threading': True,
'use_processes': False, # 32位进程数有限制
'max_file_handles': 256
}
elif ArchitectureDetector.is_low_end_device():
return {
'max_workers': 1,
'max_cache_size': 50 * 1024 * 1024, # 50MB
'enable_threading': True,
'use_processes': False,
'max_file_handles': 128
}
else:
return {
'max_workers': 4,
'max_cache_size': 500 * 1024 * 1024, # 500MB
'enable_threading': True,
'use_processes': True,
'max_file_handles': 1024
}
# ==================== 兼容性检查 ====================
def check_compatibility() -> dict:
"""
检查系统兼容性
Returns:
dict: 兼容性检查结果
"""
results = {
'compatible': True,
'warnings': [],
'errors': [],
'info': {}
}
# Python 版本检查
if sys.version_info < (3, 8):
results['compatible'] = False
results['errors'].append(f"Python 3.8+ 所需, 当前版本: {sys.version}")
# 架构信息
arch_info = ArchitectureDetector.get_architecture()
results['info']['architecture'] = arch_info
# 检查必需模块
required_modules = [
('os', '标准库'),
('json', '标准库'),
('http', '标准库'),
('sqlite3', '标准库')
]
optional_modules = [
('psutil', '系统监控 (推荐)'),
('sqlalchemy', '数据库 (推荐)'),
(' cryptography', '加密 (推荐)'),
('aiohttp', '异步HTTP (可选)'),
('paramiko', 'SSH/SFTP (可选)')
]
for module, desc in required_modules:
try:
__import__(module)
except ImportError:
results['compatible'] = False
results['errors'].append(f"必需模块缺失: {module} ({desc})")
for module, desc in optional_modules:
try:
__import__(module)
except ImportError:
results['warnings'].append(f"可选模块缺失: {module} ({desc})")
# 内存检查
try:
import psutil
mem = psutil.virtual_memory()
if mem.total < 256 * 1024 * 1024:
results['warnings'].append("内存低于 256MB,可能无法正常运行")
except Exception:
results['warnings'].append("无法检测内存,可能内存不足")
# 磁盘空间检查
try:
disk = psutil.disk_usage('.')
if disk.free < 100 * 1024 * 1024: # 100MB
results['warnings'].append("可用磁盘空间不足 100MB")
except Exception:
pass
return results
# ==================== 便捷函数 ====================
def get_memory_manager(config: dict = None) -> MemoryManager:
"""获取内存管理器"""
return MemoryManager(config)
def get_low_memory_config(preset: str = 'auto') -> LowMemoryConfig:
"""获取低端设备配置"""
return LowMemoryConfig(preset)
def detect_and_configure() -> dict:
"""
自动检测并配置
Returns:
dict: 配置信息
"""
# 检查兼容性
compat = check_compatibility()
if not compat['compatible']:
print("⚠️ 系统兼容性警告:")
for error in compat['errors']:
print(f" - {error}")
# 获取推荐配置
arch_config = ArchitectureDetector.get_recommended_config()
# 获取低端设备配置
low_mem_config = get_low_memory_config('auto')
arch_info = ArchitectureDetector.get_architecture()
return {
'compatible': compat['compatible'],
'architecture': arch_info,
'recommended': arch_config,
'low_memory': low_mem_config.get_status(),
'warnings': compat['warnings']
}
+386
View File
@@ -0,0 +1,386 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Prometheus 指标导出模块
提供 /metrics 端点暴露监控数据
"""
import os
import sys
import time
from datetime import datetime
from typing import Dict, Optional
# 添加项目根目录到路径
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
class PrometheusMetrics:
"""Prometheus 指标收集器"""
def __init__(self, config: dict = None):
self.config = config or {}
self._metrics: Dict[str, dict] = {}
# 初始化指标
self._init_metrics()
def _init_metrics(self):
"""初始化指标定义"""
self._metrics = {
# 服务器指标
'hyc_server_uptime_seconds': {
'type': 'gauge',
'description': 'Server uptime in seconds',
'value': 0
},
'hyc_server_start_time': {
'type': 'gauge',
'description': 'Server start timestamp',
'value': time.time()
},
# 文件指标
'hyc_files_total': {
'type': 'gauge',
'description': 'Total number of files in the system',
'value': 0
},
'hyc_files_size_bytes': {
'type': 'gauge',
'description': 'Total size of all files in bytes',
'value': 0
},
'hyc_downloads_total': {
'type': 'counter',
'description': 'Total number of downloads',
'value': 0
},
'hyc_downloads_today': {
'type': 'counter',
'description': 'Number of downloads today',
'value': 0
},
# 缓存指标
'hyc_cache_size_bytes': {
'type': 'gauge',
'description': 'Current cache size in bytes',
'value': 0
},
'hyc_cache_entries': {
'type': 'gauge',
'description': 'Number of cache entries',
'value': 0
},
'hyc_cache_hits_total': {
'type': 'counter',
'description': 'Total number of cache hits',
'value': 0
},
'hyc_cache_misses_total': {
'type': 'counter',
'description': 'Total number of cache misses',
'value': 0
},
'hyc_cache_hit_ratio': {
'type': 'gauge',
'description': 'Cache hit ratio (0-1)',
'value': 0
},
# 同步指标
'hyc_sync_sources_total': {
'type': 'gauge',
'description': 'Total number of sync sources',
'value': 0
},
'hyc_sync_running': {
'type': 'gauge',
'description': 'Number of currently running sync operations',
'value': 0
},
'hyc_sync_files_total': {
'type': 'counter',
'description': 'Total number of synced files',
'value': 0
},
'hyc_sync_last_timestamp': {
'type': 'gauge',
'description': 'Timestamp of last successful sync',
'value': 0
},
# 数据库指标
'hyc_db_files_total': {
'type': 'gauge',
'description': 'Number of files in database',
'value': 0
},
'hyc_db_sync_records': {
'type': 'gauge',
'description': 'Number of sync records in database',
'value': 0
},
'hyc_db_cache_records': {
'type': 'gauge',
'description': 'Number of cache records in database',
'value': 0
},
# 系统资源指标 (从监控模块获取)
'hyc_cpu_percent': {
'type': 'gauge',
'description': 'CPU usage percentage',
'value': 0
},
'hyc_memory_percent': {
'type': 'gauge',
'description': 'Memory usage percentage',
'value': 0
},
'hyc_disk_percent': {
'type': 'gauge',
'description': 'Disk usage percentage',
'value': 0
},
'hyc_disk_free_bytes': {
'type': 'gauge',
'description': 'Free disk space in bytes',
'value': 0
},
'hyc_disk_total_bytes': {
'type': 'gauge',
'description': 'Total disk space in bytes',
'value': 0
},
'hyc_network_rx_bytes': {
'type': 'counter',
'description': 'Network receive bytes',
'value': 0
},
'hyc_network_tx_bytes': {
'type': 'counter',
'description': 'Network transmit bytes',
'value': 0
},
'hyc_active_connections': {
'type': 'gauge',
'description': 'Number of active connections',
'value': 0
},
# 镜像源指标
'hyc_mirror_enabled': {
'type': 'gauge',
'description': 'Whether a mirror is enabled (1=enabled, 0=disabled)',
'labels': ['mirror_type'],
'value': {}
},
'hyc_mirror_last_sync': {
'type': 'gauge',
'description': 'Timestamp of last mirror sync',
'labels': ['mirror_type'],
'value': {}
},
}
def set_uptime(self, seconds: float):
"""设置运行时间"""
self._metrics['hyc_server_uptime_seconds']['value'] = seconds
def set_files(self, count: int, size_bytes: int):
"""设置文件统计"""
self._metrics['hyc_files_total']['value'] = count
self._metrics['hyc_files_size_bytes']['value'] = size_bytes
def set_downloads(self, total: int, today: int):
"""设置下载统计"""
self._metrics['hyc_downloads_total']['value'] = total
self._metrics['hyc_downloads_today']['value'] = today
def set_cache(self, size_bytes: int, entries: int, hits: int, misses: int):
"""设置缓存统计"""
self._metrics['hyc_cache_size_bytes']['value'] = size_bytes
self._metrics['hyc_cache_entries']['value'] = entries
self._metrics['hyc_cache_hits_total']['value'] = hits
self._metrics['hyc_cache_misses_total']['value'] = misses
# 计算命中率
total = hits + misses
if total > 0:
self._metrics['hyc_cache_hit_ratio']['value'] = hits / total
else:
self._metrics['hyc_cache_hit_ratio']['value'] = 0
def set_sync(self, running: int, files_total: int, last_timestamp: float):
"""设置同步统计"""
self._metrics['hyc_sync_running']['value'] = running
self._metrics['hyc_sync_files_total']['value'] = files_total
self._metrics['hyc_sync_last_timestamp']['value'] = last_timestamp
def set_db_stats(self, files: int, sync_records: int, cache_records: int):
"""设置数据库统计"""
self._metrics['hyc_db_files_total']['value'] = files
self._metrics['hyc_db_sync_records']['value'] = sync_records
self._metrics['hyc_db_cache_records']['value'] = cache_records
def set_system(self, cpu: float, memory: float, disk: float,
disk_free: int, disk_total: int,
network_rx: int, network_tx: int):
"""设置系统资源统计"""
self._metrics['hyc_cpu_percent']['value'] = cpu
self._metrics['hyc_memory_percent']['value'] = memory
self._metrics['hyc_disk_percent']['value'] = disk
self._metrics['hyc_disk_free_bytes']['value'] = disk_free
self._metrics['hyc_disk_total_bytes']['value'] = disk_total
self._metrics['hyc_network_rx_bytes']['value'] = network_rx
self._metrics['hyc_network_tx_bytes']['value'] = network_tx
def set_connections(self, count: int):
"""设置连接数"""
self._metrics['hyc_active_connections']['value'] = count
def set_mirror_status(self, mirror_type: str, enabled: bool, last_sync: float):
"""设置镜像源状态"""
key = 'hyc_mirror_enabled'
if 'labels' not in self._metrics[key]:
self._metrics[key]['labels'] = ['mirror_type']
if 'value' not in self._metrics[key]:
self._metrics[key]['value'] = {}
self._metrics[key]['value'][mirror_type] = 1 if enabled else 0
key = 'hyc_mirror_last_sync'
if 'labels' not in self._metrics[key]:
self._metrics[key]['labels'] = ['mirror_type']
if 'value' not in self._metrics[key]:
self._metrics[key]['value'] = {}
self._metrics[key]['value'][mirror_type] = last_sync
def increment_downloads(self, count: int = 1):
"""增加下载计数"""
self._metrics['hyc_downloads_total']['value'] += count
def increment_cache_hits(self, count: int = 1):
"""增加缓存命中计数"""
self._metrics['hyc_cache_hits_total']['value'] += count
def increment_cache_misses(self, count: int = 1):
"""增加缓存未命中计数"""
self._metrics['hyc_cache_misses_total']['value'] += count
def increment_sync_files(self, count: int = 1):
"""增加同步文件计数"""
self._metrics['hyc_sync_files_total']['value'] += count
def generate_metrics(self) -> str:
"""生成 Prometheus 格式的指标输出"""
output = []
output.append("# Prometheus metrics for HYC Mirror Server")
output.append(f"# Generated at: {datetime.now().isoformat()}")
output.append("")
for name, metric in self._metrics.items():
desc = metric.get('description', '')
mtype = metric.get('type', 'gauge')
output.append(f"# TYPE {name} {mtype}")
output.append(f"# HELP {name} {desc}")
value = metric.get('value')
# 处理带标签的指标
if 'labels' in metric and isinstance(value, dict):
labels = metric['labels']
for label_values, v in value.items():
if isinstance(label_values, str):
label_str = f'{",".join([f"{labels[0]}={label_values}"])}'
else:
label_str = ','.join([f"{l}={v}" for l, v in zip(labels, label_values)])
output.append(f"{name}{{{label_str}}} {v}")
# 处理普通指标
elif isinstance(value, dict):
# 旧格式,可能是直接存储
for k, v in value.items():
output.append(f"{name}{{type=\"{k}\"}} {v}")
else:
output.append(f"{name} {value}")
output.append("")
return '\n'.join(output)
# ==================== 指标中间件 ====================
class MetricsMiddleware:
"""HTTP 请求指标中间件"""
def __init__(self, metrics: PrometheusMetrics = None):
self.metrics = metrics or PrometheusMetrics()
self._request_count = 0
self._request_duration_total = 0
self._errors_4xx = 0
self._errors_5xx = 0
def record_request(self, duration: float, status_code: int):
"""记录请求"""
self._request_count += 1
self._request_duration_total += duration
if 400 <= status_code < 500:
self._errors_4xx += 1
elif status_code >= 500:
self._errors_5xx += 1
def get_request_count(self) -> int:
return self._request_count
def get_request_duration_total(self) -> float:
return self._request_duration_total
def get_errors_4xx(self) -> int:
return self._errors_4xx
def get_errors_5xx(self) -> int:
return self._errors_5xx
def update_metrics(self):
"""更新 Prometheus 指标"""
# 添加请求相关指标
if 'hyc_http_requests_total' not in self.metrics._metrics:
self.metrics._metrics['hyc_http_requests_total'] = {
'type': 'counter',
'description': 'Total HTTP requests',
'value': self._request_count
}
else:
self.metrics._metrics['hyc_http_requests_total']['value'] = self._request_count
if 'hyc_http_request_duration_seconds_total' not in self.metrics._metrics:
self.metrics._metrics['hyc_http_request_duration_seconds_total'] = {
'type': 'counter',
'description': 'Total HTTP request duration in seconds',
'value': self._request_duration_total
}
else:
self.metrics._metrics['hyc_http_request_duration_seconds_total']['value'] = self._request_duration_total
if 'hyc_http_requests_4xx_total' not in self.metrics._metrics:
self.metrics._metrics['hyc_http_requests_4xx_total'] = {
'type': 'counter',
'description': 'Total HTTP 4xx errors',
'value': self._errors_4xx
}
else:
self.metrics._metrics['hyc_http_requests_4xx_total']['value'] = self._errors_4xx
if 'hyc_http_requests_5xx_total' not in self.metrics._metrics:
self.metrics._metrics['hyc_http_requests_5xx_total'] = {
'type': 'counter',
'description': 'Total HTTP 5xx errors',
'value': self._errors_5xx
}
else:
self.metrics._metrics['hyc_http_requests_5xx_total']['value'] = self._errors_5xx
+374
View File
@@ -0,0 +1,374 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
定时任务调度器
支持 cron 表达式和简单间隔的定时任务
"""
import os
import time
import logging
import threading
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Callable
from enum import Enum
logger = logging.getLogger(__name__)
class TaskStatus(Enum):
"""任务状态"""
IDLE = "idle"
RUNNING = "running"
ERROR = "error"
DISABLED = "disabled"
class ScheduledTask:
"""定时任务"""
def __init__(self, name: str, task_type: str, config: dict,
callback: Callable, logger=None):
"""
初始化定时任务
Args:
name: 任务名称
task_type: 任务类型 ('cron' 或 'interval')
config: 任务配置
callback: 回调函数
logger: 日志器
"""
self.name = name
self.task_type = task_type # 'cron' 或 'interval'
self.config = config or {}
self.callback = callback
self.logger = logger or logging.getLogger(__name__)
# 状态
self.status = TaskStatus.IDLE
self.last_run: Optional[datetime] = None
self.next_run: Optional[datetime] = None
self.last_error: Optional[str] = None
self.run_count = 0
# 配置解析
self._parse_config()
def _parse_config(self):
"""解析任务配置"""
if self.task_type == 'cron':
# Cron 表达式: "minute hour day month weekday"
# 例如: "0 3 * * *" 每天凌晨3点
cron = self.config.get('cron', '0 0 * * *')
parts = cron.split()
if len(parts) == 5:
self.cron_parts = {
'minute': self._parse_cron_part(parts[0], 0, 59),
'hour': self._parse_cron_part(parts[1], 0, 23),
'day': self._parse_cron_part(parts[2], 1, 31),
'month': self._parse_cron_part(parts[3], 1, 12),
'weekday': self._parse_cron_part(parts[4], 0, 6)
}
else:
self.logger.warning(f"无效的 cron 表达式: {cron}")
self.cron_parts = None
elif self.task_type == 'interval':
# 间隔: seconds, minutes, hours
interval = self.config.get('interval', {})
self.interval_seconds = (
interval.get('seconds', 0) +
interval.get('minutes', 0) * 60 +
interval.get('hours', 0) * 3600 +
interval.get('days', 0) * 86400
)
if self.interval_seconds <= 0:
self.interval_seconds = 3600 # 默认1小时
# 是否启用
self.enabled = self.config.get('enabled', True)
def _parse_cron_part(self, part: str, min_val: int, max_val: int) -> List[int]:
"""解析 cron 表达式的一部分"""
result = []
if part == '*':
return list(range(min_val, max_val + 1))
# 处理列表: "1,2,3"
if ',' in part:
return self._parse_cron_part(part.replace(',', ' '), min_val, max_val)
# 处理范围: "1-5"
if '-' in part:
start, end = part.split('-')
return list(range(int(start), int(end) + 1))
# 处理步进: "*/5"
if '/' in part:
base, step = part.split('/')
base_list = self._parse_cron_part(base or '*', min_val, max_val)
step = int(step)
return base_list[::step]
# 单个值
try:
val = int(part)
if min_val <= val <= max_val:
return [val]
except ValueError:
pass
return []
def should_run_now(self) -> bool:
"""检查是否应该在当前时刻运行"""
if not self.enabled:
return False
now = datetime.now()
if self.task_type == 'cron' and self.cron_parts:
return self._matches_cron(now)
elif self.task_type == 'interval':
if self.last_run is None:
return True
elapsed = (now - self.last_run).total_seconds()
return elapsed >= self.interval_seconds
return False
def _matches_cron(self, dt: datetime) -> bool:
"""检查时间是否匹配 cron 表达式"""
if not self.cron_parts:
return False
return (
dt.minute in self.cron_parts['minute'] and
dt.hour in self.cron_parts['hour'] and
dt.day in self.cron_parts['day'] and
dt.month in self.cron_parts['month'] and
dt.weekday() in self.cron_parts['weekday']
)
def get_next_run_time(self) -> Optional[datetime]:
"""计算下次运行时间"""
if not self.enabled:
return None
now = datetime.now()
if self.task_type == 'cron' and self.cron_parts:
# 找到下一个匹配的时间点
for i in range(365 * 24 * 60): # 最多查找1年
candidate = now + timedelta(minutes=i)
if self._matches_cron(candidate):
return candidate
elif self.task_type == 'interval':
if self.last_run:
return self.last_run + timedelta(seconds=self.interval_seconds)
return now
return None
def run(self) -> bool:
"""执行任务"""
if self.status == TaskStatus.RUNNING:
self.logger.warning(f"任务 {self.name} 已在运行中")
return False
self.status = TaskStatus.RUNNING
self.last_run = datetime.now()
self.last_error = None
try:
self.logger.info(f"开始执行定时任务: {self.name}")
result = self.callback(self.name, self.config)
self.run_count += 1
self.logger.info(f"定时任务 {self.name} 执行完成")
return True
except Exception as e:
self.last_error = str(e)
self.status = TaskStatus.ERROR
self.logger.error(f"定时任务 {self.name} 执行失败: {e}")
return False
finally:
if self.status != TaskStatus.ERROR:
self.status = TaskStatus.IDLE
def to_dict(self) -> dict:
"""转换为字典"""
return {
'name': self.name,
'type': self.task_type,
'enabled': self.enabled,
'status': self.status.value,
'config': self.config,
'last_run': self.last_run.isoformat() if self.last_run else None,
'next_run': self.next_run.isoformat() if self.next_run else None,
'run_count': self.run_count,
'last_error': self.last_error
}
class Scheduler:
"""定时任务调度器"""
def __init__(self, config: dict = None):
self.config = config or {}
self.tasks: Dict[str, ScheduledTask] = {}
self._running = False
self._thread: Optional[threading.Thread] = None
self._lock = threading.Lock()
# 默认检查间隔
self.check_interval = self.config.get('check_interval', 10)
# 事件回调
self.on_task_start: Optional[Callable] = None
self.on_task_complete: Optional[Callable] = None
self.on_task_error: Optional[Callable] = None
def add_task(self, name: str, task_type: str, config: dict,
callback: Callable) -> bool:
"""
添加定时任务
Args:
name: 任务名称
task_type: 任务类型 ('cron' 或 'interval')
config: 任务配置
callback: 回调函数
Returns:
是否成功
"""
with self._lock:
if name in self.tasks:
logger.warning(f"任务 {name} 已存在,将被替换")
self.tasks[name] = ScheduledTask(name, task_type, config, callback, logger)
return True
def remove_task(self, name: str) -> bool:
"""移除任务"""
with self._lock:
if name in self.tasks:
del self.tasks[name]
return True
return False
def get_task(self, name: str) -> Optional[ScheduledTask]:
"""获取任务"""
return self.tasks.get(name)
def get_all_tasks(self) -> List[dict]:
"""获取所有任务状态"""
with self._lock:
for task in self.tasks.values():
task.next_run = task.get_next_run_time()
return [task.to_dict() for task in self.tasks.values()]
def start(self):
"""启动调度器"""
if self._running:
logger.warning("调度器已在运行中")
return
self._running = True
self._thread = threading.Thread(target=self._run_loop, daemon=True)
self._thread.start()
logger.info("定时任务调度器已启动")
def stop(self):
"""停止调度器"""
self._running = False
if self._thread:
self._thread.join(timeout=5)
logger.info("定时任务调度器已停止")
def _run_loop(self):
"""运行循环"""
while self._running:
try:
now = datetime.now()
with self._lock:
for name, task in self.tasks.items():
if task.should_run_now():
# 使用线程池执行任务
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=1) as executor:
executor.submit(task.run)
time.sleep(self.check_interval)
except Exception as e:
logger.error(f"调度器循环错误: {e}")
time.sleep(5)
def run_task_now(self, name: str) -> bool:
"""立即运行指定任务"""
task = self.get_task(name)
if task:
return task.run()
return False
def enable_task(self, name: str, enabled: bool = True) -> bool:
"""启用/禁用任务"""
task = self.get_task(name)
if task:
task.enabled = enabled
return True
return False
def update_task_config(self, name: str, config: dict) -> bool:
"""更新任务配置"""
task = self.get_task(name)
if task:
task.config.update(config)
task._parse_config()
return True
return False
# ==================== 同步任务工厂 ====================
# def create_sync_task_callback(sync_manager):
# """创建同步任务的回调函数"""
# def sync_task_callback(task_name: str, config: dict):
# """同步任务回调"""
# sync_manager.start_sync(task_name)
# return True
# return sync_task_callback
# ==================== 默认任务配置 ====================
# DEFAULT_SCHEDULED_TASKS = { ... }
DEFAULT_SCHEDULED_TASKS = {
# 数据库清理 - 每天凌晨2点
'cleanup_db': {
'type': 'cron',
'config': {
'cron': '0 2 * * *',
'enabled': True
}
},
# 缓存清理 - 每6小时
'cleanup_cache': {
'type': 'interval',
'config': {
'interval': {'hours': 6},
'enabled': True
}
},
# 健康检查 - 每5分钟
'health_check': {
'type': 'interval',
'config': {
'interval': {'minutes': 5},
'enabled': True
}
}
}
+480
View File
@@ -0,0 +1,480 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
安全增强模块
提供 IP 白名单/黑名单、请求速率限制、HTTPS 支持
"""
import os
import sys
import time
import json
import hashlib
import threading
import logging
import ssl
import ipaddress
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass, field
from functools import wraps
from collections import defaultdict
from queue import Queue
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
logger = logging.getLogger(__name__)
# ==================== IP 管理器 ====================
class IPManager:
"""IP 管理器 - 白名单/黑名单"""
def __init__(self, config: dict = None):
self.config = config or {}
self.whitelist: List[str] = []
self.blacklist: List[str] = []
self._load_lists()
def _load_lists(self):
"""加载 IP 列表"""
# 加载白名单
whitelist_file = self.config.get('whitelist_file', 'whitelist.txt')
if os.path.exists(whitelist_file):
with open(whitelist_file, 'r') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
self.whitelist.append(line)
# 加载黑名单
blacklist_file = self.config.get('blacklist_file', 'blacklist.txt')
if os.path.exists(blacklist_file):
with open(blacklist_file, 'r') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
self.blacklist.append(line)
def is_allowed(self, ip: str) -> Tuple[bool, str]:
"""
检查 IP 是否允许访问
返回: (是否允许, 原因)
"""
# 检查白名单
if self.whitelist:
for pattern in self.whitelist:
if self._match_ip(ip, pattern):
return True, "白名单"
# 检查黑名单
for pattern in self.blacklist:
if self._match_ip(ip, pattern):
return False, "黑名单"
return True, "允许"
def _match_ip(self, ip: str, pattern: str) -> bool:
"""匹配 IP"""
try:
# 单个 IP
if pattern == ip:
return True
# CIDR 范围
if '/' in pattern:
network = ipaddress.ip_network(pattern, strict=False)
return ipaddress.ip_address(ip) in network
# 通配符 (例如: 192.168.1.*)
if pattern.endswith('*'):
prefix = pattern.rstrip('*').rstrip('.')
return ip.startswith(prefix)
except Exception:
pass
return False
def add_to_whitelist(self, ip: str):
"""添加到白名单"""
if ip not in self.whitelist:
self.whitelist.append(ip)
self._save_list('whitelist')
def add_to_blacklist(self, ip: str):
"""添加到黑名单"""
if ip not in self.blacklist:
self.blacklist.append(ip)
self._save_list('blacklist')
def remove_from_whitelist(self, ip: str):
"""从白名单移除"""
if ip in self.whitelist:
self.whitelist.remove(ip)
self._save_list('whitelist')
def remove_from_blacklist(self, ip: str):
"""从黑名单移除"""
if ip in self.blacklist:
self.blacklist.remove(ip)
self._save_list('blacklist')
def _save_list(self, list_type: str):
"""保存列表到文件"""
filename = f'{list_type}.txt'
data = '\n'.join(self.whitelist if list_type == 'whitelist' else self.blacklist)
with open(filename, 'w') as f:
f.write(data)
def get_status(self) -> dict:
"""获取状态"""
return {
'whitelist_count': len(self.whitelist),
'blacklist_count': len(self.blacklist),
'whitelist': self.whitelist[:10],
'blacklist': self.blacklist[:10]
}
# ==================== 速率限制器 ====================
class RateLimiter:
"""请求速率限制器"""
def __init__(self, config: dict = None):
self.config = config or {}
self.requests: Dict[str, List[float]] = defaultdict(list)
self.lock = threading.Lock()
# 配置
self.requests_per_minute = self.config.get('requests_per_minute', 100)
self.burst_limit = self.config.get('burst_limit', 20)
self.window_seconds = 60
def is_allowed(self, identifier: str) -> Tuple[bool, int]:
"""
检查请求是否允许
返回: (是否允许, 剩余配额)
"""
now = time.time()
window_start = now - self.window_seconds
with self.lock:
# 清理过期记录
self.requests[identifier] = [
t for t in self.requests[identifier]
if t > window_start
]
# 检查限制
if len(self.requests[identifier]) >= self.requests_per_minute:
return False, 0
# 记录请求
self.requests[identifier].append(now)
remaining = self.requests_per_minute - len(self.requests[identifier])
return True, remaining
def get_usage(self, identifier: str) -> dict:
"""获取使用情况"""
now = time.time()
window_start = now - self.window_seconds
with self.lock:
requests = [
t for t in self.requests[identifier]
if t > window_start
]
return {
'requests': len(requests),
'limit': self.requests_per_minute,
'remaining': self.requests_per_minute - len(requests),
'reset_in': int(self.window_seconds - (now - min(requests) if requests else now))
}
def get_status(self) -> dict:
"""获取全局状态"""
total_requests = sum(len(v) for v in self.requests.values())
return {
'active_ips': len(self.requests),
'total_requests': total_requests,
'limit_per_minute': self.requests_per_minute,
'burst_limit': self.burst_limit
}
def reset(self):
"""重置所有记录"""
with self.lock:
self.requests.clear()
# ==================== HTTPS 管理器 ====================
class HTTPSManager:
"""HTTPS 证书管理器"""
def __init__(self, config: dict = None):
self.config = config or {}
self.cert_file = self.config.get('ssl_cert')
self.key_file = self.config.get('ssl_key')
self.context = None
def is_enabled(self) -> bool:
"""是否启用 HTTPS"""
return bool(self.cert_file and self.key_file)
def create_context(self) -> Optional[ssl.SSLContext]:
"""创建 SSL 上下文"""
if not self.is_enabled():
return None
try:
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
# 加载证书
context.load_cert_chain(self.cert_file, self.key_file)
# 安全配置
context.minimum_version = ssl.TLSVersion.TLSv1_2
context.set_ciphers('ECDHE+AESGCM:DHE+AESGCM:ECDHE+CHACHA20')
self.context = context
return context
except Exception as e:
logger.error(f"创建 SSL 上下文失败: {e}")
return None
@staticmethod
def generate_self_signed_cert(cert_path: str, key_path: str, common_name: str = 'localhost') -> bool:
"""生成自签名证书(用于测试)"""
try:
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
import datetime as dt
# 生成私钥
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
# 生成证书
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "CN"),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Shanghai"),
x509.NameAttribute(NameOID.LOCALITY_NAME, "Shanghai"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "HYC Download Station"),
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
])
cert = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
.public_key(private_key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(dt.datetime.utcnow())
.not_valid_after(dt.datetime.utcnow() + dt.timedelta(days=365))
.add_extension(
x509.SubjectAlternativeName([
x509.DNSName(common_name),
x509.DNSName("localhost"),
]),
critical=False,
)
.sign(private_key, hashes.SHA256(), default_backend())
)
# 保存证书
with open(cert_path, 'wb') as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))
# 保存私钥
with open(key_path, 'wb') as f:
f.write(private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
))
return True
except Exception as e:
logger.error(f"生成自签名证书失败: {e}")
return False
def get_status(self) -> dict:
"""获取状态"""
return {
'enabled': self.is_enabled(),
'cert_file': self.cert_file,
'key_file': self.key_file
}
# ==================== 安全中间件 ====================
class SecurityMiddleware:
"""安全中间件"""
def __init__(self, config: dict = None):
self.config = config or {}
# 初始化各组件
self.ip_manager = IPManager(self.config.get('ip', {}))
self.rate_limiter = RateLimiter(self.config.get('rate_limit', {}))
self.https_manager = HTTPSManager(self.config.get('ssl', {}))
def check_request(self, handler) -> Tuple[bool, str]:
"""
检查请求是否安全
返回: (是否通过, 原因)
"""
client_ip = self._get_client_ip(handler)
# IP 检查
allowed, reason = self.ip_manager.is_allowed(client_ip)
if not allowed:
return False, f"IP被阻止: {reason}"
# 速率限制
allowed, _ = self.rate_limiter.is_allowed(client_ip)
if not allowed:
return False, "请求过于频繁"
return True, "通过"
def _get_client_ip(self, handler) -> str:
"""获取客户端 IP"""
# 检查代理头
forwarded = handler.headers.get('X-Forwarded-For')
if forwarded:
return forwarded.split(',')[0].strip()
real_ip = handler.headers.get('X-Real-IP')
if real_ip:
return real_ip
return handler.client_address[0] if hasattr(handler, 'client_address') else 'unknown'
def get_security_headers(self) -> dict:
"""获取安全响应头"""
return {
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'SAMEORIGIN',
'X-XSS-Protection': '1; mode=block',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
}
def get_status(self) -> dict:
"""获取安全状态"""
return {
'ip': self.ip_manager.get_status(),
'rate_limit': self.rate_limiter.get_status(),
'ssl': self.https_manager.get_status()
}
# ==================== 审计日志 ====================
class AuditLogger:
"""审计日志记录器"""
def __init__(self, config: dict = None):
self.config = config or {}
self.logs: Queue = Queue(maxsize=10000)
self.log_file = self.config.get('audit_log', 'audit.log')
self.enabled = self.config.get('enabled', True)
# 启动日志写入线程
if self.enabled:
self._start_writer()
def log(self, event_type: str, data: dict):
"""记录事件"""
if not self.enabled:
return
entry = {
'timestamp': time.time(),
'type': event_type,
**data
}
if self.logs.full():
self.logs.get() # 移除最旧的
self.logs.put(entry)
def _start_writer(self):
"""启动日志写入线程"""
def writer():
while True:
try:
entry = self.logs.get(timeout=1)
self._write_entry(entry)
except Exception:
continue
thread = threading.Thread(target=writer, daemon=True)
thread.start()
def _write_entry(self, entry: dict):
"""写入日志条目"""
try:
with open(self.log_file, 'a', encoding='utf-8') as f:
line = json.dumps(entry, ensure_ascii=False)
f.write(line + '\n')
except Exception as e:
logger.error(f"写入审计日志失败: {e}")
def get_recent_logs(self, event_type: str = None, limit: int = 100) -> List[dict]:
"""获取最近的日志"""
result = []
with self.logs.mutex:
for entry in list(self.logs.queue):
if event_type and entry.get('type') != event_type:
continue
result.append(entry)
if len(result) >= limit:
break
return result[-limit:]
def get_status(self) -> dict:
"""获取状态"""
with self.logs.mutex:
return {
'enabled': self.enabled,
'log_file': self.log_file,
'pending_logs': self.logs.qsize(),
'max_size': self.logs.maxsize
}
# ==================== 便捷函数 ====================
def get_security_middleware(config: dict = None) -> SecurityMiddleware:
"""获取安全中间件"""
return SecurityMiddleware(config)
def get_ip_manager(config: dict = None) -> IPManager:
"""获取 IP 管理器"""
return IPManager(config)
def get_rate_limiter(config: dict = None) -> RateLimiter:
"""获取速率限制器"""
return RateLimiter(config)
+205
View File
@@ -0,0 +1,205 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""服务器核心模块 - 线程池版本"""
import os
import sys
import ssl
import signal
import mimetypes
import threading
import socketserver
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor
from .config import ConfigManager
from .mirror_sync import MirrorSyncManager
class ThreadPoolHTTPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
"""使用线程池的 HTTP 服务器"""
allow_reuse_address = True
daemon_threads = True # 使用守护线程
def __init__(self, server_address, RequestHandlerClass, max_workers=50):
self.max_workers = max_workers
self._executor = ThreadPoolExecutor(
max_workers=max_workers,
thread_name_prefix="http_handler"
)
super().__init__(server_address, RequestHandlerClass)
def process_request(self, request, client_address):
"""使用线程池处理请求"""
self._executor.submit(self._handle_request, request, client_address)
def _handle_request(self, request, client_address):
"""实际处理请求"""
try:
self.finish_request(request, client_address)
except Exception:
self.handle_error(request, client_address)
finally:
self.shutdown_request(request)
def server_close(self):
"""关闭服务器和线程池"""
# Python 3.8 兼容处理
import sys
try:
self._executor.shutdown(wait=False, cancel_futures=True)
except TypeError:
# Python 3.8 不支持 cancel_futures 参数
self._executor.shutdown(wait=False)
super().server_close()
class MirrorServer:
"""镜像服务器主类"""
def __init__(self, config):
if isinstance(config, dict):
self.config_manager = ConfigManager(config)
else:
self.config_manager = config
self.config = self.config_manager.config
self.server = None
self.sync_manager = None
self.is_running = False
def _validate_config(self, config):
"""验证和修复配置"""
return self.config_manager._validate_config(config)
def start(self):
"""启动服务器"""
try:
# 创建下载目录
base_dir = self.config['base_dir']
if not os.path.exists(base_dir):
os.makedirs(base_dir)
print(f"创建下载目录: {os.path.abspath(base_dir)}")
# 初始化MIME类型
mimetypes.init()
# 记录启动时间
self.config['start_time'] = __import__('time').time()
# 创建同步管理器(仅当启用时)
if self.config.get('enable_sync', True):
self.sync_manager = MirrorSyncManager(self.config)
self.sync_manager.start()
# 创建系统监控器(仅当启用时)
self.monitor = None
if self.config.get('enable_monitor', True):
try:
from .monitor import SystemMonitor
self.monitor = SystemMonitor(self.config)
print(f" ✓ 系统监控已启用 (间隔: {self.config.get('monitor_interval', 5)}秒)")
except ImportError as e:
print(f" ✗ 系统监控导入失败: {e}")
except Exception as e:
print(f" ✗ 系统监控初始化失败: {e}")
# 延迟导入 handler(避免循环导入)
from handlers.http_handler import MirrorServerHandler
# 获取线程数配置
max_workers = min(self.config.get('max_workers', 10), 10) # 限制最大线程数
# 创建服务器
server_address = (self.config['host'], self.config['port'])
self.server = ThreadPoolHTTPServer(
server_address,
MirrorServerHandler,
max_workers=max_workers
)
# 传递配置到处理器
MirrorServerHandler.config = self.config
MirrorServerHandler.sync_manager = self.sync_manager
MirrorServerHandler.monitor = self.monitor
# 设置调试模式
MirrorServerHandler._setup_debug(self.config)
# 设置超时
self.server.timeout = self.config.get('timeout', 30)
# 启用HTTPS
if self.config.get('ssl_cert') and self.config.get('ssl_key'):
if not self._setup_ssl():
return False
self.is_running = True
return True
except Exception as e:
import traceback
print(f"服务器启动失败: {e}")
traceback.print_exc()
return False
def _setup_ssl(self):
"""设置SSL"""
try:
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(self.config['ssl_cert'], self.config['ssl_key'])
self.server.socket = context.wrap_socket(self.server.socket, server_side=True)
print(f"启用HTTPS,证书: {self.config['ssl_cert']}")
return True
except Exception as e:
print(f"启用HTTPS失败: {e}")
return False
def stop(self):
"""停止服务器"""
print("正在停止服务器...")
if self.sync_manager:
try:
self.sync_manager.stop()
except Exception as e:
print(f"停止同步管理器时出错: {e}")
if self.server:
try:
self.server.server_close()
except Exception as e:
print(f"关闭服务器连接时出错: {e}")
self.is_running = False
print("服务器已停止")
def serve_forever(self):
"""运行服务器"""
if not self.server:
print("服务器未启动")
return
# 打印服务器信息(已在 main.py 中显示,此处仅保留最简信息)
protocol = "https" if self.config.get('ssl_cert') else "http"
sync_count = len(self.sync_manager.sync_sources) if self.sync_manager else 0
print(f"\n▶ 服务器运行于: {protocol}://{self.config['host']}:{self.config['port']}")
print(f"▶ 同步源数: {sync_count} | 最大线程: {self.server.max_workers}")
print("▶ 按 Ctrl+C 停止服务器")
try:
self.server.serve_forever()
except KeyboardInterrupt:
print("\n正在关闭服务器...")
finally:
self.stop()
# 全局变量,用于信号处理器访问服务器实例(预留)
# _server_instance = None
# def signal_handler(signum, frame):
# """处理退出信号"""
# print(f"\n收到信号 {signum},正在关闭服务器...")
# import os
# os._exit(0)
+1031
View File
File diff suppressed because it is too large Load Diff
+475
View File
@@ -0,0 +1,475 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
定时同步调度器
负责本地数据和数据库之间的定时同步
"""
import os
import sys
import time
import json
import hashlib
import threading
import logging
from datetime import datetime
from typing import Dict, List, Optional, Callable
from concurrent.futures import ThreadPoolExecutor
# 添加项目根目录到路径
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from core.database import DatabaseManager, get_db
from core.scheduler import Scheduler, ScheduledTask
logger = logging.getLogger(__name__)
class SyncScheduler:
"""同步调度器"""
def __init__(self, config: dict, db: DatabaseManager = None):
self.config = config
self.db = db
self._running = False
self._executor = ThreadPoolExecutor(max_workers=4)
# 同步配置
self.sync_interval = config.get('database', {}).get('sync_interval', 60)
self.auto_scan = config.get('auto_scan', True)
self.scan_interval = config.get('scan_interval', 300) # 5分钟扫描一次
# 同步状态
self.last_sync_time = 0
self.last_scan_time = 0
self.sync_in_progress = False
self.scan_in_progress = False
# 回调函数
self.on_file_added: Optional[Callable] = None
self.on_file_deleted: Optional[Callable] = None
self.on_file_updated: Optional[Callable] = None
self.on_sync_complete: Optional[Callable] = None
# 待同步队列
self._pending_add = [] # 待添加的文件
self._pending_update = [] # 待更新的文件
self._pending_delete = [] # 待删除的文件
# 定时任务调度器
self.task_scheduler = None
self.scheduled_syncs: Dict[str, dict] = {}
def _init_scheduled_syncs(self):
"""初始化定时同步任务"""
if not self.config.get('enable_sync', True):
return
# 从配置加载定时同步设置
sync_sources = self.config.get('sync_sources', {})
scheduled_sources = {}
for name, source_config in sync_sources.items():
schedule = source_config.get('schedule', {})
if schedule.get('enabled', False):
scheduled_sources[name] = {
'type': schedule.get('type', 'interval'), # 'cron' 或 'interval'
'config': {
'cron': schedule.get('cron'),
'interval': schedule.get('interval', {}),
'enabled': True
}
}
if scheduled_sources:
self.task_scheduler = Scheduler()
for name, sched_config in scheduled_sources.items():
self.task_scheduler.add_task(
name=f"sync_{name}",
task_type=sched_config['type'],
config=sched_config['config'],
callback=self._create_sync_callback(name)
)
self.scheduled_syncs = scheduled_sources
def _create_sync_callback(self, source_name: str):
"""创建同步回调函数"""
def sync_callback(task_name: str, config: dict):
logger.info(f"定时同步任务触发: {source_name}")
self.start_sync(source_name)
return True
return sync_callback
def start(self):
"""启动同步调度器"""
if self._running:
logger.warning("SyncScheduler 已经运行中")
return
self._running = True
self._executor.submit(self._sync_loop)
self._executor.submit(self._scan_loop)
# 初始化并启动定时同步
self._init_scheduled_syncs()
if self.task_scheduler:
self.task_scheduler.start()
logger.info(f"同步调度器已启动,间隔: {self.sync_interval}秒")
def stop(self):
"""停止同步调度器"""
self._running = False
# 停止定时任务调度器
if self.task_scheduler:
self.task_scheduler.stop()
self.task_scheduler = None
self._executor.shutdown(wait=True)
logger.info("同步调度器已停止")
def _sync_loop(self):
"""同步循环"""
while self._running:
try:
if time.time() - self.last_sync_time >= self.sync_interval:
self.perform_sync()
time.sleep(1)
except Exception as e:
logger.error(f"同步循环错误: {e}")
time.sleep(5)
def _scan_loop(self):
"""扫描循环 - 检测本地文件变化"""
while self._running:
try:
if self.auto_scan and time.time() - self.last_scan_time >= self.scan_interval:
self.scan_local_files()
time.sleep(5)
except Exception as e:
logger.error(f"扫描循环错误: {e}")
time.sleep(10)
def queue_add(self, file_info: dict):
"""队列添加文件"""
self._pending_add.append(file_info)
def queue_update(self, file_info: dict):
"""队列更新文件"""
self._pending_update.append(file_info)
def queue_delete(self, file_id: str):
"""队列删除文件"""
self._pending_delete.append(file_id)
def perform_sync(self):
"""执行同步"""
if self.sync_in_progress:
logger.warning("同步已在进行中,跳过")
return
self.sync_in_progress = True
start_time = time.time()
try:
logger.info("开始执行数据库同步...")
# 同步待添加的文件
added = 0
for file_info in self._pending_add[:]:
try:
self._sync_add_file(file_info)
self._pending_add.remove(file_info)
added += 1
except Exception as e:
logger.error(f"同步添加文件失败: {e}")
# 同步待更新的文件
updated = 0
for file_info in self._pending_update[:]:
try:
self._sync_update_file(file_info)
self._pending_update.remove(file_info)
updated += 1
except Exception as e:
logger.error(f"同步更新文件失败: {e}")
# 同步待删除的文件
deleted = 0
for file_id in self._pending_delete[:]:
try:
self._sync_delete_file(file_id)
self._pending_delete.remove(file_id)
deleted += 1
except Exception as e:
logger.error(f"同步删除文件失败: {e}")
# 同步统计
self.db.reset_pending_count()
self.last_sync_time = time.time()
duration = time.time() - start_time
logger.info(f"同步完成: 添加{added}, 更新{updated}, 删除{deleted}, 耗时{duration:.2f}秒")
# 回调
if self.on_sync_complete:
self.on_sync_complete({
'added': added,
'updated': updated,
'deleted': deleted,
'duration': duration
})
except Exception as e:
logger.error(f"同步过程错误: {e}")
finally:
self.sync_in_progress = False
def scan_local_files(self):
"""扫描本地文件"""
if self.scan_in_progress:
return
self.scan_in_progress = True
try:
base_dir = self.config.get('base_dir', './downloads')
if not os.path.exists(base_dir):
self.last_scan_time = time.time()
return
# 扫描文件
scanned_files = []
for root, dirs, files in os.walk(base_dir):
for filename in files:
full_path = os.path.join(root, filename)
rel_path = os.path.relpath(full_path, base_dir).replace("\\", "/")
stat = os.stat(full_path)
file_info = {
'path': rel_path,
'name': filename,
'size': stat.st_size,
'mtime': stat.st_mtime,
'ctime': stat.st_ctime
}
scanned_files.append(file_info)
# 与数据库对比
db_files = self.db.list_files(limit=100000)
db_paths = {f.path for f in db_files if not f.is_dir}
# 检测新增
local_paths = {f['path'] for f in scanned_files}
new_paths = local_paths - db_paths
for path in new_paths:
file_info = next((f for f in scanned_files if f['path'] == path), None)
if file_info:
file_id = hashlib.md5(path.encode()).hexdigest()
self._sync_add_file({
'file_id': file_id,
'path': path,
'name': file_info['name'],
'size': file_info['size'],
'updated_at': file_info['mtime']
})
# 检测删除
deleted_paths = db_paths - local_paths
for path in deleted_paths:
record = self.db.get_file_by_path(path)
if record:
self.db.delete_file(record.file_id)
self.last_scan_time = time.time()
except Exception as e:
logger.error(f"扫描本地文件错误: {e}")
finally:
self.scan_in_progress = False
def _sync_add_file(self, file_info: dict):
"""同步添加文件"""
existing = self.db.get_file_by_path(file_info['path'])
if existing:
# 已存在,更新
self.db.update_file(
existing.file_id,
size=file_info.get('size', 0),
updated_at=file_info.get('updated_at', time.time()),
hash=file_info.get('hash'),
sync_status='synced'
)
else:
# 新增
file_id = file_info.get('file_id') or hashlib.md5(
file_info['path'].encode()
).hexdigest()
self.db.add_file(
file_id=file_id,
path=file_info['path'],
name=file_info['name'],
size=file_info.get('size', 0),
hash=file_info.get('hash'),
is_dir=False,
created_at=file_info.get('created_at'),
updated_at=file_info.get('updated_at', time.time())
)
if self.on_file_added:
self.on_file_added(file_info)
def _sync_update_file(self, file_info: dict):
"""同步更新文件"""
file_id = file_info.get('file_id')
if file_id:
self.db.update_file(
file_id,
size=file_info.get('size'),
updated_at=file_info.get('updated_at', time.time()),
hash=file_info.get('hash'),
sync_status='synced'
)
if self.on_file_updated:
self.on_file_updated(file_info)
def _sync_delete_file(self, file_id: str):
"""同步删除文件"""
self.db.delete_file(file_id)
if self.on_file_deleted:
self.on_file_deleted({'file_id': file_id})
def get_status(self) -> dict:
"""获取同步状态"""
return {
'running': self._running,
'last_sync_time': self.last_sync_time,
'last_scan_time': self.last_scan_time,
'sync_in_progress': self.sync_in_progress,
'scan_in_progress': self.scan_in_progress,
'pending_add': len(self._pending_add),
'pending_update': len(self._pending_update),
'pending_delete': len(self._pending_delete),
'pending_operations': self.db.get_pending_operations() if self.db else 0
}
def force_sync(self):
"""强制立即同步"""
self.last_sync_time = 0
self.perform_sync()
# ==================== 文件操作包装器 ====================
class DatabaseBackedFileOperations:
"""数据库支持的文件操作"""
def __init__(self, config: dict, db: DatabaseManager, scheduler: SyncScheduler = None):
self.config = config
self.db = db
self.scheduler = scheduler
self.base_dir = config.get('base_dir', './downloads')
def add_file_record(self, path: str, name: str, size: int = 0,
hash: str = None, is_dir: bool = False) -> dict:
"""添加文件记录到数据库"""
import hashlib
file_id = hashlib.md5(path.encode()).hexdigest()
file_info = {
'file_id': file_id,
'path': path,
'name': name,
'size': size,
'hash': hash,
'is_dir': is_dir,
'created_at': time.time(),
'updated_at': time.time()
}
if self.scheduler:
self.scheduler.queue_add(file_info)
else:
self.db.add_file(
file_id=file_id,
path=path,
name=name,
size=size,
hash=hash,
is_dir=is_dir,
created_at=time.time(),
updated_at=time.time()
)
return file_info
def update_file_record(self, file_id: str, **kwargs):
"""更新文件记录"""
if self.scheduler:
self.scheduler.queue_update({'file_id': file_id, **kwargs})
else:
self.db.update_file(file_id, **kwargs)
def delete_file_record(self, file_id: str, hard: bool = False):
"""删除文件记录"""
if self.scheduler:
self.scheduler.queue_delete(file_id)
else:
self.db.delete_file(file_id, hard=hard)
def record_download(self, file_path: str, file_size: int = 0,
client_ip: str = None, duration: float = 0,
success: bool = True, error_message: str = None):
"""记录下载"""
self.db.add_download_record(
file_path=file_path,
file_size=file_size,
client_ip=client_ip,
duration=duration,
success=success,
error_message=error_message
)
# 更新下载计数
record = self.db.get_file_by_path(file_path)
if record:
self.db.increment_download_count(record.file_id)
def record_cache_hit(self, cache_key: str, cache_type: str):
"""记录缓存命中"""
record = self.db.get_cache_record(cache_key)
if record:
self.db.increment_cache_hits(cache_key)
else:
self.db.add_cache_record(
cache_key=cache_key,
cache_type=cache_type,
hits=1,
last_hit=time.time()
)
# ==================== 便捷函数 ====================
def get_sync_scheduler(config: dict) -> SyncScheduler:
"""获取同步调度器"""
db = get_db(config)
return SyncScheduler(config, db)
def init_database_sync(config: dict, db=None) -> tuple:
"""初始化数据库和同步"""
if db is None:
db = get_db(config)
scheduler = SyncScheduler(config, db)
file_ops = DatabaseBackedFileOperations(config, db, scheduler)
return db, scheduler, file_ops
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""工具函数模块"""
import os
import re
import hashlib
def format_file_size(size_bytes: int) -> str:
"""格式化文件大小为可读格式"""
if size_bytes == 0:
return "0 B"
size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
i = 0
while size_bytes >= 1024 and i < len(size_names) - 1:
size_bytes /= 1024.0
i += 1
return f"{size_bytes:.2f} {size_names[i]}"
def get_file_hash(filepath: str, algorithm: str = 'sha256') -> str:
"""计算文件哈希值"""
if not filepath or not isinstance(filepath, str):
return "Error: Invalid filepath"
if not os.path.exists(filepath):
return "Error: File not found"
try:
hash_func = hashlib.new(algorithm)
with open(filepath, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_func.update(chunk)
return hash_func.hexdigest()
except Exception as e:
return f"Error: {str(e)}"
def parse_size(size_str: str) -> int:
"""解析文件大小字符串为字节数"""
size_str = size_str.upper().strip()
match = re.match(r'^(\d+(?:\.\d+)?)\s*([BKMGT]?)B?$', size_str)
if not match:
raise ValueError(f"无效的文件大小格式: {size_str}")
number = float(match.group(1))
unit = match.group(2) or 'B'
units = {
'B': 1,
'K': 1024,
'M': 1024 ** 2,
'G': 1024 ** 3,
'T': 1024 ** 4
}
if unit not in units:
raise ValueError(f"无效的单位: {unit}")
return int(number * units[unit])
def sanitize_filename(filename: str) -> str:
"""清理文件名,防止路径遍历和安全问题"""
filename = os.path.basename(filename) # 移除路径分隔符
# 移除危险字符
filename = re.sub(r'[<>:"|?*\\\x00-\x1f]', '_', filename)
# 限制长度
if len(filename) > 255:
name, ext = os.path.splitext(filename)
filename = name[:255 - len(ext)] + ext
# 防止空文件名
if not filename or filename in ('.', '..'):
filename = 'unnamed_file'
return filename
def is_safe_path(base_dir: str, path: str) -> bool:
"""检查路径是否安全(防止目录遍历)"""
try:
abs_path = os.path.abspath(path)
abs_base = os.path.abspath(base_dir)
common_path = os.path.commonpath([abs_path, abs_base])
return common_path == abs_base
except ValueError:
return False
+25
View File
@@ -0,0 +1,25 @@
# ============================================
# HYC下载站 v2.2 - 环境变量配置示例
# ============================================
# 数据路径
DATA_PATH=./data
DOWNLOADS_PATH=./downloads
DB_PATH=./postgres
# 数据库配置
DB_TYPE=postgresql
DB_HOST=hyc-db
DB_PORT=5432
DB_NAME=hyc
DB_USER=postgres
DB_PASS=your_password_here
# 外部数据库连接 (可选,覆盖上述配置)
# DB_CONN_STR=postgresql://user:pass@host:5432/database
# Redis (可选)
REDIS_PATH=./redis
# 时区
TZ=Asia/Shanghai
+73
View File
@@ -0,0 +1,73 @@
# ============================================
# HYC下载站 v2.2 - Docker 构建配置
# ============================================
# 构建阶段
FROM python:3.11-slim AS builder
WORKDIR /app
# 安装构建依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
musl-dev \
libffi-dev \
libssl-dev \
&& rm -rf /var/lib/apt/lists/*
# 安装 Python 依赖
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# ============================================
# 运行阶段
FROM python:3.11-slim
LABEL maintainer="HYC Download Station"
LABEL description="HYC下载站 v2.2 - 镜像文件服务器 + 下载加速源"
# 环境变量
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV HYC_HOME=/app
# 创建工作目录
WORKDIR ${HYC_HOME}
# 复制 Python 和依赖
COPY --from=builder /install /usr/local
# 复制应用代码
COPY main.py .
COPY settings.json .
COPY requirements.txt .
COPY core/ ./core/
COPY api/ ./api/
COPY handlers/ ./handlers/
COPY mirrors/ ./mirrors/
COPY scripts/ ./scripts/
# 创建数据目录
RUN mkdir -p \
/data \
/downloads \
&& chown -R nobody:nogroup /data /downloads
# 设置工作用户
USER nobody
# 挂载卷
VOLUME ["/data", "/downloads"]
# 暴露端口
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/api/v1/health')" || exit 1
# 启动命令
CMD ["python", "main.py", \
"--host", "0.0.0.0", \
"--port", "8080", \
"--base-dir", "/downloads"]
+87
View File
@@ -0,0 +1,87 @@
# ============================================
# HYC下载站 v2.2 - 轻量级镜像 (X86_32/ARMv7)
# 适用于低端设备: 2CPU/1G 内存
# ============================================
# 使用 Alpine Linux 作为基础镜像 (极简)
FROM alpine:3.19 AS builder
# 安装 Python 和构建工具
RUN apk add --no-cache \
python3 \
py3-pip \
gcc \
musl-dev \
libffi-dev \
openssl-dev \
cargo \
rust
# 创建虚拟环境
ENV VENV=/opt/venv
RUN python3 -m venv $VENV
ENV PATH="$VENV/bin:$PATH"
# 安装依赖 (使用 --no-cache-dir 减少体积)
COPY requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir -r /tmp/requirements.txt
# ============================================
# 运行阶段 - 极简镜像
# ============================================
FROM alpine:3.19
# 安装最小运行时依赖
RUN apk add --no-cache \
python3 \
libffi \
openssl \
libstdc++ \
tzdata \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone
# 创建非 root 用户
RUN adduser -D -s /bin/sh hyc
# 设置工作目录
WORKDIR /app
# 从构建阶段复制虚拟环境
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# 复制应用代码
COPY --chown=hyc:hyc main.py .
COPY --chown=hyc:hyc core/ ./core/
COPY --chown=hyc:hyc api/ ./api/
COPY --chown=hyc:hyc handlers/ ./handlers/
COPY --chown=hyc:hyc mirrors/ ./mirrors/
# 创建数据目录
RUN mkdir -p /data /downloads && chown -R hyc:hyc /data /downloads
# 设置用户
USER hyc
# 挂载卷
VOLUME ["/data", "/downloads"]
# 暴露端口
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/api/v1/health')" || exit 1
# 启动命令 (使用虚拟环境中的 Python)
CMD ["python", "main.py", \
"--host", "0.0.0.0", \
"--port", "8080", \
"--base-dir", "/downloads", \
"--config", "/data/config.json", \
"--memory-limit", "512M"]
# 镜像元数据
LABEL maintainer="HYC Download Station"
LABEL description="HYC下载站 v2.2 - 轻量级版本"
+125
View File
@@ -0,0 +1,125 @@
# ============================================
# HYC下载站 v2.2 - 多架构交叉编译 Dockerfile
# 支持: amd64, arm64, arm/v7, i386
# 使用: docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t hyc:v2.2 .
# ============================================
# ============================================
# 构建阶段 - 所有架构通用
# ============================================
FROM python:3.11-alpine AS builder
# 安装构建依赖 (按架构)
ARG TARGETARCH
RUN case "$TARGETARCH" in \
amd64|x86_64) ARCH_OPTS="-march=x86-64" ;; \
arm64|aarch64) ARCH_OPTS="-march=armv8-a" ;; \
armv7|arm) ARCH_OPTS="-march=armv7-a" ;; \
i386|i686) ARCH_OPTS="-march=i686" ;; \
*) ARCH_OPTS="-march=native" ;; \
esac && \
apk add --no-cache \
python3 \
py3-pip \
gcc \
musl-dev \
libffi-dev \
openssl-dev \
cargo \
rust
# 创建优化编译的虚拟环境
ENV VENV=/opt/venv
ENV CFLAGS="$ARCH_OPTS"
ENV CXXFLAGS="$ARCH_OPTS"
ENV LDFLAGS="-Wl,-O1,--sort-common,--as-needed,-z,relro,-z,now"
RUN python3 -m venv $VENV && \
$VENV/bin/pip install --upgrade pip wheel
# 安装依赖
COPY requirements.txt /tmp/requirements.txt
RUN $VENV/bin/pip install --no-cache-dir \
--platform manylinux2014_${TARGETARCH} \
--only-binary=:all: \
-r /tmp/requirements.txt 2>/dev/null || \
$VENV/bin/pip install --no-cache-dir -r /tmp/requirements.txt
# ============================================
# 运行阶段 - 按架构选择基础镜像
# ============================================
# amd64
FROM --platform=$TARGETOS/$TARGETARCH alpine:3.19 AS runner-amd64
COPY --from=builder /opt/venv /opt/venv
COPY --from=builder /app /app
CMD ["/opt/venv/bin/python", "/app/main.py"]
# arm64
FROM --platform=$TARGETOS/$TARGETARCH alpine:3.19 AS runner-arm64
COPY --from=builder /opt/venv /opt/venv
COPY --from=builder /app /app
CMD ["/opt/venv/bin/python", "/app/main.py"]
# arm/v7
FROM --platform=$TARGETOS/$TARGETARCH alpine:3.19 AS runner-armv7
COPY --from=builder /opt/venv /opt/venv
COPY --from=builder /app /app
CMD ["/opt/venv/bin/python", "/app/main.py"]
# i386
FROM --platform=$TARGETOS/$TARGETARCH alpine:3.19 AS runner-i386
COPY --from=builder /opt/venv /opt/venv
COPY --from=builder /app /app
CMD ["/opt/venv/bin/python", "/app/main.py"]
# ============================================
# 最终清单镜像
# ============================================
FROM --platform=$TARGETOS/$TARGETARCH alpine:3.19 AS final
# 安装运行时依赖
RUN apk add --no-cache \
libffi \
openssl \
libstdc++ \
tzdata \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone
# 创建用户
RUN adduser -D -s /bin/sh hyc
# 复制虚拟环境
COPY --from=builder --chown=hyc:hyc /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# 复制应用
COPY --chown=hyc:hyc main.py .
COPY --chown=hyc:hyc settings.json .
COPY --chown=hyc:hyc requirements.txt .
COPY --chown=hyc:hyc core/ ./core/
COPY --chown=hyc:hyc api/ ./api/
COPY --chown=hyc:hyc handlers/ ./handlers/
COPY --chown=hyc:hyc mirrors/ ./mirrors/
COPY --chown=hyc:hyc scripts/ ./scripts/
# 创建数据目录
RUN mkdir -p /data /downloads && chown -R hyc:hyc /data /downloads
USER hyc
VOLUME ["/data", "/downloads"]
EXPOSE 8080
# 低内存模式
ENV HYC_LOW_MEMORY=1
ENV HYC_WORKERS=1
ENV PYTHONOPTIMIZE=2
ENV PYTHONDONTWRITEBYTECODE=1
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/api/v1/health')" || exit 1
CMD ["python", "main.py", \
"--host", "0.0.0.0", \
"--port", "8080", \
"--memory-limit", "256M"]
+27
View File
@@ -0,0 +1,27 @@
# ============================================
# PyInstaller 多架构构建镜像
# ============================================
FROM python:3.11 AS builder
WORKDIR /app
# 安装构建依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
g++ \
musl-dev \
libffi-dev \
libssl-dev \
&& rm -rf /var/lib/apt/lists/*
# 复制整个项目
COPY . .
# 安装 PyInstaller 和项目依赖
RUN pip install --no-cache-dir pyinstaller -r requirements.txt
# 构建可执行文件(使用 .spec 文件生成单文件)
RUN pyinstaller pyinstaller.spec
FROM scratch
COPY --from=builder /app/dist/hyc-download /
+52
View File
@@ -0,0 +1,52 @@
# ============================================
# HYC下载站 v2.3 - 轻量级 Docker Compose
# 适用于: Raspberry Pi, 2CPU/1G 设备
# ============================================
services:
# 轻量级应用服务
hyc-server:
build:
context: ..
dockerfile: docker/Dockerfile.lite
container_name: hyc-download-server
hostname: hyc-server
restart: unless-stopped
ports:
- "8080:8080"
environment:
- TZ=Asia/Shanghai
# 低端设备优化
- PRESET=ultra_low
- PYTHONOPTIMIZE=2
- HYC_LOW_MEMORY=1
volumes:
- ${DATA_PATH:-./data}:/data
- ${DOWNLOADS_PATH:-./downloads}:/downloads
deploy:
resources:
limits:
# 内存限制
memory: 512M
cpus: '1.0'
reservations:
memory: 256M
cpus: '0.5'
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/api/v1/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
labels:
- "com.hyc.version=2.2"
- "com.hyc.preset=ultra_low"
networks:
default:
driver: bridge
volumes:
data:
downloads:
caches:
+57
View File
@@ -0,0 +1,57 @@
# ============================================
# HYC下载站 v2.3 - 树莓派专用配置
# 适用于: Raspberry Pi 3/4/5 (ARMv7/Arm64)
# ============================================
services:
# ARM 优化版本
hyc-server:
# 使用 ARM 专用镜像
image: ${REGISTRY:-hyc-download-station}:${TAG:-v2.3}-arm64
container_name: hyc-download-server
hostname: hyc-server
restart: unless-stopped
privileged: true # 树莓派可能需要
ports:
- "8080:8080"
environment:
- TZ=Asia/Shanghai
# 树莓派优化
- PRESET=low
- PYTHONOPTIMIZE=2
- HYC_LOW_MEMORY=1
# ARM 优化
- PYTHONCPUCOUNT=2
volumes:
- ${DATA_PATH:-./data}:/data
- ${DOWNLOADS_PATH:-./downloads}:/downloads
deploy:
resources:
limits:
# Raspberry Pi 4: 4GB RAM
memory: 1G
cpus: '2.0'
reservations:
memory: 512M
cpus: '1.0'
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/api/v1/health')"]
interval: 30s
timeout: 10s
retries: 3
labels:
- "com.hyc.platform=raspberry-pi"
- "com.hyc.arch=arm64"
# 温度监控 (树莓派专用)
devices:
- /sys/class/thermal/thermal_zone0:/sys/class/thermal/thermal_zone0
networks:
default:
driver: bridge
volumes:
data:
downloads:
caches:
+87
View File
@@ -0,0 +1,87 @@
# ============================================
# HYC下载站 v2.3 - Docker Compose 配置
# ============================================
services:
# 主应用服务
hyc-server:
build:
context: ..
dockerfile: docker/Dockerfile
container_name: hyc-download-server
hostname: hyc-server
restart: unless-stopped
ports:
- "8080:8080"
environment:
- TZ=Asia/Shanghai
# 数据库配置
- DB_TYPE=${DB_TYPE:-sqlite}
- DB_PATH=/data/hyc.db
# PostgreSQL (可选)
- DB_HOST=${DB_HOST:-}
- DB_PORT=${DB_PORT:-5432}
- DB_NAME=${DB_NAME:-hyc}
- DB_USER=${DB_USER:-postgres}
- DB_PASS=${DB_PASS:-}
# 外部数据库连接 (可选)
- DB_CONN_STR=${DB_CONN_STR:-}
volumes:
- ${DATA_PATH:-./data}:/data
- ${DOWNLOADS_PATH:-./downloads}:/downloads
networks:
- hyc-network
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/api/v1/health')"]
interval: 30s
timeout: 10s
retries: 3
labels:
- "com.hyc.version=2.2"
# PostgreSQL 数据库 (可选)
hyc-db:
image: postgres:15-alpine
container_name: hyc-postgres
restart: unless-stopped
environment:
- POSTGRES_DB=${DB_NAME:-hyc}
- POSTGRES_USER=${DB_USER:-postgres}
- POSTGRES_PASSWORD=${DB_PASS:-postgres}
- TZ=Asia/Shanghai
volumes:
- ${DB_PATH:-./postgres}:/var/lib/postgresql/data
networks:
- hyc-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-hyc}"]
interval: 30s
timeout: 10s
retries: 3
# Redis 缓存 (可选,用于速率限制)
redis:
image: redis:7-alpine
container_name: hyc-redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- ${REDIS_PATH:-./redis}:/data
networks:
- hyc-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3
networks:
hyc-network:
driver: bridge
volumes:
data:
downloads:
caches:
postgres:
redis:
+4
View File
@@ -0,0 +1,4 @@
# HTTP处理模块初始化
from .http_handler import MirrorServerHandler
__all__ = ['MirrorServerHandler']
File diff suppressed because it is too large Load Diff
+44
View File
@@ -0,0 +1,44 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[('api', 'api'), ('core', 'core'), ('handlers', 'handlers'), ('mirrors', 'mirrors'), ('settings.json', '.')],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='hyc-download',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='hyc-download',
)
+157
View File
@@ -0,0 +1,157 @@
# ============================================
# HYC下载站 v2.3 - Kubernetes 部署指南
# ============================================
## 快速部署
### 1. 创建命名空间 (可选)
```bash
kubectl create namespace hyc
kubectl config set-context --current --namespace=hyc
```
### 2. 部署应用
```bash
# 使用内置 PostgreSQL 和 Redis (可选)
kubectl apply -f deployment.yaml
kubectl apply -f postgres.yaml
kubectl apply -f redis.yaml
# 或只部署应用,使用外部数据库
kubectl apply -f deployment.yaml
```
### 3. 验证部署
```bash
# 查看 Pods
kubectl get pods -l app=hyc-download-server
# 查看日志
kubectl logs -l app=hyc-download-server -f
# 查看服务
kubectl get svc hyc-server
```
### 4. 访问应用
```bash
# Port Forward (开发环境)
kubectl port-forward svc/hyc-server 8080:8080
# 浏览器访问
# http://localhost:8080
# 管理界面: http://localhost:8080/api/ui/
```
## 生产环境部署
### 1. 构建并推送镜像
```bash
# 构建镜像
docker build -t hx100cv/hyc-download-station:v2.3 .
# 推送镜像
docker push hx100cv/hyc-download-station:v2.3
# 更新 deployment.yaml 中的镜像地址
```
### 2. 配置域名和 TLS
```bash
# 编辑 deployment.yaml,修改 Ingress 配置
# 添加 TLS secret
kubectl create secret tls hyc-tls-secret --cert=certificate.crt --key=private.key
# 应用配置
kubectl apply -f deployment.yaml
```
### 3. 配置资源限制
```yaml
# 根据实际需求调整 deployment.yaml 中的资源限制
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 2000m
memory: 2Gi
```
### 4. 配置 HPA (自动扩缩容)
```bash
# HPA 已内置,查看状态
kubectl get hpa hyc-hpa
kubectl top pods
```
## 使用外部数据库
### PostgreSQL
```bash
# 设置环境变量或 Secret
export DB_TYPE=postgresql
export DB_HOST=your-postgres-host
export DB_PORT=5432
export DB_NAME=hyc
export DB_USER=postgres
export DB_PASSWORD=your-password
# 或使用连接字符串
export DB_CONN_STR=postgresql://user:pass@host:5432/database
```
### MySQL
```bash
export DB_TYPE=mysql
export DB_HOST=your-mysql-host
export DB_PORT=3306
export DB_NAME=hyc
export DB_USER=root
export DB_PASSWORD=your-password
```
## 监控
### Prometheus + Grafana
```bash
# ServiceMonitor 已内置
# 确保 Prometheus Operator 已安装
kubectl get servicemonitor hyc-monitor
# Grafana Dashboard (导入 json/dashboard.json)
```
### 日志
```bash
# 查看应用日志
kubectl logs -l app=hyc-download-server --tail=100
# 实时日志
kubectl logs -l app=hyc-download-server -f
```
## 升级
```bash
# 更新镜像版本
kubectl set image deployment/hyc-server hyc-server=hx100cv/hyc-download-station:v2.4
# 查看滚动更新
kubectl rollout status deployment/hyc-server
# 回滚 (如有问题)
kubectl rollout undo deployment/hyc-server
```
## 卸载
```bash
# 删除所有资源
kubectl delete -f deployment.yaml
kubectl delete -f postgres.yaml
kubectl delete -f redis.yaml
# 删除 PVC (数据将丢失)
kubectl delete pvc hyc-data-pvc hyc-downloads-pvc hyc-cache-pvc
```
+237
View File
@@ -0,0 +1,237 @@
# ============================================
# HYC下载站 v2.3 - Kubernetes 部署配置
# ============================================
---
# ConfigMap - 应用配置
apiVersion: v1
kind: ConfigMap
metadata:
name: hyc-config
labels:
app: hyc-download-server
data:
HYC_HOST: "0.0.0.0"
HYC_PORT: "8080"
HYC_BASE_DIR: "/downloads"
HYC_ENABLE_MONITOR: "true"
HYC_ENABLE_SYNC: "true"
HYC_ENABLE_MIRRORS: "true"
HYC_ENABLE_WS: "true"
HYC_ENABLE_SSE: "true"
---
# Secret - 数据库凭据
apiVersion: v1
kind: Secret
metadata:
name: hyc-secrets
type: Opaque
stringData:
# PostgreSQL 凭据
DB_HOST: "hyc-postgres"
DB_PORT: "5432"
DB_NAME: "hyc"
DB_USER: "postgres"
DB_PASSWORD: "your-password-here"
# Redis 凭据 (可选)
REDIS_HOST: "hyc-redis"
REDIS_PORT: "6379"
---
# PersistentVolumeClaim - 数据存储
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: hyc-data-pvc
labels:
app: hyc-download-server
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
# PersistentVolumeClaim - 下载目录
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: hyc-downloads-pvc
labels:
app: hyc-download-server
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
---
# PersistentVolumeClaim - 缓存目录
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: hyc-cache-pvc
labels:
app: hyc-download-server
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
---
# Deployment - 主应用
apiVersion: apps/v1
kind: Deployment
metadata:
name: hyc-server
labels:
app: hyc-download-server
spec:
replicas: 2
selector:
matchLabels:
app: hyc-download-server
template:
metadata:
labels:
app: hyc-download-server
spec:
containers:
- name: hyc-server
image: hyc-download-station:v2.3
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
name: http
envFrom:
- configMapRef:
name: hyc-config
- secretRef:
name: hyc-secrets
volumeMounts:
- name: data-volume
mountPath: /data
- name: downloads-volume
mountPath: /downloads
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 2000m
memory: 2Gi
livenessProbe:
httpGet:
path: /api/v1/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/v1/health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: data-volume
persistentVolumeClaim:
claimName: hyc-data-pvc
- name: downloads-volume
persistentVolumeClaim:
claimName: hyc-downloads-pvc
- name: cache-volume
persistentVolumeClaim:
claimName: hyc-cache-pvc
---
# Service - 内部服务
apiVersion: v1
kind: Service
metadata:
name: hyc-server
labels:
app: hyc-download-server
spec:
type: ClusterIP
ports:
- port: 8080
targetPort: 8080
protocol: TCP
name: http
selector:
app: hyc-download-server
---
# Ingress - 外部访问
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hyc-ingress
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "2G"
nginx.ingress.kubernetes.io/proxy-connect-timeout: "300"
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
spec:
ingressClassName: nginx
rules:
- host: hyc.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: hyc-server
port:
number: 8080
---
# HorizontalPodAutoscaler - 自动扩缩容
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: hyc-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: hyc-server
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
---
# ServiceMonitor - Prometheus 监控
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: hyc-monitor
labels:
release: prometheus
spec:
selector:
matchLabels:
app: hyc-download-server
endpoints:
- port: http
path: /metrics
interval: 15s
+28
View File
@@ -0,0 +1,28 @@
# ============================================
# HYC下载站 Helm Chart
# ============================================
apiVersion: v2
name: hyc-download-station
description: HYC下载站 v2.3 - 镜像文件服务器 + 下载加速源
version: 2.2.0
appVersion: "2.2.0"
keywords:
- download-station
- mirror
- docker-registry
- apt-mirror
- pypi-mirror
home: https://github.com/hyc-download-station
maintainers:
- name: HYC Team
email: [email protected]
dependencies:
- name: postgresql
version: 12.x.x
condition: postgresql.enabled
repository: "https://charts.bitnami.com/bitnami"
- name: redis
version: 18.x.x
condition: redis.enabled
repository: "https://charts.bitnami.com/bitnami"
+31
View File
@@ -0,0 +1,31 @@
=============================================
HYC下载站 v2.3 已成功部署!
=============================================
1. 获取应用 URL:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
http://{{ $host.host }}
{{- end }}
{{- else }}
# Port Forward (开发环境)
kubectl port-forward svc/{{ include "hyc.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }}
# 访问地址
http://localhost:{{ .Values.service.port }}
{{- end }}
2. 管理界面:
http://<your-url>/api/ui/
3. API 文档:
http://<your-url>/api/docs/
4. 查看状态:
kubectl get pods -l {{ include "hyc.selectorLabels" . | trimSuffix "-" | replace "=" "=" | replace " " "" }}
5. 查看日志:
kubectl logs -l {{ include "hyc.selectorLabels" . | trimSuffix "-" | replace "=" "=" | replace " " "" }} -f
=============================================
+59
View File
@@ -0,0 +1,59 @@
{{/*
创建完整名称
*/}}
{{- define "hyc.fullname" -}}
{{- printf "%s-%s" .Release.Name (include "hyc.name" .) | trunc 63 -}}
{{- end -}}
{{/*
标签
*/}}
{{- define "hyc.labels" -}}
helm.sh/chart: {{ include "hyc.chart" . }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: {{ .Chart.Name }}
{{- end -}}
{{/*
选择器标签
*/}}
{{- define "hyc.selectorLabels" -}}
app.kubernetes.io/name: {{ include "hyc.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{/*
图表名称
*/}}
{{- define "hyc.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 -}}
{{- end -}}
{{/*
图表版本
*/}}
{{- define "hyc.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version -}}
{{- end -}}
{{/*
PostgreSQL 主机
*/}}
{{- define "hyc.postgresql.host" -}}
{{- if .Values.postgresql.enabled -}}
{{- printf "%s-%s" .Release.Name "postgresql" -}}
{{- else -}}
{{- .Values.config.database.postgresql.host | default "localhost" -}}
{{- end -}}
{{- end -}}
{{/*
Redis 主机
*/}}
{{- define "hyc.redis.host" -}}
{{- if .Values.redis.enabled -}}
{{- printf "%s-%s" .Release.Name "redis" -}}
{{- else -}}
{{- .Values.config.database.redis.host | default "localhost" -}}
{{- end -}}
{{- end -}}
+142
View File
@@ -0,0 +1,142 @@
{{- if not .Values.postgresql.enabled }}
{{- if not .Values.redis.enabled }}
---
# 独立部署 (无依赖)
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "hyc.fullname" . }}-config
data:
HYC_HOST: {{ .Values.config.host | quote }}
HYC_PORT: {{ .Values.config.port | quote }}
HYC_BASE_DIR: {{ .Values.config.baseDir | quote }}
HYC_CACHE_DIR: {{ .Values.config.cacheDir | quote }}
HYC_ENABLE_MONITOR: {{ .Values.config.enableMonitor | quote }}
HYC_ENABLE_SYNC: {{ .Values.config.enableSync | quote }}
HYC_ENABLE_MIRRORS: {{ .Values.config.enableMirrors | quote }}
HYC_ENABLE_WS: {{ .Values.config.enableWs | quote }}
HYC_ENABLE_SSE: {{ .Values.config.enableSse | quote }}
{{- end }}
{{- end }}
---
apiVersion: v1
kind: Secret
metadata:
name: {{ include "hyc.fullname" . }}-secrets
type: Opaque
stringData:
{{- if .Values.postgresql.enabled }}
DB_HOST: {{ include "hyc.postgresql.host" . }}
DB_PORT: "5432"
DB_NAME: {{ .Values.postgresql.auth.database }}
DB_USER: {{ .Values.postgresql.auth.username }}
DB_PASSWORD: {{ .Values.postgresql.auth.password }}
{{- else if .Values.config.database.external.enabled }}
DB_CONN_STR: {{ .Values.config.database.external.connectionString }}
{{- end }}
{{- if .Values.redis.enabled }}
REDIS_HOST: {{ include "hyc.redis.host" . }}
REDIS_PORT: "6379"
{{- end }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "hyc.fullname" . }}
labels:
{{- include "hyc.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "hyc.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "hyc.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: {{ .Values.config.port }}
name: http
envFrom:
{{- if not .Values.postgresql.enabled }}
{{- if not .Values.config.database.external.enabled }}
- configMapRef:
name: {{ include "hyc.fullname" . }}-config
{{- end }}
{{- end }}
- secretRef:
name: {{ include "hyc.fullname" . }}-secrets
volumeMounts:
- name: data-volume
mountPath: /data
- name: downloads-volume
mountPath: {{ .Values.config.baseDir }}
- name: caches-volume
mountPath: {{ .Values.config.cacheDir }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- if .Values.livenessProbe.enabled }}
livenessProbe:
httpGet:
path: /api/v1/health
port: {{ .Values.config.port }}
initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.livenessProbe.periodSeconds }}
timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }}
failureThreshold: {{ .Values.livenessProbe.failureThreshold }}
{{- end }}
{{- if .Values.readinessProbe.enabled }}
readinessProbe:
httpGet:
path: /api/v1/health
port: {{ .Values.config.port }}
initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.readinessProbe.periodSeconds }}
timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }}
failureThreshold: {{ .Values.readinessProbe.failureThreshold }}
{{- end }}
volumes:
- name: data-volume
{{- if .Values.persistence.data.enabled }}
persistentVolumeClaim:
claimName: {{ include "hyc.fullname" . }}-data-pvc
{{- else }}
emptyDir: {}
{{- end }}
- name: downloads-volume
{{- if .Values.persistence.downloads.enabled }}
persistentVolumeClaim:
claimName: {{ include "hyc.fullname" . }}-downloads-pvc
{{- else }}
emptyDir: {}
{{- end }}
- name: caches-volume
{{- if .Values.persistence.caches.enabled }}
persistentVolumeClaim:
claimName: {{ include "hyc.fullname" . }}-caches-pvc
{{- else }}
emptyDir: {}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
+37
View File
@@ -0,0 +1,37 @@
{{- if .Values.autoscaling.enabled }}
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "hyc.fullname" . }}-hpa
labels:
{{- include "hyc.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "hyc.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- toYaml .Values.autoscaling.metrics | nindent 4 }}
{{- end }}
{{- if .Values.metrics.enabled }}
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: {{ include "hyc.fullname" . }}-monitor
labels:
{{- include "hyc.labels" . | nindent 4 }}
release: {{ .Values.metrics.release }}
spec:
selector:
matchLabels:
{{- include "hyc.selectorLabels" . | nindent 6 }}
endpoints:
- port: http
path: /metrics
interval: {{ .Values.metrics.interval }}
{{- end }}
+56
View File
@@ -0,0 +1,56 @@
{{- if .Values.persistence.data.enabled }}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "hyc.fullname" . }}-data-pvc
labels:
{{- include "hyc.labels" . | nindent 4 }}
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .Values.persistence.data.size }}
{{- if .Values.persistence.data.storageClass }}
storageClassName: {{ .Values.persistence.data.storageClass }}
{{- end }}
{{- end }}
{{- if .Values.persistence.downloads.enabled }}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "hyc.fullname" . }}-downloads-pvc
labels:
{{- include "hyc.labels" . | nindent 4 }}
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .Values.persistence.downloads.size }}
{{- if .Values.persistence.downloads.storageClass }}
storageClassName: {{ .Values.persistence.downloads.storageClass }}
{{- end }}
{{- end }}
{{- if .Values.persistence.caches.enabled }}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "hyc.fullname" . }}-caches-pvc
labels:
{{- include "hyc.labels" . | nindent 4 }}
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .Values.persistence.caches.size }}
{{- if .Values.persistence.caches.storageClass }}
storageClassName: {{ .Values.persistence.caches.storageClass }}
{{- end }}
{{- end }}
+39
View File
@@ -0,0 +1,39 @@
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "hyc.fullname" . }}
labels:
{{- include "hyc.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "hyc.selectorLabels" . | nindent 4 }}
{{- if .Values.ingress.enabled }}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "hyc.fullname" . }}-ingress
labels:
{{- include "hyc.labels" . | nindent 4 }}
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "2G"
nginx.ingress.kubernetes.io/proxy-connect-timeout: "300"
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
spec:
ingressClassName: {{ .Values.ingress.className }}
{{- if .Values.ingress.tls }}
tls:
{{- toYaml .Values.ingress.tls | nindent 4 }}
{{- end }}
rules:
{{- toYaml .Values.ingress.hosts | nindent 4 }}
{{- end }}
+169
View File
@@ -0,0 +1,169 @@
# ============================================
# HYC下载站 Helm Chart - 默认配置
# ============================================
# 副本数
replicaCount: 2
# 镜像配置
image:
repository: hyc-download-station
tag: v2.3
pullPolicy: IfNotPresent
# 镜像拉取密钥 (如需要私有仓库)
imagePullSecrets: []
# 服务配置
service:
type: ClusterIP
port: 8080
# 入口配置 (Ingress)
ingress:
enabled: true
className: nginx
hosts:
- host: hyc.example.com
paths:
- path: /
pathType: Prefix
tls: []
# - secretName: hyc-tls
# hosts:
# - hyc.example.com
# 资源配置
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 2000m
memory: 2Gi
# 持久化配置
persistence:
data:
enabled: true
storageClass: ""
size: 10Gi
downloads:
enabled: true
storageClass: ""
size: 100Gi
caches:
enabled: true
storageClass: ""
size: 50Gi
# PostgreSQL (可选)
postgresql:
enabled: false
auth:
database: hyc
username: postgres
password: "postgres-password"
# Redis (可选)
redis:
enabled: false
auth:
enabled: true
password: "redis-password"
# 应用配置
config:
# 基础配置
host: "0.0.0.0"
port: 8080
baseDir: "/downloads"
apiVersion: "v2"
# 功能开关
enableMonitor: true
enableSync: true
enableMirrors: true
enableWs: true
enableSse: true
# 数据库配置
database:
enabled: true
type: "postgresql"
syncInterval: 60
# 速率限制
rateLimit:
requestsPerMinute: 100
burstLimit: 20
# 安全配置
security:
# IP 白名单/黑名单
ip:
whitelist: []
blacklist: []
# SSL/TLS
ssl:
enabled: false
cert: ""
key: ""
# 节点选择器
nodeSelector: {}
# 容忍
tolerations: []
# 亲和性
affinity: {}
# 探针
livenessProbe:
enabled: true
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
enabled: true
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
# 滚动更新策略
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
# 拓扑分布 (可选)
topologySpreadConstraints: []
# 自动扩缩容
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
# 监控指标
metrics:
enabled: false
release: prometheus
interval: 15s
+113
View File
@@ -0,0 +1,113 @@
# ============================================
# PostgreSQL 数据库部署 (可选)
# ============================================
---
# StorageClass (如果需要动态供应)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: hyc-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
---
# PersistentVolume - PostgreSQL 数据
apiVersion: v1
kind: PersistentVolume
metadata:
name: hyc-postgres-pv
spec:
capacity:
storage: 20Gi
accessModes:
- ReadWriteOnce
storageClassName: hyc-storage
hostPath:
path: /data/hyc-postgres
type: DirectoryOrCreate
---
# PersistentVolumeClaim - PostgreSQL
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: hyc-postgres-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
storageClassName: hyc-storage
---
# Secret - PostgreSQL 凭据
apiVersion: v1
kind: Secret
metadata:
name: hyc-postgres-secret
type: Opaque
stringData:
POSTGRES_DB: hyc
POSTGRES_USER: postgres
POSTGRES_PASSWORD: your-secure-password
---
# Service - PostgreSQL
apiVersion: v1
kind: Service
metadata:
name: hyc-postgres
spec:
selector:
app: hyc-postgres
ports:
- port: 5432
targetPort: 5432
clusterIP: None
---
# Deployment - PostgreSQL
apiVersion: apps/v1
kind: Deployment
metadata:
name: hyc-postgres
spec:
replicas: 1
selector:
matchLabels:
app: hyc-postgres
template:
metadata:
labels:
app: hyc-postgres
spec:
containers:
- name: postgres
image: postgres:15-alpine
envFrom:
- secretRef:
name: hyc-postgres-secret
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 1000m
memory: 1Gi
livenessProbe:
exec:
command:
- pg_isready
- -U
- postgres
initialDelaySeconds: 30
periodSeconds: 10
volumes:
- name: postgres-data
persistentVolumeClaim:
claimName: hyc-postgres-pvc
+93
View File
@@ -0,0 +1,93 @@
# ============================================
# Redis 缓存部署 (可选,用于速率限制)
# ============================================
---
# PersistentVolumeClaim - Redis
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: hyc-redis-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
# Secret - Redis 凭据 (可选)
apiVersion: v1
kind: Secret
metadata:
name: hyc-redis-secret
type: Opaque
stringData:
REDIS_PASSWORD: your-redis-password
---
# Service - Redis
apiVersion: v1
kind: Service
metadata:
name: hyc-redis
spec:
selector:
app: hyc-redis
ports:
- port: 6379
targetPort: 6379
clusterIP: None
---
# Deployment - Redis
apiVersion: apps/v1
kind: Deployment
metadata:
name: hyc-redis
spec:
replicas: 1
selector:
matchLabels:
app: hyc-redis
template:
metadata:
labels:
app: hyc-redis
spec:
containers:
- name: redis
image: redis:7-alpine
command:
- redis-server
- --appendonly
- "yes"
- --maxmemory
- "256mb"
- --maxmemory-policy
- allkeys-lru
volumeMounts:
- name: redis-data
mountPath: /data
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
livenessProbe:
exec:
command:
- redis-cli
- ping
initialDelaySeconds: 30
periodSeconds: 10
volumes:
- name: redis-data
persistentVolumeClaim:
claimName: hyc-redis-pvc
---
# RedisCluster (可选,集群模式)
# 如果需要高可用,可以改用 Redis Operator
+606
View File
@@ -0,0 +1,606 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
HYC下载站 v2.3 - 完整增强版
主入口文件
支持镜像同步、下载加速源、系统监控、实时通信等
"""
import os
import sys
import signal
import argparse
import time
# PyInstaller 资源路径处理
def get_resource_path(relative_path):
"""获取打包后的资源路径"""
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
# 打包后的路径
return os.path.join(sys._MEIPASS, relative_path)
return os.path.join(os.path.dirname(os.path.abspath(__file__)), relative_path)
# 添加项目根目录到Python路径
base_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, base_dir)
from core.config import ConfigManager, load_config_file, deep_merge, load_settings_with_override
from core.server import MirrorServer
from core.utils import parse_size
from core.database import init_database, load_db_config_from_env, merge_config
from core.sync_scheduler import init_database_sync
from core.optimization import (
MemoryManager, LowMemoryConfig, ArchitectureDetector, check_compatibility
)
def signal_handler(signum, _frame):
"""处理退出信号"""
print(f"\n收到信号 {signum},正在关闭服务器...")
import os
os._exit(0)
def parse_arguments():
"""解析命令行参数"""
parser = argparse.ArgumentParser(
description='HYC下载站 v2.3 - 镜像文件服务器 + 下载加速源',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
示例:
# 基本启动
python main.py -p 8080 -d ./downloads
# 使用配置文件
python main.py --settings settings.json --config custom.json
# 认证配置
python main.py --auth-type token --auth-token your_token_here
# 功能开关
python main.py --enable-monitor --enable-sync --enable-mirrors
python main.py --disable-ws --disable-sse # 适合低端设备
# 镜像加速源 (通过 settings.json 配置各个镜像的启用/禁用)
python main.py --enable-mirrors
# 低端设备优化
python main.py --preset ultra_low
python main.py --memory-limit 256M --workers 2
# 调试
python main.py --debug
python main.py --debug-types http auth api v2 error
# 检查系统兼容性
python main.py --check-compat
'''
)
# 服务器配置
parser.add_argument('--host', default='0.0.0.0', help='监听地址 (默认: 0.0.0.0)')
parser.add_argument('-p', '--port', type=int, default=8080, help='监听端口 (默认: 8080)')
parser.add_argument('-d', '--base-dir', default='./downloads', help='文件存储目录 (默认: ./downloads)')
parser.add_argument('--server-name', default='HYC下载站', help='服务器名称')
# HTTPS配置
parser.add_argument('--ssl-cert', help='SSL证书文件路径')
parser.add_argument('--ssl-key', help='SSL私钥文件路径')
# 认证配置
auth_group = parser.add_argument_group('认证配置')
auth_group.add_argument('--auth-type', choices=['none', 'basic', 'token'], default=None,
help='认证类型: none(无), basic(基本认证), token(令牌认证)')
auth_group.add_argument('--auth-user', default='admin', help='基本认证用户名')
auth_group.add_argument('--auth-pass', help='基本认证密码')
auth_group.add_argument('--auth-token', help='令牌认证密钥')
# 功能配置
func_group = parser.add_argument_group('功能配置')
func_group.add_argument('--directory-listing', type=bool, default=True,
help='启用目录浏览 (默认: True)')
func_group.add_argument('--enable-stats', type=bool, default=True,
help='启用下载统计 (默认: True)')
func_group.add_argument('--show-hash', action='store_true', help='显示文件哈希值')
func_group.add_argument('--ignore-hidden', action='store_true', default=True,
help='忽略隐藏文件')
func_group.add_argument('--max-upload-size', default='1G', help='最大上传文件大小')
func_group.add_argument('--api-version', choices=['v1', 'v2'], default='v2',
help='API版本 (默认: v2)')
# 实时通信配置
realtime_group = parser.add_argument_group('实时通信配置')
realtime_group.add_argument('--enable-ws', action='store_true', default=True,
help='启用WebSocket (默认: True)')
realtime_group.add_argument('--enable-sse', action='store_true', default=True,
help='启用SSE (默认: True)')
# 系统监控配置
monitor_group = parser.add_argument_group('系统监控配置')
monitor_group.add_argument('--enable-monitor', action='store_true', default=True,
help='启用系统监控 (默认: True)')
monitor_group.add_argument('--monitor-interval', type=int, default=5,
help='监控数据采集间隔(秒) (默认: 5)')
# 同步配置
sync_group = parser.add_argument_group('同步配置')
sync_group.add_argument('--enable-sync', action='store_true', default=True,
help='启用镜像同步 (默认: True)')
sync_group.add_argument('--sync-config', help='同步配置文件路径')
# 镜像加速源配置
mirror_group = parser.add_argument_group('镜像加速源配置')
mirror_group.add_argument('--enable-mirrors', action='store_true', default=True,
help='启用下载加速源 (默认: True)')
# 注意: 各个镜像的启用/禁用通过 settings.json 中的 mirrors.xxx.enabled 配置
# 下载限速
rate_group = parser.add_argument_group('下载限速配置')
rate_group.add_argument('--rate-limit', type=int, default=0,
help='全局下载限速(字节/秒) (默认: 0=不限速)')
# 低端设备优化配置
optimize_group = parser.add_argument_group('低端设备优化配置')
optimize_group.add_argument('--preset', choices=['ultra_low', 'low', 'medium', 'high', 'auto'],
default='auto', help='设备预设 (默认: auto)')
optimize_group.add_argument('--memory-limit', default='512M',
help='内存限制 (例如: 256M, 512M, 1G)')
optimize_group.add_argument('--workers', type=int, default=0,
help='工作进程数 (0=自动)')
optimize_group.add_argument('--chunk-size', default='128K',
help='文件传输块大小 (默认: 128K)')
optimize_group.add_argument('--buffer-size', default='256K',
help='缓冲区大小 (默认: 256K)')
optimize_group.add_argument('--disable-ws', action='store_true',
help='禁用WebSocket (低端设备)')
optimize_group.add_argument('--disable-sse', action='store_true',
help='禁用SSE (低端设备)')
optimize_group.add_argument('--disable-hash', action='store_true',
help='禁用文件哈希计算 (低端设备)')
optimize_group.add_argument('--check-compat', action='store_true',
help='检查系统兼容性后退出')
# 日志配置
log_group = parser.add_argument_group('日志配置')
log_group.add_argument('--access-log', help='访问日志文件路径')
log_group.add_argument('--verbose', '-v', action='count', default=0, help='详细输出')
log_group.add_argument('--quiet', '-q', action='store_true', help='静默模式')
# 细粒度调试开关
debug_group = parser.add_argument_group('调试选项')
debug_group.add_argument('--debug', '-D', action='store_true',
help='启用所有调试输出')
debug_group.add_argument('--debug-log', dest='debug_log_file',
help='调试日志文件路径 (debug 输出将写入此文件)')
debug_group.add_argument('--debug-http', action='store_true',
help='调试 HTTP 请求')
debug_group.add_argument('--debug-auth', action='store_true',
help='调试 认证检查')
debug_group.add_argument('--debug-api', action='store_true',
help='调试 API 路由')
debug_group.add_argument('--debug-v2', action='store_true',
help='调试 V2 API')
debug_group.add_argument('--debug-error', action='store_true',
help='调试 错误堆栈')
debug_group.add_argument('--debug-download', action='store_true',
help='调试 下载记录')
debug_group.add_argument('--debug-types', '--debug-list',
nargs='+', metavar='TYPE',
choices=['http', 'auth', 'api', 'v2', 'error', 'download'],
help='指定调试类型列表 (http auth api v2 error download)')
# 配置文件
parser.add_argument('--settings', '--default-config', dest='settings',
help='默认配置文件路径 (settings.json)')
parser.add_argument('--config', help='覆盖配置文件路径 (JSON格式,会覆盖默认配置)')
return parser.parse_args()
def build_config_from_args(args):
"""从命令行参数构建配置"""
cmd_config = {}
# 服务器基本配置
basic_args = ['host', 'port', 'server_name', 'ssl_cert', 'ssl_key',
'auth_type', 'auth_user', 'auth_pass', 'auth_token',
'access_log', 'verbose',
'api_version', 'directory_listing',
'enable_stats', 'show_hash', 'ignore_hidden', 'max_upload_size']
for arg_name in basic_args:
arg_value = getattr(args, arg_name, None)
if arg_value is not None:
cmd_config[arg_name] = arg_value
# Debug 配置处理
# 优先级: --debug > --debug-types > individual --debug-xxx
debug_types = []
if args.debug_types:
# 用户指定了类型列表
debug_types = list(args.debug_types)
cmd_config['debug'] = debug_types
elif args.debug_http or args.debug_auth or args.debug_api or args.debug_v2 or args.debug_error or args.debug_download:
# 用户指定了单个类型
if args.debug_http:
debug_types.append('http')
if args.debug_auth:
debug_types.append('auth')
if args.debug_api:
debug_types.append('api')
if args.debug_v2:
debug_types.append('v2')
if args.debug_error:
debug_types.append('error')
if args.debug_download:
debug_types.append('download')
cmd_config['debug'] = debug_types
elif args.debug:
# 开启所有
cmd_config['debug'] = True
else:
# 未开启任何 debug
cmd_config['debug'] = False
# Debug 日志文件
if args.debug_log_file:
cmd_config['debug_log_file'] = args.debug_log_file
# 路径处理
if args.base_dir:
cmd_config['base_dir'] = os.path.abspath(args.base_dir)
# 实时通信
cmd_config['enable_ws'] = args.enable_ws
cmd_config['enable_sse'] = args.enable_sse
# 系统监控
cmd_config['enable_monitor'] = args.enable_monitor
cmd_config['monitor_interval'] = args.monitor_interval
# 同步
cmd_config['enable_sync'] = args.enable_sync
# 镜像加速源
cmd_config['enable_mirrors'] = args.enable_mirrors
# 各个镜像的启用/禁用通过 settings.json 中的 mirrors.xxx.enabled 配置
# 下载限速
cmd_config['rate_limit'] = args.rate_limit
# 静默模式
if args.quiet:
cmd_config['verbose'] = -1
return cmd_config
def main():
"""主入口函数"""
args = parse_arguments()
# 确定默认配置文件路径
project_root = os.path.dirname(os.path.abspath(__file__))
settings_path = args.settings or os.path.join(project_root, 'settings.json')
# 打印配置来源信息
print(f"[配置加载]")
print(f" 默认配置: {settings_path}")
# 加载配置(优先级从低到高:默认配置 -> 覆盖配置 -> 环境变量 -> 命令行参数)
# 使用深度合并,只替换覆盖配置中有的字段
# 1. 加载默认配置 (settings.json)
from core.config import load_json_config
default_config = load_json_config(settings_path) or {}
print(f" 默认配置加载: {'成功' if default_config else '使用内联默认'}")
# 2. 从覆盖配置文件加载
override_config = {}
if args.config:
override_config = load_json_config(args.config) or {}
print(f" 覆盖配置: {args.config} ({len(override_config)} 个顶层键)")
else:
print(f" 覆盖配置: 未指定")
# 3. 深度合并默认配置和覆盖配置
config = deep_merge(default_config, override_config)
# 4. 从环境变量加载数据库配置
env_db_config = load_db_config_from_env()
if env_db_config:
config = deep_merge(config, env_db_config)
# 5. 同步配置
if args.sync_config:
sync_config = load_json_config(args.sync_config)
if sync_config:
if 'sync_sources' in sync_config:
config['sync_sources'] = deep_merge(
config.get('sync_sources', {}),
sync_config['sync_sources']
)
if 'mirrors' in sync_config:
config['mirrors'] = deep_merge(
config.get('mirrors', {}),
sync_config['mirrors']
)
# 6. 命令行参数覆盖(最高优先级)
cmd_config = build_config_from_args(args)
config = deep_merge(config, cmd_config)
# 7. 处理 auth_token(优先从文件读取,否则生成新的)
import secrets
# 确定 token 文件路径
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
# 打包环境:使用 _MEIPASS 目录
token_file = os.path.join(sys._MEIPASS, 'auth_token.txt')
else:
# 非打包:使用脚本所在目录
token_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'auth_token.txt')
# 尝试从文件读取
existing_token = None
if os.path.exists(token_file):
try:
with open(token_file, 'r') as f:
existing_token = f.read().strip()
if existing_token:
config['auth_token'] = existing_token
print(f" 已从文件加载 auth_token: {config['auth_token'][:16]}...")
except Exception as e:
print(f" 警告: 读取 auth_token 失败: {e}")
# 如果没有现有 token,生成新的
if not existing_token:
config['auth_token'] = secrets.token_hex(32)
print(f" 已生成新的 auth_token: {config['auth_token'][:16]}...")
# 保存新的 token 到文件
try:
with open(token_file, 'w') as f:
f.write(config['auth_token'])
print(f" 已保存 auth_token 到: {token_file}")
except Exception as e:
print(f" 警告: 保存 auth_token 失败: {e}")
print(f" 最终配置: {len(config)} 个顶层配置项")
# ==================== 系统兼容性检查 ====================
if args.check_compat:
print("\n[系统兼容性检查]")
compat = check_compatibility()
print(f"\n兼容状态: {'✓ 通过' if compat['compatible'] else '✗ 存在问题'}")
arch = ArchitectureDetector.get_architecture()
print(f"\n架构信息:")
print(f" - 平台: {arch['platform']}")
print(f" - 机器: {arch['machine']}")
print(f" - 架构: {arch['architecture']}")
recommended = ArchitectureDetector.get_recommended_config()
print(f"\n推荐配置:")
for k, v in recommended.items():
print(f" - {k}: {v}")
if compat['warnings']:
print(f"\n警告:")
for w in compat['warnings']:
print(f" ⚠ {w}")
if compat['errors']:
print(f"\n错误:")
for e in compat['errors']:
print(f" ✗ {e}")
sys.exit(0 if compat['compatible'] else 1)
# ==================== 低端设备优化 ====================
print("\n[设备检测与优化]")
# 检测架构
arch = ArchitectureDetector.get_architecture()
# 应用低端设备配置
preset = config.get('preset', 'auto')
low_mem_config = LowMemoryConfig(preset)
# 获取设备信息
device_info = low_mem_config.get_device_info()
# 显示设备信息
if device_info['total_ram_mb'] > 0:
print(f" [硬件配置]")
print(f" - 总内存: {device_info['total_ram_mb']:.0f} MB")
print(f" - 可用内存: {device_info['available_ram_mb']:.0f} MB ({100-device_info['percent_used']:.1f}% 可用)")
print(f" - CPU核心: {device_info['cpu_count']} 核心")
print(f" - 系统架构: {arch['machine']} ({arch['architecture']})")
else:
print(f" - 系统架构: {arch['machine']} ({arch['architecture']})")
print(f"\n [性能优化]")
print(f" - 优化模式: {preset}")
# 应用配置
if preset != 'auto':
status = low_mem_config.get_status()
print(f" - 预设方案: {status.get('description', preset)}")
config = low_mem_config.apply_to_config(config)
# 覆盖命令行参数
if args.memory_limit:
config['memory_limit'] = parse_size(args.memory_limit)
if args.workers > 0:
config['workers'] = args.workers
if args.chunk_size:
config['chunk_size'] = parse_size(args.chunk_size)
if args.buffer_size:
config['buffer_size'] = parse_size(args.buffer_size)
# 禁用可选功能
if args.disable_ws:
config['enable_ws'] = False
if args.disable_sse:
config['enable_sse'] = False
if args.disable_hash:
config['calculate_hash'] = False
# 启动内存管理器
memory_manager = MemoryManager({
'enabled': True,
'memory_limit': config.get('memory_limit', 512 * 1024 * 1024),
'gc_interval': config.get('gc_interval', 300), # 定时GC间隔
'enable_scheduled_gc': config.get('enable_scheduled_gc', True)
})
memory_manager.start()
config['_memory_manager'] = memory_manager
mem_status = memory_manager.get_status()
print(f" - 内存限制: {mem_status['memory_limit_mb']} MB")
print(f" - 工作进程: {config.get('workers', 1)}")
print(f" - 传输块: {config.get('chunk_size', 128 * 1024) // 1024} KB")
print(f" - GC间隔: {config.get('gc_interval', 300)}秒")
print(f" - 缓存大小: {config.get('max_cache_size', 0) // (1024*1024)} MB")
# 设置信号处理
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# 打印启动信息
print()
print("╔" + "═" * 58 + "╗")
print("║" + " " * 22 + "HYC下载站 v2.3" + " " * 22 + "║")
print("╚" + "═" * 58 + "╝")
print()
print("【服务器配置】")
print(f" ▶ 监听地址: {config.get('host')}:{config.get('port')}")
print(f" ▶ 文件目录: {os.path.abspath(config.get('base_dir', './downloads'))}")
print(f" ▶ API版本: {config.get('api_version', 'v2')}")
print(f" ▶ 认证方式: {config.get('auth_type', 'none')}")
max_upload = config.get('max_upload_size', '1G')
if isinstance(max_upload, str):
max_upload = parse_size(max_upload)
else:
max_upload = int(max_upload)
print(f" ▶ 最大上传: {max_upload // (1024*1024)} MB")
# 数据库状态
db_config = config.get('database', {})
db_enabled = db_config.get('enabled', True)
print()
print("【功能模块】")
print(f" {'●' if config.get('enable_monitor') else '○'} 系统监控", end='')
print(f" {'●' if config.get('enable_sync') else '○'} 镜像同步", end='')
print(f" {'●' if config.get('enable_mirrors') else '○'} 加速源")
print(f" {'●' if config.get('enable_ws') else '○'} WebSocket", end='')
print(f" {'●' if config.get('enable_sse') else '○'} SSE", end='')
print(f" {'●' if db_enabled else '○'} 数据库")
if db_enabled:
print(f" └── 类型: {db_config.get('type', 'sqlite')} | 同步间隔: {db_config.get('sync_interval', 60)}s")
print()
print("【性能参数】")
print(f" ▶ 最大线程: {config.get('max_workers', 10)}")
print(f" ▶ 工作进程: {config.get('workers', 1)}")
print(f" ▶ 内存限制: {config.get('memory_limit', 512*1024*1024) // (1024*1024)} MB")
print(f" ▶ 连接超时: {config.get('timeout', 30)}s")
print()
print("=" * 60)
# 初始化数据库
db = None
scheduler = None
if db_enabled:
print("\n[初始化数据库...]")
try:
db = init_database(config)
# 健康检查
health = db.health_check()
if health.get('healthy'):
print(f" ✓ 数据库连接成功 ({db_config.get('type', 'sqlite')})")
print(f" ✓ 表数量: {health.get('table_count', 0)}")
else:
print(f" ✗ 数据库健康检查失败: {health.get('error')}")
# 检查并更新Schema
if db.check_schema_version():
print(f" ✓ 数据库结构版本检查通过")
# 获取统计
db_stats = db.get_stats()
print(f"\n 数据库统计:")
print(f" - 文件记录: {db_stats['total_files']}")
print(f" - 同步记录: {db_stats['total_sync_records']}")
print(f" - 缓存记录: {db_stats['total_cache_records']}")
print(f" - 下载记录: {db_stats['total_download_records']}")
# 将数据库实例添加到配置中
config['_db_instance'] = db
except Exception as e:
print(f"\n ✗ 数据库初始化失败: {e}")
print(" ⚠ 服务器将继续运行,但不使用数据库功能")
# 初始化同步调度器(不使用数据库)
if config.get('enable_sync'):
_, scheduler, _ = init_database_sync(config)
scheduler.start()
print(" - 同步调度器已启动")
# ==================== 优雅关闭处理 ====================
import atexit
def cleanup():
"""服务器关闭时清理资源"""
print("\n正在关闭服务器...")
# 关闭数据库连接池
if db:
try:
db.engine.dispose()
print("✓ 数据库连接已关闭")
except Exception as e:
print(f"✗ 关闭数据库连接时出错: {e}")
# 停止同步调度器
if scheduler:
try:
scheduler.stop()
print("✓ 同步调度器已停止")
except Exception as e:
print(f"✗ 停止同步调度器时出错: {e}")
print("服务器已关闭")
# 注册关闭处理函数
atexit.register(cleanup)
# 设置服务器启动时间(用于计算运行时间)
config['start_time'] = time.time()
# 创建并启动服务器
try:
server = MirrorServer(config)
if server.start():
server.serve_forever()
else:
print("服务器启动失败")
sys.exit(1)
except Exception as e:
print(f"错误: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()
+583
View File
@@ -0,0 +1,583 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
镜像源处理器包
支持 Docker、APT、YUM、PyPI、npm、Go、Maven、Gradle 等镜像源代理
"""
from .docker import DockerMirror
from .apt import APTMirror
from .yum import YUMMirror
from .pypi import PyPIMirror
from .npm import NpmMirror
from .go import GoProxy
from .http import HttpMirror
# 镜像处理器映射
# 特殊类型使用专用处理器,其他使用通用HTTP处理器
MIRROR_HANDLERS = {
# 专用处理器
'docker': DockerMirror,
'apt': APTMirror,
'yum': YUMMirror,
'pypi': PyPIMirror,
'npm': NpmMirror,
'go': GoProxy,
# ======== 包管理器 ========
# Python
'pip': HttpMirror,
'pipenv': HttpMirror,
'poetry': HttpMirror,
'conda': HttpMirror,
'anaconda': HttpMirror,
'bandersnatch': HttpMirror,
'twine': HttpMirror,
# Node.js
'yarn': HttpMirror,
'pnpm': HttpMirror,
'bower': HttpMirror,
# Java
'maven': HttpMirror,
'gradle': HttpMirror,
# .NET
'nuget': HttpMirror,
# Ruby
'gem': HttpMirror,
'rubygems': HttpMirror,
# Rust
'cargo': HttpMirror,
'rustup': HttpMirror,
# PHP
'composer': HttpMirror,
'packagist': HttpMirror,
# Swift/Apple
'cocoapods': HttpMirror,
# C/C++
'conan': HttpMirror,
'vcpkg': HttpMirror,
# Dart/Flutter
'pub': HttpMirror,
'dart': HttpMirror,
'flutter': HttpMirror,
# Haskell
'hackage': HttpMirror,
'stackage': HttpMirror,
# OCaml
'opam': HttpMirror,
# D语言
'dub': HttpMirror,
# Nim
'nimble': HttpMirror,
# V语言
'v': HttpMirror,
# Julia
'julia': HttpMirror,
# Lua
'lua': HttpMirror,
'luarocks': HttpMirror,
# Elm
'elm': HttpMirror,
# Perl
'cpan': HttpMirror,
'cpanm': HttpMirror,
# R
'cran': HttpMirror,
# LaTeX
'ctan': HttpMirror,
# Haskell
'haskell': HttpMirror,
# ======== 容器/云原生 ========
'helm': HttpMirror,
'kubernetes': HttpMirror,
' Quay.io': HttpMirror,
'quay': HttpMirror,
'ghcr': HttpMirror,
'gcr': HttpMirror,
'harbor': HttpMirror,
# ======== 开发工具 ========
'jetbrains': HttpMirror,
'vscode': HttpMirror,
'cuda': HttpMirror,
# ======== 语言运行时 ========
'node': HttpMirror,
'python': HttpMirror,
'ruby': HttpMirror,
'php': HttpMirror,
'java': HttpMirror,
'dotnet': HttpMirror,
# ======== Linux 发行版 ========
'alpine': HttpMirror,
'arch': HttpMirror,
'aur': HttpMirror,
'centos': HttpMirror,
'debian': HttpMirror,
'fedora': HttpMirror,
'gentoo': HttpMirror,
'opensuse': HttpMirror,
'void': HttpMirror,
'freebsd': HttpMirror,
'netbsd': HttpMirror,
'openbsd': HttpMirror,
'rocky': HttpMirror,
'alma': HttpMirror,
'kali': HttpMirror,
'ubuntu': HttpMirror,
'mint': HttpMirror,
'manjaro': HttpMirror,
'slackware': HttpMirror,
'mageia': HttpMirror,
'openmandriva': HttpMirror,
# ======== 系统包管理器 ========
'homebrew': HttpMirror,
'brew': HttpMirror,
'chocolatey': HttpMirror,
'snap': HttpMirror,
'flatpak': HttpMirror,
'appimage': HttpMirror,
'winget': HttpMirror,
'scoop': HttpMirror,
# ======== 数据库/运维 ========
'postgresql': HttpMirror,
'mysql': HttpMirror,
'mariadb': HttpMirror,
'mongodb': HttpMirror,
'redis': HttpMirror,
'influxdata': HttpMirror,
'grafana': HttpMirror,
'prometheus': HttpMirror,
'elastic': HttpMirror,
'bitnami': HttpMirror,
# ======== 代码托管/源码 ========
'git': HttpMirror,
'github': HttpMirror,
'gitlab': HttpMirror,
'bitbucket': HttpMirror,
'sourceforge': HttpMirror,
# ======== 其他 ========
'pacman': HttpMirror,
'nix': HttpMirror,
'guix': HttpMirror,
'termux': HttpMirror,
'msys2': HttpMirror,
'google-fonts': HttpMirror,
'aurora': HttpMirror,
'cloudflare': HttpMirror,
'fastly': HttpMirror,
# ======== 自定义 ========
'http': HttpMirror,
'custom': HttpMirror,
}
def get_mirror_handler(mirror_type: str):
"""获取镜像处理器类"""
return MIRROR_HANDLERS.get(mirror_type)
# 各镜像类型的默认上游URL
DEFAULT_UPSTREAM_URLS = {
# ======== 包管理器 ========
# Python
'pip': 'https://pypi.org/simple',
'pipenv': 'https://pypi.org/simple',
'poetry': 'https://pypi.org/simple',
'conda': 'https://repo.anaconda.com',
'anaconda': 'https://repo.anaconda.com',
'bandersnatch': 'https://pypi.org/simple',
'twine': 'https://pypi.org/simple',
# Node.js
'yarn': 'https://registry.yarnpkg.com',
'pnpm': 'https://registry.npmjs.org',
'bower': 'https://registry.bower.io',
# Java
'maven': 'https://repo1.maven.org/maven2',
'gradle': 'https://services.gradle.org/distributions',
# .NET
'nuget': 'https://api.nuget.org/v3',
# Ruby
'gem': 'https://rubygems.org',
'rubygems': 'https://rubygems.org',
# Rust
'cargo': 'https://crates.io',
'rustup': 'https://static.rust-lang.org',
# PHP
'composer': 'https://repo.packagist.org',
'packagist': 'https://repo.packagist.org',
# Swift/Apple
'cocoapods': 'https://cdn.cocoapods.org',
# C/C++
'conan': 'https://center.conan.io',
'vcpkg': 'https://github.com/microsoft/vcpkg/archive/refs/heads/main.tar.gz',
# Dart/Flutter
'pub': 'https://pub.dev',
'dart': 'https://storage.googleapis.com/flutter_infra_release',
'flutter': 'https://storage.flutter-io.cn',
# Haskell
'hackage': 'https://hackage.haskell.org',
'stackage': 'https://www.stackage.org',
# OCaml
'opam': 'https://opam.ocaml.org',
# D语言
'dub': 'https://code.dlang.org',
# Nim
'nimble': 'https://nimble.directory',
# V语言
'v': 'https://vpm.itsgriffin.com',
# Julia
'julia': 'https://pkg.julialang.org',
# Lua
'lua': 'https://luarocks.org',
'luarocks': 'https://luarocks.org',
# Elm
'elm': 'https://package.elm-lang.org',
# Perl
'cpan': 'https://www.cpan.org',
'cpanm': 'https://www.cpan.org',
# R
'cran': 'https://cran.r-project.org',
# LaTeX
'ctan': 'https://ctan.math.illinois.edu',
# Haskell
'haskell': 'https://haskell.org',
# ======== 容器/云原生 ========
'helm': 'https://charts.helm.sh',
'kubernetes': 'https://dl.k8s.io/release',
'quay': 'https://quay.io',
'quay.io': 'https://quay.io',
'ghcr': 'https://ghcr.io',
'gcr': 'https://gcr.io',
'harbor': 'https://harbor.io',
# ======== 开发工具 ========
'jetbrains': 'https://www.jetbrains.com',
'vscode': 'https://code.visualstudio.com',
'cuda': 'https://developer.download.nvidia.com/compute/cuda/repos',
# ======== 语言运行时 ========
'node': 'https://nodejs.org/dist',
'python': 'https://www.python.org/ftp/python',
'ruby': 'https://www.ruby-lang.org',
'php': 'https://www.php.net/distributions',
'java': 'https://download.oracle.com/java',
'dotnet': 'https://dotnetcli.azureedge.net',
# ======== Linux 发行版 ========
'alpine': 'https://dl-cdn.alpinelinux.org',
'arch': 'https://mirror.archlinux.org',
'aur': 'https://aur.archlinux.org',
'centos': 'https://mirrors.aliyun.com/centos',
'debian': 'https://deb.debian.org/debian',
'fedora': 'https://mirrors.fedoraproject.org',
'gentoo': 'https://distfiles.gentoo.org',
'opensuse': 'https://download.opensuse.org',
'void': 'https://repo.voidlinux.org',
'freebsd': 'https://pkg.freebsd.org',
'netbsd': 'https://cdn.netbsd.org',
'openbsd': 'https://cdn.openbsd.org',
'rocky': 'https://download.rockylinux.org',
'alma': 'https://repo.almalinux.org',
'kali': 'http://.kali.org',
'ubuntu': 'https://releases.ubuntu.com',
'mint': 'https://packages.linuxmint.com',
'manjaro': 'https://repo.manjaro.org',
'slackware': 'https://mirrors.slackware.com',
'mageia': 'https://www.mageia.org',
'openmandriva': 'https://download.openmandriva.org',
# ======== 系统包管理器 ========
'homebrew': 'https://github.com/Homebrew/brew',
'brew': 'https://github.com/Homebrew/brew',
'chocolatey': 'https://chocolatey.org/api/v2',
'snap': 'https://snapcraft.io',
'flatpak': 'https://flathub.org',
'appimage': 'https://github.com/AppImage/AppImageHub/releases',
'winget': 'https://github.com/microsoft/winget-pkgs',
'scoop': 'https://scoop.sh',
# ======== 数据库/运维 ========
'postgresql': 'https://www.postgresql.org',
'mysql': 'https://dev.mysql.com',
'mariadb': 'https://mariadb.org',
'mongodb': 'https://www.mongodb.org',
'redis': 'https://redis.io',
'influxdata': 'https://portal.influxdata.com',
'grafana': 'https://grafana.com',
'prometheus': 'https://prometheus.io',
'elastic': 'https://www.elastic.co',
'bitnami': 'https://bitnami.com',
# ======== 代码托管/源码 ========
'git': 'https://github.com',
'github': 'https://github.com',
'gitlab': 'https://gitlab.com',
'bitbucket': 'https://bitbucket.org',
'sourceforge': 'https://sourceforge.net',
# ======== 其他 ========
'pacman': 'https://mirror.archlinux.org',
'nix': 'https://nix-community.org',
'guix': 'https://guix.gnu.org',
'termux': 'https://termux.net',
'msys2': 'https://repo.msys2.org',
'google-fonts': 'https://fonts.google.com',
'aurora': 'https://auroralinux.org',
'cloudflare': 'https://cloudflare.com',
'fastly': 'https://fastly.com',
}
def get_default_upstream(mirror_type: str) -> str:
"""获取镜像类型的默认上游URL"""
return DEFAULT_UPSTREAM_URLS.get(mirror_type, 'https://mirror.example.com')
def list_available_mirrors() -> list:
"""列出可用的镜像类型"""
return [
# ======== 专用处理器 ========
{
'type': 'docker',
'name': 'Docker Registry',
'description': 'Docker镜像代理'
},
{
'type': 'apt',
'name': 'APT (Debian/Ubuntu)',
'description': 'APT软件源代理'
},
{
'type': 'yum',
'name': 'YUM/DNF (RHEL/CentOS)',
'description': 'YUM/DNF软件源代理'
},
{
'type': 'pypi',
'name': 'PyPI',
'description': 'Python包索引代理'
},
{
'type': 'npm',
'name': 'npm Registry',
'description': 'Node.js包管理器代理'
},
{
'type': 'go',
'name': 'Go Modules',
'description': 'Go模块代理'
},
# ======== 包管理器 ========
# Python
{'type': 'pip', 'name': 'pip', 'description': 'Python pip包管理器'},
{'type': 'pipenv', 'name': 'Pipenv', 'description': 'Python Pipenv代理'},
{'type': 'poetry', 'name': 'Poetry', 'description': 'Python Poetry包管理器'},
{'type': 'conda', 'name': 'Conda', 'description': 'Python Conda包管理器'},
{'type': 'anaconda', 'name': 'Anaconda', 'description': 'Anaconda Python发行版'},
# Node.js
{'type': 'yarn', 'name': 'Yarn', 'description': 'Node.js Yarn包管理器'},
{'type': 'pnpm', 'name': 'pnpm', 'description': 'Node.js pnpm包管理器'},
{'type': 'bower', 'name': 'Bower', 'description': '前端包管理器'},
# Java
{'type': 'maven', 'name': 'Maven Central', 'description': 'Java/Maven包管理器'},
{'type': 'gradle', 'name': 'Gradle', 'description': 'Gradle构建工具分发'},
# .NET
{'type': 'nuget', 'name': 'NuGet', 'description': '.NET包管理器'},
# Ruby
{'type': 'gem', 'name': 'RubyGems', 'description': 'Ruby包管理器'},
{'type': 'rubygems', 'name': 'RubyGems', 'description': 'Ruby官方仓库'},
# Rust
{'type': 'cargo', 'name': 'Crates.io', 'description': 'Rust语言包管理器'},
{'type': 'rustup', 'name': 'Rustup', 'description': 'Rust工具链'},
# PHP
{'type': 'composer', 'name': 'Composer', 'description': 'PHP包管理器'},
{'type': 'packagist', 'name': 'Packagist', 'description': 'PHP官方包仓库'},
# Swift/Apple
{'type': 'cocoapods', 'name': 'CocoaPods', 'description': 'iOS/macOS包管理器'},
# C/C++
{'type': 'conan', 'name': 'Conan', 'description': 'C/C++包管理器'},
{'type': 'vcpkg', 'name': 'vcpkg', 'description': 'C/C++包管理器'},
# Dart/Flutter
{'type': 'pub', 'name': 'Pub.dev', 'description': 'Dart/Flutter包管理器'},
{'type': 'dart', 'name': 'Dart SDK', 'description': 'Dart SDK'},
{'type': 'flutter', 'name': 'Flutter', 'description': 'Flutter SDK'},
# Haskell
{'type': 'hackage', 'name': 'Hackage', 'description': 'Haskell包管理器'},
{'type': 'stackage', 'name': 'Stackage', 'description': 'Haskell Stackage'},
# OCaml
{'type': 'opam', 'name': 'OPAM', 'description': 'OCaml包管理器'},
# D语言
{'type': 'dub', 'name': 'Dub', 'description': 'D语言包管理器'},
# Nim
{'type': 'nimble', 'name': 'Nimble', 'description': 'Nim语言包管理器'},
# V语言
{'type': 'v', 'name': 'V PM', 'description': 'V语言包管理器'},
# Julia
{'type': 'julia', 'name': 'Julia Packages', 'description': 'Julia语言包管理器'},
# Lua
{'type': 'lua', 'name': 'Lua', 'description': 'Lua语言'},
{'type': 'luarocks', 'name': 'LuaRocks', 'description': 'Lua包管理器'},
# Elm
{'type': 'elm', 'name': 'Elm Packages', 'description': 'Elm语言包管理器'},
# Perl
{'type': 'cpan', 'name': 'CPAN', 'description': 'Perl包管理器'},
{'type': 'cpanm', 'name': 'cpanm', 'description': 'Perl cpanminus'},
# R
{'type': 'cran', 'name': 'CRAN', 'description': 'R语言包仓库'},
# LaTeX
{'type': 'ctan', 'name': 'CTAN', 'description': 'LaTeX包管理器'},
# ======== 容器/云原生 ========
{'type': 'helm', 'name': 'Helm Charts', 'description': 'Kubernetes Helm包'},
{'type': 'kubernetes', 'name': 'Kubernetes', 'description': 'Kubernetes发行版'},
{'type': 'quay', 'name': 'Quay.io', 'description': 'Quay容器仓库'},
{'type': 'ghcr', 'name': 'GitHub Container Registry', 'description': 'GitHub容器仓库'},
{'type': 'gcr', 'name': 'Google Container Registry', 'description': 'Google容器仓库'},
{'type': 'harbor', 'name': 'Harbor', 'description': 'Harbor容器仓库'},
# ======== 开发工具 ========
{'type': 'jetbrains', 'name': 'JetBrains', 'description': 'JetBrains IDE'},
{'type': 'vscode', 'name': 'VS Code', 'description': 'Visual Studio Code'},
{'type': 'cuda', 'name': 'CUDA', 'description': 'NVIDIA CUDA工具包'},
# ======== 语言运行时 ========
{'type': 'node', 'name': 'Node.js', 'description': 'Node.js官方'},
{'type': 'python', 'name': 'Python', 'description': 'Python官方'},
{'type': 'ruby', 'name': 'Ruby', 'description': 'Ruby官方'},
{'type': 'php', 'name': 'PHP', 'description': 'PHP官方'},
{'type': 'java', 'name': 'Java JDK', 'description': 'Oracle/OpenJDK'},
{'type': 'dotnet', 'name': '.NET SDK', 'description': '.NET SDK'},
# ======== Linux 发行版 ========
{'type': 'alpine', 'name': 'Alpine Linux', 'description': 'Alpine Linux'},
{'type': 'arch', 'name': 'Arch Linux', 'description': 'Arch Linux'},
{'type': 'aur', 'name': 'AUR', 'description': 'Arch Linux AUR'},
{'type': 'centos', 'name': 'CentOS', 'description': 'CentOS'},
{'type': 'debian', 'name': 'Debian', 'description': 'Debian'},
{'type': 'fedora', 'name': 'Fedora', 'description': 'Fedora'},
{'type': 'gentoo', 'name': 'Gentoo', 'description': 'Gentoo'},
{'type': 'opensuse', 'name': 'openSUSE', 'description': 'openSUSE'},
{'type': 'void', 'name': 'Void Linux', 'description': 'Void Linux'},
{'type': 'freebsd', 'name': 'FreeBSD', 'description': 'FreeBSD'},
{'type': 'netbsd', 'name': 'NetBSD', 'description': 'NetBSD'},
{'type': 'openbsd', 'name': 'OpenBSD', 'description': 'OpenBSD'},
{'type': 'rocky', 'name': 'Rocky Linux', 'description': 'Rocky Linux'},
{'type': 'alma', 'name': 'AlmaLinux', 'description': 'AlmaLinux'},
{'type': 'kali', 'name': 'Kali Linux', 'description': 'Kali Linux'},
{'type': 'ubuntu', 'name': 'Ubuntu', 'description': 'Ubuntu'},
{'type': 'mint', 'name': 'Linux Mint', 'description': 'Linux Mint'},
{'type': 'manjaro', 'name': 'Manjaro', 'description': 'Manjaro Linux'},
{'type': 'slackware', 'name': 'Slackware', 'description': 'Slackware'},
{'type': 'mageia', 'name': 'Mageia', 'description': 'Mageia Linux'},
{'type': 'openmandriva', 'name': 'OpenMandriva', 'description': 'OpenMandriva'},
# ======== 系统包管理器 ========
{'type': 'homebrew', 'name': 'Homebrew', 'description': 'macOS Homebrew'},
{'type': 'brew', 'name': 'Brew', 'description': 'Homebrew'},
{'type': 'chocolatey', 'name': 'Chocolatey', 'description': 'Windows Chocolatey'},
{'type': 'snap', 'name': 'Snap Store', 'description': 'Snap应用商店'},
{'type': 'flatpak', 'name': 'Flatpak', 'description': 'Flatpak应用'},
{'type': 'appimage', 'name': 'AppImage', 'description': 'AppImage应用'},
{'type': 'winget', 'name': 'winget', 'description': 'Windows winget'},
{'type': 'scoop', 'name': 'Scoop', 'description': 'Windows Scoop'},
# ======== 数据库/运维 ========
{'type': 'postgresql', 'name': 'PostgreSQL', 'description': 'PostgreSQL数据库'},
{'type': 'mysql', 'name': 'MySQL', 'description': 'MySQL数据库'},
{'type': 'mariadb', 'name': 'MariaDB', 'description': 'MariaDB数据库'},
{'type': 'mongodb', 'name': 'MongoDB', 'description': 'MongoDB数据库'},
{'type': 'redis', 'name': 'Redis', 'description': 'Redis数据库'},
{'type': 'influxdata', 'name': 'InfluxData', 'description': 'InfluxDB时序数据库'},
{'type': 'grafana', 'name': 'Grafana', 'description': 'Grafana可视化'},
{'type': 'prometheus', 'name': 'Prometheus', 'description': 'Prometheus监控'},
{'type': 'elastic', 'name': 'Elastic', 'description': 'Elasticsearch'},
# ======== 代码托管 ========
{'type': 'github', 'name': 'GitHub', 'description': 'GitHub代码托管'},
{'type': 'gitlab', 'name': 'GitLab', 'description': 'GitLab代码托管'},
{'type': 'bitbucket', 'name': 'Bitbucket', 'description': 'Bitbucket代码托管'},
{'type': 'sourceforge', 'name': 'SourceForge', 'description': 'SourceForge开源托管'},
# ======== 其他 ========
{'type': 'nix', 'name': 'Nix/NixOS', 'description': 'Nix包管理器'},
{'type': 'guix', 'name': 'Guix', 'description': 'Guix包管理器'},
{'type': 'termux', 'name': 'Termux', 'description': 'Termux包管理器'},
{'type': 'msys2', 'name': 'MSYS2', 'description': 'MSYS2包管理器'},
{'type': 'google-fonts', 'name': 'Google Fonts', 'description': 'Google字体库'},
# ======== 自定义 ========
{'type': 'custom', 'name': '自定义HTTP', 'description': '自定义HTTP镜像源'}
]
+432
View File
@@ -0,0 +1,432 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
APT镜像代理处理器
支持Debian/Ubuntu软件源
"""
import os
import json
import time
import gzip
import re
import urllib.request
from typing import Dict, List, Optional, Tuple
from datetime import datetime
class APTMirror:
"""APT镜像代理"""
def __init__(self, config: dict):
self.config = config
# 配置 - 使用 storage_dir(基于 base_dir)
self.mirrors = config.get('mirrors', [
'http://archive.ubuntu.com/ubuntu',
'http://security.ubuntu.com/ubuntu'
])
self.storage_dir = config.get('storage_dir', './downloads/apt')
self.base_dir = config.get('base_dir', './downloads')
self.default_suite = config.get('suite', 'jammy')
self.default_components = config.get('components', ['main', 'restricted', 'universe', 'multiverse'])
self.default_arch = config.get('arch', 'amd64')
# 确保存储目录存在
os.makedirs(self.storage_dir, exist_ok=True)
def handle_request(self, handler, path: str) -> bool:
"""
处理APT请求
路径格式: /ubuntu/dists/jammy/main/binary-amd64/Packages.gz
"""
try:
# 解析路径
parts = path.strip('/').split('/')
if len(parts) < 5:
# 返回镜像列表或帮助信息
return self._handle_index(handler)
# 提取组件
distro = parts[0] # ubuntu, debian 等
dist_type = parts[1] # dists
suite = parts[2] # jammy, focal 等
component = parts[3] # main, updates 等
rest = '/'.join(parts[4:])
# 确定请求类型
if rest.endswith('Packages.gz'):
return self._handle_packages(handler, distro, suite, component, rest)
elif rest.endswith('Packages'):
return self._handle_packages_uncompressed(handler, distro, suite, component, rest)
elif rest.endswith('Release'):
return self._handle_release(handler, distro, suite, component, rest)
elif rest.endswith('Release.gpg'):
return self._handle_release_gpg(handler, distro, suite, component, rest)
elif rest.endswith('InRelease'):
return self._handle_inrelease(handler, distro, suite, component, rest)
else:
# 其他文件(源码包等)
return self._handle_file(handler, distro, suite, rest)
except Exception as e:
handler.send_error(500, str(e))
return False
def _handle_index(self, handler) -> bool:
"""处理索引请求"""
handler.send_json_response({
'mirrors': self.mirrors,
'default_suite': self.default_suite,
'default_components': self.default_components,
'cache_stats': self.get_cache_stats()
})
return True
def _handle_packages(self, handler, distro: str, suite: str, component: str, path: str) -> bool:
"""处理Packages.gz请求"""
cache_key = f"packages:{distro}:{suite}:{component}:{self.default_arch}"
# 检查架构
if 'binary-' in path:
arch = path.split('binary-')[1].split('/')[0]
else:
arch = self.default_arch
cache_key = f"packages:{distro}:{suite}:{component}:{arch}"
cached = self._get_cache(cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'application/x-gzip')
handler.send_header('Content-Length', str(len(cached)))
handler.end_headers()
handler.wfile.write(cached)
return True
# 从上游获取
for mirror in self.mirrors:
url = f"{mirror}/{path}"
try:
data = self._fetch(url)
if data:
# 缓存
if self.cache_enabled:
self._set_cache(cache_key, data)
handler.send_response(200)
handler.send_header('Content-Type', 'application/x-gzip')
handler.send_header('Content-Length', str(len(data)))
handler.end_headers()
handler.wfile.write(data)
return True
except Exception as e:
continue
handler.send_error(502, "Failed to fetch from all mirrors")
return False
def _handle_packages_uncompressed(self, handler, distro: str, suite: str, component: str, path: str) -> bool:
"""处理未压缩的Packages文件"""
# 先获取gz版本
gz_path = path + '.gz'
for mirror in self.mirrors:
url = f"{mirror}/{gz_path}"
try:
data = self._fetch(url)
if data:
# 解压
packages_data = gzip.decompress(data)
handler.send_response(200)
handler.send_header('Content-Type', 'text/plain')
handler.send_header('Content-Length', str(len(packages_data)))
handler.end_headers()
handler.wfile.write(packages_data)
return True
except Exception:
continue
handler.send_error(502, "Failed to fetch packages")
return False
def _handle_release(self, handler, distro: str, suite: str, component: str, path: str) -> bool:
"""处理Release文件"""
cache_key = f"release:{distro}:{suite}"
cached = self._get_cache(cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'text/plain')
handler.send_header('Content-Length', str(len(cached)))
handler.end_headers()
handler.wfile.write(cached)
return True
# 获取Release文件
release_path = f"/{distro}/dists/{suite}/Release"
for mirror in self.mirrors:
url = mirror + release_path
try:
data = self._fetch(url)
if data:
if self.cache_enabled:
self._set_cache(cache_key, data)
handler.send_response(200)
handler.send_header('Content-Type', 'text/plain')
handler.send_header('Content-Length', str(len(data)))
handler.end_headers()
handler.wfile.write(data)
return True
except Exception:
continue
handler.send_error(502, "Failed to fetch Release")
return False
def _handle_release_gpg(self, handler, distro: str, suite: str, component: str, path: str) -> bool:
"""处理Release.gpg文件"""
cache_key = f"release_gpg:{distro}:{suite}"
cached = self._get_cache(cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'application/pgp-signature')
handler.end_headers()
handler.wfile.write(cached)
return True
# 尝试获取
gpg_path = f"/{distro}/dists/{suite}/Release.gpg"
for mirror in self.mirrors:
url = mirror + gpg_path
try:
data = self._fetch(url)
if data:
if self.cache_enabled:
self._set_cache(cache_key, data)
handler.send_response(200)
handler.send_header('Content-Type', 'application/pgp-signature')
handler.end_headers()
handler.wfile.write(data)
return True
except Exception:
continue
handler.send_error(404, "Release.gpg not found")
return False
def _handle_inrelease(self, handler, distro: str, suite: str, component: str, path: str) -> bool:
"""处理InRelease文件 - 获取或生成签名后的Release信息"""
cache_key = f"inrelease:{distro}:{suite}"
cached = self._get_cache(cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'text/plain')
handler.send_header('Content-Length', str(len(cached)))
handler.end_headers()
handler.wfile.write(cached)
return True
# 尝试从上游获取 InRelease
inrelease_path = f"/{distro}/dists/{suite}/InRelease"
for mirror in self.mirrors:
url = mirror + inrelease_path
try:
data = self._fetch(url)
if data:
if self.cache_enabled:
self._set_cache(cache_key, data)
handler.send_response(200)
handler.send_header('Content-Type', 'text/plain')
handler.send_header('Content-Length', str(len(data)))
handler.end_headers()
handler.wfile.write(data)
return True
except Exception:
continue
# 如果没有 InRelease,尝试生成一个(基于 Release + 方括号注释)
# 注意:这不是有效的签名,但可以用于不验证签名的客户端
release_cache_key = f"release:{distro}:{suite}"
release_data = self._get_cache(release_cache_key)
if not release_data:
# 尝试获取 Release
release_path = f"/{distro}/dists/{suite}/Release"
for mirror in self.mirrors:
url = mirror + release_path
try:
release_data = self._fetch(url)
if release_data:
break
except Exception:
continue
if release_data:
# 添加注释说明这是未签名的 Release
comment = f"# Note: This is a synthesized InRelease (original InRelease not available)\n"
inrelease_data = comment + release_data.decode('utf-8', errors='replace')
if self.cache_enabled:
self._set_cache(cache_key, inrelease_data.encode())
handler.send_response(200)
handler.send_header('Content-Type', 'text/plain')
handler.send_header('Content-Length', str(len(inrelease_data)))
handler.end_headers()
handler.wfile.write(inrelease_data.encode())
return True
handler.send_error(502, "Failed to fetch InRelease")
return False
def _handle_file(self, handler, distro: str, suite: str, path: str) -> bool:
"""处理普通文件请求(如源码包)"""
cache_key = f"file:{distro}:{path.replace('/', ':')}"
cached = self._get_cache(cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'application/octet-stream')
handler.send_header('Content-Length', str(len(cached)))
handler.end_headers()
handler.wfile.write(cached)
return True
# 从上游获取
for mirror in self.mirrors:
url = f"{mirror}/{path}"
try:
data = self._fetch(url)
if data:
if self.cache_enabled:
self._set_cache(cache_key, data)
handler.send_response(200)
handler.send_header('Content-Type', 'application/octet-stream')
handler.send_header('Content-Length', str(len(data)))
handler.end_headers()
handler.wfile.write(data)
return True
except Exception:
continue
handler.send_error(404, "File not found")
return False
def _fetch(self, url: str) -> Optional[bytes]:
"""从URL获取数据"""
try:
req = urllib.request.Request(url)
req.add_header('User-Agent', 'APT-Mirror/1.0')
with urllib.request.urlopen(req, timeout=30) as response:
return response.read()
except Exception:
return None
def _get_cache(self, cache_key: str) -> Optional[bytes]:
"""获取缓存"""
if not self.cache_enabled:
return None
cache_path = self._get_cache_path(cache_key)
meta_path = cache_path + '.meta'
if not os.path.exists(cache_path):
return None
# 检查过期
if os.path.exists(meta_path):
try:
with open(meta_path, 'r') as f:
meta = json.load(f)
if time.time() > meta.get('expires', 0):
return None
except Exception:
pass
try:
with open(cache_path, 'rb') as f:
return f.read()
except Exception:
return None
def _set_cache(self, cache_key: str, data: bytes):
"""设置缓存"""
cache_path = self._get_cache_path(cache_key)
meta_path = cache_path + '.meta'
os.makedirs(os.path.dirname(cache_path), exist_ok=True)
try:
with open(cache_path, 'wb') as f:
f.write(data)
meta = {
'cached_at': time.time(),
'expires': time.time() + self.cache_ttl,
'size': len(data)
}
with open(meta_path, 'w') as f:
json.dump(meta, f)
except Exception as e:
print(f"APT缓存写入失败: {e}")
def _get_cache_path(self, cache_key: str) -> str:
"""获取缓存路径"""
subdir = cache_key[:2]
return os.path.join(self.storage_dir, subdir, cache_key)
def get_cache_stats(self) -> dict:
"""获取缓存统计"""
if not os.path.exists(self.storage_dir):
return {'files': 0, 'size': 0}
total_size = 0
file_count = 0
for root, dirs, files in os.walk(self.storage_dir):
for f in files:
if not f.endswith('.meta'):
file_count += 1
total_size += os.path.getsize(os.path.join(root, f))
return {
'files': file_count,
'size': total_size,
'size_formatted': self._format_size(total_size)
}
def _format_size(self, size_bytes: int) -> str:
"""格式化文件大小"""
if size_bytes == 0:
return "0 B"
units = ["B", "KB", "MB", "GB"]
i = 0
while size_bytes >= 1024 and i < len(units) - 1:
size_bytes /= 1024.0
i += 1
return f"{size_bytes:.2f} {units[i]}"
+359
View File
@@ -0,0 +1,359 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Docker镜像代理处理器
支持Docker Registry API v2
"""
import os
import json
import time
import uuid
import urllib.request
import base64
import hashlib
import hmac
from typing import Dict, List, Optional, Tuple
from datetime import datetime
class DockerMirror:
"""Docker镜像代理"""
def __init__(self, config: dict):
self.config = config
# 配置 - 使用 storage_dir(基于 base_dir)
self.registry_url = config.get('registry_url', 'https://registry-1.docker.io')
self.mirror_url = config.get('mirror_url', '')
self.storage_dir = config.get('storage_dir', './downloads/docker')
self.base_dir = config.get('base_dir', './downloads')
# 认证(可选)
self.username = config.get('username')
self.password = config.get('password')
# 确保存储目录存在
os.makedirs(self.storage_dir, exist_ok=True)
def handle_request(self, handler, path: str) -> bool:
"""
处理Docker镜像请求
路径格式: /v2/library/ubuntu/tags/list 或 /v2/library/ubuntu/manifests/latest
"""
try:
# 解析路径
parts = path.strip('/').split('/')
if len(parts) < 2 or parts[0] != 'v2':
handler.send_error(400, "Invalid Docker API path")
return False
# 提取组件
if parts[1] == 'library':
# 官方镜像
image = 'library/' + '/'.join(parts[2:-2]) if len(parts) > 4 else 'library/' + parts[2]
action = parts[-2] # tags 或 manifests
reference = parts[-1]
else:
# 非官方镜像
image = '/'.join(parts[1:-2])
action = parts[-2]
reference = parts[-1]
# 根据操作类型处理
if action == 'tags' and reference == 'list':
return self._handle_tag_list(handler, image.rstrip('/tags'))
elif action == 'manifests':
return self._handle_manifest(handler, image, reference)
elif action == 'blobs':
return self._handle_blob(handler, image, reference)
elif action == 'token':
return self._handle_token(handler)
else:
handler.send_error(404, "Unknown action")
return False
except Exception as e:
handler.send_error(500, str(e))
return False
def _handle_tag_list(self, handler, image: str) -> bool:
"""处理标签列表请求"""
cache_key = f"tags:{image}"
cached = self._get_cache(handler, cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'application/json')
handler.send_header('Content-Length', str(len(cached)))
handler.end_headers()
handler.wfile.write(cached)
return True
# 从上游获取
url = f"{self.registry_url}/v2/{image}/tags/list"
try:
data = self._fetch_from_upstream(url)
# 缓存
if self.cache_enabled:
self._set_cache(cache_key, data)
handler.send_response(200)
handler.send_header('Content-Type', 'application/json')
handler.send_header('Content-Length', str(len(data)))
handler.end_headers()
handler.wfile.write(data)
return True
except Exception as e:
handler.send_error(500, f"Failed to fetch tags: {str(e)}")
return False
def _handle_manifest(self, handler, image: str, reference: str) -> bool:
"""处理清单请求"""
cache_key = f"manifest:{image}:{reference}"
cached = self._get_cache(handler, cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'application/vnd.docker.distribution.manifest.v2+json')
handler.send_header('Content-Length', str(len(cached)))
handler.send_header('Docker-Content-Digest', f"sha256:{hashlib.sha256(cached).hexdigest()}")
handler.end_headers()
handler.wfile.write(cached)
return True
# 从上游获取
url = f"{self.registry_url}/v2/{image}/manifests/{reference}"
try:
req = urllib.request.Request(url)
req.add_header('Accept', 'application/vnd.docker.distribution.manifest.v2+json')
if self.username and self.password:
auth = base64.b64encode(f"{self.username}:{self.password}".encode()).decode()
req.add_header('Authorization', f"Basic {auth}")
with urllib.request.urlopen(req) as response:
data = response.read()
# 缓存
if self.cache_enabled:
self._set_cache(cache_key, data)
digest = f"sha256:{hashlib.sha256(data).hexdigest()}"
handler.send_response(200)
handler.send_header('Content-Type', 'application/vnd.docker.distribution.manifest.v2+json')
handler.send_header('Content-Length', str(len(data)))
handler.send_header('Docker-Content-Digest', digest)
handler.end_headers()
handler.wfile.write(data)
return True
except Exception as e:
handler.send_error(500, f"Failed to fetch manifest: {str(e)}")
return False
def _handle_blob(self, handler, image: str, digest: str) -> bool:
"""处理Blob层下载"""
# 移除 sha256: 前缀
if digest.startswith('sha256:'):
digest = digest[7:]
cache_key = f"blob:{digest}"
cached = self._get_cache(handler, cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'application/octet-stream')
handler.send_header('Content-Length', str(len(cached)))
handler.send_header('Docker-Content-Digest', f"sha256:{digest}")
handler.end_headers()
handler.wfile.write(cached)
return True
# 从上游获取
url = f"{self.registry_url}/v2/{image}/blobs/sha256:{digest}"
try:
req = urllib.request.Request(url)
if self.username and self.password:
auth = base64.b64encode(f"{self.username}:{self.password}".encode()).decode()
req.add_header('Authorization', f"Basic {auth}")
with urllib.request.urlopen(req) as response:
data = response.read()
# 缓存
if self.cache_enabled:
self._set_cache(cache_key, data)
handler.send_response(200)
handler.send_header('Content-Type', 'application/octet-stream')
handler.send_header('Content-Length', str(len(data)))
handler.send_header('Docker-Content-Digest', f"sha256:{digest}")
handler.end_headers()
handler.wfile.write(data)
return True
except Exception as e:
handler.send_error(500, f"Failed to fetch blob: {str(e)}")
return False
def _handle_token(self, handler) -> bool:
"""处理Token请求 - 生成真实的访问令牌"""
# 解析认证信息
auth_header = handler.headers.get('Authorization', '')
username = None
password = None
if auth_header.startswith('Basic '):
try:
decoded = base64.b64decode(auth_header[6:]).decode('utf-8')
username, password = decoded.split(':', 1)
except Exception:
pass
# 验证凭据(如果有)
if self.username and self.password:
if username != self.username or password != self.password:
handler.send_error(401, "Invalid credentials")
return False
# 生成唯一的访问令牌
token_id = str(uuid.uuid4())
issued_at = int(time.time())
expires_in = 300 # 5分钟
expires_at = issued_at + expires_in
# 创建令牌信息(简化版 JWT 结构)
token_data = {
"iss": "hyc-mirror",
"sub": username or "anonymous",
"aud": self.registry_url,
"iat": issued_at,
"exp": expires_at,
"access": [
{"type": "repository", "actions": ["pull"]},
{"type": "registry", "actions": ["catalog"]}
]
}
# 使用 HMAC-SHA256 对令牌进行简单签名
secret_key = f"hyc-mirror-{self.registry_url}".encode()
signature = hmac.new(
secret_key,
f"{token_id}:{issued_at}".encode(),
hashlib.sha256
).hexdigest()[:32]
full_token = f"{token_id}-{signature}"
handler.send_json_response({
"token": full_token,
"expires_in": expires_in,
"issued_at": issued_at
})
return True
def _fetch_from_upstream(self, url: str) -> bytes:
"""从上游获取数据"""
req = urllib.request.Request(url)
if self.username and self.password:
auth = base64.b64encode(f"{self.username}:{self.password}".encode()).decode()
req.add_header('Authorization', f"Basic {auth}")
with urllib.request.urlopen(req, timeout=30) as response:
return response.read()
def _get_cache(self, handler, cache_key: str) -> Optional[bytes]:
"""获取缓存"""
if not self.cache_enabled:
return None
cache_path = self._get_cache_path(cache_key)
meta_path = cache_path + '.meta'
if not os.path.exists(cache_path):
return None
# 检查是否过期
if os.path.exists(meta_path):
try:
with open(meta_path, 'r') as f:
meta = json.load(f)
if time.time() > meta.get('expires', 0):
return None
except Exception:
pass
try:
with open(cache_path, 'rb') as f:
return f.read()
except Exception:
return None
def _set_cache(self, cache_key: str, data: bytes):
"""设置缓存"""
cache_path = self._get_cache_path(cache_key)
meta_path = cache_path + '.meta'
os.makedirs(os.path.dirname(cache_path), exist_ok=True)
try:
with open(cache_path, 'wb') as f:
f.write(data)
meta = {
'cached_at': time.time(),
'expires': time.time() + self.cache_ttl,
'size': len(data)
}
with open(meta_path, 'w') as f:
json.dump(meta, f)
except Exception as e:
print(f"Docker缓存写入失败: {e}")
def _get_cache_path(self, cache_key: str) -> str:
"""获取缓存路径"""
subdir = cache_key[:2]
return os.path.join(self.storage_dir, subdir, cache_key)
def get_cache_stats(self) -> dict:
"""获取缓存统计"""
if not os.path.exists(self.storage_dir):
return {'files': 0, 'size': 0}
total_size = 0
file_count = 0
for root, dirs, files in os.walk(self.storage_dir):
for f in files:
if not f.endswith('.meta'):
file_count += 1
total_size += os.path.getsize(os.path.join(root, f))
return {
'files': file_count,
'size': total_size,
'size_formatted': self._format_size(total_size)
}
def _format_size(self, size_bytes: int) -> str:
"""格式化文件大小"""
if size_bytes == 0:
return "0 B"
units = ["B", "KB", "MB", "GB"]
i = 0
while size_bytes >= 1024 and i < len(units) - 1:
size_bytes /= 1024.0
i += 1
return f"{size_bytes:.2f} {units[i]}"
+525
View File
@@ -0,0 +1,525 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Go模块代理处理器
支持Go模块代理协议
"""
import os
import json
import time
import urllib.request
import urllib.parse
import urllib.error
from typing import Dict, List, Optional
from datetime import datetime
class GoProxy:
"""Go模块代理"""
def __init__(self, config: dict):
self.config = config
# 配置 - 使用 storage_dir(基于 base_dir)
self.upstream_url = config.get('upstream_url', 'https://proxy.golang.org')
self.storage_dir = config.get('storage_dir', './downloads/go')
self.base_dir = config.get('base_dir', './downloads')
self.mode = config.get('mode', 'proxy') # proxy | direct
# 确保存储目录存在
os.makedirs(self.storage_dir, exist_ok=True)
def handle_request(self, handler, path: str) -> bool:
"""
处理Go模块请求
路径格式: /golang.org/x/net/@v/list
/golang.org/x/net/@v/v1.0.0.info
/golang.org/x/net/@v/v1.0.0.zip
/golang.org/x/net/@latest
"""
try:
parts = path.strip('/').split('/')
if len(parts) < 2:
return self._handle_index(handler)
# 解析模块路径和操作
module_parts = []
for i, part in enumerate(parts):
if part.startswith('@'):
# 找到操作部分
module_path = '/'.join(parts[:i])
action = parts[i:]
break
else:
# 没有找到操作符
module_path = '/'.join(parts)
action = []
if not action:
handler.send_error(400, "Invalid Go module path")
return False
action_type = action[0]
if action_type == '@v':
# 版本相关操作
if len(action) >= 3:
version = action[2]
return self._handle_version(handler, module_path, version)
elif len(action) == 2:
# /@v/list
return self._handle_version_list(handler, module_path)
elif action_type == '@latest':
# /@latest
return self._handle_latest(handler, module_path)
elif action_type == '@all':
# /@all
return self._handle_all(handler, module_path)
elif action_type == '@list':
# /@list
return self._handle_module_list(handler, module_path)
else:
handler.send_error(400, f"Unknown action: {action_type}")
return False
except Exception as e:
handler.send_error(500, str(e))
return False
def _handle_index(self, handler) -> bool:
"""处理索引请求"""
handler.send_json_response({
'proxy_url': self.upstream_url,
'mode': self.mode,
'cache_stats': self.get_cache_stats()
})
return True
def _handle_version_list(self, handler, module: str) -> bool:
"""处理版本列表请求 /@v/list"""
cache_key = f"vlist:{module}"
cached = self._get_cache(cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'text/plain; charset=utf-8')
handler.end_headers()
handler.wfile.write(cached)
return True
# 从上游获取
url = f"{self.upstream_url}/{module}/@v/list"
try:
data = self._fetch(url)
if self.cache_enabled:
self._set_cache(cache_key, data)
handler.send_response(200)
handler.send_header('Content-Type', 'text/plain; charset=utf-8')
handler.end_headers()
handler.wfile.write(data)
return True
except urllib.error.HTTPError as e:
if e.code == 404:
handler.send_error(404, f"Module not found: {module}")
else:
handler.send_error(502, f"Failed to fetch: {str(e)}")
return False
def _handle_version_info(self, handler, module: str, version: str) -> bool:
"""处理版本信息请求 /@v/version.info"""
cache_key = f"info:{module}:{version}"
cached = self._get_cache(cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'application/json')
handler.end_headers()
handler.wfile.write(cached)
return True
url = f"{self.upstream_url}/{module}/@v/{version}.info"
try:
data = self._fetch(url)
if self.cache_enabled:
self._set_cache(cache_key, data)
handler.send_response(200)
handler.send_header('Content-Type', 'application/json')
handler.end_headers()
handler.wfile.write(data)
return True
except urllib.error.HTTPError as e:
handler.send_error(502, f"Failed to fetch info: {str(e)}")
return False
def _handle_version(self, handler, module: str, suffix: str) -> bool:
"""处理版本相关请求"""
if suffix.endswith('.info'):
version = suffix[:-5]
return self._handle_version_info(handler, module, version)
elif suffix.endswith('.zip'):
version = suffix[:-4]
return self._handle_zip(handler, module, version)
elif suffix.endswith('.mod'):
version = suffix[:-4]
return self._handle_mod(handler, module, version)
elif suffix.endswith('.sum'):
version = suffix[:-4]
return self._handle_sum(handler, module, version)
else:
handler.send_error(400, f"Unknown suffix: {suffix}")
return False
def _handle_latest(self, handler, module: str) -> bool:
"""处理最新版本请求 /@latest"""
cache_key = f"latest:{module}"
cached = self._get_cache(cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'application/json')
handler.end_headers()
handler.wfile.write(cached)
return True
url = f"{self.upstream_url}/{module}/@latest"
try:
data = self._fetch(url)
if self.cache_enabled:
self._set_cache(cache_key, data)
handler.send_response(200)
handler.send_header('Content-Type', 'application/json')
handler.end_headers()
handler.wfile.write(data)
return True
except urllib.error.HTTPError as e:
handler.send_error(502, f"Failed to fetch latest: {str(e)}")
return False
def _handle_zip(self, handler, module: str, version: str) -> bool:
"""处理zip下载"""
cache_key = f"zip:{module}:{version}"
cached = self._get_cache(cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'application/zip')
handler.send_header('Content-Length', str(len(cached)))
handler.end_headers()
handler.wfile.write(cached)
return True
url = f"{self.upstream_url}/{module}/@v/{version}.zip"
try:
data = self._fetch(url)
if self.cache_enabled:
self._set_cache(cache_key, data)
handler.send_response(200)
handler.send_header('Content-Type', 'application/zip')
handler.send_header('Content-Length', str(len(data)))
handler.end_headers()
handler.wfile.write(data)
return True
except urllib.error.HTTPError as e:
handler.send_error(502, f"Failed to fetch zip: {str(e)}")
return False
def _handle_mod(self, handler, module: str, version: str) -> bool:
"""处理mod文件"""
cache_key = f"mod:{module}:{version}"
cached = self._get_cache(cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'text/plain; charset=utf-8')
handler.send_header('Content-Length', str(len(cached)))
handler.end_headers()
handler.wfile.write(cached)
return True
url = f"{self.upstream_url}/{module}/@v/{version}.mod"
try:
data = self._fetch(url)
if self.cache_enabled:
self._set_cache(cache_key, data)
handler.send_response(200)
handler.send_header('Content-Type', 'text/plain; charset=utf-8')
handler.send_header('Content-Length', str(len(data)))
handler.end_headers()
handler.wfile.write(data)
return True
except urllib.error.HTTPError as e:
handler.send_error(502, f"Failed to fetch mod: {str(e)}")
return False
def _handle_sum(self, handler, module: str, version: str) -> bool:
"""处理sum文件"""
cache_key = f"sum:{module}:{version}"
cached = self._get_cache(cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'text/plain; charset=utf-8')
handler.end_headers()
handler.wfile.write(cached)
return True
url = f"{self.upstream_url}/{module}/@v/{version}.sum"
try:
data = self._fetch(url)
if self.cache_enabled:
self._set_cache(cache_key, data)
handler.send_response(200)
handler.send_header('Content-Type', 'text/plain; charset=utf-8')
handler.end_headers()
handler.wfile.write(data)
return True
except urllib.error.HTTPError as e:
if e.code == 404:
# 没有sum文件时返回空
handler.send_response(200)
handler.send_header('Content-Type', 'text/plain; charset=utf-8')
handler.end_headers()
handler.wfile.write(b'')
else:
handler.send_error(502, f"Failed to fetch sum: {str(e)}")
return False
def _handle_all(self, handler, module: str) -> bool:
"""处理/@all请求 - 返回模块及其所有依赖的zip包"""
cache_key = f"all:{module}"
cached = self._get_cache(cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'application/zip')
handler.send_header('Content-Length', str(len(cached)))
handler.end_headers()
handler.wfile.write(cached)
return True
# 从上游获取所有依赖的zip
url = f"{self.upstream_url}/{module}/@all.zip"
try:
data = self._fetch(url)
if not data:
handler.send_error(404, f"Module not found: {module}")
return False
if self.cache_enabled:
self._set_cache(cache_key, data)
handler.send_response(200)
handler.send_header('Content-Type', 'application/zip')
handler.send_header('Content-Length', str(len(data)))
handler.end_headers()
handler.wfile.write(data)
return True
except urllib.error.HTTPError as e:
if e.code == 404:
handler.send_error(404, f"Module not found: {module}")
else:
handler.send_error(502, f"Failed to fetch @all: {str(e)}")
return False
def _handle_module_list(self, handler, module: str) -> bool:
"""处理/@list请求 - 返回模块及其依赖的路径列表"""
cache_key = f"list:{module}"
cached = self._get_cache(cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'text/plain; charset=utf-8')
handler.end_headers()
handler.wfile.write(cached)
return True
# 首先获取模块的 go.mod 文件以提取依赖
mod_url = f"{self.upstream_url}/{module}/@v/{module}.mod"
try:
mod_data = self._fetch(mod_url)
if not mod_data:
handler.send_error(404, f"Module not found: {module}")
return False
# 解析go.mod获取依赖
modules = [module]
mod_content = mod_data.decode('utf-8', errors='replace')
# 提取require语句中的依赖
import re
require_pattern = r'require\s+\(([^\)]+)\)'
inline_require_pattern = r'require\s+([^\s]+)\s+([^\s]+)'
# 处理多行require
matches = re.findall(require_pattern, mod_content, re.DOTALL)
for match in matches:
for line in match.strip().split('\n'):
line = line.strip()
if line and not line.startswith('//'):
parts = line.split()
if parts:
modules.append(parts[0])
# 处理单行require
matches = re.findall(inline_require_pattern, mod_content)
for match in matches:
if match[0] not in modules:
modules.append(match[0])
# 生成列表输出
list_output = '\n'.join(sorted(set(modules))) + '\n'
if self.cache_enabled:
self._set_cache(cache_key, list_output.encode())
handler.send_response(200)
handler.send_header('Content-Type', 'text/plain; charset=utf-8')
handler.end_headers()
handler.wfile.write(list_output.encode())
return True
except urllib.error.HTTPError as e:
if e.code == 404:
handler.send_error(404, f"Module not found: {module}")
else:
handler.send_error(502, f"Failed to fetch @list: {str(e)}")
return False
def _fetch(self, url: str) -> Optional[bytes]:
"""从URL获取数据"""
try:
req = urllib.request.Request(url)
req.add_header('User-Agent', 'Go-Mirror/1.0')
with urllib.request.urlopen(req, timeout=60) as response:
return response.read()
except Exception:
return None
def _get_cache(self, cache_key: str) -> Optional[bytes]:
"""获取缓存"""
if not self.cache_enabled:
return None
cache_path = self._get_cache_path(cache_key)
meta_path = cache_path + '.meta'
if not os.path.exists(cache_path):
return None
if os.path.exists(meta_path):
try:
with open(meta_path, 'r') as f:
meta = json.load(f)
if time.time() > meta.get('expires', 0):
return None
except Exception:
pass
try:
with open(cache_path, 'rb') as f:
return f.read()
except Exception:
return None
def _set_cache(self, cache_key: str, data: bytes):
"""设置缓存"""
cache_path = self._get_cache_path(cache_key)
meta_path = cache_path + '.meta'
os.makedirs(os.path.dirname(cache_path), exist_ok=True)
try:
with open(cache_path, 'wb') as f:
f.write(data)
meta = {
'cached_at': time.time(),
'expires': time.time() + self.cache_ttl,
'size': len(data)
}
with open(meta_path, 'w') as f:
json.dump(meta, f)
except Exception as e:
print(f"Go缓存写入失败: {e}")
def _get_cache_path(self, cache_key: str) -> str:
"""获取缓存路径"""
subdir = cache_key[:2]
return os.path.join(self.storage_dir, subdir, cache_key)
def get_cache_stats(self) -> dict:
"""获取缓存统计"""
if not os.path.exists(self.storage_dir):
return {'files': 0, 'size': 0}
total_size = 0
file_count = 0
for root, dirs, files in os.walk(self.storage_dir):
for f in files:
if not f.endswith('.meta'):
file_count += 1
total_size += os.path.getsize(os.path.join(root, f))
return {
'files': file_count,
'size': total_size,
'size_formatted': self._format_size(total_size)
}
def _format_size(self, size_bytes: int) -> str:
"""格式化文件大小"""
if size_bytes == 0:
return "0 B"
units = ["B", "KB", "MB", "GB"]
i = 0
while size_bytes >= 1024 and i < len(units) - 1:
size_bytes /= 1024.0
i += 1
return f"{size_bytes:.2f} {units[i]}"
+485
View File
@@ -0,0 +1,485 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
通用HTTP镜像代理处理器
支持Maven、Gradle、RubyGems、Cargo、NuGet、CocoaPods、CRAN、CTAN、CUDA、Pacman等基于HTTP的镜像
"""
import os
import re
import json
import time
import urllib.request
import urllib.parse
import urllib.error
from typing import Dict, List, Optional
from datetime import datetime
from html import escape
class HttpMirror:
"""通用HTTP镜像代理 - 支持多种包管理器"""
# 不同包管理器的默认上游URL
DEFAULT_UPSTREAM = {
'maven': 'https://repo1.maven.org/maven2',
'gradle': 'https://services.gradle.org/distributions',
'gem': 'https://rubygems.org',
'cargo': 'https://crates.io',
'nuget': 'https://api.nuget.org/v3',
'cocoapods': 'https://cdn.cocoapods.org',
'cran': 'https://cran.r-project.org',
'ctan': 'https://ctan.math.illinois.edu',
'cuda': 'https://developer.download.nvidia.com/compute/cuda/repos',
'pacman': 'https://mirror.archlinux.org',
}
def __init__(self, config: dict):
self.config = config
self.mirror_type = config.get('type', 'http')
# 配置
self.upstream_url = config.get('upstream_url', self.DEFAULT_UPSTREAM.get(self.mirror_type, 'https://mirror.example.com'))
self.storage_dir = config.get('storage_dir', f'./downloads/{self.mirror_type}')
self.base_dir = config.get('base_dir', './downloads')
self.cache_enabled = config.get('cache_enabled', True)
self.cache_ttl = config.get('cache_ttl', 3600) # 默认1小时
# 确保存储目录存在
os.makedirs(self.storage_dir, exist_ok=True)
def handle_request(self, handler, path: str) -> bool:
"""处理HTTP镜像请求"""
try:
# 移除前导斜杠
path = path.strip('/')
if not path or path == self.mirror_type:
return self._handle_index(handler)
# 根据镜像类型分发请求
if self.mirror_type == 'maven':
return self._handle_maven(handler, path)
elif self.mirror_type == 'gradle':
return self._handle_gradle(handler, path)
elif self.mirror_type == 'gem':
return self._handle_gem(handler, path)
elif self.mirror_type == 'cargo':
return self._handle_cargo(handler, path)
elif self.mirror_type == 'nuget':
return self._handle_nuget(handler, path)
elif self.mirror_type == 'cocoapods':
return self._handle_cocoapods(handler, path)
elif self.mirror_type == 'cran':
return self._handle_cran(handler, path)
elif self.mirror_type == 'ctan':
return self._handle_ctan(handler, path)
elif self.mirror_type == 'cuda':
return self._handle_cuda(handler, path)
elif self.mirror_type == 'pacman':
return self._handle_pacman(handler, path)
else:
# 默认作为普通HTTP文件处理
return self._handle_generic(handler, path)
except Exception as e:
handler.send_error(500, str(e))
return False
def _handle_index(self, handler) -> bool:
"""返回镜像索引信息"""
handler.send_json_response({
'type': self.mirror_type,
'upstream_url': self.upstream_url,
'storage_dir': self.storage_dir,
'cache_enabled': self.cache_enabled,
'cache_stats': self.get_cache_stats()
})
return True
# ========== Maven ==========
def _handle_maven(self, handler, path: str) -> bool:
"""处理Maven请求: /groupId/artifactId/version/artifactId-version.jar"""
# 先检查本地
local_path = os.path.join(self.storage_dir, path)
if os.path.exists(local_path) and os.path.isfile(local_path):
return self._serve_local_file(handler, local_path)
# 从上游获取
url = f"{self.upstream_url}/{path}"
return self._proxy_request(handler, url)
# ========== Gradle ==========
def _handle_gradle(self, handler, path: str) -> bool:
"""处理Gradle请求: /gradle-x.x-bin.zip"""
# 先检查本地
local_path = os.path.join(self.storage_dir, path)
if os.path.exists(local_path) and os.path.isfile(local_path):
return self._serve_local_file(handler, local_path)
# 从上游获取
url = f"{self.upstream_url}/{path}"
return self._proxy_request(handler, url)
# ========== RubyGems ==========
def _handle_gem(self, handler, path: str) -> bool:
"""处理RubyGems请求"""
# API请求: /api/v1/gems/xxx.json
# Spec请求: /quick/Marshal.4.8/xxx-x.x.0.gemspec.rz
# 下载请求: /gems/xxx-x.x.0.gem
# 先检查本地
local_path = os.path.join(self.storage_dir, path)
if os.path.exists(local_path) and os.path.isfile(local_path):
return self._serve_local_file(handler, local_path)
# 从上游获取
url = f"{self.upstream_url}/{path}"
return self._proxy_request(handler, url)
# ========== Cargo ==========
def _handle_cargo(self, handler, path: str) -> bool:
"""处理Cargo请求"""
# API: /api/v1/crates
# 下载: /crates/xxx/xxx-x.x.x.crate
# 先检查本地
local_path = os.path.join(self.storage_dir, path)
if os.path.exists(local_path) and os.path.isfile(local_path):
return self._serve_local_file(handler, local_path)
# 从上游获取
url = f"{self.upstream_url}/{path}"
return self._proxy_request(handler, url)
# ========== NuGet ==========
def _handle_nuget(self, handler, path: str) -> bool:
"""处理NuGet请求"""
# API v3: /v3-flatcontainer/xxx.nupkg
# 先检查本地
local_path = os.path.join(self.storage_dir, path)
if os.path.exists(local_path) and os.path.isfile(local_path):
return self._serve_local_file(handler, local_path)
# 从上游获取
url = f"{self.upstream_url}/{path}"
return self._proxy_request(handler, url)
# ========== CocoaPods ==========
def _handle_cocoapods(self, handler, path: str) -> bool:
"""处理CocoaPods请求"""
# Specs: /Specs/xxx.podspec.json
# Pods: /Pods/xxx/xxx.pod
# 先检查本地
local_path = os.path.join(self.storage_dir, path)
if os.path.exists(local_path) and os.path.isfile(local_path):
return self._serve_local_file(handler, local_path)
# 从上游获取
url = f"{self.upstream_url}/{path}"
return self._proxy_request(handler, url)
# ========== CRAN ==========
def _handle_cran(self, handler, path: str) -> bool:
"""处理CRAN请求"""
# /src/contrib/xxx.tar.gz
# 先检查本地
local_path = os.path.join(self.storage_dir, path)
if os.path.exists(local_path) and os.path.isfile(local_path):
return self._serve_local_file(handler, local_path)
# 从上游获取
url = f"{self.upstream_url}/{path}"
return self._proxy_request(handler, url)
# ========== CTAN ==========
def _handle_ctan(self, handler, path: str) -> bool:
"""处理CTAN请求"""
# /macros/latex/xxx.zip
# 先检查本地
local_path = os.path.join(self.storage_dir, path)
if os.path.exists(local_path) and os.path.isfile(local_path):
return self._serve_local_file(handler, local_path)
# 从上游获取
url = f"{self.upstream_url}/{path}"
return self._proxy_request(handler, url)
# ========== CUDA ==========
def _handle_cuda(self, handler, path: str) -> bool:
"""处理CUDA请求"""
# /xxx/xxx.deb
# 先检查本地
local_path = os.path.join(self.storage_dir, path)
if os.path.exists(local_path) and os.path.isfile(local_path):
return self._serve_local_file(handler, local_path)
# 从上游获取
url = f"{self.upstream_url}/{path}"
return self._proxy_request(handler, url)
# ========== Pacman ==========
def _handle_pacman(self, handler, path: str) -> bool:
"""处理Pacman请求"""
# /os/x86_64/xxx.db.tar.gz
# /os/x86_64/xxx-x.x.x-x-x86_64.pkg.tar.zst
# 先检查本地
local_path = os.path.join(self.storage_dir, path)
if os.path.exists(local_path) and os.path.isfile(local_path):
return self._serve_local_file(handler, local_path)
# 从上游获取
url = f"{self.upstream_url}/{path}"
return self._proxy_request(handler, url)
# ========== Generic HTTP ==========
def _handle_generic(self, handler, path: str) -> bool:
"""通用的HTTP文件代理"""
# 先检查本地
local_path = os.path.join(self.storage_dir, path)
if os.path.exists(local_path) and os.path.isfile(local_path):
return self._serve_local_file(handler, local_path)
# 从上游获取
url = f"{self.upstream_url}/{path}"
return self._proxy_request(handler, url)
# ========== 辅助方法 ==========
def _serve_local_file(self, handler, local_path: str) -> bool:
"""服务本地文件"""
try:
file_size = os.path.getsize(local_path)
# 尝试确定内容类型
content_type = self._guess_content_type(local_path)
handler.send_response(200)
handler.send_header('Content-Type', content_type)
handler.send_header('Content-Length', str(file_size))
handler.send_header('X-Local', 'true')
handler.end_headers()
with open(local_path, 'rb') as f:
handler.wfile.write(f.read())
return True
except Exception as e:
handler.send_error(500, f"Error serving file: {str(e)}")
return False
def _proxy_request(self, handler, url: str) -> bool:
"""代理请求到上游"""
try:
# 检查缓存
cache_key = self._get_cache_key(url)
if self.cache_enabled:
cached = self._get_cache(cache_key)
if cached:
return self._serve_cached(handler, cached, url)
# 从上游获取
req = urllib.request.Request(url)
req.add_header('User-Agent', f'HTTP-Mirror/1.0 ({self.mirror_type})')
# 处理Range请求
range_header = handler.headers.get('Range')
if range_header:
req.add_header('Range', range_header)
try:
response = urllib.request.urlopen(req, timeout=60)
except urllib.error.HTTPError as e:
if e.code == 404:
handler.send_error(404, f"File not found: {url}")
else:
handler.send_error(502, f"Upstream error: {str(e)}")
return False
except Exception as e:
handler.send_error(502, f"Failed to connect upstream: {str(e)}")
return False
# 获取响应头
content_type = response.headers.get('Content-Type', 'application/octet-stream')
content_length = response.headers.get('Content-Length')
content_range = response.headers.get('Content-Range')
# 处理部分内容响应
if content_range:
handler.send_response(206)
handler.send_header('Content-Range', content_range)
else:
handler.send_response(200)
handler.send_header('Content-Type', content_type)
if content_length:
handler.send_header('Content-Length', content_length)
handler.send_header('X-Upstream', self.upstream_url)
handler.end_headers()
# 读取并转发内容,同时缓存
data = response.read()
if self.cache_enabled and response.status == 200:
self._set_cache(cache_key, data, content_type)
handler.wfile.write(data)
return True
except Exception as e:
handler.send_error(502, f"Proxy error: {str(e)}")
return False
def _serve_cached(self, handler, cached: dict, url: str):
"""服务缓存的文件"""
handler.send_response(200)
handler.send_header('Content-Type', cached.get('content_type', 'application/octet-stream'))
handler.send_header('Content-Length', len(cached.get('data', b'')))
handler.send_header('X-Cached', 'true')
handler.end_headers()
handler.wfile.write(cached.get('data', b''))
return True
def _get_cache_key(self, url: str) -> str:
"""生成缓存键"""
return urllib.parse.quote(url, safe='')
def _get_cache_path(self, cache_key: str) -> str:
"""获取缓存文件路径"""
# 使用前两个字符作为子目录
subdir = cache_key[:2] if len(cache_key) >= 2 else 'cache'
return os.path.join(self.storage_dir, '.cache', subdir, cache_key)
def _get_cache(self, cache_key: str) -> Optional[dict]:
"""获取缓存"""
if not self.cache_enabled:
return None
cache_path = self._get_cache_path(cache_key)
meta_path = cache_path + '.meta'
if not os.path.exists(cache_path):
return None
# 检查是否过期
if os.path.exists(meta_path):
try:
with open(meta_path, 'r') as f:
meta = json.load(f)
if time.time() > meta.get('expires', 0):
return None
except Exception:
pass
# 读取缓存数据
try:
with open(cache_path, 'rb') as f:
data = f.read()
with open(meta_path, 'r') as f:
meta = json.load(f)
return {
'data': data,
'content_type': meta.get('content_type', 'application/octet-stream')
}
except Exception:
return None
def _set_cache(self, cache_key: str, data: bytes, content_type: str = 'application/octet-stream'):
"""设置缓存"""
try:
cache_path = self._get_cache_path(cache_key)
meta_path = cache_path + '.meta'
os.makedirs(os.path.dirname(cache_path), exist_ok=True)
# 写入数据
with open(cache_path, 'wb') as f:
f.write(data)
# 写入元数据
meta = {
'cached_at': time.time(),
'expires': time.time() + self.cache_ttl,
'size': len(data),
'content_type': content_type,
'url': cache_key
}
with open(meta_path, 'w') as f:
json.dump(meta, f)
except Exception as e:
print(f"[{self.mirror_type}] Cache write failed: {e}")
def _guess_content_type(self, path: str) -> str:
"""根据文件扩展名猜测内容类型"""
ext = os.path.splitext(path)[1].lower()
content_types = {
'.jar': 'application/java-archive',
'.war': 'application/java-archive',
'.ear': 'application/java-archive',
'.pom': 'application/xml',
'.xml': 'application/xml',
'.json': 'application/json',
'.gem': 'application/octet-stream',
'.crate': 'application/octet-stream',
'.nupkg': 'application/zip',
'.tar.gz': 'application/gzip',
'.tgz': 'application/gzip',
'.zip': 'application/zip',
'.deb': 'application/deb',
'.rpm': 'application/x-rpm',
'.pkg.tar.zst': 'application/zstd',
'.podspec': 'text/plain',
'.tar': 'application/x-tar',
'.pdf': 'application/pdf',
'.html': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
}
return content_types.get(ext, 'application/octet-stream')
def get_cache_stats(self) -> dict:
"""获取缓存统计"""
cache_dir = os.path.join(self.storage_dir, '.cache')
if not os.path.exists(cache_dir):
return {'files': 0, 'size': 0, 'size_formatted': '0 B'}
total_size = 0
file_count = 0
try:
for root, dirs, files in os.walk(cache_dir):
for f in files:
if not f.endswith('.meta'):
file_count += 1
total_size += os.path.getsize(os.path.join(root, f))
except Exception:
pass
return {
'files': file_count,
'size': total_size,
'size_formatted': self._format_size(total_size)
}
def _format_size(self, size_bytes: int) -> str:
"""格式化文件大小"""
if size_bytes == 0:
return "0 B"
units = ["B", "KB", "MB", "GB", "TB"]
i = 0
size = float(size_bytes)
while size >= 1024 and i < len(units) - 1:
size /= 1024.0
i += 1
return f"{size:.2f} {units[i]}"
+280
View File
@@ -0,0 +1,280 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
npm镜像代理处理器
支持Node.js包管理器
"""
import os
import json
import time
import urllib.request
import urllib.parse
from typing import Dict, List, Optional
from datetime import datetime
class NpmMirror:
"""npm镜像代理"""
def __init__(self, config: dict):
self.config = config
# 配置 - 使用 storage_dir(基于 base_dir)
self.upstream_url = config.get('upstream_url', 'https://registry.npmjs.org')
self.storage_dir = config.get('storage_dir', './downloads/npm')
self.base_dir = config.get('base_dir', './downloads')
# 确保存储目录存在
os.makedirs(self.storage_dir, exist_ok=True)
def handle_request(self, handler, path: str) -> bool:
"""
处理npm请求
路径格式: /lodash 或 /-/package/lodash/dist
"""
try:
parts = path.strip('/').split('/')
if not parts:
return self._handle_index(handler)
if parts[0] == '-':
# Scoped package 或其他特殊请求
if len(parts) >= 4 and parts[1] == 'package':
return self._handleScopedPackage(handler, parts[2], parts[3] if len(parts) > 3 else None)
elif len(parts) >= 3 and parts[1] == 'package':
return self._handle_package(handler, parts[2], None)
else:
handler.send_error(400, "Invalid npm API path")
return False
elif parts[0] == '@':
# Scoped package
if len(parts) >= 2:
scope = parts[0]
package = '/'.join(parts[1:])
return self._handle_scoped_package(handler, scope, package)
else:
handler.send_error(400, "Invalid scoped package")
return False
elif parts[0] == '-/':
# npm特殊路径
return self._handle_special(handler, '/'.join(parts))
elif len(parts) == 1:
# 单个包名
return self._handle_package(handler, parts[0], None)
else:
# 其他请求
return self._handle_package(handler, parts[0], parts[1] if len(parts) > 1 else None)
except Exception as e:
handler.send_error(500, str(e))
return False
def _handle_index(self, handler) -> bool:
"""处理索引请求"""
handler.send_json_response({
'registry_url': self.upstream_url,
'cache_stats': self.get_cache_stats()
})
return True
def _handle_package(self, handler, package: str, version: str = None) -> bool:
"""处理包元数据请求"""
cache_key = f"package:{package}:{version or 'latest'}"
cached = self._get_cache(cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'application/json')
handler.end_headers()
handler.wfile.write(cached)
return True
# 从上游获取
if version:
url = f"{self.upstream_url}/{package}/{version}"
else:
url = f"{self.upstream_url}/{package}/latest"
try:
data = self._fetch(url)
if self.cache_enabled:
self._set_cache(cache_key, data)
handler.send_response(200)
handler.send_header('Content-Type', 'application/json')
handler.end_headers()
handler.wfile.write(data)
return True
except urllib.error.HTTPError as e:
handler.send_error(404, f"Package not found: {package}")
return False
def _handle_scoped_package(self, handler, scope: str, package: str) -> bool:
"""处理scoped包"""
full_name = f"{scope}/{package}"
return self._handle_package(handler, full_name, None)
def _handleScopedPackage(self, handler, scope: str, package: str) -> bool:
"""处理特殊路径的scoped包"""
full_name = f"{scope}/{package}"
return self._handle_package(handler, full_name, None)
def _handle_special(self, handler, path: str) -> bool:
"""处理特殊npm路径"""
# 简化实现:转发到上游
url = f"{self.upstream_url}/{path}"
try:
data = self._fetch(url)
handler.send_response(200)
handler.send_header('Content-Type', 'application/json')
handler.end_headers()
handler.wfile.write(data)
return True
except Exception as e:
handler.send_error(502, f"Failed to fetch: {str(e)}")
return False
def _handle_tarball(self, handler, package: str, filename: str) -> bool:
"""处理tarball下载"""
cache_key = f"tarball:{package}:{filename}"
cached = self._get_cache(cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'application/octet-stream')
handler.send_header('Content-Length', str(len(cached)))
handler.end_headers()
handler.wfile.write(cached)
return True
# 从上游获取
url = f"{self.upstream_url}/{package}/-/{filename}"
try:
data = self._fetch(url)
if self.cache_enabled:
self._set_cache(cache_key, data)
handler.send_response(200)
handler.send_header('Content-Type', 'application/octet-stream')
handler.send_header('Content-Length', str(len(data)))
handler.end_headers()
handler.wfile.write(data)
return True
except Exception as e:
handler.send_error(502, f"Failed to fetch tarball: {str(e)}")
return False
def _fetch(self, url: str) -> Optional[bytes]:
"""从URL获取数据"""
try:
req = urllib.request.Request(url)
req.add_header('User-Agent', 'npm-Mirror/1.0')
req.add_header('Accept', 'application/json')
with urllib.request.urlopen(req, timeout=30) as response:
return response.read()
except Exception:
return None
def _get_cache(self, cache_key: str) -> Optional[bytes]:
"""获取缓存"""
if not self.cache_enabled:
return None
cache_path = self._get_cache_path(cache_key)
meta_path = cache_path + '.meta'
if not os.path.exists(cache_path):
return None
if os.path.exists(meta_path):
try:
with open(meta_path, 'r') as f:
meta = json.load(f)
if time.time() > meta.get('expires', 0):
return None
except Exception:
pass
try:
with open(cache_path, 'rb') as f:
return f.read()
except Exception:
return None
def _set_cache(self, cache_key: str, data: bytes):
"""设置缓存"""
cache_path = self._get_cache_path(cache_key)
meta_path = cache_path + '.meta'
os.makedirs(os.path.dirname(cache_path), exist_ok=True)
try:
with open(cache_path, 'wb') as f:
f.write(data)
meta = {
'cached_at': time.time(),
'expires': time.time() + self.cache_ttl,
'size': len(data)
}
with open(meta_path, 'w') as f:
json.dump(meta, f)
except Exception as e:
print(f"npm缓存写入失败: {e}")
def _get_cache_path(self, cache_key: str) -> str:
"""获取缓存路径"""
subdir = cache_key[:2]
return os.path.join(self.storage_dir, subdir, cache_key)
def get_cache_stats(self) -> dict:
"""获取缓存统计"""
if not os.path.exists(self.storage_dir):
return {'files': 0, 'size': 0}
total_size = 0
file_count = 0
for root, dirs, files in os.walk(self.storage_dir):
for f in files:
if not f.endswith('.meta'):
file_count += 1
total_size += os.path.getsize(os.path.join(root, f))
return {
'files': file_count,
'size': total_size,
'size_formatted': self._format_size(total_size)
}
def _format_size(self, size_bytes: int) -> str:
"""格式化文件大小"""
if size_bytes == 0:
return "0 B"
units = ["B", "KB", "MB", "GB"]
i = 0
while size_bytes >= 1024 and i < len(units) - 1:
size_bytes /= 1024.0
i += 1
return f"{size_bytes:.2f} {units[i]}"
+788
View File
@@ -0,0 +1,788 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
PyPI镜像代理处理器
支持Python包索引
"""
import os
import json
import re
import time
import urllib.request
import urllib.parse
import urllib.error
from typing import Dict, List, Optional
from datetime import datetime
class PyPIMirror:
"""PyPI镜像代理"""
def __init__(self, config: dict):
self.config = config
# 配置 - storage_dir 基于 base_dir
self.upstream_url = config.get('upstream_url', 'https://pypi.org')
self.base_dir = config.get('base_dir', './downloads')
storage_subdir = config.get('storage_dir', 'pypi')
self.storage_dir = os.path.join(self.base_dir, storage_subdir)
self.simple_dir = os.path.join(self.storage_dir, 'simple')
self.web_dir = os.path.join(self.storage_dir, 'web')
# 确保存储目录存在
os.makedirs(self.simple_dir, exist_ok=True)
os.makedirs(self.web_dir, exist_ok=True)
def handle_request(self, handler, path: str) -> bool:
"""
处理PyPI请求
路径格式: /simple/requests/ 或 /packages/xxx.tar.gz 或 /pypi/web/package 或 /pypi/packages/hash/file
"""
try:
import sys
# 如果路径以 pypi/ 开头,也需要去掉
path = path.lstrip('/')
if path.startswith('pypi/'):
path = path[5:]
parts = path.strip('/').split('/')
# 过滤空字符串
parts = [p for p in parts if p]
if not parts:
return self._handle_index(handler)
if parts[0] == 'simple':
# Simple API
import sys
if len(parts) == 1:
# /simple/ - 返回根索引
return self._handle_index(handler)
elif len(parts) == 2:
# /simple/package/
return self._handle_simple_index(handler, parts[1])
elif len(parts) >= 3:
# /simple/package/version/ 或 /simple/package/version#egg=...
return self._handle_package_file(handler, parts[1], '/'.join(parts[2:]))
else:
handler.send_error(400, "Invalid simple API path")
return False
elif parts[0] == 'web':
# /web/package/ 或 /web/package/json
# pip sends /pypi/web/<package>/json
package = parts[1] if len(parts) >= 2 else ''
return self._handle_web_api(handler, package)
elif parts[0] == 'packages':
# 包下载
filename = '/'.join(parts[1:])
return self._handle_package_download(handler, filename)
elif parts[0] == 'legacy':
# 旧版PyPI兼容
return self._handle_legacy(handler, '/'.join(parts[1:]))
else:
handler.send_error(404, "Unknown API")
return False
except Exception as e:
handler.send_error(500, str(e))
return False
def _handle_index(self, handler) -> bool:
"""处理索引请求 - 返回所有可用包的列表"""
# 从上游获取包列表
url = self.upstream_url.rstrip('/')
if url.endswith('/simple'):
url = url # 保持 /simple
else:
url = url + '/simple'
try:
req = urllib.request.Request(url)
req.add_header('Accept', 'text/html')
req.add_header('User-Agent', 'PyPI-Mirror/1.0')
with urllib.request.urlopen(req, timeout=30) as response:
data = response.read().decode('utf-8')
# 转换相对链接
# 清华源返回的可能是完整的HTML,需要转换链接
data = self._convert_simple_index_html(data)
data_bytes = data.encode('utf-8')
handler.send_response(200)
handler.send_header('Content-Type', 'text/html; charset=utf-8')
handler.send_header('Content-Length', str(len(data_bytes)))
handler.end_headers()
handler.wfile.write(data_bytes)
return True
except Exception as e:
handler.send_error(502, f"Failed to fetch package index: {str(e)}")
return False
def _convert_simple_index_html(self, html: str) -> str:
"""转换根索引页面的HTML"""
import re
# 替换上游链接
def convert_link(match):
href = match.group(1)
text = match.group(2)
if href.startswith('/simple/'):
return match.group(0) # 已经是相对路径
elif href.startswith('https://pypi.tuna.tsinghua.edu.cn/simple/'):
simple_part = href.split("/simple/")[-1]
return f'<a href="/simple/{simple_part}">{text}</a>'
elif href.startswith('https://'):
# 其他上游链接,提取包名
pkg_name = href.rstrip('/').split('/')[-1]
return f'<a href="/simple/{pkg_name}/">{text}</a>'
return match.group(0)
# 匹配 <a href="...">text</a>
return re.sub(r'<a[^>]+href="([^"]+)"[^>]*>([^<]*)</a>', convert_link, html)
def _handle_simple_index(self, handler, package: str) -> bool:
"""处理Simple API索引请求"""
import json
import time
package = package.lower()
# 调试 - 确保函数被调用
debug_file = '/tmp/pypi_debug.log'
with open(debug_file, 'a') as f:
f.write(f"[HANDLE_SIMPLE] START package={package}\n")
# 检查客户端Accept header
accept = handler.headers.get('Accept', '')
wants_json = 'application/vnd.pypi.simple.v1+json' in accept
# 调试
debug_file = '/tmp/pypi_debug.log'
with open(debug_file, 'a') as f:
f.write(f"[SIMPLE_INDEX] package={package}, wants_json={wants_json}, accept={accept[:50]}\n")
# 根据请求格式选择正确的缓存key,统一使用 simple/ 前缀
cache_key = f"simple/{package}"
cached = self._get_cache(cache_key)
# 调试缓存
debug_file = '/tmp/pypi_debug.log'
with open(debug_file, 'a') as f:
f.write(f"[CACHE_CHECK] cache_key={cache_key}, cached={'YES' if cached else 'NO'}\n")
if cached:
# 返回缓存,使用正确的Content-Type
if wants_json:
handler.send_response(200)
handler.send_header('Content-Type', 'application/vnd.pypi.simple.v1+json; charset=utf-8')
else:
handler.send_response(200)
handler.send_header('Content-Type', 'text/html; charset=utf-8')
handler.send_header('Content-Length', str(len(cached)))
handler.end_headers()
handler.wfile.write(cached)
return True
# 从上游获取HTML
# upstream_url 已经是完整路径(如 https://pypi.tuna.tsinghua.edu.cn/simple)
# 所以只需要添加 /package/
url = f"{self.upstream_url}/{package}/"
try:
req = urllib.request.Request(url)
req.add_header('Accept', 'text/html')
req.add_header('User-Agent', 'PyPI-Mirror/1.0')
with urllib.request.urlopen(req, timeout=30) as response:
data = response.read().decode('utf-8')
# 根据客户端请求返回不同格式,统一使用 simple/ 路径
if wants_json:
# 转换为JSON格式
json_data = self._convert_to_json(package, data)
proxy_data = json.dumps(json_data)
content_type = 'application/vnd.pypi.simple.v1+json; charset=utf-8'
cache_key = f"simple/{package}"
else:
# 转换为HTML格式
debug_file = '/tmp/pypi_debug.log'
with open(debug_file, 'a') as f:
f.write(f"[CONVERT_CALL] Before conversion\n")
try:
proxy_data = self._convert_simple_html(package, data)
with open(debug_file, 'a') as f:
f.write(f"[CONVERT_CALL] After conversion\n")
except Exception as e:
with open(debug_file, 'a') as f:
f.write(f"[CONVERT_ERROR] {e}\n")
proxy_data = data # fallback to raw data
content_type = 'application/vnd.pypi.simple.v1+html; charset=utf-8'
if True:
self._set_cache(cache_key, proxy_data.encode('utf-8'))
proxy_bytes = proxy_data.encode('utf-8')
handler.send_response(200)
handler.send_header('Content-Type', content_type)
handler.send_header('Content-Length', str(len(proxy_bytes)))
handler.end_headers()
handler.wfile.write(proxy_bytes)
return True
except urllib.error.HTTPError as e:
if e.code == 404:
handler.send_error(404, f"Package not found: {package}")
else:
handler.send_error(502, f"Failed to fetch from upstream: {str(e)}")
return False
def _convert_to_json(self, package: str, html: str) -> dict:
"""将HTML转换为JSON格式"""
import re
# 解析HTML中的链接
links = []
# 匹配 <a href="...">text</a>
pattern = r'<a[^>]+href="([^"]+)"[^>]*>([^<]*)</a>'
matches = re.findall(pattern, html)
for href, text in matches:
# 提取文件名(从链接文本)
filename = text.strip() if text.strip() else ''
# 如果没有链接文本,从URL中提取
if not filename:
if '#' in href:
filename = href.split('#')[0].split('/')[-1]
else:
filename = href.split('/')[-1]
# JSON格式中URL不应该包含fragment
# pip从filename字段提取版本号(如 Flask-1.0.0.tar.gz -> 1.0.0)
# 解析URL并转换为代理路径
url = href
if href.startswith('../'):
# 相对路径 - 需要转换为代理路径
# 格式: ../../packages/hash1/hash2/fullhash/filename#sha256=...
# 或: ../../packages/hash1/hash2/filename
# parts = ['..', '..', 'packages', 'hash1', 'hash2', 'fullhash', 'filename', ...]
parts = href.split('/')
try:
pkg_idx = parts.index('packages')
# 提取从 packages 后面到文件名之前的所有部分作为 hash 路径
# 文件名是最后一个非空部分(可能包含 #fragment)
# 找到文件名的位置(最后一个部分)
filename_idx = len(parts) - 1
while filename_idx > pkg_idx and not parts[filename_idx]:
filename_idx -= 1
# hash_path 是 packages 后面到文件名之前的所有部分
if filename_idx > pkg_idx + 1:
hash_path = '/'.join(parts[pkg_idx+1:filename_idx])
else:
hash_path = parts[pkg_idx+1] if pkg_idx + 1 < len(parts) else ''
# 文件名: 检查 pkg_idx+3 是否存在且不是哈希
if pkg_idx + 3 < len(parts):
fname_full = parts[pkg_idx+3]
# 如果 fname_full 看起来像哈希(包含 sha256= 或长度>=32的十六进制),则使用原始 filename
# 清华源格式: .../hash/filename#sha256=...
# 其中 hash 是 28-30 位十六进制
is_hash_like = ('sha256=' in fname_full or 'sha512=' in fname_full or
(len(fname_full) >= 28 and all(c in '0123456789abcdef' for c in fname_full[:28].lower())))
if is_hash_like:
# 这是哈希,不是文件名
fname = filename if filename else ''
else:
# 这是文件名
fname = fname_full.split('#')[0]
if not filename:
filename = fname
else:
fname = filename if filename else ''
# URL不包含fragment
url = f"/pypi/packages/{hash_path}/{fname}"
except ValueError:
pass
elif href.startswith('/pypi/'):
# 绝对路径(如 /pypi/packages/hash/filename#egg=package-version)
# 去掉fragment
href_clean = href.split('#')[0]
parts = href_clean.split('/')
# parts = ['', 'pypi', 'packages', 'hash', 'filename']
if len(parts) >= 5:
hash_path = parts[3]
fname = parts[4]
if not filename:
filename = fname
url = f"/pypi/packages/{hash_path}/{fname}"
elif href.startswith('http'):
# 绝对URL - 转换为代理路径
if 'files.pythonhosted.org' in href or 'files.pypi.org' in href:
# 格式: https://files.pythonhosted.org/packages/hash1/hash2/完整哈希/filename
# 例如: https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl
parts = href.split('/packages/')
if len(parts) >= 2:
path_after_packages = parts[1]
# 完整路径: hash1/hash2/完整哈希/filename
url = f"/pypi/packages/{path_after_packages}"
fname = path_after_packages.split('/')[-1]
if not filename:
filename = fname
elif 'pypi.tuna.tsinghua.edu.cn' in href or 'mirrors.tuna.tsinghua.edu.cn' in href:
parts = href.rsplit('/', 1)
if len(parts) == 2:
path_part = parts[0]
fname = parts[1]
hash_path = path_part.split('/')[-1]
if not filename:
filename = fname
url = f"/pypi/packages/{hash_path}/{fname}"
link_entry = {
"filename": filename,
"url": url
}
links.append(link_entry)
return {
"meta": {
"api-version": "1.0",
"repository-version": "1.0"
},
"name": package,
"files": links
}
def _handle_package_file(self, handler, package: str, filename: str) -> bool:
"""处理包文件请求"""
import urllib.parse
# 解析文件名
# 新格式: /pypi/packages/hash/filename#pip=package-version
# 或旧格式: /pypi/packages/filename?url=...
parsed = urllib.parse.urlparse(f"/{filename}")
actual_filename = parsed.path.lstrip('/')
query_params = urllib.parse.parse_qs(parsed.query)
fragment = urllib.parse.parse_qs(parsed.fragment) if parsed.fragment else {}
# 从fragment中提取包信息(用于缓存键)
actual_package = package
if 'pip' in fragment:
# 格式: #pip=flask-2.0.0
pip_info = fragment['pip'][0]
if '-' in pip_info:
# 提取版本号
parts = pip_info.split('-', 1)
if len(parts) == 2:
actual_package = parts[0]
cache_key = f"packages/{actual_filename}"
cached = self._get_cache(cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'application/octet-stream')
handler.send_header('Content-Length', str(len(cached)))
handler.end_headers()
handler.wfile.write(cached)
return True
# 尝试从上游获取
# 格式: /pypi/packages/hash/filename -> 构造上游URL
possible_urls = []
# 获取基础URL(去掉 /simple 后缀)
base_url = self.upstream_url.rstrip('/')
if base_url.endswith('/simple'):
base_url = base_url[:-7]
# 新格式: hash/filename -> 尝试清华源
# 使用 actual_filename(已解析的纯文件路径)
possible_urls.append(f"{base_url}/packages/{actual_filename}")
# 尝试官方源
possible_urls.append(f"https://files.pythonhosted.org/packages/{actual_filename}")
# 如果有查询参数中的URL,也尝试
if 'url' in query_params:
possible_urls.insert(0, urllib.parse.unquote(query_params['url'][0]))
data = None
last_error = None
for url in possible_urls:
try:
import sys
req = urllib.request.Request(url)
req.add_header('User-Agent', 'PyPI-Mirror/1.0')
with urllib.request.urlopen(req, timeout=60) as response:
data = response.read()
break # 成功获取,退出循环
except Exception as e:
import sys
last_error = e
continue
if data is None:
handler.send_error(502, f"Failed to fetch package: {last_error}")
return False
if True:
self._set_cache(cache_key, data)
handler.send_response(200)
handler.send_header('Content-Type', 'application/octet-stream')
handler.send_header('Content-Length', str(len(data)))
handler.end_headers()
handler.wfile.write(data)
return True
def _handle_web_api(self, handler, package: str) -> bool:
"""处理Web API请求"""
import sys
package = package.lower()
cache_key = f"web/{package}"
cached = self._get_cache(cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'application/vnd.pypi.simple.v1+json; charset=utf-8')
handler.send_header('Content-Length', str(len(cached)))
handler.end_headers()
handler.wfile.write(cached)
return True
# 从上游获取 - 需要去掉 /simple 后缀
base_url = self.upstream_url.rstrip('/')
if base_url.endswith('/simple'):
base_url = base_url[:-7]
url = f"{base_url}/pypi/{package}/json"
try:
req = urllib.request.Request(url)
req.add_header('Accept', 'application/json')
req.add_header('User-Agent', 'PyPI-Mirror/1.0')
with urllib.request.urlopen(req, timeout=30) as response:
data = response.read().decode('utf-8')
data_json = json.loads(data)
# 转换URL
data_json = self._convert_package_json(package, data_json)
proxy_data = json.dumps(data_json)
if True:
self._set_cache(cache_key, proxy_data.encode('utf-8'))
proxy_bytes = proxy_data.encode('utf-8')
handler.send_response(200)
handler.send_header('Content-Type', 'application/vnd.pypi.simple.v1+json; charset=utf-8')
handler.send_header('Content-Length', str(len(proxy_bytes)))
handler.end_headers()
handler.wfile.write(proxy_bytes)
return True
except urllib.error.HTTPError as e:
handler.send_error(502, f"Failed to fetch package info: {str(e)}")
return False
def _handle_package_download(self, handler, filename: str) -> bool:
"""处理包下载请求"""
import sys
# 使用 packages/ 前缀,保持标准 PyPI 目录结构
cache_key = f"packages/{filename}"
cached = self._get_cache(cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'application/octet-stream')
handler.send_header('Content-Length', str(len(cached)))
handler.end_headers()
handler.wfile.write(cached)
return True
# 从上游获取 - 注意去掉 /simple 后缀
base_url = self.upstream_url.rstrip('/')
if base_url.endswith('/simple'):
base_url = base_url[:-7] # 去掉 /simple
url = f"{base_url}/packages/{filename}"
try:
req = urllib.request.Request(url)
req.add_header('User-Agent', 'PyPI-Mirror/1.0')
with urllib.request.urlopen(req, timeout=120) as response:
data = response.read()
if True:
self._set_cache(cache_key, data)
handler.send_response(200)
handler.send_header('Content-Type', 'application/octet-stream')
handler.send_header('Content-Length', str(len(data)))
handler.end_headers()
handler.wfile.write(data)
return True
except Exception as e:
handler.send_error(502, f"Failed to download: {str(e)}")
return False
def _handle_legacy(self, handler, path: str) -> bool:
"""处理旧版PyPI兼容"""
handler.send_error(410, "Legacy PyPI API is deprecated")
return False
def _convert_simple_html(self, package: str, html: str) -> str:
"""转换Simple API HTML,替换URL为代理地址"""
import urllib.parse
import sys
import os
# 写入调试文件
debug_file = '/tmp/pypi_debug.log'
with open(debug_file, 'a') as f:
f.write(f"[PyPI] Converting HTML for package: {package}\n")
# 打印前几个链接用于调试
import re
test_matches = re.findall(r'href="([^"]+)"', html)[:3]
with open(debug_file, 'a') as f:
f.write(f"[PyPI] Sample links: {test_matches}\n")
def convert_absolute_url(match):
"""转换绝对URL为代理链接"""
original_url = match.group(1) if match.lastindex else match.group(0)
# 提取文件名和完整的hash路径
# 格式: https://pypi.tuna.tsinghua.edu.cn/packages/hash1/hash2/fullhash/filename
# 我们将其转换为: /pypi/packages/hash1/hash2/fullhash/filename#pip=<package>
parts = original_url.rsplit('/', 1)
if len(parts) == 2:
path_part = parts[0]
filename = parts[1]
# 提取从 packages/ 后面的完整路径(包含完整hash)
try:
pkg_idx = path_part.index('/packages/')
hash_path = path_part[pkg_idx + 10:] # 去掉 /packages/
except ValueError:
hash_path = filename
return f'href="/pypi/packages/{hash_path}/{filename}#pip={package}-{filename.split("-")[1] if "-" in filename else ""}"'
return match.group(0)
# 替换绝对URL - pypi.tuna.tsinghua.edu.cn (清华源)
html = re.sub(
r'(https://pypi\.tuna\.tsinghua\.edu\.cn/packages/[^"\']+)',
convert_absolute_url,
html
)
# 替换绝对URL - mirrors.tuna.tsinghua.edu.cn
html = re.sub(
r'(https://mirrors\.tuna\.tsinghua\.edu\.cn/pypi/packages/[^"\']+)',
convert_absolute_url,
html
)
# 替换绝对URL - files.pypi.org
html = re.sub(
r'(https://files\.pypi\.org/packages/[^"\']+)',
convert_absolute_url,
html
)
# 替换绝对URL - files.pythonhosted.org
html = re.sub(
r'(https://files\.pythonhosted\.org/packages/[^"\']+)',
convert_absolute_url,
html
)
# 替换相对路径链接 - ../../packages/hash1/hash2/fullhash/filename -> /pypi/packages/hash1/hash2/fullhash/filename#pip=...
def convert_relative_match(match):
"""转换相对路径链接"""
href = match.group(1)
debug_file = '/tmp/pypi_debug.log'
with open(debug_file, 'a') as f:
f.write(f"[CONVERT] Input href: {href[:80]}...\n")
# 提取文件名
filename = href.split('/')[-1].split('#')[0]
# 提取完整的hash路径(从 packages/ 后面的所有部分除了文件名)
parts = href.split('/')
with open(debug_file, 'a') as f:
f.write(f"[CONVERT] Parts: {parts}\n")
try:
pkg_idx = parts.index('packages')
# packages 后面到倒数第二个是 hash 路径,最后一个是文件名
hash_parts = parts[pkg_idx+1:-1] # 除了最后一个(文件名)
with open(debug_file, 'a') as f:
f.write(f"[CONVERT] hash_parts: {hash_parts}\n")
hash_path = '/'.join(hash_parts) if hash_parts else filename
with open(debug_file, 'a') as f:
f.write(f"[CONVERT] hash_path: {hash_path}\n")
except ValueError:
hash_path = filename
with open(debug_file, 'a') as f:
f.write(f"[CONVERT] ValueError, hash_path: {hash_path}\n")
# 提取版本号 - 从文件名中提取,如 Flask-0.1.tar.gz -> 0.1
base_name = filename
# 去掉扩展名
for ext in ['.tar.gz', '.whl', '.tar.bz2', '.tar.xz']:
if base_name.endswith(ext):
base_name = base_name[:-len(ext)]
break
# 尝试多种大小写组合来去掉包名前缀
version = base_name
for pkg_name in [package, package.lower(), package.upper(), package.capitalize()]:
if base_name.lower().startswith(pkg_name.lower() + '-'):
version = base_name[len(pkg_name)+1:]
break
return f'href="/pypi/packages/{hash_path}/{filename}#egg={package}-{version}"'
# 匹配相对路径的链接
html = re.sub(
r'href="(\.\./\.\./packages/[^"]+)"',
convert_relative_match,
html
)
html = re.sub(
r"href='(\.\./\.\./packages/[^']+)'",
convert_relative_match,
html
)
return html
def _convert_package_json(self, package: str, data: dict) -> dict:
"""转换Package JSON,替换URL为代理地址"""
# 转换URL函数
def convert_url(url):
# 处理 files.pythonhosted.org 和 files.pypi.org
# 格式: https://files.pythonhosted.org/packages/<hash1>/<hash2>/<完整哈希>/<filename>
# 例如: https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl
if 'files.pythonhosted.org' in url or 'files.pypi.org' in url:
path_parts = url.split('/packages/')
if len(path_parts) >= 2:
# 直接使用 /packages/ 后的完整路径
path_after_packages = path_parts[1]
return f'/pypi/packages/{path_after_packages}'
return url
# 转换urls
if 'urls' in data:
for item in data['urls']:
if 'url' in item:
item['url'] = convert_url(item['url'])
return data
def _fetch(self, url: str) -> Optional[bytes]:
"""从URL获取数据"""
try:
req = urllib.request.Request(url)
req.add_header('User-Agent', 'PyPI-Mirror/1.0')
with urllib.request.urlopen(req, timeout=30) as response:
return response.read()
except Exception:
return None
def _get_cache(self, cache_key: str) -> Optional[bytes]:
"""获取缓存"""
if not True:
return None
cache_path = self._get_cache_path(cache_key)
meta_path = cache_path + '.meta'
if not os.path.exists(cache_path):
return None
if os.path.exists(meta_path):
try:
with open(meta_path, 'r') as f:
meta = json.load(f)
if time.time() > meta.get('expires', 0):
return None
except Exception:
pass
try:
with open(cache_path, 'rb') as f:
return f.read()
except Exception:
return None
def _set_cache(self, cache_key: str, data: bytes):
"""设置缓存"""
cache_path = self._get_cache_path(cache_key)
meta_path = cache_path + '.meta'
os.makedirs(os.path.dirname(cache_path), exist_ok=True)
try:
with open(cache_path, 'wb') as f:
f.write(data)
meta = {
'cached_at': time.time(),
'expires': time.time() + 86400,
'size': len(data)
}
with open(meta_path, 'w') as f:
json.dump(meta, f)
except Exception as e:
print(f"PyPI缓存写入失败: {e}")
def _get_cache_path(self, cache_key: str) -> str:
"""获取缓存路径"""
# cache_key 格式: packages/fe/df/88ccbee.../filename
# 存储路径: downloads/pypi-cn/packages/fe/df/88ccbee.../filename
# 确保使用正斜杠
safe_key = cache_key.replace('\\', '/')
return os.path.join(self.storage_dir, safe_key)
def get_cache_stats(self) -> dict:
"""获取缓存统计"""
if not os.path.exists(self.storage_dir):
return {'files': 0, 'size': 0}
total_size = 0
file_count = 0
for root, dirs, files in os.walk(self.storage_dir):
for f in files:
if not f.endswith('.meta'):
file_count += 1
total_size += os.path.getsize(os.path.join(root, f))
return {
'files': file_count,
'size': total_size,
'size_formatted': self._format_size(total_size)
}
def _format_size(self, size_bytes: int) -> str:
"""格式化文件大小"""
if size_bytes == 0:
return "0 B"
units = ["B", "KB", "MB", "GB"]
i = 0
while size_bytes >= 1024 and i < len(units) - 1:
size_bytes /= 1024.0
i += 1
return f"{size_bytes:.2f} {units[i]}"
+384
View File
@@ -0,0 +1,384 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
YUM/DNF镜像代理处理器
支持RHEL/CentOS/Rocky/AlmaLinux软件源
"""
import os
import json
import time
import gzip
import xml.etree.ElementTree as ET
import urllib.request
from typing import Dict, List, Optional
from datetime import datetime
class YUMMirror:
"""YUM/DNF镜像代理"""
def __init__(self, config: dict):
self.config = config
# 配置 - 使用 storage_dir(基于 base_dir)
self.base_url = config.get('base_url', 'http://mirror.centos.org/centos')
self.storage_dir = config.get('storage_dir', './downloads/yum')
self.base_dir = config.get('base_dir', './downloads')
self.repo_id = config.get('repo_id', 'baseos')
self.arch = config.get('arch', 'x86_64')
# 确保存储目录存在
os.makedirs(self.storage_dir, exist_ok=True)
def handle_request(self, handler, path: str) -> bool:
"""
处理YUM请求
路径格式: /centos/7/updates/x86_64/repodata/repomd.xml
"""
try:
parts = path.strip('/').split('/')
if len(parts) < 3:
return self._handle_index(handler)
distro = parts[0] # centos, rocky, alma
version = parts[1] # 7, 8, 9
repo = parts[2] # baseos, appstream, updates
rest = '/'.join(parts[3:])
# 确定文件类型
if 'repomd.xml' in rest:
return self._handle_repomd(handler, distro, version, repo)
elif 'primary.xml.gz' in rest:
return self._handle_primary(handler, distro, version, repo, 'primary')
elif 'filelists.xml.gz' in rest:
return self._handle_filelists(handler, distro, version, repo, 'filelists')
elif 'other.xml.gz' in rest:
return self._handle_other(handler, distro, version, repo, 'other')
else:
return self._handle_repo_file(handler, distro, version, repo, rest)
except Exception as e:
handler.send_error(500, str(e))
return False
def _handle_index(self, handler) -> bool:
"""处理索引请求"""
handler.send_json_response({
'base_url': self.base_url,
'repo_id': self.repo_id,
'arch': self.arch,
'cache_stats': self.get_cache_stats()
})
return True
def _handle_repomd(self, handler, distro: str, version: str, repo: str) -> bool:
"""处理repomd.xml请求"""
cache_key = f"repomd:{distro}:{version}:{repo}"
cached = self._get_cache(cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'application/xml')
handler.send_header('Content-Length', str(len(cached)))
handler.end_headers()
handler.wfile.write(cached)
return True
# 从上游获取
url = f"{self.base_url}/{version}/{repo}/{self.arch}/repodata/repomd.xml"
try:
data = self._fetch(url)
if self.cache_enabled:
self._set_cache(cache_key, data)
handler.send_response(200)
handler.send_header('Content-Type', 'application/xml')
handler.send_header('Content-Length', str(len(data)))
handler.end_headers()
handler.wfile.write(data)
return True
except Exception as e:
handler.send_error(502, f"Failed to fetch repomd: {str(e)}")
return False
def _handle_primary(self, handler, distro: str, version: str, repo: str, db_type: str) -> bool:
"""处理primary.xml.gz"""
cache_key = f"primary:{distro}:{version}:{repo}:{self.arch}"
cached = self._get_cache(cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'application/x-gzip')
handler.send_header('Content-Length', str(len(cached)))
handler.end_headers()
handler.wfile.write(cached)
return True
# 先获取repomd.xml找到对应的数据库文件
repomd_url = f"{self.base_url}/{version}/{repo}/{self.arch}/repodata/repomd.xml"
try:
repomd_data = self._fetch(repomd_url)
# 解析repomd.xml找到primary文件
root = ET.fromstring(repomd_data)
ns = {'repomd': 'http://linux.duke.edu/metadata/repo'}
data_location = None
for elem in root.findall('.//repomd:data', ns):
if elem.get('type') == 'primary':
data_location = elem.find('repomd:location', ns).get('href')
break
if data_location:
db_url = f"{self.base_url}/{version}/{repo}/{self.arch}/repodata/{data_location}"
data = self._fetch(db_url)
if self.cache_enabled:
self._set_cache(cache_key, data)
handler.send_response(200)
handler.send_header('Content-Type', 'application/x-gzip')
handler.send_header('Content-Length', str(len(data)))
handler.end_headers()
handler.wfile.write(data)
return True
except Exception as e:
pass
handler.send_error(502, "Failed to fetch primary database")
return False
def _handle_filelists(self, handler, distro: str, version: str, repo: str, db_type: str) -> bool:
"""处理filelists.xml.gz"""
cache_key = f"filelists:{distro}:{version}:{repo}:{self.arch}"
cached = self._get_cache(cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'application/x-gzip')
handler.send_header('Content-Length', str(len(cached)))
handler.end_headers()
handler.wfile.write(cached)
return True
# 先获取repomd.xml找到对应的数据库文件
repomd_url = f"{self.base_url}/{version}/{repo}/{self.arch}/repodata/repomd.xml"
try:
repomd_data = self._fetch(repomd_url)
# 解析repomd.xml找到filelists文件
root = ET.fromstring(repomd_data)
ns = {'repomd': 'http://linux.duke.edu/metadata/repo'}
data_location = None
for elem in root.findall('.//repomd:data', ns):
if elem.get('type') == 'filelists':
data_location = elem.find('repomd:location', ns).get('href')
break
if data_location:
db_url = f"{self.base_url}/{version}/{repo}/{self.arch}/repodata/{data_location}"
data = self._fetch(db_url)
if self.cache_enabled:
self._set_cache(cache_key, data)
handler.send_response(200)
handler.send_header('Content-Type', 'application/x-gzip')
handler.send_header('Content-Length', str(len(data)))
handler.end_headers()
handler.wfile.write(data)
return True
except Exception as e:
pass
handler.send_error(502, "Failed to fetch filelists database")
return False
def _handle_other(self, handler, distro: str, version: str, repo: str, db_type: str) -> bool:
"""处理other.xml.gz"""
cache_key = f"other:{distro}:{version}:{repo}:{self.arch}"
cached = self._get_cache(cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'application/x-gzip')
handler.send_header('Content-Length', str(len(cached)))
handler.end_headers()
handler.wfile.write(cached)
return True
# 先获取repomd.xml找到对应的数据库文件
repomd_url = f"{self.base_url}/{version}/{repo}/{self.arch}/repodata/repomd.xml"
try:
repomd_data = self._fetch(repomd_url)
# 解析repomd.xml找到other文件
root = ET.fromstring(repomd_data)
ns = {'repomd': 'http://linux.duke.edu/metadata/repo'}
data_location = None
for elem in root.findall('.//repomd:data', ns):
if elem.get('type') == 'other':
data_location = elem.find('repomd:location', ns).get('href')
break
if data_location:
db_url = f"{self.base_url}/{version}/{repo}/{self.arch}/repodata/{data_location}"
data = self._fetch(db_url)
if self.cache_enabled:
self._set_cache(cache_key, data)
handler.send_response(200)
handler.send_header('Content-Type', 'application/x-gzip')
handler.send_header('Content-Length', str(len(data)))
handler.end_headers()
handler.wfile.write(data)
return True
except Exception as e:
pass
handler.send_error(502, "Failed to fetch other database")
return False
def _handle_repo_file(self, handler, distro: str, version: str, repo: str, path: str) -> bool:
"""处理仓库中的其他文件"""
cache_key = f"file:{distro}:{version}:{repo}:{path.replace('/', ':')}"
cached = self._get_cache(cache_key)
if cached:
handler.send_response(200)
handler.send_header('Content-Type', 'application/octet-stream')
handler.end_headers()
handler.wfile.write(cached)
return True
url = f"{self.base_url}/{version}/{repo}/{self.arch}/{path}"
try:
data = self._fetch(url)
if self.cache_enabled:
self._set_cache(cache_key, data)
handler.send_response(200)
handler.send_header('Content-Type', 'application/octet-stream')
handler.end_headers()
handler.wfile.write(data)
return True
except Exception as e:
handler.send_error(404, f"File not found: {str(e)}")
return False
def _fetch(self, url: str) -> Optional[bytes]:
"""从URL获取数据"""
try:
req = urllib.request.Request(url)
req.add_header('User-Agent', 'YUM-Mirror/1.0')
with urllib.request.urlopen(req, timeout=30) as response:
return response.read()
except Exception:
return None
def _get_cache(self, cache_key: str) -> Optional[bytes]:
"""获取缓存"""
if not self.cache_enabled:
return None
cache_path = self._get_cache_path(cache_key)
meta_path = cache_path + '.meta'
if not os.path.exists(cache_path):
return None
if os.path.exists(meta_path):
try:
with open(meta_path, 'r') as f:
meta = json.load(f)
if time.time() > meta.get('expires', 0):
return None
except Exception:
pass
try:
with open(cache_path, 'rb') as f:
return f.read()
except Exception:
return None
def _set_cache(self, cache_key: str, data: bytes):
"""设置缓存"""
cache_path = self._get_cache_path(cache_key)
meta_path = cache_path + '.meta'
os.makedirs(os.path.dirname(cache_path), exist_ok=True)
try:
with open(cache_path, 'wb') as f:
f.write(data)
meta = {
'cached_at': time.time(),
'expires': time.time() + self.cache_ttl,
'size': len(data)
}
with open(meta_path, 'w') as f:
json.dump(meta, f)
except Exception as e:
print(f"YUM缓存写入失败: {e}")
def _get_cache_path(self, cache_key: str) -> str:
"""获取缓存路径"""
subdir = cache_key[:2]
return os.path.join(self.storage_dir, subdir, cache_key)
def get_cache_stats(self) -> dict:
"""获取缓存统计"""
if not os.path.exists(self.storage_dir):
return {'files': 0, 'size': 0}
total_size = 0
file_count = 0
for root, dirs, files in os.walk(self.storage_dir):
for f in files:
if not f.endswith('.meta'):
file_count += 1
total_size += os.path.getsize(os.path.join(root, f))
return {
'files': file_count,
'size': total_size,
'size_formatted': self._format_size(total_size)
}
def _format_size(self, size_bytes: int) -> str:
"""格式化文件大小"""
if size_bytes == 0:
return "0 B"
units = ["B", "KB", "MB", "GB"]
i = 0
while size_bytes >= 1024 and i < len(units) - 1:
size_bytes /= 1024.0
i += 1
return f"{size_bytes:.2f} {units[i]}"
+310
View File
@@ -0,0 +1,310 @@
# -*- mode: python ; coding: utf-8 -*-
"""
PyInstaller 打包配置
"""
import os
import sys
# 获取项目根目录
def get_abs_path(path):
"""获取绝对路径"""
if os.path.isabs(path):
return path
return os.path.join(os.getcwd(), path)
a = Analysis(
['main.py'],
pathex=[os.getcwd()],
binaries=[],
datas=[
(get_abs_path('api'), 'api'),
(get_abs_path('core'), 'core'),
(get_abs_path('handlers'), 'handlers'),
(get_abs_path('mirrors'), 'mirrors'),
(get_abs_path('settings.json'), '.'),
],
hiddenimports=[
# aiohttp
'aiohttp',
'aiohttp.abc',
'aiohttp.connector',
'aiohttp.client',
'aiohttp.client_proto',
'aiohttp.client_reqrep',
'aiohttp.cookiejar',
'aiohttp.fileresponse',
'aiohttp.hdrs',
'aiohttp.http_exceptions',
'aiohttp.http_parser',
'aiohttp.http_protocol',
'aiohttp.http_server',
'aiohttp.http_websocket',
'aiohttp.multipart',
'aiohttp.payload',
'aiohttp.proxy',
'aiohttp.streams',
'aiohttp.tcp_helpers',
'aiohttp.web',
'aiohttp.web_app',
'aiohttp.web_exceptions',
'aiohttp.web_fileresponse',
'aiohttp.web_log',
'aiohttp.web_middlewares',
'aiohttp.web_protocol',
'aiohttp.web_request',
'aiohttp.web_response',
'aiohttp.web_routedef',
'aiohttp.router',
'aiohttp.web_server',
'aiohttp.web_urldispatcher',
'aiohttp.web_ws',
'aiohttp.websocket',
'aiohttp.websocket_client',
'aiohttp.websocket_handshake',
'aiohttp.websocket_protocol',
# aiohttp.multipart
'multipart',
'email.generator',
'email.policy',
'email.utils',
# sqlalchemy
'sqlalchemy',
'sqlalchemy.engine',
'sqlalchemy.engine.base',
'sqlalchemy.engine.cursor',
'sqlalchemy.engine.default',
'sqlalchemy.engine.interfaces',
'sqlalchemy.engine.row',
'sqlalchemy.engine.util',
'sqlalchemy.orm',
'sqlalchemy.orm.base',
'sqlalchemy.orm.clear_mappers',
'sqlalchemy.orm.decl_api',
'sqlalchemy.orm.declarative',
'sqlalchemy.orm.descriptor_props',
'sqlalchemy.orm.interfaces',
'sqlalchemy.orm.mapper',
'sqlalchemy.orm.properties',
'sqlalchemy.orm.query',
'sqlalchemy.orm.relationships',
'sqlalchemy.orm.session',
'sqlalchemy.orm.strategies',
'sqlalchemy.orm.strategy_options',
'sqlalchemy.pool',
'sqlalchemy.pool.base',
'sqlalchemy.pool.impl',
'sqlalchemy.dialects',
'sqlalchemy.dialects.sqlite',
'sqlalchemy.dialects.postgresql',
'sqlalchemy.dialects.mysql',
# apscheduler
'apscheduler',
'apscheduler.schedulers',
'apscheduler.schedulers.background',
'apscheduler.schedulers.base',
'apscheduler.schedulers.blocking',
'apscheduler.triggers',
'apscheduler.triggers.cron',
'apscheduler.triggers.date',
'apscheduler.triggers.interval',
'apscheduler.executors',
'apscheduler.executors.pool',
'apscheduler.job',
'apscheduler.events',
# paramiko
'paramiko',
'paramiko.transport',
'paramiko.client',
'paramiko.config',
'paramiko.hostkeys',
'paramiko.message',
'paramiko.packet',
'paramiko.primitives',
'paramiko.util',
'paramiko.ecdh',
'paramiko.ed25519key',
'paramiko.rsakey',
'paramiko.dsskey',
'paramiko.pkey',
'paramiko.agent',
'paramiko.sftp',
'paramiko.sftp_attr',
'paramiko.sftp_handle',
'paramiko.sftp_server',
'paramiko.sftp_si',
'paramiko.ssh_exception',
'paramiko.auth_handler',
'paramiko.channel',
'paramiko.shell',
'paramiko.exec',
# requests
'requests',
'requests.api',
'requests.auth',
'requests.certs',
'requests.compat',
'requests.cookies',
'requests.exceptions',
'requests.hooks',
'requests.models',
'requests.sessions',
'requests.status_codes',
'requests.structures',
'requests.utils',
# pymysql
'pymysql',
'pymysql.connections',
'pymysql.cursors',
'pymysql.converters',
'pymysql.err',
'pymysql.optionfile',
'pymysql.protocol',
'pymysql.times',
'pymysql.constants',
'pymysql.charset',
# psycopg2
'psycopg2',
'psycopg2._psycopg2',
'psycopg2.sql',
'psycopg2.extensions',
'psycopg2.extras',
# cryptography
'cryptography',
'cryptography.hazmat',
'cryptography.hazmat.backends',
'cryptography.hazmat.primitives',
'cryptography.hazmat.primitives.asymmetric',
'cryptography.hazmat.primitives.ciphers',
'cryptography.hazmat.primitives.hashes',
'cryptography.hazmat.primitives.serialization',
'cryptography.hazmat.primitives.kdf.pbkdf2',
'cryptography.x509',
'cryptography.x509.name',
'cryptography.x509.extensions',
'cryptography.fernet',
# 其他常用
'urllib3',
'urllib3.util',
'urllib3.exceptions',
'urllib3.request',
'urllib3.response',
'urllib3.poolmanager',
'charset_normalizer',
'charset_normalizer.utils',
'charset_normalizer.models',
'idna',
'idna.core',
'idna.idnadata',
'cachetools',
'croniter',
'psutil',
'psutil._psutil_linux',
'psutil._psutil_osx',
'psutil._psutil_windows',
'python_multipart',
'python_multipart.multipart',
'dataclasses',
'inspect',
'concurrent.futures',
'subprocess',
'threading',
'socket',
'ssl',
'hmac',
'hashlib',
'base64',
'struct',
'tempfile',
'shutil',
'pickle',
'json',
'os',
'sys',
'signal',
'time',
'datetime',
'math',
'logging',
'warnings',
'ctypes',
'ctypes.util',
'ctypes.wintypes',
'_ctypes',
'_socket',
'_ssl',
'select',
'errno',
'fcntl',
'grp',
'pwd',
'termios',
'tty',
'fcntl',
'resource',
'msvcrt',
'win32api',
'win32con',
'win32file',
'pywintypes',
'pythoncom',
# pyasn1 (paramiko 依赖)
'pyasn1',
'pyasn1.type',
'pyasn1.codec',
'pyasn1.codec.der',
'pyasn1.codec.der.encoder',
'pyasn1.codec.der.decoder',
'pyasn1.util',
'pyasn1.error',
# bcrypt (cryptography 依赖)
'bcrypt',
'bcrypt.__about__',
# cffi (cryptography 依赖)
'cffi',
'cffi.api',
'cffi.ffiplatform',
'cffi.backend_ctypes',
'cffi.recompiler',
'cffi.verifier',
# packaging (cryptography 依赖)
'packaging',
'packaging.version',
'packaging.specifiers',
'packaging.requirements',
# six (paramiko 依赖)
'six',
# ipaddress (paramiko 依赖)
'ipaddress',
# json (标准库)
'json.decoder',
'json.encoder',
# typing
'typing_extensions',
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=None,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=None)
# 使用 EXE() 创建 --onefile 单文件模式
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name='hyc-download',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
console=True,
)
+19
View File
@@ -0,0 +1,19 @@
psutil>=5.9.0
pyinstaller>=6.0.0
python-multipart>=0.0.6
paramiko>=2.11.0
aiohttp>=3.9.0
cachetools>=5.3.0
requests>=2.31.0
croniter>=2.0.0
# Database (SQLite 内置, 以下为可选)
sqlalchemy>=2.0.0
apscheduler>=3.10.0
# PostgreSQL 驱动
psycopg2-binary>=2.9.0
# MySQL 驱动
pymysql>=1.1.0
cryptography>=41.0.0
+93
View File
@@ -0,0 +1,93 @@
#!/bin/bash
# ============================================
# HYC下载站 v2.3 - 多架构 Docker 构建脚本
# 支持: amd64, arm64, arm/v7, i386
# ============================================
set -e
# 颜色
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
VERSION=${1:-v2.3}
REGISTRY=${2:-hyc-download-station}
TAG=${3:-$VERSION}
# 支持的平台
PLATFORMS="linux/amd64,linux/arm64,linux/arm/v7,linux/386"
echo "========================================"
echo " HYC下载站 v2.3 - 多架构构建"
echo "========================================"
echo " 版本: $TAG"
echo " 平台: $PLATFORMS"
echo
# 检查 Docker
if ! command -v docker &> /dev/null; then
echo -e "${RED}✗ Docker 未安装${NC}"
exit 1
fi
# 检查 BuildX
if ! docker buildx version &> /dev/null; then
echo -e "${YELLOW}⚠ Docker BuildX 未安装,尝试安装...${NC}"
docker buildx install || true
fi
# 创建构建器
echo "[1/4] 创建构建器..."
docker buildx rm hyc-builder 2>/dev/null || true
docker buildx create \
--name hyc-builder \
--platform $PLATFORMS \
--use
# 引导构建器
echo "[2/4] 引导构建器..."
docker buildx inspect --bootstrap
# 构建镜像
echo "[3/4] 构建镜像..."
echo " 构建中,请稍候..."
START_TIME=$(date +%s)
docker buildx build \
--platform $PLATFORMS \
--tag "$REGISTRY:$TAG" \
--tag "$REGISTRY:latest" \
--push \
--file docker/Dockerfile.multiarch \
.
END_TIME=$(date +%s)
BUILD_TIME=$((END_TIME - START_TIME))
echo -e "${GREEN}✓ 构建完成 (${BUILD_TIME}秒)${NC}"
# 推送清单
echo "[4/4] 创建清单..."
docker manifest create "$REGISTRY:$TAG" \
"$REGISTRY:$TAG-amd64" \
"$REGISTRY:$TAG-arm64" \
"$REGISTRY:$TAG-arm-v7" \
"$REGISTRY:$TAG-386" 2>/dev/null || true
docker manifest push "$REGISTRY:$TAG" || true
echo
echo "========================================"
echo -e "${GREEN}✓ 多架构构建完成${NC}"
echo "========================================"
echo
echo "镜像标签:"
echo " - $REGISTRY:$TAG"
echo " - $REGISTRY:latest"
echo
echo "推送到仓库:"
echo " docker pull $REGISTRY:$TAG"
echo
+183
View File
@@ -0,0 +1,183 @@
#!/bin/bash
# ============================================
# HYC下载站 v2.3 - 系统兼容性检查脚本
# 支持: X86_32, X86_64, ARMv7, ARM64, MIPS 等
# ============================================
set -e
# 颜色
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo "========================================"
echo " HYC下载站 v2.3 - 兼容性检查"
echo "========================================"
echo
# 检测架构
ARCH=$(uname -m)
OS=$(uname -s)
echo "系统信息:"
echo " - 架构: $ARCH"
echo " - 系统: $OS"
echo
# 架构特定配置
case "$ARCH" in
x86_64)
echo "✓ x86_64 (64位) - 完全支持"
RECOMMENDED_WORKERS=4
;;
i386|i686)
echo "⚠ i386/i686 (32位) - 有限支持"
RECOMMENDED_WORKERS=2
echo " 建议: --workers 2"
;;
armv7l|armv7hl)
echo "✓ ARMv7 (32位) - 完全支持"
RECOMMENDED_WORKERS=2
;;
armv8l|aarch64)
echo "✓ ARM64 (64位) - 完全支持"
RECOMMENDED_WORKERS=4
;;
armv6l)
echo "⚠ ARMv6 - 有限支持 (可能需要重新编译)"
RECOMMENDED_WORKERS=1
;;
mips*)
echo "⚠ MIPS - 实验性支持"
RECOMMENDED_WORKERS=1
;;
*)
echo "⚠ 未知架构: $ARCH - 需要测试"
RECOMMENDED_WORKERS=1
;;
esac
echo
# Python 版本检查
PYTHON_VERSION=$(python3 --version 2>&1 || echo "未安装")
echo "Python 版本: $PYTHON_VERSION"
PYTHON_MAJOR=$(python3 -c 'import sys; print(sys.version_info.major)' 2>/dev/null || echo "0")
PYTHON_MINOR=$(python3 -c 'import sys; print(sys.version_info.minor)' 2>/dev/null || echo "0")
if [ "$PYTHON_MAJOR" -ge 3 ] && [ "$PYTHON_MINOR" -ge 8 ]; then
echo "✓ Python 3.8+ - 支持"
else
echo "✗ 需要 Python 3.8 或更高版本"
exit 1
fi
echo
# 内存检查
echo "内存检查:"
if command -v free &> /dev/null; then
TOTAL_MEM=$(free -m | awk '/^Mem:/{print $2}')
echo " - 总内存: ${TOTAL_MEM}MB"
if [ "$TOTAL_MEM" -lt 256 ]; then
echo "⚠ 内存低于 256MB - 使用 ultra_low 预设"
echo " python main.py --preset ultra_low"
elif [ "$TOTAL_MEM" -lt 512 ]; then
echo "⚠ 内存 256-512MB - 使用 low 预设"
echo " python main.py --preset low"
elif [ "$TOTAL_MEM" -lt 1024 ]; then
echo "✓ 内存 512MB-1GB - 使用 medium 预设 (默认)"
else
echo "✓ 内存 1GB+ - 使用 high 预设 (默认)"
fi
else
echo " (无法检测,使用默认配置)"
fi
echo
# 磁盘空间检查
echo "磁盘空间检查:"
DISK_FREE=$(df -BG . | awk '/^\//{print $4}' | sed 's/G//')
echo " - 可用: ${DISK_FREE}GB"
if [ "$DISK_FREE" -lt 1 ]; then
echo "⚠ 可用空间不足 1GB"
fi
echo
# 必需依赖检查
echo "必需依赖检查:"
MISSING_DEPS=""
check_dep() {
if command -v "$1" &> /dev/null; then
echo " ✓ $1"
else
echo " ✗ $1 (必需)"
MISSING_DEPS="$MISSING_DEPS $1"
fi
}
check_dep "python3"
echo
# 可选依赖检查
echo "可选依赖检查:"
check_opt() {
if command -v "$1" &> /dev/null; then
echo " ✓ $1"
else
echo " ⚠ $1 (可选)"
fi
}
check_opt "psutil" || pip3 install psutil
check_opt "sqlalchemy" || pip3 install sqlalchemy
echo
# Docker 构建检查 (如果使用 Docker)
if [ -f "../Dockerfile" ]; then
echo "Docker 构建检查:"
if command -v docker &> /dev/null; then
echo " ✓ Docker 可用"
# 检查 buildx 支持
if docker buildx version &> /dev/null; then
echo " ✓ Docker BuildX 支持多架构构建"
echo
echo "多架构构建命令:"
echo " docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t hyc:v2.3 ."
fi
else
echo " ⚠ Docker 不可用 (需要安装 Docker)"
fi
echo
fi
# 推荐启动命令
echo "========================================"
echo " 推荐启动命令"
echo "========================================"
# 根据检测结果生成推荐命令
CMD="python main.py --preset "
if [ "$TOTAL_MEM" -lt 256 ]; then
CMD="${CMD}ultra_low"
elif [ "$TOTAL_MEM" -lt 512 ]; then
CMD="${CMD}low"
elif [ "$TOTAL_MEM" -lt 1024 ]; then
CMD="${CMD}medium"
else
CMD="${CMD}high"
fi
echo
echo "$CMD"
echo
# 完整命令示例
echo "完整示例:"
echo " $CMD --host 0.0.0.0 --port 8080 --base-dir ./downloads"
echo
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
平台检测工具
自动检测系统架构并推荐配置
"""
import sys
import os
# 添加项目根目录
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from core.optimization import (
ArchitectureDetector, LowMemoryConfig, check_compatibility
)
def main():
print("=" * 60)
print(" HYC下载站 v2.3 - 平台检测")
print("=" * 60)
print()
# 架构检测
print("[1/3] 架构信息")
arch = ArchitectureDetector.get_architecture()
for key, value in arch.items():
print(f" {key}: {value}")
print()
# 兼容性检查
print("[2/3] 兼容性检查")
compat = check_compatibility()
print(f" 兼容状态: {'✓ 通过' if compat['compatible'] else '✗ 有问题'}")
if compat['warnings']:
print(" 警告:")
for w in compat['warnings']:
print(f" ⚠ {w}")
print()
# 推荐配置
print("[3/3] 推荐配置")
recommended = ArchitectureDetector.get_recommended_config()
for key, value in recommended.items():
print(f" {key}: {value}")
# 低内存配置
low_mem = LowMemoryConfig('auto')
status = low_mem.get_status()
print()
print(" 自动检测预设:")
print(f" - 预设: {status['preset']}")
print(f" - 描述: {status['description']}")
print()
print(" 建议启动命令:")
cmd = f"python main.py --preset {status['preset']}"
if 'ultra_low' in str(status):
cmd += " --disable-ws --disable-sse"
print(f" {cmd}")
print()
# 返回状态码
return 0 if compat['compatible'] else 1
if __name__ == '__main__':
sys.exit(main())
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MC核心目录结构迁移脚本
将旧结构迁移到新结构:
旧: MCServerCore/{core}/{major}/{file.jar}
新: MCServerCore/{core}/{major}/{version}/{file.jar}
版本号提取规则:
- paper-1.20.1-6550.jar -> 1.20.1
- spongeneo-1.21.2-21.2.0-beta-13.0.0-RC1868-universal.jar -> 1.21.2
- spongevanilla-1.20.6-11.0.1-RC1916-universal.jar -> 1.20.6
"""
import os
import re
import shutil
from pathlib import Path
def extract_version(filename, core_name):
"""
从文件名中提取版本号
例如: spongevanilla-1.20.6-11.0.1-RC1916-universal.jar -> 1.20.6
例如: craftbukkit-1.9.jar -> 1.9
例如: spongevanilla-1.17-9.0.0-RC668-universal.jar -> 1.17
"""
# 去掉 .jar 后缀
name_without_jar = filename[:-4] if filename.endswith('.jar') else filename
# 模式:核心名-版本号-其他
# 版本号格式:数字.数字 或 数字.数字.数字 (如 1.9, 1.20.6, 1.21.2)
pattern = rf'^{re.escape(core_name)}-(\d+\.\d+(?:\.\d+)?)'
match = re.match(pattern, name_without_jar)
if match:
return match.group(1)
return None
def migrate_mc_structure(base_dir='./downloads/MCServerCore'):
"""迁移MC核心目录结构"""
mc_dir = Path(base_dir)
if not mc_dir.exists():
print(f"目录不存在: {mc_dir}")
return
migrated_count = 0
skipped_count = 0
errors = []
for jar_file in mc_dir.rglob('*.jar'):
try:
relative = jar_file.relative_to(mc_dir)
parts = relative.parts
except ValueError:
continue
# 格式: core/major/file.jar 或 core/major/version/file.jar
if len(parts) >= 3:
core_name = parts[0]
major = parts[1]
# 如果已经是新结构(len >= 4),跳过
if len(parts) >= 4:
print(f" 已在正确位置: {relative}")
continue
# 旧结构:core/major/file.jar
# 提取版本号
version = extract_version(jar_file.name, core_name)
if not version:
print(f" 无法提取版本号: {relative}")
skipped_count += 1
continue
# 构建新路径: core/major/version/file.jar
new_version_dir = jar_file.parent / version
new_path = new_version_dir / jar_file.name
# 如果目标目录已存在同名文件
if new_path.exists():
print(f" 目标已存在,跳过: {relative}")
skipped_count += 1
continue
try:
new_version_dir.mkdir(parents=True, exist_ok=True)
shutil.move(str(jar_file), str(new_path))
print(f" 移动: {relative} -> {new_path.relative_to(mc_dir)}")
migrated_count += 1
except Exception as e:
errors.append(f"移动失败 {relative}: {e}")
else:
print(f" 跳过(未知结构): {relative}")
skipped_count += 1
# 尝试清理空的目录
cleaned_dirs = 0
for dir_path in sorted(mc_dir.rglob('*'), reverse=True):
if dir_path.is_dir():
try:
if not any(dir_path.iterdir()):
dir_path.rmdir()
cleaned_dirs += 1
except OSError:
pass
print(f"\n迁移完成!")
print(f" 移动文件: {migrated_count}")
print(f" 跳过文件: {skipped_count}")
print(f" 清理空目录: {cleaned_dirs}")
if errors:
print(f"\n错误:")
for e in errors[:10]:
print(f" {e}")
if __name__ == '__main__':
import sys
base_dir = sys.argv[1] if len(sys.argv) > 1 else './downloads/MCServerCore'
print("=" * 60)
print("MC核心目录结构迁移工具")
print("旧结构: MCServerCore/{core}/{major}/{file.jar}")
print("新结构: MCServerCore/{core}/{major}/{version}/{file.jar}")
print("=" * 60)
print(f"基础目录: {base_dir}")
print()
migrate_mc_structure(base_dir)
+61
View File
@@ -0,0 +1,61 @@
#!/bin/bash
# ============================================
# HYC下载站 v2.3 - QEMU 仿真支持设置
# 用于在 x86_64 上构建 ARM 镜像
# ============================================
set -e
echo "========================================"
echo " QEMU 仿真支持设置"
echo "========================================"
# 检查是否需要设置
if [ "$(uname -m)" = "x86_64" ]; then
echo "检测到 x86_64 平台"
echo "将启用 ARM/i386 仿真支持"
fi
# 启用 QEMU 仿真
echo
echo "[1/3] 检查 QEMU ..."
# 注册 ARM 二进制格式
if [ -f /usr/bin/qemu-arm-static ]; then
echo "✓ qemu-arm-static 已安装"
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes || true
elif [ -f /usr/bin/qemu-arm ]; then
echo "✓ qemu-arm 已安装"
else
echo "⚠ QEMU 未安装,正在安装..."
apt-get update && apt-get install -y qemu-user-static || \
yum install -y qemu-user-static || \
apk add --no-cache qemu-arm qemu-i386
fi
# 设置 binfmt-misc
echo
echo "[2/3] 注册二进制格式..."
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 2>/dev/null || \
echo " (可能需要手动配置)"
echo
echo "[3/3] 验证设置..."
echo "可用的仿真架构:"
for arch in arm arm64 i386; do
if [ -f "/usr/bin/qemu-$arch-static" ] || [ -f "/usr/bin/qemu-$arch" ]; then
echo " ✓ $arch"
else
echo " ✗ $arch"
fi
done
echo
echo "========================================"
echo "✓ QEMU 设置完成"
echo "========================================"
echo
echo "现在可以构建多架构镜像:"
echo " ./scripts/build-multiarch.sh"
echo
+601
View File
@@ -0,0 +1,601 @@
{
"server_name": "HYC下载站",
"host": "0.0.0.0",
"port": 8080,
"base_dir": "./downloads",
"api_version": "v2",
"directory_listing": true,
"enable_stats": true,
"show_hash": true,
"ignore_hidden": true,
"max_upload_size": 1073741824,
"timeout": 30,
"enable_range": true,
"max_workers": 10,
"auth_type": "basic",
"auth_user": "admin",
"auth_pass": "admin123",
"auth_token": "admin_token_123456",
"auth_session_timeout": 3600,
"auth_cookie_max_age": 86400,
"ip_whitelist_enabled": false,
"ip_whitelist": [],
"ip_blacklist": [],
"enable_ws": true,
"enable_sse": true,
"enable_monitor": true,
"monitor_interval": 3,
"enable_sync": true,
"sync_config": null,
"sync_sources": {},
"enable_mirrors": true,
"database": {
"enabled": true,
"type": "sqlite",
"sync_interval": 60,
"sqlite": {
"path": "./data/hyc.db"
},
"postgresql": {
"enabled": false,
"host": "localhost",
"port": 5432,
"database": "hyc",
"table_prefix": "hyc_",
"user": "postgres",
"password": "",
"pool_size": 5,
"max_overflow": 10,
"ssl_mode": "prefer",
"connection_timeout": 30
},
"mysql": {
"enabled": false,
"host": "localhost",
"port": 3306,
"database": "hyc",
"table_prefix": "hyc_",
"user": "root",
"password": "",
"pool_size": 5,
"max_overflow": 10,
"charset": "utf8mb4",
"connection_timeout": 30
},
"external": {
"enabled": false,
"connection_string": "",
"table_prefix": "hyc_",
"pool_size": 5,
"max_overflow": 10
}
},
"mirrors": {
"docker-cn": {
"type": "docker",
"url": "https://docker.m.daocloud.io",
"target": "docker",
"enabled": false,
"storage_dir": "docker-cn",
"auto_sync": false,
"custom": true,
"description": "Docker 镜像 - DaoCloud",
"schedule": {
"enabled": false,
"type": "interval",
"interval": {
"hours": 6
}
}
},
"docker": {
"type": "docker",
"url": "https://registry-1.docker.io",
"target": "docker",
"enabled": false,
"storage_dir": "docker",
"auto_sync": false,
"custom": true,
"description": "Docker 官方镜像",
"schedule": {
"enabled": false,
"type": "interval",
"interval": {
"hours": 6
}
}
},
"ubuntu": {
"type": "apt",
"url": "http://archive.ubuntu.com/ubuntu",
"target": "apt/ubuntu",
"enabled": false,
"storage_dir": "ubuntu",
"auto_sync": false,
"custom": true,
"description": "Ubuntu 软件源",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
}
},
"ubuntu-cn": {
"type": "apt",
"url": "https://mirrors.tuna.tsinghua.edu.cn/ubuntu",
"target": "apt/ubuntu-cn",
"enabled": false,
"storage_dir": "ubuntu-cn",
"auto_sync": false,
"custom": true,
"description": "Ubuntu 镜像 - 清华源",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
}
},
"debian": {
"type": "apt",
"url": "http://deb.debian.org/debian",
"target": "apt/debian",
"enabled": false,
"storage_dir": "debian",
"auto_sync": false,
"custom": true,
"description": "Debian 软件源",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
}
},
"debian-cn": {
"type": "apt",
"url": "https://mirrors.tuna.tsinghua.edu.cn/debian",
"target": "apt/debian-cn",
"enabled": false,
"storage_dir": "debian-cn",
"auto_sync": false,
"custom": true,
"description": "Debian 镜像 - 清华源",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
}
},
"centos": {
"type": "yum",
"url": "http://mirror.centos.org/centos",
"target": "yum/centos",
"enabled": false,
"storage_dir": "centos",
"auto_sync": false,
"custom": true,
"description": "CentOS 软件源",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
}
},
"almalinux": {
"type": "yum",
"url": "https://repo.almalinux.org/vault",
"target": "yum/almalinux",
"enabled": false,
"storage_dir": "almalinux",
"auto_sync": false,
"custom": true,
"description": "AlmaLinux 软件源",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
}
},
"archlinux": {
"type": "http",
"url": "https://geo.mirror.pkgbuild.com",
"target": "pacman",
"enabled": false,
"storage_dir": "archlinux",
"auto_sync": false,
"custom": true,
"description": "Arch Linux 镜像",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
}
},
"pypi": {
"type": "pypi",
"url": "https://pypi.org",
"target": "pypi",
"enabled": false,
"storage_dir": "pypi",
"auto_sync": false,
"custom": true,
"description": "PyPI 官方源",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
}
},
"pypi-cn": {
"type": "pypi",
"url": "https://pypi.tuna.tsinghua.edu.cn/simple",
"target": "pypi-cn",
"enabled": false,
"storage_dir": "pypi-cn",
"auto_sync": false,
"custom": true,
"description": "PyPI 镜像 - 清华源",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
},
"updated_at": "2026-02-18T15:37:32.588006"
},
"npm": {
"type": "npm",
"url": "https://registry.npmjs.org",
"target": "npm",
"enabled": false,
"storage_dir": "npm",
"auto_sync": false,
"custom": true,
"description": "NPM 官方源",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
}
},
"npm-cn": {
"type": "npm",
"url": "https://registry.npmmirror.com",
"target": "npm-cn",
"enabled": false,
"storage_dir": "npm-cn",
"auto_sync": false,
"custom": true,
"description": "NPM 镜像 - 阿里源",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
}
},
"go": {
"type": "go",
"url": "https://proxy.golang.org",
"target": "go",
"enabled": false,
"storage_dir": "go",
"auto_sync": false,
"custom": true,
"description": "Go 模块代理",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
}
},
"go-cn": {
"type": "go",
"url": "https://goproxy.cn",
"target": "go-cn",
"enabled": false,
"storage_dir": "go-cn",
"auto_sync": false,
"custom": true,
"description": "Go 模块代理 - 七牛云",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
}
},
"maven": {
"type": "http",
"url": "https://repo.maven.apache.org/maven2",
"target": "maven",
"enabled": false,
"storage_dir": "maven",
"auto_sync": false,
"custom": true,
"description": "Maven 中央仓库",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
}
},
"maven-cn": {
"type": "http",
"url": "https://maven.aliyun.com/repository/public",
"target": "maven-cn",
"enabled": false,
"storage_dir": "maven-cn",
"auto_sync": false,
"custom": true,
"description": "Maven 镜像 - 阿里云",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
}
},
"gradle": {
"type": "http",
"url": "https://services.gradle.org/distributions",
"target": "gradle",
"enabled": false,
"storage_dir": "gradle",
"auto_sync": false,
"custom": true,
"description": "Gradle 分发包",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
}
},
"rubygems": {
"type": "http",
"url": "https://rubygems.org",
"target": "gem",
"enabled": false,
"storage_dir": "rubygems",
"auto_sync": false,
"custom": true,
"description": "RubyGems 官方源",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
}
},
"rubygems-cn": {
"type": "http",
"url": "https://gems.ruby-china.com",
"target": "gem-cn",
"enabled": false,
"storage_dir": "rubygems-cn",
"auto_sync": false,
"custom": true,
"description": "RubyGems 镜像 - Ruby China",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
}
},
"cargo": {
"type": "http",
"url": "https://crates.io",
"target": "cargo",
"enabled": false,
"storage_dir": "cargo",
"auto_sync": false,
"custom": true,
"description": "Crates.io Rust 包",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
}
},
"cargo-cn": {
"type": "http",
"url": "https://mirrors.tuna.tsinghua.edu.cn/crates.io-index",
"target": "cargo-cn",
"enabled": false,
"storage_dir": "cargo-cn",
"auto_sync": false,
"custom": true,
"description": "Cargo 镜像 - 清华源",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
}
},
"nuget": {
"type": "http",
"url": "https://api.nuget.org/v3/index.json",
"target": "nuget",
"enabled": false,
"storage_dir": "nuget",
"auto_sync": false,
"custom": true,
"description": "NuGet 官方源",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
}
},
"cocoapods": {
"type": "http",
"url": "https://cdn.cocoapods.org",
"target": "cocoapods",
"enabled": false,
"storage_dir": "cocoapods",
"auto_sync": false,
"custom": true,
"description": "CocoaPods 官方源",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
}
},
"cran": {
"type": "http",
"url": "https://cloud.r-project.org",
"target": "cran",
"enabled": false,
"storage_dir": "cran",
"auto_sync": false,
"custom": true,
"description": "CRAN R 包镜像",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
}
},
"ctan": {
"type": "http",
"url": "https://ctan.math.illinois.edu",
"target": "ctan",
"enabled": false,
"storage_dir": "ctan",
"auto_sync": false,
"custom": true,
"description": "CTAN LaTeX 包",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
}
},
"nvidia-cuda": {
"type": "http",
"url": "https://developer.download.nvidia.com/compute/cuda/repos",
"target": "cuda",
"enabled": false,
"storage_dir": "cuda",
"auto_sync": false,
"custom": true,
"description": "NVIDIA CUDA 仓库",
"schedule": {
"enabled": false,
"type": "cron",
"cron": "0 4 * * *"
}
}
},
"scheduled_tasks": {
"cleanup_db": {
"type": "cron",
"config": {
"cron": "0 2 * * *",
"enabled": false
}
}
},
"health_check": {
"enabled": true,
"interval": 60,
"timeout": 10,
"max_retries": 3,
"failure_threshold": 3
},
"failover": {
"enabled": false,
"threshold": 3
},
"alerts": {
"enabled": false,
"email": {
"enabled": false,
"smtp_host": "smtp.example.com",
"smtp_port": 587,
"smtp_user": "",
"smtp_password": "",
"from_address": "[email protected]",
"to_addresses": [],
"use_tls": true
},
"webhook": {
"enabled": false,
"url": ""
},
"rules": {
"disk_high": {
"enabled": true,
"threshold": 80,
"severity": "warning"
},
"disk_critical": {
"enabled": true,
"threshold": 90,
"severity": "critical"
},
"sync_failed": {
"enabled": true,
"severity": "error"
},
"source_unhealthy": {
"enabled": true,
"severity": "warning"
},
"service_down": {
"enabled": true,
"severity": "critical"
}
}
},
"webhooks": {
"enabled": false,
"storage": "webhooks.json",
"url": ""
},
"auth_sessions_file": "auth_sessions.json",
"ssl": {
"enabled": false,
"cert": null,
"key": null
},
"rate_limit": 100,
"debug": false,
"debug_log_file": null,
"debug_http": false,
"debug_auth": false,
"debug_api": false,
"debug_v2": false,
"debug_error": false,
"debug_download": false,
"restart": {
"enabled": true,
"graceful_timeout": 30,
"shutdown_timeout": 10,
"strategy": "graceful"
},
"version": "2.2",
"sort_by": "name",
"sort_reverse": false,
"calculate_hash": false,
"max_search_results": 100,
"session_timeout": 60,
"sync_interval": 60,
"auto_sync": false,
"auto_clean": false,
"clean_interval": 7,
"enable_browse": true,
"show_hidden": false,
"enable_websocket": true,
"max_threads": 4,
"health_check_interval": 30,
"request_timeout": 300,
"enable_alerts": true,
"disk_warning_threshold": 80,
"disk_critical_threshold": 90,
"smtp_server": "",
"smtp_port": 587,
"smtp_from": "",
"alert_email": "",
"log_file": "./logs/hyc.log",
"log_level": "INFO",
"cache_size": 1073741824,
"cache_ttl": 3600,
"pool_size": 5,
"max_overflow": 10
}
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
cd /root/vs1
python3 main.py --host 0.0.0.0 --port 8080