Files
vue-driven-cloud-storage/upload-tool/upload_tool.py
Claude Opus 0b0e5b9d7c feat: v3.1.0 OSS直连优化与代码质量提升
- 🚀 OSS 直连上传下载(用户直连OSS,不经过后端)
-  新增 Presigned URL 签名接口
-  支持自定义 OSS endpoint 配置
- 🐛 修复 buildS3Config 不支持自定义 endpoint 的问题
- 🐛 清理残留的 basic-ftp 依赖
- ♻️ 更新 package.json 项目描述和版本号
- 📝 完善 README.md 更新日志和 CORS 配置说明
- 🔒 安全性增强:签名 URL 15分钟/1小时有效期

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 17:14:16 +08:00

481 lines
16 KiB
Python
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.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
玩玩云上传工具 v3.0
支持本地存储和 OSS 云存储
"""
import sys
import os
import json
import requests
import hashlib
import time
from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QVBoxLayout,
QWidget, QProgressBar, QTextEdit, QPushButton)
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtGui import QDragEnterEvent, QDropEvent, QFont
class UploadThread(QThread):
"""上传线程 - 支持 OSS 和本地存储"""
progress = pyqtSignal(int, str) # 进度,状态信息
finished = pyqtSignal(bool, str) # 成功/失败,消息
def __init__(self, api_config, file_path, remote_path):
super().__init__()
self.api_config = api_config
self.file_path = file_path
self.remote_path = remote_path
def run(self):
try:
filename = os.path.basename(self.file_path)
self.progress.emit(10, f'正在准备上传: {filename}')
# 使用服务器 API 上传
api_base_url = self.api_config['api_base_url']
api_key = self.api_config['api_key']
# 读取文件
with open(self.file_path, 'rb') as f:
file_data = f.read()
file_size = len(file_data)
self.progress.emit(20, f'文件大小: {file_size / (1024*1024):.2f} MB')
# 分块上传(支持大文件)
chunk_size = 5 * 1024 * 1024 # 5MB 每块
uploaded = 0
# 使用 multipart/form-data 上传
files = {
'file': (filename, file_data)
}
data = {
'path': self.remote_path
}
headers = {
'X-API-Key': api_key
}
self.progress.emit(30, f'开始上传...')
# 带进度的上传
response = requests.post(
f"{api_base_url}/api/upload",
files=files,
data=data,
headers=headers,
timeout=300 # 5分钟超时
)
uploaded = file_size
self.progress.emit(90, f'上传完成,等待服务器确认...')
if response.status_code == 200:
result = response.json()
if result.get('success'):
self.progress.emit(100, f'上传完成!')
self.finished.emit(True, f'文件 {filename} 上传成功!')
else:
self.finished.emit(False, f'上传失败: {result.get("message", "未知错误")}')
else:
self.finished.emit(False, f'服务器错误: {response.status_code}')
except requests.exceptions.Timeout:
self.finished.emit(False, '上传超时,请检查网络连接')
except requests.exceptions.ConnectionError:
self.finished.emit(False, '无法连接到服务器,请检查网络')
except Exception as e:
self.finished.emit(False, f'上传失败: {str(e)}')
class ConfigCheckThread(QThread):
"""配置检查线程"""
result = pyqtSignal(bool, str, object) # 成功/失败,消息,配置信息
def __init__(self, api_base_url, api_key):
super().__init__()
self.api_base_url = api_base_url
self.api_key = api_key
def run(self):
try:
response = requests.post(
f"{self.api_base_url}/api/upload/get-config",
json={'api_key': self.api_key},
timeout=10
)
if response.status_code == 200:
data = response.json()
if data['success']:
config = data['config']
storage_type = config.get('storage_type', 'unknown')
if storage_type == 'oss':
# OSS 云存储
provider = config.get('oss_provider', '未知')
bucket = config.get('oss_bucket', '未知')
msg = f'已连接 - OSS存储 ({provider}) | Bucket: {bucket}'
else:
# 本地存储
msg = f'已连接 - 本地存储'
self.result.emit(True, msg, config)
else:
self.result.emit(False, data.get('message', '获取配置失败'), None)
else:
self.result.emit(False, f'服务器错误: {response.status_code}', None)
except requests.exceptions.Timeout:
self.result.emit(False, '连接超时,请检查网络', None)
except requests.exceptions.ConnectionError:
self.result.emit(False, '无法连接到服务器', None)
except Exception as e:
self.result.emit(False, f'连接失败: {str(e)}', None)
class UploadWindow(QMainWindow):
def __init__(self):
super().__init__()
self.config = self.load_config()
self.server_config = None
self.remote_path = '/' # 默认上传目录
self.upload_queue = [] # 上传队列
self.is_uploading = False # 是否正在上传
self.initUI()
self.check_config()
def load_config(self):
"""加载配置文件"""
try:
# PyInstaller打包后使用sys._MEIPASS
if getattr(sys, 'frozen', False):
# 打包后的exe
base_path = os.path.dirname(sys.executable)
else:
# 开发环境
base_path = os.path.dirname(__file__)
config_path = os.path.join(base_path, 'config.json')
if not os.path.exists(config_path):
from PyQt5.QtWidgets import QMessageBox
QMessageBox.critical(None, '错误', f'找不到配置文件: {config_path}\n\n请确保config.json与程序在同一目录下')
sys.exit(1)
with open(config_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
from PyQt5.QtWidgets import QMessageBox
QMessageBox.critical(None, '错误', f'加载配置失败:\n{str(e)}')
sys.exit(1)
def check_config(self):
"""检查服务器配置"""
self.log('正在连接服务器...')
self.check_thread = ConfigCheckThread(
self.config['api_base_url'],
self.config['api_key']
)
self.check_thread.result.connect(self.on_config_result)
self.check_thread.start()
def on_config_result(self, success, message, config):
"""处理配置检查结果"""
if success:
self.server_config = config
self.log(f'{message}')
# 更新状态显示
if config.get('storage_type') == 'oss':
provider_name = {
'aliyun': '阿里云OSS',
'tencent': '腾讯云COS',
'aws': 'AWS S3'
}.get(config.get('oss_provider'), config.get('oss_provider', 'OSS'))
self.status_label.setText(
f'<h2>玩玩云上传工具 v3.0</h2>'
f'<p style="color: green;">✓ 已连接 - {provider_name}</p>'
f'<p style="color: #666; font-size: 14px;">拖拽文件到此处上传</p>'
f'<p style="color: #999; font-size: 12px;">存储桶: {config.get("oss_bucket", "未知")}</p>'
)
else:
self.status_label.setText(
f'<h2>玩玩云上传工具 v3.0</h2>'
f'<p style="color: green;">✓ 已连接 - 本地存储</p>'
f'<p style="color: #666; font-size: 14px;">拖拽文件到此处上传</p>'
)
else:
self.log(f'{message}')
self.show_error(message)
def show_error(self, message):
"""显示错误信息"""
self.status_label.setText(
f'<h2>玩玩云上传工具 v3.0</h2>'
f'<p style="color: red;">✗ 错误: {message}</p>'
f'<p style="color: #666; font-size: 14px;">请检查网络连接或联系管理员</p>'
)
def initUI(self):
"""初始化界面"""
self.setWindowTitle('玩玩云上传工具 v3.0')
self.setGeometry(300, 300, 500, 450)
# 设置接受拖拽
self.setAcceptDrops(True)
# 中心部件
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 布局
layout = QVBoxLayout()
# 状态标签
self.status_label = QLabel('正在连接服务器...')
self.status_label.setAlignment(Qt.AlignCenter)
self.status_label.setFont(QFont('Arial', 11))
self.status_label.setWordWrap(True)
self.status_label.setStyleSheet('padding: 20px;')
layout.addWidget(self.status_label)
# 拖拽提示区域
self.drop_area = QLabel('📁\n\n支持多文件和文件夹')
self.drop_area.setAlignment(Qt.AlignCenter)
self.drop_area.setStyleSheet("""
QLabel {
font-size: 50px;
color: #667eea;
border: 3px dashed #667eea;
border-radius: 10px;
background-color: #f5f7fa;
padding: 40px;
}
""")
layout.addWidget(self.drop_area)
# 队列状态标签
self.queue_label = QLabel('队列: 0 个文件等待上传')
self.queue_label.setAlignment(Qt.AlignCenter)
self.queue_label.setStyleSheet('color: #2c3e50; font-weight: bold; padding: 5px;')
layout.addWidget(self.queue_label)
# 进度条
self.progress_bar = QProgressBar()
self.progress_bar.setValue(0)
self.progress_bar.setVisible(False)
self.progress_bar.setStyleSheet("""
QProgressBar {
border: 2px solid #e0e0e0;
border-radius: 5px;
text-align: center;
height: 25px;
}
QProgressBar::chunk {
background-color: #667eea;
}
""")
layout.addWidget(self.progress_bar)
# 进度信息
self.progress_label = QLabel('')
self.progress_label.setAlignment(Qt.AlignCenter)
self.progress_label.setVisible(False)
layout.addWidget(self.progress_label)
# 日志区域
self.log_text = QTextEdit()
self.log_text.setReadOnly(True)
self.log_text.setMaximumHeight(100)
self.log_text.setStyleSheet("""
QTextEdit {
background-color: #f9f9f9;
border: 1px solid #e0e0e0;
border-radius: 5px;
padding: 5px;
font-family: 'Courier New', monospace;
font-size: 11px;
}
""")
layout.addWidget(self.log_text)
central_widget.setLayout(layout)
self.log('程序已启动 - 版本 v3.0 (支持OSS云存储)')
def log(self, message):
"""添加日志"""
self.log_text.append(f'[{self.get_time()}] {message}')
# 自动滚动到底部
self.log_text.verticalScrollBar().setValue(
self.log_text.verticalScrollBar().maximum()
)
def get_time(self):
"""获取当前时间"""
from datetime import datetime
return datetime.now().strftime('%H:%M:%S')
def dragEnterEvent(self, event: QDragEnterEvent):
"""拖拽进入事件"""
if event.mimeData().hasUrls():
event.acceptProposedAction()
self.drop_area.setStyleSheet("""
QLabel {
font-size: 50px;
color: #667eea;
border: 3px dashed #667eea;
border-radius: 10px;
background-color: #e8ecf7;
padding: 40px;
}
""")
def dragLeaveEvent(self, event):
"""拖拽离开事件"""
self.drop_area.setStyleSheet("""
QLabel {
font-size: 50px;
color: #667eea;
border: 3px dashed #667eea;
border-radius: 10px;
background-color: #f5f7fa;
padding: 40px;
}
""")
def dropEvent(self, event: QDropEvent):
"""拖拽放下事件"""
self.drop_area.setStyleSheet("""
QLabel {
font-size: 50px;
color: #667eea;
border: 3px dashed #667eea;
border-radius: 10px;
background-color: #f5f7fa;
padding: 40px;
}
""")
if not self.server_config:
self.log('错误: 未连接到服务器,请等待连接完成')
self.show_error('服务器未连接,请稍后重试')
return
paths = [url.toLocalFile() for url in event.mimeData().urls()]
all_files = []
for path in paths:
if os.path.isfile(path):
all_files.append(path)
elif os.path.isdir(path):
self.log(f'扫描文件夹: {os.path.basename(path)}')
folder_files = self.scan_folder(path)
all_files.extend(folder_files)
self.log(f'找到 {len(folder_files)} 个文件')
if all_files:
self.upload_queue.extend(all_files)
self.update_queue_label()
self.log(f'添加 {len(all_files)} 个文件到上传队列')
if not self.is_uploading:
self.process_upload_queue()
def scan_folder(self, folder_path):
"""递归扫描文件夹"""
files = []
try:
for root, dirs, filenames in os.walk(folder_path):
for filename in filenames:
file_path = os.path.join(root, filename)
files.append(file_path)
except Exception as e:
self.log(f'扫描文件夹失败: {str(e)}')
return files
def update_queue_label(self):
"""更新队列标签"""
count = len(self.upload_queue)
self.queue_label.setText(f'队列: {count} 个文件等待上传')
def process_upload_queue(self):
"""处理上传队列"""
if not self.upload_queue:
self.is_uploading = False
self.update_queue_label()
self.log('✓ 所有文件上传完成!')
return
self.is_uploading = True
file_path = self.upload_queue.pop(0)
self.update_queue_label()
self.upload_file(file_path)
def upload_file(self, file_path):
"""上传文件"""
filename = os.path.basename(file_path)
# 构建远程路径
if self.remote_path == '/':
remote_path = f'/{filename}'
else:
remote_path = f'{self.remote_path}/{filename}'
self.log(f'开始上传: {filename}')
# 显示进度控件
self.progress_bar.setVisible(True)
self.progress_bar.setValue(0)
self.progress_label.setVisible(True)
self.progress_label.setText('准备上传...')
# 创建上传线程
api_config = {
'api_base_url': self.config['api_base_url'],
'api_key': self.config['api_key']
}
self.upload_thread = UploadThread(api_config, file_path, remote_path)
self.upload_thread.progress.connect(self.on_progress)
self.upload_thread.finished.connect(self.on_finished)
self.upload_thread.start()
def on_progress(self, value, message):
"""上传进度更新"""
self.progress_bar.setValue(value)
self.progress_label.setText(message)
def on_finished(self, success, message):
"""上传完成"""
self.log(message)
if success:
self.progress_label.setText('' + message)
self.progress_label.setStyleSheet('color: green; font-weight: bold;')
else:
self.progress_label.setText('' + message)
self.progress_label.setStyleSheet('color: red; font-weight: bold;')
# 继续处理队列
from PyQt5.QtCore import QTimer
QTimer.singleShot(1000, self.process_upload_queue)
def main():
app = QApplication(sys.argv)
window = UploadWindow()
window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()