526 lines
17 KiB
Python
526 lines
17 KiB
Python
#!/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]}"
|