444 lines
17 KiB
HTML
444 lines
17 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>模型文件上传</title>
|
||
<meta charset="UTF-8">
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
|
||
<style>
|
||
body {
|
||
font-family: Arial, sans-serif;
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
}
|
||
.upload-section {
|
||
border: 2px dashed #ccc;
|
||
padding: 40px;
|
||
text-align: center;
|
||
margin-bottom: 20px;
|
||
border-radius: 8px;
|
||
}
|
||
.upload-section.dragover {
|
||
border-color: #007bff;
|
||
background-color: #f0f8ff;
|
||
}
|
||
.progress-container {
|
||
margin-top: 20px;
|
||
display: none;
|
||
}
|
||
.progress-bar {
|
||
width: 100%;
|
||
height: 20px;
|
||
background-color: #f5f5f5;
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
}
|
||
.progress-fill {
|
||
height: 100%;
|
||
background-color: #007bff;
|
||
width: 0%;
|
||
transition: width 0.3s ease;
|
||
}
|
||
.status-info {
|
||
margin-top: 10px;
|
||
color: #666;
|
||
}
|
||
.upload-list {
|
||
margin-top: 30px;
|
||
}
|
||
.model-item {
|
||
padding: 10px;
|
||
border: 1px solid #ddd;
|
||
margin-bottom: 10px;
|
||
border-radius: 4px;
|
||
}
|
||
.btn {
|
||
padding: 10px 20px;
|
||
background-color: #007bff;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
}
|
||
.btn:hover {
|
||
background-color: #0056b3;
|
||
}
|
||
.btn:disabled {
|
||
background-color: #ccc;
|
||
cursor: not-allowed;
|
||
}
|
||
.form-group {
|
||
margin-bottom: 15px;
|
||
}
|
||
label {
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
font-weight: bold;
|
||
}
|
||
input[type="text"], input[type="password"] {
|
||
width: 100%;
|
||
padding: 8px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
box-sizing: border-box;
|
||
}
|
||
.error {
|
||
color: #dc3545;
|
||
margin-top: 5px;
|
||
}
|
||
.success {
|
||
color: #28a745;
|
||
margin-top: 5px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>加密模型文件上传</h1>
|
||
|
||
<div class="form-group">
|
||
<label for="encryptionKey">加密密钥(可选,建议使用)</label>
|
||
<input type="password" id="encryptionKey" placeholder="输入加密密钥(如果不提供将自动生成)">
|
||
<button onclick="generateKey()" class="btn">生成密钥</button>
|
||
<div id="keyInfo" class="success" style="display: none;"></div>
|
||
</div>
|
||
|
||
<div class="upload-section" id="dropArea">
|
||
<h3>拖放模型文件到此处,或点击选择文件</h3>
|
||
<p>支持 .pt, .pth, .onnx, .engine 格式,最大 1GB</p>
|
||
<input type="file" id="fileInput" style="display: none;">
|
||
<button onclick="document.getElementById('fileInput').click()" class="btn">选择文件</button>
|
||
<div id="fileInfo" style="margin-top: 10px;"></div>
|
||
</div>
|
||
|
||
<div class="progress-container" id="progressContainer">
|
||
<h3>上传进度</h3>
|
||
<div class="progress-bar">
|
||
<div class="progress-fill" id="progressFill"></div>
|
||
</div>
|
||
<div class="status-info">
|
||
<span id="progressText">0%</span>
|
||
<span id="chunkInfo">(0/0 分片)</span>
|
||
</div>
|
||
<div id="uploadStatus" class="status-info"></div>
|
||
<button onclick="cancelUpload()" class="btn" id="cancelBtn" style="display: none; margin-top: 10px;">取消上传</button>
|
||
</div>
|
||
|
||
<div class="upload-list">
|
||
<h3>已上传的模型文件</h3>
|
||
<button onclick="loadModels()" class="btn">刷新列表</button>
|
||
<div id="modelList"></div>
|
||
</div>
|
||
|
||
<script>
|
||
let currentSessionId = null;
|
||
let currentFile = null;
|
||
let encryptionKey = '';
|
||
let uploadInProgress = false;
|
||
|
||
// 元素引用
|
||
const dropArea = document.getElementById('dropArea');
|
||
const fileInput = document.getElementById('fileInput');
|
||
const progressContainer = document.getElementById('progressContainer');
|
||
const progressFill = document.getElementById('progressFill');
|
||
const progressText = document.getElementById('progressText');
|
||
const chunkInfo = document.getElementById('chunkInfo');
|
||
const uploadStatus = document.getElementById('uploadStatus');
|
||
const cancelBtn = document.getElementById('cancelBtn');
|
||
const fileInfo = document.getElementById('fileInfo');
|
||
const keyInfo = document.getElementById('keyInfo');
|
||
const modelList = document.getElementById('modelList');
|
||
const encryptionKeyInput = document.getElementById('encryptionKey');
|
||
|
||
// 生成加密密钥
|
||
async function generateKey() {
|
||
try {
|
||
const response = await axios.post('/api/models/generate_key');
|
||
if (response.data.status === 'success') {
|
||
const key = response.data.data.key;
|
||
encryptionKeyInput.value = key;
|
||
keyInfo.textContent = `已生成密钥: ${key.substring(0, 20)}...`;
|
||
keyInfo.style.display = 'block';
|
||
keyInfo.className = 'success';
|
||
}
|
||
} catch (error) {
|
||
keyInfo.textContent = '生成密钥失败: ' + (error.response?.data?.message || error.message);
|
||
keyInfo.style.display = 'block';
|
||
keyInfo.className = 'error';
|
||
}
|
||
}
|
||
|
||
// 拖放功能
|
||
dropArea.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
dropArea.classList.add('dragover');
|
||
});
|
||
|
||
dropArea.addEventListener('dragleave', (e) => {
|
||
e.preventDefault();
|
||
dropArea.classList.remove('dragover');
|
||
});
|
||
|
||
dropArea.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
dropArea.classList.remove('dragover');
|
||
|
||
if (e.dataTransfer.files.length > 0) {
|
||
handleFileSelect(e.dataTransfer.files[0]);
|
||
}
|
||
});
|
||
|
||
// 文件选择
|
||
fileInput.addEventListener('change', (e) => {
|
||
if (e.target.files.length > 0) {
|
||
handleFileSelect(e.target.files[0]);
|
||
}
|
||
});
|
||
|
||
// 处理文件选择
|
||
function handleFileSelect(file) {
|
||
if (uploadInProgress) {
|
||
alert('请等待当前上传完成');
|
||
return;
|
||
}
|
||
|
||
// 检查文件扩展名
|
||
const allowedExtensions = ['.pt', '.pth', '.onnx', '.engine'];
|
||
const fileExt = '.' + file.name.split('.').pop().toLowerCase();
|
||
if (!allowedExtensions.includes(fileExt)) {
|
||
alert('不支持的文件格式,请上传模型文件');
|
||
return;
|
||
}
|
||
|
||
// 检查文件大小
|
||
const maxSize = 1024 * 1024 * 1024; // 1GB
|
||
if (file.size > maxSize) {
|
||
alert('文件太大,最大支持1GB');
|
||
return;
|
||
}
|
||
|
||
currentFile = file;
|
||
encryptionKey = encryptionKeyInput.value.trim();
|
||
|
||
fileInfo.innerHTML = `
|
||
<div class="success">
|
||
<strong>已选择文件:</strong> ${file.name}<br>
|
||
<strong>文件大小:</strong> ${formatFileSize(file.size)}<br>
|
||
<strong>加密密钥:</strong> ${encryptionKey ? '已提供' : '未提供(建议提供)'}
|
||
</div>
|
||
<button onclick="startUpload()" class="btn" style="margin-top: 10px;">开始上传</button>
|
||
`;
|
||
}
|
||
|
||
// 开始上传
|
||
async function startUpload() {
|
||
if (!currentFile) {
|
||
alert('请先选择文件');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
uploadInProgress = true;
|
||
|
||
// 1. 创建上传会话
|
||
const startResponse = await axios.post('/api/models/upload/start', {
|
||
filename: currentFile.name,
|
||
total_size: currentFile.size,
|
||
encryption_key: encryptionKey || null
|
||
});
|
||
|
||
if (startResponse.data.status !== 'success') {
|
||
throw new Error(startResponse.data.message);
|
||
}
|
||
|
||
const { session_id, total_chunks, chunk_size } = startResponse.data.data;
|
||
currentSessionId = session_id;
|
||
|
||
// 显示进度条
|
||
progressContainer.style.display = 'block';
|
||
cancelBtn.style.display = 'block';
|
||
|
||
// 2. 分片上传
|
||
for (let chunkIndex = 0; chunkIndex < total_chunks; chunkIndex++) {
|
||
if (!uploadInProgress) {
|
||
break; // 用户取消了上传
|
||
}
|
||
|
||
const start = chunkIndex * chunk_size;
|
||
const end = Math.min(start + chunk_size, currentFile.size);
|
||
const chunk = currentFile.slice(start, end);
|
||
|
||
const formData = new FormData();
|
||
formData.append('session_id', session_id);
|
||
formData.append('chunk_index', chunkIndex);
|
||
formData.append('chunk', chunk);
|
||
|
||
try {
|
||
const chunkResponse = await axios.post('/api/models/upload/chunk', formData, {
|
||
headers: { 'Content-Type': 'multipart/form-data' },
|
||
onUploadProgress: (progressEvent) => {
|
||
const chunkProgress = Math.round((progressEvent.loaded / progressEvent.total) * 100);
|
||
updateProgress(chunkIndex + 1, total_chunks, chunkProgress);
|
||
}
|
||
});
|
||
|
||
if (chunkResponse.data.status !== 'success') {
|
||
throw new Error(chunkResponse.data.message);
|
||
}
|
||
|
||
// 更新整体进度
|
||
const progress = chunkResponse.data.data.progress;
|
||
updateProgress(chunkIndex + 1, total_chunks, progress);
|
||
|
||
} catch (chunkError) {
|
||
console.error('分片上传失败:', chunkError);
|
||
uploadStatus.innerHTML = `<div class="error">分片 ${chunkIndex} 上传失败: ${chunkError.message}</div>`;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (uploadInProgress) {
|
||
// 3. 轮询上传状态直到完成
|
||
await pollUploadStatus(session_id);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('上传失败:', error);
|
||
uploadStatus.innerHTML = `<div class="error">上传失败: ${error.message}</div>`;
|
||
} finally {
|
||
if (!uploadInProgress) {
|
||
uploadStatus.innerHTML = `<div class="error">上传已取消</div>`;
|
||
}
|
||
loadModels(); // 刷新模型列表
|
||
}
|
||
}
|
||
|
||
// 更新进度显示
|
||
function updateProgress(currentChunk, totalChunks, progress) {
|
||
const overallProgress = Math.round(((currentChunk - 1) / totalChunks) * 100 + (progress / totalChunks));
|
||
progressFill.style.width = overallProgress + '%';
|
||
progressText.textContent = overallProgress + '%';
|
||
chunkInfo.textContent = `(${currentChunk}/${totalChunks} 分片)`;
|
||
}
|
||
|
||
// 轮询上传状态
|
||
async function pollUploadStatus(sessionId) {
|
||
let attempts = 0;
|
||
const maxAttempts = 60; // 最多等待5分钟 (60 * 5秒)
|
||
|
||
while (attempts < maxAttempts && uploadInProgress) {
|
||
try {
|
||
const statusResponse = await axios.get(`/api/models/upload/status/${sessionId}`);
|
||
|
||
if (statusResponse.data.status === 'success') {
|
||
const statusData = statusResponse.data.data;
|
||
|
||
if (statusData.status === 'completed') {
|
||
uploadStatus.innerHTML = `
|
||
<div class="success">
|
||
<strong>上传完成!</strong><br>
|
||
加密文件: ${statusData.relative_path}<br>
|
||
模型哈希: ${statusData.model_hash}<br>
|
||
密钥哈希: ${statusData.key_hash}
|
||
</div>
|
||
`;
|
||
currentSessionId = null;
|
||
uploadInProgress = false;
|
||
return;
|
||
} else if (statusData.status === 'failed') {
|
||
uploadStatus.innerHTML = `<div class="error">上传失败: ${statusData.error}</div>`;
|
||
currentSessionId = null;
|
||
uploadInProgress = false;
|
||
return;
|
||
} else if (statusData.status === 'merging' || statusData.status === 'encrypting') {
|
||
uploadStatus.innerHTML = `<div class="success">正在${statusData.status === 'merging' ? '合并' : '加密'}文件...</div>`;
|
||
}
|
||
}
|
||
|
||
attempts++;
|
||
await sleep(5000); // 每5秒轮询一次
|
||
|
||
} catch (error) {
|
||
console.error('获取状态失败:', error);
|
||
await sleep(5000);
|
||
}
|
||
}
|
||
|
||
if (attempts >= maxAttempts) {
|
||
uploadStatus.innerHTML = `<div class="error">上传超时</div>`;
|
||
}
|
||
}
|
||
|
||
// 取消上传
|
||
function cancelUpload() {
|
||
if (currentSessionId && uploadInProgress) {
|
||
if (confirm('确定要取消上传吗?')) {
|
||
uploadInProgress = false;
|
||
axios.post(`/api/models/upload/cancel/${currentSessionId}`);
|
||
uploadStatus.innerHTML = `<div class="error">上传已取消</div>`;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 加载模型列表
|
||
async function loadModels() {
|
||
try {
|
||
const response = await axios.get('/api/models/list');
|
||
if (response.data.status === 'success') {
|
||
const models = response.data.data.models;
|
||
|
||
if (models.length === 0) {
|
||
modelList.innerHTML = '<p>暂无模型文件</p>';
|
||
return;
|
||
}
|
||
|
||
let html = '<table style="width:100%; border-collapse: collapse;">';
|
||
html += '<tr><th>文件名</th><th>大小</th><th>修改时间</th><th>操作</th></tr>';
|
||
|
||
models.forEach(model => {
|
||
const modifiedDate = new Date(model.modified * 1000).toLocaleString();
|
||
html += `
|
||
<tr class="model-item">
|
||
<td>${model.filename}</td>
|
||
<td>${formatFileSize(model.size)}</td>
|
||
<td>${modifiedDate}</td>
|
||
<td>
|
||
<button onclick="copyPath('${model.filename}')" class="btn" style="padding: 5px 10px; font-size: 12px;">复制路径</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
|
||
html += '</table>';
|
||
modelList.innerHTML = html;
|
||
}
|
||
} catch (error) {
|
||
modelList.innerHTML = `<div class="error">加载模型列表失败: ${error.message}</div>`;
|
||
}
|
||
}
|
||
|
||
// 辅助函数
|
||
function formatFileSize(bytes) {
|
||
if (bytes === 0) return '0 Bytes';
|
||
const k = 1024;
|
||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||
}
|
||
|
||
function sleep(ms) {
|
||
return new Promise(resolve => setTimeout(resolve, ms));
|
||
}
|
||
|
||
function copyPath(filename) {
|
||
const path = `encrypted_models/${filename}`;
|
||
navigator.clipboard.writeText(path).then(() => {
|
||
alert('路径已复制到剪贴板: ' + path);
|
||
});
|
||
}
|
||
|
||
// 页面加载时加载模型列表
|
||
window.onload = loadModels;
|
||
</script>
|
||
</body>
|
||
</html> |