Files
hubproxy/ghproxy/public/skopeo.html
2025-05-18 13:40:13 +08:00

511 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Docker镜像批量下载工具,docker镜像打包下载">
<meta name="keywords" content="Docker,镜像下载,skopeo,docker镜像打包">
<meta name="color-scheme" content="dark light">
<title>Docker镜像批量下载</title>
<link rel="icon" href="./favicon.ico">
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.3/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://font.sec.miui.com/font/css?family=MiSans:400,700:MiSans">
<style>
:root {
--color: #ffffff;
--fontcolor: #333;
--inputcolor: #f5f5f5;
--inputcolor-font: #333;
}
@media (prefers-color-scheme: dark) {
:root {
--color: #53535338;
--fontcolor: #b8b8b8;
--inputcolor: #012333;
--inputcolor-font: #969696d8;
}
}
body {
background-color: var(--color);
background-image: url('./bj.svg');
background-position: center;
background-repeat: no-repeat;
background-size: cover;
background-attachment: fixed;
color: var(--fontcolor);
font-family: 'Misans', Arial, sans-serif;
padding: 30px;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
position: relative;
}
*::-webkit-scrollbar {
height: 10px;
margin-top: 0px;
}
*::-webkit-scrollbar-track {
background-color: black;
}
*::-webkit-scrollbar-thumb {
background: #39c5bb;
border-radius: 10px;
}
.container {
max-width: 80%;
text-align: center;
min-height: 65%;
line-height: 1.25;
margin-top: 30px;
}
h1 {
color: var(--fontcolor);
font-weight: bold;
margin-bottom: 30px;
}
.rounded-button {
border-radius: 6px;
transition: background-color 0.3s, transform 0.2s;
padding: 10px 30px;
background-color: #555c5c;
color: rgb(255, 255, 255);
border: none;
margin-bottom: 3%;
}
.rounded-button:hover {
background-color: #39c5bcda;
transform: scale(1.05);
}
footer {
position: fixed;
bottom: 20px;
left: 0;
right: 0;
text-align: center;
padding: 10px;
line-height: 1.25;
margin-top: 20px;
}
pre {
background: #012333;
color: #39c5bc;
padding: 15px 20px 15px 20px;
margin: 0px 0;
border-radius: 0.5rem;
overflow-x: auto;
position: relative;
}
pre::before {
content: " ";
display: block;
position: absolute;
top: 6px;
left: 6px;
width: 10px;
height: 10px;
background: #bd3c35;
border-radius: 50%;
box-shadow: 20px 0 0 #d69f27, 40px 0 0 #39c5bb;
}
code {
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 0.9em;
margin-bottom: 0px;
}
@media (max-width: 768px) {
.container {
max-width: 100%;
font-size: 0.8rem;
}
}
@media (min-width: 768px) {
.container {
max-width: 65%;
font-size: 1rem;
}
h1 {
margin-bottom: 30px;
}
}
.form-group {
margin-bottom: 3%;
}
.form-control {
background-color: var(--inputcolor);
color: var(--inputcolor-font);
}
.form-control:focus {
background-color: var(--inputcolor);
color: var(--inputcolor-font);
}
#toast {
position: fixed;
top: 10%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #39c5bcde;
color: white;
padding: 15px 20px;
border-radius: 10px;
font-size: 90%;
z-index: 1000;
}
.back-button {
position: fixed;
top: 20px;
left: 20px;
padding: 2px 8px;
background-color: #f5f5f5;
border: 0px solid #eeeeee;
color: #333;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
text-decoration: none;
}
.back-button:hover {
background-color: #39c5bc;
color: white;
transform: scale(1.05);
text-decoration: none;
}
.progress-container {
margin-top: 20px;
display: none;
}
.total-progress-text {
font-size: 16px;
font-weight: bold;
margin-bottom: 20px;
padding: 10px;
background-color: var(--inputcolor);
border-radius: 10px;
display: inline-block;
}
.image-progress {
margin-top: 15px;
margin-bottom: 5px;
}
.image-progress-item {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.image-progress-name {
flex: 0 0 200px;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.image-progress-bar-container {
flex-grow: 1;
height: 15px;
background-color: #ddd;
border-radius: 5px;
margin: 0 10px;
}
.image-progress-bar {
height: 100%;
background-color: #39c5bb;
border-radius: 5px;
width: 0%;
}
.image-progress-text {
flex: 0 0 50px;
text-align: right;
}
.download-button {
display: none;
margin-top: 20px;
padding: 10px 20px;
background-color: #39c5bb;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease;
}
.download-button:hover {
background-color: #2ea89e;
transform: scale(1.05);
}
textarea.form-control {
min-height: 150px;
resize: vertical;
}
.info-text {
font-size: 0.9rem;
color: var(--fontcolor);
opacity: 0.8;
margin-bottom: 15px;
text-align: left;
}
</style>
</head>
<body>
<a href="/" class="back-button">返回</a>
<div class="container">
<h1>Docker离线镜像包下载</h1>
<div class="form-group">
<div class="info-text">每行输入一个镜像跟docker pull的格式一样多个镜像会自动打包到一起为zip包单个镜像为tar包。导入镜像后需要手动为镜像添加名称和标签例如docker tag 1856948a5aa7 镜像名称:标签</div>
<textarea class="form-control" id="imageInput" placeholder="例如:&#10;nginx&#10;stilleshan/frpc&#10;stilleshan/frpc:0.62.1"></textarea>
</div>
<div class="form-group">
<div class="info-text">镜像架构,默认为 amd64</div>
<input type="text" class="form-control" id="platformInput" placeholder="输入架构例如amd64, arm64等" value="amd64">
</div>
<button class="btn rounded-button" id="downloadButton">开始下载</button>
<div class="progress-container" id="progressContainer">
<div class="total-progress-text" id="totalProgressText">0/0 - 0%</div>
<div class="image-progress" id="imageProgressList">
<!-- Image progress items will be added here -->
</div>
<button class="download-button" id="getFileButton">下载文件</button>
</div>
</div>
<div id="toast" style="display:none;"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const imageInput = document.getElementById('imageInput');
const platformInput = document.getElementById('platformInput');
const downloadButton = document.getElementById('downloadButton');
const progressContainer = document.getElementById('progressContainer');
const totalProgressText = document.getElementById('totalProgressText');
const imageProgressList = document.getElementById('imageProgressList');
const getFileButton = document.getElementById('getFileButton');
let images = [];
let currentTaskId = null;
let websocket = null;
function parseImageList() {
const text = imageInput.value.trim();
if (!text) {
return [];
}
return text.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0);
}
function startDownload() {
images = parseImageList();
if (images.length === 0) {
showToast('请至少输入一个镜像');
return;
}
const platform = platformInput.value.trim() || 'amd64';
const requestData = {
images: images,
platform: platform
};
fetch('/api/download', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
})
.then(response => response.json())
.then(data => {
if (data.taskId) {
currentTaskId = data.taskId;
showProgressUI();
connectWebSocket(currentTaskId);
const totalCount = data.totalCount || images.length;
totalProgressText.textContent = `0/${totalCount} - 0%`;
} else {
showToast('下载任务创建失败');
}
})
.catch(error => {
showToast('请求失败: ' + error.message);
});
}
function showProgressUI() {
progressContainer.style.display = 'block';
downloadButton.style.display = 'none';
imageInput.disabled = true;
platformInput.disabled = true;
const totalCount = images.length;
totalProgressText.textContent = `0/${totalCount} - 0%`;
imageProgressList.innerHTML = '';
images.forEach(image => {
addImageProgressItem(image, 0);
});
}
function addImageProgressItem(image, progress) {
const itemDiv = document.createElement('div');
itemDiv.className = 'image-progress-item';
itemDiv.id = `progress-${image.replace(/[\/\.:]/g, '_')}`;
const nameDiv = document.createElement('div');
nameDiv.className = 'image-progress-name';
nameDiv.title = image;
nameDiv.textContent = image;
const barContainerDiv = document.createElement('div');
barContainerDiv.className = 'image-progress-bar-container';
const barDiv = document.createElement('div');
barDiv.className = 'image-progress-bar';
barDiv.style.width = `${progress}%`;
const textDiv = document.createElement('div');
textDiv.className = 'image-progress-text';
textDiv.textContent = `${Math.round(progress)}%`;
barContainerDiv.appendChild(barDiv);
itemDiv.appendChild(nameDiv);
itemDiv.appendChild(barContainerDiv);
itemDiv.appendChild(textDiv);
imageProgressList.appendChild(itemDiv);
}
function updateImageProgress(image, progress, status) {
const itemId = `progress-${image.replace(/[\/\.:]/g, '_')}`;
const item = document.getElementById(itemId);
if (item) {
const bar = item.querySelector('.image-progress-bar');
const text = item.querySelector('.image-progress-text');
bar.style.width = `${progress}%`;
text.textContent = `${Math.round(progress)}%`;
if (status === 'failed') {
bar.style.backgroundColor = '#bd3c35';
text.textContent = '失败';
} else if (status === 'completed') {
bar.style.backgroundColor = '#4CAF50';
text.textContent = '完成';
}
}
}
function connectWebSocket(taskId) {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/${taskId}`;
websocket = new WebSocket(wsUrl);
websocket.onopen = function() {
console.log('ws');
};
websocket.onmessage = function(event) {
const data = JSON.parse(event.data);
updateProgress(data);
};
websocket.onerror = function(error) {
console.error('WebSocket错误:', error);
showToast('WebSocket连接错误');
};
websocket.onclose = function() {
console.log('WebSocket连接已关闭');
};
}
function updateProgress(data) {
const progressPercent = data.totalCount > 0 ? (data.completedCount / data.totalCount) * 100 : 0;
totalProgressText.textContent = `${data.completedCount}/${data.totalCount} - ${Math.round(progressPercent)}%`;
data.images.forEach(imgData => {
updateImageProgress(imgData.image, imgData.progress, imgData.status);
});
if (data.status === 'completed') {
getFileButton.style.display = 'inline-block';
if (websocket) {
websocket.close();
}
}
}
function downloadFile() {
if (!currentTaskId) {
showToast('没有可下载的文件');
return;
}
window.location.href = `/api/files/${currentTaskId}_file`;
}
function showToast(message) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.style.display = 'block';
setTimeout(() => {
toast.style.display = 'none';
}, 3000);
}
downloadButton.addEventListener('click', startDownload);
getFileButton.addEventListener('click', downloadFile);
});
</script>
</body>
</html>