| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290 |
- /**
- * Docker Hub 服务模块
- */
- const axios = require('axios');
- const logger = require('../logger');
- const pLimit = require('p-limit');
- const axiosRetry = require('axios-retry');
- // 配置并发限制,最多5个并发请求
- const limit = pLimit(5);
- // 优化HTTP请求配置
- const httpOptions = {
- timeout: 15000, // 15秒超时
- headers: {
- 'User-Agent': 'DockerHubSearchClient/1.0',
- 'Accept': 'application/json'
- }
- };
- // 配置Axios重试
- axiosRetry(axios, {
- retries: 3, // 最多重试3次
- retryDelay: (retryCount) => {
- console.log(`[INFO] 重试 Docker Hub 请求 (${retryCount}/3)`);
- return retryCount * 1000; // 重试延迟,每次递增1秒
- },
- retryCondition: (error) => {
- // 只在网络错误或5xx响应时重试
- return axiosRetry.isNetworkOrIdempotentRequestError(error) ||
- (error.response && error.response.status >= 500);
- }
- });
- // 搜索仓库
- async function searchRepositories(term, page = 1, requestCache = null) {
- const cacheKey = `search_${term}_${page}`;
- let cachedResult = null;
-
- // 安全地检查缓存
- if (requestCache && typeof requestCache.get === 'function') {
- cachedResult = requestCache.get(cacheKey);
- }
-
- if (cachedResult) {
- console.log(`[INFO] 返回缓存的搜索结果: ${term} (页码: ${page})`);
- return cachedResult;
- }
-
- console.log(`[INFO] 搜索Docker Hub: ${term} (页码: ${page})`);
-
- try {
- // 使用更安全的直接请求方式,避免pLimit可能的问题
- const url = `https://hub.docker.com/v2/search/repositories/?query=${encodeURIComponent(term)}&page=${page}&page_size=25`;
- const response = await axios.get(url, httpOptions);
- const result = response.data;
-
- // 将结果缓存(如果缓存对象可用)
- if (requestCache && typeof requestCache.set === 'function') {
- requestCache.set(cacheKey, result);
- }
-
- return result;
- } catch (error) {
- logger.error('搜索Docker Hub失败:', error.message);
- // 重新抛出错误以便上层处理
- throw new Error(error.message || '搜索Docker Hub失败');
- }
- }
- // 获取所有标签
- async function getAllTags(imageName, isOfficial) {
- const fullImageName = isOfficial ? `library/${imageName}` : imageName;
- logger.info(`获取所有镜像标签: ${fullImageName}`);
-
- // 为所有标签请求设置超时限制
- const allTagsPromise = fetchAllTags(fullImageName);
- const timeoutPromise = new Promise((_, reject) =>
- setTimeout(() => reject(new Error('获取所有标签超时')), 30000)
- );
-
- try {
- // 使用Promise.race确保请求不会无限等待
- const allTags = await Promise.race([allTagsPromise, timeoutPromise]);
-
- // 过滤掉无效平台信息
- const cleanedTags = allTags.map(tag => {
- if (tag.images && Array.isArray(tag.images)) {
- tag.images = tag.images.filter(img => !(img.os === 'unknown' && img.architecture === 'unknown'));
- }
- return tag;
- });
-
- return {
- count: cleanedTags.length,
- results: cleanedTags,
- all_pages_loaded: true
- };
- } catch (error) {
- logger.error(`获取所有标签失败: ${error.message}`);
- throw error;
- }
- }
- // 获取特定页的标签
- async function getTagsByPage(imageName, isOfficial, page, pageSize) {
- const fullImageName = isOfficial ? `library/${imageName}` : imageName;
- logger.info(`获取镜像标签: ${fullImageName}, 页码: ${page}, 页面大小: ${pageSize}`);
-
- const tagsUrl = `https://hub.docker.com/v2/repositories/${fullImageName}/tags?page=${page}&page_size=${pageSize}`;
-
- try {
- const tagsResponse = await axios.get(tagsUrl, {
- timeout: 15000,
- headers: {
- 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36'
- }
- });
-
- // 检查响应数据有效性
- if (!tagsResponse.data || typeof tagsResponse.data !== 'object') {
- logger.warn(`镜像 ${fullImageName} 返回的数据格式不正确`);
- return { count: 0, results: [] };
- }
-
- if (!tagsResponse.data.results || !Array.isArray(tagsResponse.data.results)) {
- logger.warn(`镜像 ${fullImageName} 没有返回有效的标签数据`);
- return { count: 0, results: [] };
- }
-
- // 过滤掉无效平台信息
- const cleanedResults = tagsResponse.data.results.map(tag => {
- if (tag.images && Array.isArray(tag.images)) {
- tag.images = tag.images.filter(img => !(img.os === 'unknown' && img.architecture === 'unknown'));
- }
- return tag;
- });
-
- return {
- ...tagsResponse.data,
- results: cleanedResults
- };
- } catch (error) {
- logger.error(`获取标签列表失败: ${error.message}`, {
- url: tagsUrl,
- status: error.response?.status,
- statusText: error.response?.statusText
- });
- throw error;
- }
- }
- // 获取标签数量
- async function getTagCount(name, isOfficial, requestCache) {
- const cacheKey = `tag_count_${name}_${isOfficial}`;
- const cachedResult = requestCache?.get(cacheKey);
-
- if (cachedResult) {
- console.log(`[INFO] 返回缓存的标签计数: ${name}`);
- return cachedResult;
- }
-
- const fullImageName = isOfficial ? `library/${name}` : name;
- const apiUrl = `https://hub.docker.com/v2/repositories/${fullImageName}/tags/?page_size=1`;
-
- try {
- const result = await limit(async () => {
- const response = await axios.get(apiUrl, httpOptions);
- return {
- count: response.data.count,
- recommended_mode: response.data.count > 500 ? 'paginated' : 'full'
- };
- });
-
- if (requestCache) {
- requestCache.set(cacheKey, result);
- }
-
- return result;
- } catch (error) {
- throw error;
- }
- }
- // 递归获取所有标签
- async function fetchAllTags(fullImageName, page = 1, allTags = [], maxPages = 10) {
- if (page > maxPages) {
- logger.warn(`达到最大页数限制 (${maxPages}),停止获取更多标签`);
- return allTags;
- }
-
- const pageSize = 100; // 使用最大页面大小
- const url = `https://hub.docker.com/v2/repositories/${fullImageName}/tags?page=${page}&page_size=${pageSize}`;
-
- try {
- logger.info(`获取标签页 ${page}/${maxPages}...`);
-
- const response = await axios.get(url, {
- timeout: 10000,
- headers: {
- 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36'
- }
- });
-
- if (!response.data.results || !Array.isArray(response.data.results)) {
- logger.warn(`页 ${page} 没有有效的标签数据`);
- return allTags;
- }
-
- allTags.push(...response.data.results);
- logger.info(`已获取 ${allTags.length}/${response.data.count || 'unknown'} 个标签`);
-
- // 检查是否有下一页
- if (response.data.next && allTags.length < response.data.count) {
- // 添加一些延迟以避免请求过快
- await new Promise(resolve => setTimeout(resolve, 500));
- return fetchAllTags(fullImageName, page + 1, allTags, maxPages);
- }
-
- logger.success(`成功获取所有 ${allTags.length} 个标签`);
- return allTags;
- } catch (error) {
- logger.error(`递归获取标签失败 (页码 ${page}): ${error.message}`);
-
- // 如果已经获取了一些标签,返回这些标签而不是抛出错误
- if (allTags.length > 0) {
- return allTags;
- }
-
- // 如果没有获取到任何标签,则抛出错误
- throw error;
- }
- }
- // 统一的错误处理函数
- function handleAxiosError(error, res, message) {
- let errorDetails = '';
-
- if (error.response) {
- // 服务器响应错误的错误处理函数
- const status = error.response.status;
- errorDetails = `状态码: ${status}`;
-
- if (error.response.data && error.response.data.message) {
- errorDetails += `, 信息: ${error.response.data.message}`;
- }
-
- console.error(`[ERROR] ${message}: ${errorDetails}`);
-
- res.status(status).json({
- error: `${message} (${errorDetails})`,
- details: error.response.data
- });
- } else if (error.request) {
- // 请求已发送但没有收到响应
- if (error.code === 'ECONNRESET') {
- errorDetails = '连接被重置,这可能是由于网络不稳定或服务端断开连接';
- } else if (error.code === 'ECONNABORTED') {
- errorDetails = '请求超时,服务器响应时间过长';
- } else {
- errorDetails = `${error.code || '未知错误代码'}: ${error.message}`;
- }
-
- console.error(`[ERROR] ${message}: ${errorDetails}`);
-
- res.status(503).json({
- error: `${message} (${errorDetails})`,
- retryable: true
- });
- } else {
- // 其他错误
- errorDetails = error.message;
- console.error(`[ERROR] ${message}: ${errorDetails}`);
- console.error(`[ERROR] 错误堆栈: ${error.stack}`);
-
- res.status(500).json({
- error: `${message} (${errorDetails})`,
- retryable: true
- });
- }
- }
- module.exports = {
- searchRepositories,
- getAllTags,
- getTagsByPage,
- getTagCount,
- fetchAllTags,
- handleAxiosError
- };
|