您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在Docker Hub页面添加下载按钮,实现离线镜像下载功能
当前为
// ==UserScript== // @name Docker Hub 镜像下载器 // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description 在Docker Hub页面添加下载按钮,实现离线镜像下载功能 // @author X6nux // @match https://hub.docker.com/* // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_download // @require https://update.gf.qytechs.cn/scripts/539732/1609156/tarballjs.js // @license MIT // ==/UserScript== /** * Docker Hub 镜像下载器油猴脚本 * * 文件说明: * 本脚本是一个Tampermonkey用户脚本,用于在Docker Hub网站上添加镜像下载功能。 * 它可以直接在浏览器中下载Docker镜像并组装成标准的TAR格式文件。 * * 主要功能: * 1. 在Docker Hub镜像页面自动添加下载器界面 * 2. 支持多种CPU架构选择(amd64、arm64等) * 3. 实现分层下载和前端组装 * 4. 生成标准的Docker TAR文件 * 5. 支持架构自动检测 * * 安装使用: * 1. 安装Tampermonkey浏览器插件 * 2. 点击安装此脚本 * 3. 访问任意Docker Hub镜像页面 * 4. 使用页面顶部的下载器工具 */ (function() { 'use strict'; // ==================== 全局变量和配置 ==================== let downloadInProgress = false; // 下载进程状态标志 let manifestData = null; // 镜像清单数据存储 let downloadedLayers = new Map(); // 已下载镜像层的映射表 let tempLayerCache = new Map(); // 临时层数据缓存 let db = null; // IndexedDB数据库实例 let cachedArchitectures = null; // 缓存的架构信息,包含架构和对应的SHA256 let useTemporaryCache = false; // 是否使用临时缓存(minimal模式) let selectedMemoryMode = 'minimal'; // 内存模式选择,直接使用最小内存模式 let downloadProgressMap = new Map(); // 实时下载进度映射 let progressUpdateInterval = null; // 进度更新定时器 // API服务器配置 const API_BASE_URL = 'https://registry.lfree.org/api'; // ==================== 样式定义 ==================== /** * 添加自定义CSS样式到页面 * 功能:定义下载器界面的所有视觉样式 */ function addCustomStyles() { GM_addStyle(` /* 主要下载按钮样式 - 内联版本 */ .docker-download-btn { background: linear-gradient(145deg, #28a745, #218838); color: white; border: 2px solid #28a745; padding: 8px 16px; border-radius: 6px; font-size: 13px; font-weight: 600; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); display: inline-flex; align-items: center; gap: 6px; margin: 0 8px 0 0; box-shadow: 0 2px 8px rgba(40, 167, 69, 0.2); text-decoration: none; position: relative; overflow: hidden; vertical-align: middle; } /* 按钮悬停效果 */ .docker-download-btn:hover { background: linear-gradient(145deg, #218838, #1e7e34); transform: translateY(-2px); box-shadow: 0 6px 20px rgba(40, 167, 69, 0.4); color: white; text-decoration: none; } /* 按钮禁用状态 */ .docker-download-btn:disabled { background: linear-gradient(145deg, #6c757d, #5a6268); cursor: not-allowed; transform: none; animation: downloadProgress 2s infinite linear; } /* 架构选择下拉框样式 - 内联版本 */ .arch-selector { margin: 0 8px 0 0; padding: 6px 10px; border: 1px solid #007bff; border-radius: 4px; background: white; font-size: 12px; min-width: 120px; vertical-align: middle; cursor: pointer; } .arch-selector:hover { border-color: #0056b3; box-shadow: 0 2px 4px rgba(0, 123, 255, 0.25); } .arch-selector:focus { outline: none; border-color: #0056b3; box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25); } /* 移除进度显示区域样式,改为在按钮上显示 */ /* 下载进度动画 */ @keyframes downloadProgress { 0% { background-position: -100% 0; } 100% { background-position: 100% 0; } } /* 内联按钮容器样式 */ .docker-download-container { display: inline-flex !important; align-items: center !important; gap: 8px !important; margin: 0 10px !important; vertical-align: middle !important; background: none !important; border: none !important; padding: 0 !important; box-shadow: none !important; font-family: inherit !important; } /* 检测架构按钮特殊样式 */ .detect-arch-btn { background: linear-gradient(145deg, #6f42c1, #5a32a3); border-color: #6f42c1; } .detect-arch-btn:hover { background: linear-gradient(145deg, #5a32a3, #4e2a87); border-color: #5a32a3; } /* 响应式设计 */ @media (max-width: 768px) { .control-row { flex-direction: column; align-items: stretch; } .docker-download-btn, .arch-selector { width: 100%; margin: 5px 0; } } `); } // ==================== 数据存储管理 ==================== /** * 初始化IndexedDB数据库 * 功能:创建和配置用于存储镜像层数据的本地数据库 * @returns {Promise} 数据库初始化完成的Promise */ function initIndexedDB() { return new Promise((resolve, reject) => { const request = indexedDB.open('DockerImageStore', 1); // 数据库连接失败处理 request.onerror = () => reject(request.error); // 数据库连接成功处理 request.onsuccess = () => { db = request.result; resolve(); }; // 数据库结构升级处理 request.onupgradeneeded = (event) => { const database = event.target.result; // 创建镜像层存储表 if (!database.objectStoreNames.contains('layers')) { const layerStore = database.createObjectStore('layers', { keyPath: 'digest' }); layerStore.createIndex('imageName', 'imageName', { unique: false }); } // 创建镜像清单存储表 if (!database.objectStoreNames.contains('manifests')) { const manifestStore = database.createObjectStore('manifests', { keyPath: 'key' }); } }; }); } // ==================== 页面信息提取 ==================== /** * 从当前Docker Hub页面提取镜像信息 * 功能:自动识别并提取镜像名称和标签信息 * @returns {Object} 包含imageName和imageTag的对象 */ function extractImageInfo() { const url = window.location.pathname; const pathParts = url.split('/').filter(part => part); let imageName = ''; let imageTag = 'latest'; // Docker Hub URL格式分析和处理 // 支持的格式: // - 官方镜像: /r/nginx, /_/nginx // - 用户镜像: /r/username/imagename // - 组织镜像: /r/organization/imagename // - 镜像层页面: /layers/username/imagename/tag/images/sha256-... // 解析URL路径段 if (pathParts.length >= 4 && pathParts[0] === 'layers') { // 处理 layers 页面格式: /layers/username/imagename/tag/... const namespace = pathParts[1]; const repoName = pathParts[2]; const tag = pathParts[3]; if (namespace === '_') { // 官方镜像: /layers/_/nginx/latest/... imageName = repoName; } else { // 用户/组织镜像: /layers/username/imagename/tag/... imageName = namespace + '/' + repoName; } imageTag = tag; // 从layers URL提取镜像信息 } else if (pathParts.length >= 2 && pathParts[0] === 'r') { // 处理 /r/ 路径格式 if (pathParts.length === 2) { // 官方镜像: /r/nginx imageName = pathParts[1]; } else if (pathParts.length >= 3) { // 用户/组织镜像: /r/username/imagename imageName = pathParts[1] + '/' + pathParts[2]; } // 检查是否有tags路径来提取特定标签 if (pathParts.includes('tags') && pathParts.length > pathParts.indexOf('tags') + 1) { const tagIndex = pathParts.indexOf('tags') + 1; imageTag = pathParts[tagIndex]; } // 从/r/ URL提取镜像信息 } else if (pathParts.length >= 2 && pathParts[0] === '_') { // 官方镜像的另一种格式: /_/nginx imageName = pathParts[1]; // 从/_/ URL提取镜像信息 } else if (pathParts.length >= 2) { // 其他格式的通用处理(保留原有逻辑作为备用) imageName = pathParts[0] + '/' + pathParts[1]; // 通用格式提取镜像信息 } // 从页面DOM元素提取信息作为备用方案 if (!imageName) { // 尝试从页面标题获取镜像名称 const titleSelectors = [ 'h1[data-testid="repository-title"]', '.RepositoryNameHeader__repositoryName', 'h1.repository-title', '.repository-name' ]; for (const selector of titleSelectors) { const titleElement = document.querySelector(selector); if (titleElement) { imageName = titleElement.textContent.trim(); break; } } // 尝试从面包屑导航获取 if (!imageName) { const breadcrumbLinks = document.querySelectorAll('[data-testid="breadcrumb"] a, .breadcrumb a'); if (breadcrumbLinks.length >= 2) { imageName = breadcrumbLinks[breadcrumbLinks.length - 1].textContent.trim(); } } } // 提取标签信息 const tagSelectors = [ '[data-testid="tag-name"]', '.tag-name', '.current-tag', '.active-tag' ]; for (const selector of tagSelectors) { const tagElement = document.querySelector(selector); if (tagElement) { const tagText = tagElement.textContent.trim(); imageTag = tagText.replace(':', '').replace('Tag: ', ''); break; } } // 从URL查询参数获取标签信息 const urlParams = new URLSearchParams(window.location.search); const tagFromUrl = urlParams.get('tag'); if (tagFromUrl) { imageTag = tagFromUrl; } // 最终结果处理和修正 if (imageName) { // 对于官方镜像(不包含斜杠的镜像名),添加library前缀 if (!imageName.includes('/') && imageName !== '') { imageName = 'library/' + imageName; // 添加library前缀 } } console.log('最终提取的镜像信息:', { imageName, imageTag, url }); return { imageName, imageTag }; } // ==================== UI界面创建 ==================== /** * 创建内联下载按钮 * 功能:生成直接集成到Docker Hub界面中的下载按钮 * @returns {HTMLElement} 下载按钮DOM元素 */ function createInlineDownloadButton() { const button = document.createElement('button'); button.className = 'docker-download-btn'; button.id = 'downloadBtn'; button.innerHTML = '🚀 下载镜像'; button.title = '点击下载Docker镜像'; return button; } /** * 创建架构选择下拉框 * 功能:生成紧凑的架构选择器,支持自动架构检测 * @returns {HTMLElement} 架构选择器DOM元素 */ function createArchSelector() { const select = document.createElement('select'); select.className = 'arch-selector'; select.id = 'archSelector'; select.title = '选择目标架构(将自动检测页面架构)'; select.innerHTML = ` <option value="">自动检测</option> <option value="linux/amd64">linux/amd64</option> <option value="linux/arm64">linux/arm64</option> <option value="linux/arm/v7">linux/arm/v7</option> <option value="linux/arm/v6">linux/arm/v6</option> <option value="linux/386">linux/386</option> <option value="windows/amd64">windows/amd64</option> `; // 异步设置自动检测的架构和更新可用架构列表 setTimeout(async () => { const indicator = document.getElementById('archIndicator'); try { // 显示检测状态 if (indicator) { indicator.style.display = 'inline'; indicator.textContent = '🔍 获取架构列表...'; } // 首先获取镜像的可用架构列表 const availableArchs = await getAvailableArchitectures(); if (availableArchs && availableArchs.length > 0) { // 更新架构选择器选项 updateArchSelectorOptions(select, availableArchs); addLog(`已更新架构选择器,包含 ${availableArchs.length} 个可用架构`); // 架构信息已更新到选择器中 if (indicator) { indicator.textContent = '🔍 检测当前架构...'; } } // 然后检测当前页面的架构 const detectedArch = await autoDetectArchitecture(); if (detectedArch) { // 检查选项中是否存在检测到的架构 const existingOption = select.querySelector(`option[value="${detectedArch}"]`); if (existingOption) { select.value = detectedArch; addLog(`架构选择器已设置为检测到的架构: ${detectedArch}`); } else { // 如果选项中不存在,添加新选项 const newOption = document.createElement('option'); newOption.value = detectedArch; newOption.textContent = detectedArch; newOption.selected = true; select.appendChild(newOption); addLog(`已添加并选择检测到的架构: ${detectedArch}`); } // 更新选择器标题显示当前检测到的架构 select.title = `当前架构: ${detectedArch} (自动检测)`; // 更新状态指示器 if (indicator) { indicator.textContent = `✅ ${detectedArch}`; indicator.style.color = '#28a745'; setTimeout(() => { indicator.style.display = 'none'; }, 3000); } } else { // 检测失败时的处理 if (indicator) { indicator.textContent = '❌ 检测失败'; indicator.style.color = '#dc3545'; setTimeout(() => { indicator.style.display = 'none'; }, 3000); } } } catch (error) { addLog(`自动架构检测失败: ${error.message}`, 'error'); if (indicator) { indicator.textContent = '❌ 检测失败'; indicator.style.color = '#dc3545'; setTimeout(() => { indicator.style.display = 'none'; }, 3000); } } }, 1000); // 延迟1秒等待页面完全加载 return select; } /** * 创建检测架构按钮 * 功能:生成架构检测按钮 * @returns {HTMLElement} 检测按钮DOM元素 */ function createDetectArchButton() { const button = document.createElement('button'); button.className = 'docker-download-btn detect-arch-btn'; button.id = 'detectArchBtn'; button.innerHTML = '🔍 检测架构'; button.title = '检测镜像支持的架构'; return button; } // 移除进度显示区域创建函数,改为在按钮上显示进度 // ==================== 日志和工具函数 ==================== /** * 添加日志信息(仅在控制台显示) * 功能:在控制台记录下载进度和状态信息 * @param {string} message - 要显示的日志消息 * @param {string} type - 日志类型(info, error, success) */ function addLog(message, type = 'info') { console.log('Docker Downloader:', message); } /** * 更新下载按钮上的进度显示(增强版) * 功能:在下载按钮上显示详细的下载信息和进度 * @param {string} text - 要显示的文本 * @param {string} status - 状态类型(downloading, complete, error) * @param {Object} details - 详细信息对象 */ function updateButtonProgress(text, status = 'downloading', details = {}) { const downloadBtn = document.getElementById('downloadBtn'); if (!downloadBtn) return; // 构建详细的按钮文本 let buttonText = text; // 如果有详细信息,添加到按钮文本中 if (details.progress !== undefined) { buttonText += ` ${details.progress}%`; } if (details.speed && details.speed > 0) { buttonText += ` (${formatSpeed(details.speed)})`; } if (details.current && details.total) { buttonText += ` [${details.current}/${details.total}]`; } if (details.size && status !== 'downloading') { buttonText += ` ${formatSize(details.size)}`; } downloadBtn.textContent = buttonText; // 根据状态设置按钮样式 switch (status) { case 'downloading': downloadBtn.style.background = 'linear-gradient(145deg, #007bff, #0056b3)'; downloadBtn.style.color = 'white'; downloadBtn.disabled = true; break; case 'complete': downloadBtn.style.background = 'linear-gradient(145deg, #28a745, #218838)'; downloadBtn.style.color = 'white'; downloadBtn.disabled = false; setTimeout(() => { downloadBtn.textContent = '🚀 下载镜像'; downloadBtn.style.background = 'linear-gradient(145deg, #28a745, #218838)'; }, 3000); break; case 'error': downloadBtn.style.background = 'linear-gradient(145deg, #dc3545, #c82333)'; downloadBtn.style.color = 'white'; downloadBtn.disabled = false; setTimeout(() => { downloadBtn.textContent = '🚀 下载镜像'; downloadBtn.style.background = 'linear-gradient(145deg, #28a745, #218838)'; }, 3000); break; case 'analyzing': downloadBtn.style.background = 'linear-gradient(145deg, #6f42c1, #5a32a3)'; downloadBtn.style.color = 'white'; downloadBtn.disabled = true; break; case 'assembling': downloadBtn.style.background = 'linear-gradient(145deg, #fd7e14, #e8690b)'; downloadBtn.style.color = 'white'; downloadBtn.disabled = true; break; default: downloadBtn.style.background = 'linear-gradient(145deg, #28a745, #218838)'; downloadBtn.style.color = 'white'; downloadBtn.disabled = false; } } /** * 智能选择内存策略 * 功能:根据镜像大小和用户设置选择最适合的内存模式 */ function chooseMemoryStrategy() { if (!manifestData) return; const totalSize = manifestData.totalSize; addLog(`📊 镜像总大小: ${formatSize(totalSize)}`); // 根据用户选择的模式和镜像大小确定存储策略 if (selectedMemoryMode === 'minimal') { useTemporaryCache = true; addLog(`💾 最小内存模式:所有层数据使用临时缓存`); } else if (selectedMemoryMode === 'normal') { useTemporaryCache = false; addLog(`💾 标准模式:所有层数据使用IndexedDB存储`); } else if (selectedMemoryMode === 'stream') { useTemporaryCache = totalSize > 200 * 1024 * 1024; // 200MB阈值 if (useTemporaryCache) { addLog(`🌊 流式模式:镜像较大 (${formatSize(totalSize)}),使用临时缓存避免IndexedDB限制`); } else { addLog(`🌊 流式模式:镜像较小 (${formatSize(totalSize)}),使用IndexedDB存储`); } } else if (selectedMemoryMode === 'auto') { // 自动模式根据镜像总大小智能选择 if (totalSize > 1024 * 1024 * 1024) { // 1GB+ useTemporaryCache = true; addLog(`🤖 自动模式:检测到超大镜像 (${formatSize(totalSize)}),自动选择临时缓存模式避免OOM`); } else if (totalSize > 500 * 1024 * 1024) { // 500MB+ useTemporaryCache = true; addLog(`🤖 自动模式:检测到大镜像 (${formatSize(totalSize)}),自动选择临时缓存模式`); } else { useTemporaryCache = false; addLog(`🤖 自动模式:检测到中小镜像 (${formatSize(totalSize)}),自动选择标准存储模式`); } } // 额外的智能提示 if (totalSize > 2 * 1024 * 1024 * 1024) { // 2GB+ addLog(`⚠️ 超大镜像警告: ${formatSize(totalSize)} - 建议使用最小内存模式,确保设备有足够内存`); } else if (totalSize > 1024 * 1024 * 1024) { // 1GB+ addLog(`⚠️ 大镜像提示: ${formatSize(totalSize)} - 下载可能耗时较长,请保持网络连接稳定`); } } /** * 启动实时进度更新 * 功能:定期更新下载进度显示,提供流畅的用户体验 */ function startRealTimeProgressUpdate() { if (progressUpdateInterval) { clearInterval(progressUpdateInterval); } progressUpdateInterval = setInterval(() => { updateRealTimeProgress(); }, 500); // 每500ms更新一次进度 } /** * 停止实时进度更新 * 功能:清理进度更新定时器 */ function stopRealTimeProgressUpdate() { if (progressUpdateInterval) { clearInterval(progressUpdateInterval); progressUpdateInterval = null; } } /** * 更新实时进度显示 * 功能:计算并显示当前的总体下载进度 */ function updateRealTimeProgress() { if (!manifestData || downloadProgressMap.size === 0) return; let totalDownloaded = 0; let totalSpeed = 0; let completedLayers = 0; // 统计所有层的下载进度 for (const [digest, progressInfo] of downloadProgressMap.entries()) { totalDownloaded += progressInfo.downloaded; totalSpeed += progressInfo.speed; if (progressInfo.completed) { completedLayers++; } } const totalSize = manifestData.totalSize; const progress = totalSize > 0 ? Math.min(Math.round((totalDownloaded / totalSize) * 100), 100) : 0; // 更新按钮显示 updateButtonProgress('⬇️ 下载镜像层', 'downloading', { progress: progress, current: completedLayers, total: manifestData.layers.length, speed: totalSpeed }); } /** * 格式化文件大小显示 * 功能:将字节数转换为人类可读的格式(B, KB, MB, GB) * @param {number} bytes - 要格式化的字节数 * @returns {string} 格式化后的大小字符串 */ function formatSize(bytes) { const sizes = ['B', 'KB', 'MB', 'GB']; if (bytes === 0) return '0 B'; const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; } /** * 格式化下载速度 * 功能:将字节/秒转换为可读的速度单位 * @param {number} bytesPerSecond - 字节/秒 * @returns {string} 格式化的速度字符串 */ function formatSpeed(bytesPerSecond) { if (bytesPerSecond === 0) return '0 B/s'; const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s']; const i = parseInt(Math.floor(Math.log(bytesPerSecond) / Math.log(1024))); return Math.round(bytesPerSecond / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; } /** * 创建镜像信息展示区域 * 功能:创建类似download.html的镜像信息展示卡片 * @returns {HTMLElement} 镜像信息展示DOM元素 */ function createImageInfoDisplay() { const infoContainer = document.createElement('div'); infoContainer.id = 'dockerImageInfo'; infoContainer.className = 'docker-image-info'; infoContainer.style.cssText = ` background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 8px; padding: 12px; margin-top: 8px; font-size: 12px; display: none; box-shadow: 0 2px 4px rgba(0,0,0,0.1); max-width: 500px; `; infoContainer.innerHTML = ` <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;"> <h4 style="margin: 0; color: #495057; font-size: 14px;">📋 镜像信息</h4> <button id="toggleInfoBtn" style="background: none; border: none; cursor: pointer; color: #6c757d; font-size: 12px;">▼</button> </div> <div id="infoContent" style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px;"> <div class="info-item"> <div class="info-label" style="color: #6c757d; font-weight: 600; margin-bottom: 2px;">镜像名称</div> <div class="info-value" id="displayImageName" style="color: #495057; font-family: monospace; font-size: 11px;">-</div> </div> <div class="info-item"> <div class="info-label" style="color: #6c757d; font-weight: 600; margin-bottom: 2px;">当前架构</div> <div class="info-value" id="displayArchitecture" style="color: #495057; font-family: monospace;">-</div> </div> <div class="info-item"> <div class="info-label" style="color: #6c757d; font-weight: 600; margin-bottom: 2px;">总大小</div> <div class="info-value" id="displayTotalSize" style="color: #495057;">-</div> </div> <div class="info-item"> <div class="info-label" style="color: #6c757d; font-weight: 600; margin-bottom: 2px;">层数量</div> <div class="info-value" id="displayLayerCount" style="color: #495057;">-</div> </div> <div class="info-item" style="grid-column: span 2;"> <div class="info-label" style="color: #6c757d; font-weight: 600; margin-bottom: 2px;">下载进度</div> <div id="displayProgress" style="color: #495057;"> <div style="background: #e9ecef; border-radius: 10px; height: 6px; margin: 4px 0;"> <div id="progressBar" style="background: #007bff; height: 100%; border-radius: 10px; width: 0%; transition: width 0.3s ease;"></div> </div> <div id="progressText" style="font-size: 11px; color: #6c757d;">准备中...</div> </div> </div> <div class="info-item" style="grid-column: span 2;"> <div class="info-label" style="color: #6c757d; font-weight: 600; margin-bottom: 2px;">可用架构</div> <div id="displayAvailableArchs" style="color: #495057; font-size: 11px;">检测中...</div> </div> </div> `; // 添加折叠/展开功能 const toggleBtn = infoContainer.querySelector('#toggleInfoBtn'); const content = infoContainer.querySelector('#infoContent'); toggleBtn.addEventListener('click', () => { if (content.style.display === 'none') { content.style.display = 'grid'; toggleBtn.textContent = '▼'; } else { content.style.display = 'none'; toggleBtn.textContent = '▶'; } }); return infoContainer; } /** * 更新镜像信息展示 * 功能:更新信息展示区域的各项数据 * @param {Object} info - 镜像信息对象 */ function updateImageInfoDisplay(info) { const infoContainer = document.getElementById('dockerImageInfo'); if (!infoContainer) return; // 显示信息容器 infoContainer.style.display = 'block'; // 更新各项信息 if (info.imageName) { document.getElementById('displayImageName').textContent = info.imageName; } if (info.architecture) { document.getElementById('displayArchitecture').textContent = info.architecture; } if (info.totalSize) { document.getElementById('displayTotalSize').textContent = formatSize(info.totalSize); } if (info.layerCount !== undefined) { document.getElementById('displayLayerCount').textContent = info.layerCount; } if (info.availableArchs) { const archsElement = document.getElementById('displayAvailableArchs'); if (info.availableArchs.length > 0) { archsElement.innerHTML = info.availableArchs.map(arch => `<span style="background: #e3f2fd; padding: 2px 6px; border-radius: 3px; margin: 1px; display: inline-block; font-family: monospace;">${arch}</span>` ).join(''); } else { archsElement.textContent = '获取中...'; } } } /** * 更新下载进度展示 * 功能:更新进度条和进度文本 * @param {number} percent - 进度百分比 * @param {number} speed - 下载速度(字节/秒) */ function updateProgressDisplay(percent, speed = 0) { const progressBar = document.getElementById('progressBar'); const progressText = document.getElementById('progressText'); if (progressBar && progressText) { progressBar.style.width = `${percent}%`; let statusText = ''; if (percent === 0) { statusText = '准备中...'; progressBar.style.background = '#6c757d'; } else if (percent < 100) { statusText = `下载中 ${percent}%`; if (speed > 0) { statusText += ` (${formatSpeed(speed)})`; } progressBar.style.background = '#007bff'; } else { statusText = '下载完成'; progressBar.style.background = '#28a745'; } progressText.textContent = statusText; } } // ==================== 镜像分析功能 ==================== /** * 分析镜像信息,获取清单数据 * 功能:调用后端API获取镜像的层级结构和元数据 * @param {string} imageName - 镜像名称 * @param {string} imageTag - 镜像标签 * @param {string} architecture - 目标架构(可选) * @returns {Promise<Object>} 镜像清单数据的Promise */ async function analyzeImage(imageName, imageTag, architecture = '') { addLog(`开始分析镜像: ${imageName}:${imageTag}`); // 构建API请求URL let apiUrl = `${API_BASE_URL}/manifest?image=${encodeURIComponent(imageName)}&tag=${encodeURIComponent(imageTag)}`; if (architecture) { apiUrl += `&architecture=${encodeURIComponent(architecture)}`; addLog(`指定架构: ${architecture}`); } try { const response = await fetch(apiUrl); if (!response.ok) { throw new Error(`获取镜像信息失败: ${response.status}`); } const data = await response.json(); addLog(`镜像分析完成,共 ${data.layerCount} 层,总大小 ${formatSize(data.totalSize)}`); return data; } catch (error) { addLog(`分析失败: ${error.message}`, 'error'); throw error; } } /** * 检测镜像支持的架构 * 功能:获取镜像的所有可用架构平台信息 * @param {string} imageName - 镜像名称 * @param {string} imageTag - 镜像标签 * @returns {Promise<Array>} 可用架构列表的Promise */ async function detectArchitectures(imageName, imageTag) { addLog(`开始检测镜像架构: ${imageName}:${imageTag}`); try { const response = await fetch(`${API_BASE_URL}/manifest?image=${encodeURIComponent(imageName)}&tag=${encodeURIComponent(imageTag)}`); if (!response.ok) { throw new Error(`获取镜像信息失败: ${response.status}`); } const data = await response.json(); if (data.multiArch && data.availablePlatforms) { addLog(`检测完成,发现 ${data.availablePlatforms.length} 种架构`); return data.availablePlatforms; } else { addLog('当前镜像仅支持单一架构'); return []; } } catch (error) { addLog(`架构检测失败: ${error.message}`, 'error'); throw error; } } // ==================== 下载功能 ==================== /** * 下载单个镜像层(支持实时进度更新) * 功能:从API下载指定的镜像层数据并存储到本地缓存,支持实时进度反馈 * @param {Object} layer - 层信息对象,包含digest、type、size等 * @param {string} fullImageName - 完整的镜像名称 * @returns {Promise} 下载完成的Promise */ async function downloadLayer(layer, fullImageName) { const layerIndex = manifestData.layers.indexOf(layer); try { addLog(`开始下载层: ${layer.digest.substring(0, 12)}... (类型: ${layer.type}, 大小: ${formatSize(layer.size || 0)}, 索引: ${layerIndex})`); // 初始化进度跟踪 downloadProgressMap.set(layer.digest, { downloaded: 0, total: layer.size || 0, speed: 0, completed: false, startTime: Date.now() }); // 根据层类型构建不同的API端点URL let apiEndpoint; if (layer.type === 'config') { // 配置层的下载端点 apiEndpoint = `${API_BASE_URL}/config?image=${encodeURIComponent(fullImageName)}&digest=${encodeURIComponent(layer.digest)}`; } else { // 普通镜像层的下载端点 apiEndpoint = `${API_BASE_URL}/layer?image=${encodeURIComponent(fullImageName)}&digest=${encodeURIComponent(layer.digest)}`; } // 发起HTTP下载请求 const response = await fetch(apiEndpoint); if (!response.ok) { throw new Error(`下载层失败: ${response.status}`); } // 使用流式读取来支持实时进度更新 const reader = response.body.getReader(); const chunks = []; let receivedLength = 0; const progressInfo = downloadProgressMap.get(layer.digest); while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); receivedLength += value.length; // 更新进度信息 const now = Date.now(); const elapsed = (now - progressInfo.startTime) / 1000; // 秒 progressInfo.downloaded = receivedLength; progressInfo.speed = elapsed > 0 ? receivedLength / elapsed : 0; downloadProgressMap.set(layer.digest, progressInfo); } // 组装完整的数据 const arrayBuffer = new Uint8Array(receivedLength); let position = 0; for (const chunk of chunks) { arrayBuffer.set(chunk, position); position += chunk.length; } // 存储层数据 if (useTemporaryCache) { // 使用临时缓存(minimal模式) tempLayerCache.set(layer.digest, arrayBuffer.buffer); addLog(`层数据存储到临时缓存: ${layer.digest.substring(0, 12)}... (${formatSize(arrayBuffer.byteLength)})`); } else { // 使用IndexedDB存储 await storeLayerData(layer.digest, arrayBuffer.buffer); addLog(`层数据存储到IndexedDB: ${layer.digest.substring(0, 12)}... (${formatSize(arrayBuffer.byteLength)})`); } downloadedLayers.set(layer.digest, true); // 标记为完成 progressInfo.completed = true; downloadProgressMap.set(layer.digest, progressInfo); addLog(`层下载完成: ${layer.digest.substring(0, 12)}... (${formatSize(arrayBuffer.byteLength)})`); } catch (error) { addLog(`层下载失败: ${layer.digest.substring(0, 12)}... - ${error.message}`, 'error'); // 清理进度信息 downloadProgressMap.delete(layer.digest); throw error; } } /** * 存储层数据到IndexedDB * 功能:将下载的层数据存储到IndexedDB数据库 * @param {string} digest - 层摘要 * @param {ArrayBuffer} data - 层数据 * @returns {Promise} 存储完成的Promise */ async function storeLayerData(digest, data) { return new Promise((resolve, reject) => { const transaction = db.transaction(['layers'], 'readwrite'); const store = transaction.objectStore('layers'); const layerRecord = { digest: digest, data: data, timestamp: Date.now() }; const request = store.put(layerRecord); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); } /** * 从存储中获取层数据 * 功能:优先从临时缓存获取,然后从IndexedDB获取 * @param {string} digest - 层摘要 * @returns {Promise<ArrayBuffer>} 层数据 */ async function getLayerData(digest) { // 首先检查临时缓存 if (tempLayerCache.has(digest)) { return tempLayerCache.get(digest); } // 然后检查IndexedDB return new Promise((resolve, reject) => { const transaction = db.transaction(['layers'], 'readonly'); const store = transaction.objectStore('layers'); const request = store.get(digest); request.onsuccess = () => { if (request.result) { resolve(request.result.data); } else { reject(new Error('未找到层数据: ' + digest)); } }; request.onerror = () => reject(request.error); }); } // ==================== 文件生成功能 ==================== /** * 生成下载文件名 * 功能:根据镜像名称、标签和架构生成规范的文件名 * @param {string} imageName - 镜像名称 * @param {string} imageTag - 镜像标签 * @param {string} architecture - 架构信息 * @returns {string} 生成的TAR文件名 */ function generateFilename(imageName, imageTag, architecture) { // 处理镜像名称,移除Docker Hub的library前缀 let cleanImageName = imageName; if (cleanImageName.startsWith('library/')) { cleanImageName = cleanImageName.substring(8); } // 替换文件名中的特殊字符为安全字符 cleanImageName = cleanImageName.replace(/[\/\\:]/g, '_').replace(/[<>:"|?*]/g, '-'); const cleanTag = imageTag.replace(/[\/\\:]/g, '_').replace(/[<>:"|?*]/g, '-'); const cleanArch = architecture.replace(/[\/\\:]/g, '_').replace(/[<>:"|?*]/g, '-') || 'amd64'; // 返回格式:imagename_tag_architecture.tar return `${cleanImageName}_${cleanTag}_${cleanArch}.tar`; } /** * 生成随机摘要值 * 功能:当镜像配置不可用时生成伪随机SHA256摘要 * @returns {string} 64位十六进制摘要字符串 */ function generateFakeDigest() { const chars = '0123456789abcdef'; let result = ''; for (let i = 0; i < 64; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; } /** * 使用tarballjs创建Docker TAR文件 * 功能:将所有下载的层组装成符合Docker标准的TAR格式文件 * @param {Map} layerDataMap - 层数据映射表 * @param {string} filename - 输出文件名 * @returns {Promise} 创建完成的Promise */ async function createDockerTar(layerDataMap, filename) { addLog('开始创建Docker TAR文件...'); // 检查tarballjs库是否可用 if (!window.tarball || !window.tarball.TarWriter) { throw new Error('tarballjs库未加载,无法创建TAR文件'); } try { const tar = new tarball.TarWriter(); // 第一步:处理镜像配置文件 const manifest = manifestData.manifest; let configDigest = null; let configData = null; // 尝试从manifest中获取配置摘要 if (manifest.config && manifest.config.digest) { configDigest = manifest.config.digest; const rawConfigData = layerDataMap.get(configDigest); if (rawConfigData) { configData = new Uint8Array(rawConfigData); addLog(`配置数据准备完成,大小: ${configData.length} 字节`); } } // 如果没有配置数据,创建默认配置 if (!configData) { configDigest = 'sha256:' + generateFakeDigest(); const configObj = { architecture: "amd64", os: "linux", config: {}, rootfs: { type: "layers", diff_ids: manifestData.layers .filter(l => l.type === 'layer') .map(l => l.digest) } }; configData = new TextEncoder().encode(JSON.stringify(configObj)); addLog(`生成默认配置,大小: ${configData.length} 字节`); } // 第二步:添加配置文件到TAR const configFileName = configDigest + '.json'; const configBlob = new Blob([configData], { type: 'application/json' }); const configFile = new File([configBlob], configFileName); tar.addFile(configFileName, configFile); addLog(`添加配置文件: ${configFileName}`); // 第三步:添加所有镜像层到TAR const layerDigests = []; let layerIndex = 0; for (const layer of manifestData.layers) { if (layer.type === 'layer' && layer.digest) { const layerDigest = layer.digest; layerDigests.push(layerDigest); const layerData = layerDataMap.has(layerDigest) ? layerDataMap.get(layerDigest) : await getLayerData(layerDigest); if (layerData) { // 每个层创建独立目录结构: digest/layer.tar const layerFileName = layerDigest + '/layer.tar'; const layerUint8Array = new Uint8Array(layerData); const layerBlob = new Blob([layerUint8Array], { type: 'application/octet-stream' }); const layerFile = new File([layerBlob], 'layer.tar'); tar.addFile(layerFileName, layerFile); addLog(`添加层文件 ${layerIndex + 1}/${manifestData.layers.filter(l => l.type === 'layer').length}: ${layerFileName}`); layerIndex++; } } } // 第四步:创建Docker manifest.json文件 let repoTag = manifestData.imageName; if (repoTag.startsWith('library/')) { repoTag = repoTag.substring(8); } if (!repoTag.includes(':')) { repoTag += ':latest'; } const dockerManifest = [{ Config: configFileName, RepoTags: [repoTag], Layers: layerDigests.map(digest => digest + '/layer.tar') }]; const manifestBlob = new Blob([JSON.stringify(dockerManifest)], { type: 'application/json' }); const manifestFile = new File([manifestBlob], 'manifest.json'); tar.addFile('manifest.json', manifestFile); addLog('添加manifest.json文件'); // 第五步:创建repositories文件 const repositories = {}; let repoName, tag; if (manifestData.imageName.includes(':')) { const parts = manifestData.imageName.split(':'); repoName = parts[0]; tag = parts[1]; } else { repoName = manifestData.imageName; tag = 'latest'; } if (repoName.startsWith('library/')) { repoName = repoName.substring(8); } repositories[repoName] = {}; repositories[repoName][tag] = configDigest.replace('sha256:', ''); const repositoriesBlob = new Blob([JSON.stringify(repositories)], { type: 'application/json' }); const repositoriesFile = new File([repositoriesBlob], 'repositories'); tar.addFile('repositories', repositoriesFile); addLog('添加repositories文件'); // 第六步:下载生成的TAR文件 addLog('开始生成并下载TAR文件...'); tar.download(filename); addLog(`TAR文件下载已触发: ${filename}`); } catch (error) { addLog(`创建TAR文件失败: ${error.message}`, 'error'); throw error; } } // ==================== 完整下载流程 ==================== /** * 执行完整的镜像下载流程 * 功能:包括分析、下载、组装的完整自动化流程 * @param {string} imageName - 镜像名称 * @param {string} imageTag - 镜像标签 * @param {string} architecture - 目标架构 */ async function performDownload(imageName, imageTag, architecture) { // 防止重复下载 if (downloadInProgress) { addLog('下载正在进行中,请等待当前下载完成', 'error'); return; } const downloadBtn = document.getElementById('downloadBtn'); const originalText = downloadBtn.textContent; try { // 设置下载状态 downloadInProgress = true; // 清理之前的下载数据 tempLayerCache.clear(); downloadedLayers.clear(); downloadProgressMap.clear(); // 第一步:分析镜像 addLog('=== 开始镜像下载流程 ==='); addLog(`镜像: ${imageName}:${imageTag}`); if (architecture) { addLog(`架构: ${architecture}`); } else { addLog('架构: 自动检测'); } updateButtonProgress('🔍 分析镜像', 'analyzing', { size: 0 }); manifestData = await analyzeImage(imageName, imageTag, architecture); // 第二步:选择内存策略 chooseMemoryStrategy(); // 第三步:启动实时进度更新并开始下载 updateButtonProgress('⬇️ 下载镜像层', 'downloading', { progress: 0, current: 0, total: manifestData.layers.length, size: manifestData.totalSize }); addLog(`开始下载 ${manifestData.layers.length} 个镜像层`); // 启动实时进度更新 startRealTimeProgressUpdate(); const fullImageName = imageName; const downloadPromises = manifestData.layers.map(async (layer) => { await downloadLayer(layer, fullImageName); }); await Promise.all(downloadPromises); // 停止实时进度更新 stopRealTimeProgressUpdate(); addLog('所有镜像层下载完成'); // 第四步:组装Docker TAR文件 updateButtonProgress('🔧 组装镜像', 'assembling', { progress: 100, current: manifestData.layers.length, total: manifestData.layers.length, size: manifestData.totalSize }); addLog('开始组装Docker TAR文件'); const filename = generateFilename(imageName, imageTag, architecture || 'amd64'); // 根据存储模式传递不同的数据源 if (useTemporaryCache) { await createDockerTar(tempLayerCache, filename); } else { // 对于IndexedDB模式,传递空Map,让createDockerTar使用getLayerData获取数据 await createDockerTar(new Map(), filename); } addLog('=== 镜像下载流程完成 ==='); updateButtonProgress('✅ 下载完成', 'complete', { size: manifestData.totalSize }); } catch (error) { addLog(`下载失败: ${error.message}`, 'error'); updateButtonProgress('❌ 下载失败', 'error'); // 确保停止实时进度更新 stopRealTimeProgressUpdate(); } finally { downloadInProgress = false; // 清理进度数据 downloadProgressMap.clear(); } } // ==================== UI交互功能 ==================== /** * 更新架构选择器选项 * 功能:根据检测到的可用架构更新下拉选择器 * @param {Array} platforms - 可用平台列表 */ function updateArchSelector(platforms) { const archSelector = document.getElementById('archSelector'); if (!archSelector || !platforms || platforms.length === 0) return; // 清空现有选项 archSelector.innerHTML = ''; // 添加检测到的架构选项 platforms.forEach(platform => { if (platform && platform.platform) { const option = document.createElement('option'); option.value = platform.platform; option.textContent = platform.platform; archSelector.appendChild(option); } }); // 智能选择首选架构 // 优先级:linux/amd64 > linux/arm64 > 其他linux架构 > 第一个可用架构 let selectedPlatform = platforms.find(p => p.platform === 'linux/amd64') || platforms.find(p => p.platform === 'linux/arm64') || platforms.find(p => p.os === 'linux') || platforms[0]; if (selectedPlatform) { archSelector.value = selectedPlatform.platform; addLog(`自动选择架构: ${selectedPlatform.platform}`); } } /** * 绑定UI事件处理器 * 功能:为下载按钮和架构检测按钮绑定点击事件 */ function bindEventHandlers() { // 主下载按钮点击事件 const downloadBtn = document.getElementById('downloadBtn'); if (downloadBtn) { downloadBtn.addEventListener('click', async () => { const { imageName, imageTag } = extractImageInfo(); const archSelector = document.getElementById('archSelector'); let architecture = archSelector ? archSelector.value : ''; // 验证镜像信息是否提取成功 if (!imageName) { addLog('无法获取镜像名称,请确保在正确的Docker Hub页面', 'error'); alert('无法获取镜像名称!\n\n请确保您在正确的Docker Hub镜像页面:\n- 官方镜像:hub.docker.com/r/nginx\n- 用户镜像:hub.docker.com/r/username/imagename'); return; } // 如果没有手动选择架构或选择了"自动检测",则使用自动检测功能 if (!architecture || architecture === '') { addLog('未手动选择架构,启用自动检测...'); architecture = await autoDetectArchitecture(); // 更新架构选择器显示检测结果 if (archSelector && architecture) { const existingOption = archSelector.querySelector(`option[value="${architecture}"]`); if (existingOption) { archSelector.value = architecture; } else { // 添加检测到的架构选项 const newOption = document.createElement('option'); newOption.value = architecture; newOption.textContent = architecture; newOption.selected = true; archSelector.appendChild(newOption); } addLog(`自动检测并设置架构: ${architecture}`); } } // 开始下载流程 await performDownload(imageName, imageTag, architecture); }); } else { console.log('下载按钮未找到,稍后重试绑定事件'); } } // ==================== 页面集成功能 ==================== /** * 查找合适的位置插入下载按钮 * 功能:简化版本,直接查找页面标题 * @returns {Array} 包含插入点信息的数组 */ function findInsertionPoints() { const insertionPoints = []; // 如果已经存在下载器,不重复添加 if (document.querySelector('[data-docker-downloader]')) { console.log('页面已存在下载器,跳过插入点查找'); return insertionPoints; } // 方法1: 查找h1标题 const title = document.querySelector('h1'); if (title) { console.log('找到h1标题:', title.textContent.substring(0, 50)); insertionPoints.push({ type: 'title', element: title.parentElement || title, position: 'inside', description: 'H1标题区域' }); } // 方法2: 查找其他可能的标题元素 const altTitles = document.querySelectorAll('h2, h3, [data-testid*="title"], .repository-title, .repo-title'); altTitles.forEach((altTitle, index) => { if (altTitle.textContent.trim()) { console.log(`找到备用标题${index + 1}:`, altTitle.textContent.substring(0, 50)); insertionPoints.push({ type: 'alt-title', element: altTitle.parentElement || altTitle, position: 'inside', description: `备用标题区域${index + 1}` }); } }); // 方法3: 查找页面主要内容区域 const mainContent = document.querySelector('main, .main-content, .content, #content'); if (mainContent && insertionPoints.length === 0) { console.log('找到主要内容区域'); insertionPoints.push({ type: 'main', element: mainContent, position: 'prepend', description: '主要内容区域顶部' }); } // 方法4: 最后的备用方案 - body元素 if (insertionPoints.length === 0) { console.log('使用body作为最后的插入点'); insertionPoints.push({ type: 'body', element: document.body, position: 'prepend', description: '页面顶部' }); } console.log(`找到 ${insertionPoints.length} 个可能的插入点`); return insertionPoints; } /** * 在指定位置插入下载按钮 * 功能:根据插入点类型和位置插入相应的按钮 * @param {Object} insertionPoint - 插入点信息对象 */ function insertDownloadButtons(insertionPoint) { const { element, position, type } = insertionPoint; if (!element) { console.error('插入点元素不存在'); return; } // 创建按钮容器 const buttonContainer = document.createElement('span'); buttonContainer.className = 'docker-download-container'; buttonContainer.setAttribute('data-docker-downloader', 'true'); buttonContainer.style.cssText = ` display: inline-flex; align-items: center; gap: 8px; margin: 0 10px; vertical-align: middle; `; // 创建下载按钮 const downloadBtn = createInlineDownloadButton(); // 创建架构选择器(紧凑型) const archSelector = createArchSelector(); archSelector.style.fontSize = '11px'; archSelector.style.padding = '4px 8px'; archSelector.style.minWidth = '100px'; // 创建架构检测状态指示器 const archIndicator = document.createElement('span'); archIndicator.id = 'archIndicator'; archIndicator.style.cssText = ` font-size: 11px; color: #666; margin-left: 5px; display: none; `; archIndicator.textContent = '🔍 检测中...'; // 将元素添加到容器 buttonContainer.appendChild(downloadBtn); buttonContainer.appendChild(archSelector); buttonContainer.appendChild(archIndicator); // 根据位置类型插入按钮 switch (position) { case 'after': // 在元素后面插入 if (element.nextSibling) { element.parentNode.insertBefore(buttonContainer, element.nextSibling); } else { element.parentNode.appendChild(buttonContainer); } break; case 'before': // 在元素前面插入 element.parentNode.insertBefore(buttonContainer, element); break; case 'inside': // 在元素内部插入 element.appendChild(buttonContainer); break; case 'prepend': // 在元素内部最前面插入 element.insertBefore(buttonContainer, element.firstChild); break; default: // 默认在元素后面插入 element.parentNode.appendChild(buttonContainer); } // 移除复杂的悬停逻辑,保持架构选择器始终可见 // 进度信息直接显示在按钮上,不需要额外的进度区域 console.log(`下载按钮已插入到: ${insertionPoint.description}`); } /** * 检查是否在Docker Hub镜像页面 * 功能:验证当前页面是否适合显示下载器 * @returns {boolean} 是否在合适的镜像页面 */ function isDockerHubImagePage() { const url = window.location.href; const pathname = window.location.pathname; // 首先检查是否在Docker Hub域名 if (!url.includes('hub.docker.com')) { console.log('不在Docker Hub域名'); return false; } // 排除首页和其他非镜像页面 const excludePatterns = [ /^\/$/, // 首页 /^\/search/, // 搜索页面 /^\/explore/, // 探索页面 /^\/extensions/, // 扩展页面 /^\/pricing/, // 价格页面 /^\/signup/, // 注册(不可用)页面 /^\/login/, // 登录(不可用)页面 /^\/u\//, // 用户页面 /^\/orgs\//, // 组织页面 /^\/repositories/, // 仓库列表页面 /^\/settings/, // 设置页面 /^\/billing/, // 账单页面 /^\/support/ // 支持页面 ]; // 如果匹配排除模式,直接返回false const isExcludedPage = excludePatterns.some(pattern => pattern.test(pathname)); if (isExcludedPage) { console.log('页面被排除:', pathname); return false; } // 检查是否在镜像相关页面(严格检测) const imagePagePatterns = [ /^\/r\/[^\/]+$/, // /r/nginx (官方镜像) /^\/r\/[^\/]+\/[^\/]+$/, // /r/username/imagename (用户镜像) /^\/_\/[^\/]+$/, // /_/nginx (官方镜像另一格式) /^\/layers\/[^\/]+\/[^\/]+\/[^\/]+/, // /layers/username/imagename/tag/... (镜像层详情页面) /^\/r\/[^\/]+\/tags/, // 官方镜像标签页面 /^\/r\/[^\/]+\/[^\/]+\/tags/ // 用户镜像标签页面 ]; const isImagePage = imagePagePatterns.some(pattern => pattern.test(pathname)); console.log('页面路径检测:', pathname, '是镜像页面:', isImagePage); return isImagePage; } // ==================== 初始化和启动 ==================== /** * 清理旧的下载器界面 * 功能:删除页面上任何已存在的下载器界面 */ function cleanupOldDownloaders() { // 删除旧的大型下载器界面 const oldDownloaders = document.querySelectorAll('.docker-downloader-container'); oldDownloaders.forEach(element => { if (element.querySelector('.docker-downloader-title')) { element.remove(); console.log('已删除旧的下载器界面'); } }); // 删除重复的按钮容器 const containers = document.querySelectorAll('.docker-download-container'); if (containers.length > 1) { // 保留第一个,删除其余的 for (let i = 1; i < containers.length; i++) { containers[i].remove(); console.log('已删除重复的下载按钮'); } } } /** * 初始化下载器主函数 * 功能:执行所有必要的初始化步骤并启动下载器 */ async function initDownloader() { try { console.log('开始初始化Docker下载器...'); console.log('当前URL:', window.location.href); console.log('当前路径:', window.location.pathname); // 等待页面DOM加载完成 if (document.readyState === 'loading') { console.log('等待DOM加载完成...'); await new Promise(resolve => { document.addEventListener('DOMContentLoaded', resolve); }); } // 检查是否在合适的Docker Hub页面 const isImagePage = isDockerHubImagePage(); console.log('是否为镜像页面:', isImagePage); if (!isImagePage) { console.log('不在Docker Hub镜像页面,跳过下载器初始化'); return; } // 清理旧的下载器界面 console.log('清理旧的下载器界面...'); cleanupOldDownloaders(); // 避免重复初始化 - 检查是否还有按钮存在 const existingBtn = document.querySelector('.docker-download-btn'); if (existingBtn) { console.log('下载按钮仍然存在,跳过初始化'); return; } // 添加CSS样式 addCustomStyles(); // 初始化本地数据库 await initIndexedDB(); // 等待页面元素完全加载 await new Promise(resolve => setTimeout(resolve, 1500)); // 查找插入点并插入下载按钮 console.log('开始查找插入点...'); const insertionPoints = findInsertionPoints(); console.log('找到插入点数量:', insertionPoints.length); if (insertionPoints.length > 0) { // 选择最佳插入点 let bestPoint = insertionPoints[0]; // 使用找到的第一个有效点 console.log('选择插入点:', bestPoint.description, bestPoint); insertDownloadButtons(bestPoint); // 在按钮插入后绑定事件处理器 setTimeout(() => { bindEventHandlers(); // 设置架构变化监听器 setupArchChangeListener(); }, 100); } else { console.log('未找到插入点,这不应该发生,因为findInsertionPoints已经有备用方案'); // 强制备用方案:直接在body中插入 const forcePoint = { element: document.body, position: 'prepend', type: 'force', description: '强制插入到页面顶部' }; console.log('使用强制插入点:', forcePoint.description); insertDownloadButtons(forcePoint); // 在按钮插入后绑定事件处理器 setTimeout(() => { bindEventHandlers(); // 设置架构变化监听器 setupArchChangeListener(); }, 100); } // 记录初始化完成 addLog('Docker Hub 下载按钮已准备就绪'); // 显示当前检测到的镜像信息 const { imageName, imageTag } = extractImageInfo(); if (imageName) { addLog(`检测到镜像: ${imageName}:${imageTag}`); } else { addLog('等待镜像信息加载...'); } } catch (error) { console.error('初始化下载器失败:', error); // 即使初始化失败也不要影响页面正常使用 } } /** * 设置页面变化监听器 * 功能:监听单页应用的路由变化并重新初始化 */ function setupPageChangeListener() { let currentUrl = window.location.href; let initTimeout = null; // 防抖函数,避免频繁初始化 function debouncedInit() { if (initTimeout) { clearTimeout(initTimeout); } initTimeout = setTimeout(() => { console.log('页面变化检测到,重新初始化下载器'); initDownloader(); }, 1000); // 减少延迟时间 } // 更敏感的URL变化检测 function checkUrlChange() { if (window.location.href !== currentUrl) { console.log('URL变化检测:', currentUrl, '->', window.location.href); currentUrl = window.location.href; debouncedInit(); } } // 使用MutationObserver监听DOM变化 let lastCheck = 0; const observer = new MutationObserver((mutations) => { const now = Date.now(); if (now - lastCheck > 1000) { // 减少检查间隔 lastCheck = now; checkUrlChange(); // 检查是否有新的页面内容加载 for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { // 检查是否包含镜像相关内容 const text = node.textContent || ''; if (text.includes('MANIFEST DIGEST') || text.includes('OS/ARCH') || text.includes('Image Layers') || node.querySelector && node.querySelector('[data-testid]')) { console.log('检测到镜像页面内容加载'); debouncedInit(); break; } } } } } } }); // 开始观察页面内容变化 observer.observe(document.body, { childList: true, subtree: true, attributes: false, characterData: false }); // 监听浏览器前进后退按钮 window.addEventListener('popstate', () => { console.log('浏览器导航事件'); debouncedInit(); }); // 监听pushState和replaceState(SPA路由变化) const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function() { originalPushState.apply(history, arguments); console.log('pushState事件'); setTimeout(checkUrlChange, 100); }; history.replaceState = function() { originalReplaceState.apply(history, arguments); console.log('replaceState事件'); setTimeout(checkUrlChange, 100); }; // 定期检查URL变化(备用方案) setInterval(checkUrlChange, 2000); console.log('页面变化监听器已设置'); } // ==================== 架构自动检测功能 ==================== /** * 从页面DOM自动检测当前选中的架构 * 功能:从Docker Hub页面的架构选择器中提取当前选中的架构信息 * @returns {string} 检测到的架构字符串,如 'linux/arm64' */ function detectArchFromPageDOM() { try { // 方法1: 优先检测OS/ARCH部分的架构选择器(基于您的截图) // 查找包含"OS/ARCH"文本的区域附近的选择器 const osArchHeaders = document.querySelectorAll('*'); for (const header of osArchHeaders) { const headerText = header.textContent.trim(); if (headerText === 'OS/ARCH' || headerText.includes('OS/ARCH')) { // 在OS/ARCH标题附近查找选择器 const parent = header.parentElement; if (parent) { // 查找父元素及其兄弟元素中的选择器 const nearbySelectors = parent.querySelectorAll('select, [role="combobox"], .MuiSelect-select'); for (const selector of nearbySelectors) { let archText = ''; if (selector.tagName === 'SELECT') { const selectedOption = selector.options[selector.selectedIndex]; archText = selectedOption ? selectedOption.textContent.trim() : selector.value; } else { archText = selector.textContent.trim(); } if (archText.match(/^(linux|windows|darwin)\/.+/i)) { addLog(`从OS/ARCH区域检测到架构: ${archText}`); return archText.toLowerCase(); } } // 也检查父元素的下一个兄弟元素 let nextElement = parent.nextElementSibling; while (nextElement) { const selectors = nextElement.querySelectorAll('select, [role="combobox"], .MuiSelect-select'); for (const selector of selectors) { let archText = ''; if (selector.tagName === 'SELECT') { const selectedOption = selector.options[selector.selectedIndex]; archText = selectedOption ? selectedOption.textContent.trim() : selector.value; } else { archText = selector.textContent.trim(); } if (archText.match(/^(linux|windows|darwin)\/.+/i)) { addLog(`从OS/ARCH区域下方检测到架构: ${archText}`); return archText.toLowerCase(); } } nextElement = nextElement.nextElementSibling; // 只检查接下来的几个兄弟元素,避免检查过远 if (nextElement && nextElement.getBoundingClientRect().top - parent.getBoundingClientRect().top > 200) { break; } } } } } // 方法2: 检测标准的架构下拉选择器 const osArchSelectors = [ 'select', // 标准select元素 'select[aria-label*="arch"]', // 带有arch标签的select 'select[aria-label*="OS"]', // 带有OS标签的select '.MuiSelect-select', // MUI选择器 '[role="combobox"]' // 下拉框角色 ]; // 收集所有可能的架构选择器及其文本 const archCandidates = []; for (const selector of osArchSelectors) { const elements = document.querySelectorAll(selector); for (const element of elements) { let archText = ''; let elementInfo = ''; // 检查select元素的选中值 if (element.tagName === 'SELECT') { const selectedValue = element.value; const selectedOption = element.options[element.selectedIndex]; archText = selectedOption ? selectedOption.textContent.trim() : selectedValue; elementInfo = `SELECT(${element.className})`; } else { // 检查其他元素的文本内容 archText = element.textContent.trim(); elementInfo = `${element.tagName}(${element.className})`; } if (archText.match(/^(linux|windows|darwin)\/.+/i)) { archCandidates.push({ text: archText.toLowerCase(), element: element, info: elementInfo, position: element.getBoundingClientRect() }); } } } // 如果找到多个候选者,选择最合适的一个 if (archCandidates.length > 0) { addLog(`找到 ${archCandidates.length} 个架构候选:`); archCandidates.forEach((candidate, index) => { addLog(` 候选${index + 1}: ${candidate.text} (${candidate.info}) 位置: ${Math.round(candidate.position.top)}`); }); // 优先选择位置较低的(通常OS/ARCH部分在页面下方) archCandidates.sort((a, b) => b.position.top - a.position.top); const selected = archCandidates[0]; addLog(`选择架构: ${selected.text} (${selected.info}) - 位置最低`); return selected.text; } // 方法2: 检测MUI架构选择器(基于您提供的DOM结构) const muiSelectors = [ '.MuiSelect-select.MuiSelect-outlined.MuiInputBase-input.MuiOutlinedInput-input.MuiInputBase-inputSizeSmall', // 完整MUI选择器 '.MuiSelect-select.MuiSelect-outlined', // MUI下拉选择器 '.MuiInputBase-input.MuiOutlinedInput-input', // MUI输入框 'div[role="combobox"]' // div形式的下拉框 ]; for (const selector of muiSelectors) { const elements = document.querySelectorAll(selector); for (const element of elements) { const text = element.textContent.trim(); // 检查是否包含架构格式 (os/arch) if (text.match(/^(linux|windows|darwin)\/.+/i)) { addLog(`从MUI选择器检测到架构: ${text}`); return text.toLowerCase(); } } } // 方法3: 专门检测您提供的MUI容器结构 const muiContainers = document.querySelectorAll('.MuiInputBase-root.MuiOutlinedInput-root'); for (const container of muiContainers) { const selectDiv = container.querySelector('div[role="combobox"]'); if (selectDiv) { const text = selectDiv.textContent.trim(); if (text.match(/^(linux|windows|darwin)\/.+/i)) { addLog(`从MUI容器检测到架构: ${text}`); return text.toLowerCase(); } } } // 方法2: 查找包含架构信息的其他元素 const archPatterns = [ /linux\/amd64/i, /linux\/arm64/i, /linux\/arm\/v7/i, /linux\/arm\/v6/i, /linux\/386/i, /windows\/amd64/i, /darwin\/amd64/i, /darwin\/arm64/i ]; // 搜索页面中所有可能包含架构信息的元素 const allElements = document.querySelectorAll('*'); for (const element of allElements) { const text = element.textContent.trim(); for (const pattern of archPatterns) { if (pattern.test(text) && text.length < 50) { // 避免匹配过长的文本 const match = text.match(pattern); if (match) { addLog(`从页面元素检测到架构: ${match[0]}`); return match[0].toLowerCase(); } } } } addLog('未能从页面DOM检测到架构信息'); return null; } catch (error) { addLog(`DOM架构检测失败: ${error.message}`, 'error'); return null; } } /** * 从URL中的SHA256值检测架构(使用缓存信息) * 功能:使用缓存的架构-SHA256映射快速检测架构 * @returns {Promise<string|null>} 检测到的架构字符串 */ async function detectArchFromSHA256() { try { const url = window.location.href; const sha256Match = url.match(/sha256-([a-f0-9]{64})/i); if (!sha256Match) { addLog('URL中未找到SHA256值'); return null; } const sha256 = sha256Match[1]; addLog(`从URL提取SHA256: ${sha256.substring(0, 12)}...`); // 确保有缓存的架构信息 if (!cachedArchitectures) { addLog('没有缓存的架构信息,先获取架构列表'); await getAvailableArchitectures(); } if (!cachedArchitectures || !cachedArchitectures.platformMap) { addLog('无法获取架构信息进行SHA256检测'); return null; } // 首先检查平台映射中是否有直接匹配的SHA256 for (const [architecture, platformInfo] of cachedArchitectures.platformMap) { if (platformInfo.sha256 && platformInfo.sha256 === sha256) { addLog(`✅ 通过缓存的平台信息匹配到架构: ${architecture}`); return architecture; } if (platformInfo.digest && platformInfo.digest.includes(sha256)) { addLog(`✅ 通过缓存的摘要信息匹配到架构: ${architecture}`); return architecture; } } // 如果缓存中没有找到,需要深度检查每个架构的层信息 addLog('缓存中未找到匹配,深度检查各架构的层信息...'); const { imageName, imageTag } = extractImageInfo(); if (!imageName) { addLog('无法提取镜像信息进行深度SHA256检测'); return null; } for (const architecture of cachedArchitectures.architectures) { addLog(` 深度检查架构: ${architecture}`); try { const archManifestUrl = `${API_BASE_URL}/manifest?image=${encodeURIComponent(imageName)}&tag=${encodeURIComponent(imageTag)}&architecture=${encodeURIComponent(architecture)}`; const archResponse = await fetch(archManifestUrl); if (archResponse.ok) { const archData = await archResponse.json(); // 检查这个架构的所有层是否包含我们的SHA256 if (archData.layers) { for (const layer of archData.layers) { if (layer.digest && layer.digest.includes(sha256)) { addLog(`✅ SHA256匹配镜像层! 架构: ${architecture}`); // 更新缓存信息 const platformInfo = cachedArchitectures.platformMap.get(architecture); if (platformInfo) { platformInfo.layerSha256 = sha256; } return architecture; } } } // 也检查配置层 if (archData.config && archData.config.digest && archData.config.digest.includes(sha256)) { addLog(`✅ SHA256匹配配置层! 架构: ${architecture}`); // 更新缓存信息 const platformInfo = cachedArchitectures.platformMap.get(architecture); if (platformInfo) { platformInfo.configSha256 = sha256; } return architecture; } } } catch (archError) { addLog(` 检查架构 ${architecture} 时出错: ${archError.message}`); } } addLog('SHA256架构检测未找到匹配结果'); return null; } catch (error) { addLog(`SHA256架构检测失败: ${error.message}`, 'error'); return null; } } /** * 综合架构自动检测 * 功能:结合多种方法自动检测当前页面的架构信息 * @returns {Promise<string|null>} 检测到的架构字符串 */ async function autoDetectArchitecture() { addLog('开始自动架构检测...'); // 优先级1: 从URL的SHA256检测(最准确) const sha256Arch = await detectArchFromSHA256(); if (sha256Arch) { return sha256Arch; } // 优先级2: 从页面DOM检测 const domArch = detectArchFromPageDOM(); if (domArch) { return domArch; } // 优先级3: 使用默认架构 const defaultArch = 'linux/amd64'; addLog(`使用默认架构: ${defaultArch}`); return defaultArch; } /** * 获取镜像的可用架构列表并缓存架构-SHA256映射 * 功能:从API获取镜像支持的所有架构及其对应的SHA256 * @returns {Promise<Array>} 可用架构列表 */ async function getAvailableArchitectures() { try { // 提取镜像信息 const { imageName, imageTag } = extractImageInfo(); if (!imageName) { addLog('无法提取镜像信息,跳过架构列表获取'); return []; } // 检查是否已有缓存 const cacheKey = `${imageName}:${imageTag}`; if (cachedArchitectures && cachedArchitectures.cacheKey === cacheKey) { addLog('使用缓存的架构信息'); return cachedArchitectures.architectures; } addLog(`获取镜像架构列表: ${imageName}:${imageTag}`); // 调用API获取镜像清单 const manifestUrl = `${API_BASE_URL}/manifest?image=${encodeURIComponent(imageName)}&tag=${encodeURIComponent(imageTag)}`; const response = await fetch(manifestUrl); if (!response.ok) { throw new Error(`获取镜像清单失败: ${response.status}`); } const data = await response.json(); if (data.multiArch && data.availablePlatforms) { addLog(`发现多架构镜像,包含 ${data.availablePlatforms.length} 个架构`); // 缓存架构信息,包含架构和对应的SHA256/digest cachedArchitectures = { cacheKey: cacheKey, architectures: data.availablePlatforms.map(platform => platform.platform), platformMap: new Map(data.availablePlatforms.map(platform => [ platform.platform, { digest: platform.digest, sha256: platform.digest ? platform.digest.replace('sha256:', '') : null } ])) }; addLog(`已缓存 ${data.availablePlatforms.length} 个架构的SHA256映射`); return cachedArchitectures.architectures; } else { addLog('发现单架构镜像,使用默认架构列表'); // 为单架构镜像创建缓存 cachedArchitectures = { cacheKey: cacheKey, architectures: ['linux/amd64'], platformMap: new Map([['linux/amd64', { digest: null, sha256: null }]]) }; return cachedArchitectures.architectures; } } catch (error) { addLog(`获取架构列表失败: ${error.message}`, 'error'); return []; } } /** * 更新架构选择器的选项 * 功能:根据获取到的可用架构更新下拉选择器选项 * @param {HTMLElement} selector - 架构选择器元素 * @param {Array} architectures - 可用架构列表 */ function updateArchSelectorOptions(selector, architectures) { if (!selector || !architectures || architectures.length === 0) { return; } // 保存当前选中的值 const currentValue = selector.value; // 清空现有选项,但保留"自动检测"选项 selector.innerHTML = '<option value="">自动检测</option>'; // 添加获取到的架构选项 architectures.forEach(arch => { const option = document.createElement('option'); option.value = arch; option.textContent = arch; selector.appendChild(option); }); // 如果之前有选中的值且仍然存在,恢复选中状态 if (currentValue && architectures.includes(currentValue)) { selector.value = currentValue; } addLog(`架构选择器已更新: ${architectures.join(', ')}`); } /** * 设置页面架构选择器变化监听 * 功能:监听Docker Hub页面上的OS/ARCH选择器变化,自动更新我们的架构选择器 */ function setupArchChangeListener() { // 监听所有可能的架构选择器变化 const archSelectors = [ 'select', // 标准select元素 '.MuiSelect-select', // MUI选择器 '[role="combobox"]' // 下拉框角色 ]; archSelectors.forEach(selector => { const elements = document.querySelectorAll(selector); elements.forEach(element => { // 为每个可能的架构选择器添加变化监听 if (element.tagName === 'SELECT') { element.addEventListener('change', handleArchChange); addLog(`为SELECT元素添加了变化监听: ${element.className}`); } else { // 对于非select元素,使用MutationObserver监听内容变化 const observer = new MutationObserver(handleArchChange); observer.observe(element, { childList: true, subtree: true, characterData: true }); addLog(`为元素添加了MutationObserver: ${element.className}`); } }); }); // 使用全局MutationObserver监听页面架构相关变化 const globalObserver = new MutationObserver((mutations) => { mutations.forEach(mutation => { // 检查变化的节点是否包含架构信息 if (mutation.type === 'childList' || mutation.type === 'characterData') { const target = mutation.target; if (target.textContent && target.textContent.match(/^(linux|windows|darwin)\/.+/i)) { handleArchChange(); } } }); }); // 监听页面主要内容区域的变化 const mainContent = document.querySelector('main, body'); if (mainContent) { globalObserver.observe(mainContent, { childList: true, subtree: true, characterData: true }); } addLog('架构变化监听器已设置'); } /** * 处理页面架构选择器变化 * 功能:当页面架构选择器发生变化时,自动更新我们的架构选择器 */ async function handleArchChange() { try { // 防止频繁触发 if (handleArchChange.timeout) { clearTimeout(handleArchChange.timeout); } handleArchChange.timeout = setTimeout(async () => { addLog('检测到页面架构变化,正在更新...'); const ourArchSelector = document.getElementById('archSelector'); const archIndicator = document.getElementById('archIndicator'); // 显示更新状态 if (archIndicator) { archIndicator.style.display = 'inline'; archIndicator.textContent = '🔄 更新中...'; archIndicator.style.color = '#007bff'; } // 重新获取可用架构列表 const availableArchs = await getAvailableArchitectures(); if (availableArchs && availableArchs.length > 0 && ourArchSelector) { updateArchSelectorOptions(ourArchSelector, availableArchs); } // 重新检测当前架构 const detectedArch = await autoDetectArchitecture(); if (detectedArch && ourArchSelector) { // 检查选项中是否存在检测到的架构 const existingOption = ourArchSelector.querySelector(`option[value="${detectedArch}"]`); if (existingOption) { ourArchSelector.value = detectedArch; } else { // 如果选项中不存在,添加新选项 const newOption = document.createElement('option'); newOption.value = detectedArch; newOption.textContent = detectedArch; newOption.selected = true; ourArchSelector.appendChild(newOption); } ourArchSelector.title = `当前架构: ${detectedArch} (页面同步)`; addLog(`架构选择器已同步更新为: ${detectedArch}`); // 更新指示器 if (archIndicator) { archIndicator.textContent = `✅ ${detectedArch}`; archIndicator.style.color = '#28a745'; setTimeout(() => { archIndicator.style.display = 'none'; }, 2000); } } else if (archIndicator) { archIndicator.textContent = '❌ 更新失败'; archIndicator.style.color = '#dc3545'; setTimeout(() => { archIndicator.style.display = 'none'; }, 2000); } }, 500); // 500ms防抖 } catch (error) { addLog(`架构变化处理失败: ${error.message}`, 'error'); } } // ==================== 脚本入口点 ==================== // 脚本启动日志 console.log('Docker Hub 镜像下载器脚本已加载'); // 启动初始化流程 initDownloader(); // 设置页面变化监听 setupPageChangeListener(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址