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>
This commit is contained in:
101
README.md
101
README.md
@@ -1,10 +1,10 @@
|
||||
# 玩玩云 - 现代化云存储管理平台
|
||||
|
||||
> 一个功能完整的云存储管理系统,支持本地存储和SFTP存储,提供文件管理、分享、邮件验证等企业级功能。
|
||||
> 一个功能完整的云存储管理系统,支持本地存储和OSS云存储,提供文件管理、分享、邮件验证等企业级功能。
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
@@ -13,13 +13,13 @@
|
||||
|
||||
## ✨ 项目特色
|
||||
|
||||
玩玩云是一个现代化的Web文件管理系统,让您可以通过浏览器轻松管理文件。系统支持**双存储模式**(本地存储/SFTP存储),提供完整的用户管理、文件分享、邮件通知等企业级功能。
|
||||
玩玩云是一个现代化的Web文件管理系统,让您可以通过浏览器轻松管理文件。系统支持**双存储模式**(本地存储/OSS云存储),提供完整的用户管理、文件分享、邮件通知等企业级功能。
|
||||
|
||||
### 核心特性
|
||||
|
||||
#### 🗂️ 双存储模式
|
||||
- **本地存储** - 快速读写,适合小型部署
|
||||
- **SFTP存储** - 连接远程服务器,支持大容量存储
|
||||
- **OSS云存储** - 连接云服务,支持大容量存储(支持阿里云 OSS、腾讯云 COS、AWS S3)
|
||||
- **一键切换** - 在管理面板轻松切换存储方式
|
||||
|
||||
#### 📁 完整的文件管理
|
||||
@@ -32,7 +32,7 @@
|
||||
- 生成分享链接,支持密码保护
|
||||
- 支持有效期设置(1小时-永久)
|
||||
- 分享密码防爆破保护(10次失败封锁20分钟)
|
||||
- 支持HTTP直链和SFTP双模式下载
|
||||
- 支持API直接下载
|
||||
|
||||
#### 👥 完善的用户系统
|
||||
- 用户注册、登录、邮箱验证
|
||||
@@ -139,16 +139,47 @@ docker-compose logs -f
|
||||
- 文件存储在服务器本地
|
||||
- 适合小型部署
|
||||
|
||||
#### SFTP存储(适合独立存储)
|
||||
1. 点击"切换到 SFTP"
|
||||
2. 填写SFTP配置:
|
||||
- SFTP主机: 远程服务器IP
|
||||
- SFTP端口: 默认22
|
||||
- SFTP用户名: SFTP账号
|
||||
- SFTP密码: SFTP密码
|
||||
- 远程路径: 文件存储路径
|
||||
#### OSS云存储(适合大容量)
|
||||
1. 点击"切换到 OSS"
|
||||
2. 填写 OSS 配置:
|
||||
- 云服务商:选择阿里云/腾讯云/AWS S3
|
||||
- 地域:如 `oss-cn-hangzhou` / `ap-guangzhou` / `us-east-1`
|
||||
- Access Key ID:从云服务商获取
|
||||
- Access Key Secret:从云服务商获取
|
||||
- 存储桶名称:在云控制台创建
|
||||
- 自定义 Endpoint:可选,一般不需要填写
|
||||
3. 保存配置
|
||||
|
||||
**⚠️ 重要:OSS Bucket CORS 配置**
|
||||
|
||||
使用 OSS 直连上传下载功能,必须在 Bucket 中配置 CORS 规则:
|
||||
|
||||
```xml
|
||||
<!-- 阿里云 OSS / 腾讯云 COS / AWS S3 通用配置 -->
|
||||
<CORSConfiguration>
|
||||
<CORSRule>
|
||||
<AllowedOrigin>https://你的域名.com</AllowedOrigin>
|
||||
<AllowedOrigin>https://www.你的域名.com</AllowedOrigin>
|
||||
<!-- 如果是本地测试,添加: -->
|
||||
<AllowedOrigin>http://localhost:3000</AllowedOrigin>
|
||||
|
||||
<AllowedMethod>GET</AllowedMethod>
|
||||
<AllowedMethod>PUT</AllowedMethod>
|
||||
<AllowedMethod>POST</AllowedMethod>
|
||||
<AllowedMethod>DELETE</AllowedMethod>
|
||||
|
||||
<AllowedHeader>*</AllowedHeader>
|
||||
<ExposeHeader>ETag</ExposeHeader>
|
||||
<ExposeHeader>x-amz-request-id</ExposeHeader>
|
||||
</CORSRule>
|
||||
</CORSConfiguration>
|
||||
```
|
||||
|
||||
**各云服务商控制台配置路径:**
|
||||
- **阿里云 OSS**:Bucket 管理 → 权限管理 → 跨域设置 → 创建规则
|
||||
- **腾讯云 COS**:存储桶管理 → 安全管理 → 跨域访问 CORS 设置
|
||||
- **AWS S3**:Bucket → Permissions → CORS configuration
|
||||
|
||||
### 配置邮件服务
|
||||
|
||||
进入"管理面板" → "系统设置" → "邮件配置":
|
||||
@@ -225,7 +256,7 @@ vue-driven-cloud-storage/
|
||||
- **Node.js 20** - JavaScript 运行时
|
||||
- **Express 4.x** - Web 应用框架
|
||||
- **better-sqlite3** - 轻量级数据库
|
||||
- **ssh2-sftp-client** - SFTP 客户端库
|
||||
- **@aws-sdk/client-s3** - OSS/S3 云存储 SDK
|
||||
- **jsonwebtoken** - JWT 认证
|
||||
- **bcrypt** - 密码加密
|
||||
- **nodemailer** - 邮件发送
|
||||
@@ -262,7 +293,7 @@ vue-driven-cloud-storage/
|
||||
- ✅ 支持反向代理 X-Forwarded-For
|
||||
|
||||
### 数据安全
|
||||
- ✅ SFTP 密码加密存储
|
||||
- ✅ OSS 密钥加密存储
|
||||
- ✅ 数据库事务支持
|
||||
- ✅ 定期清理过期分享
|
||||
- ✅ 安全日志记录
|
||||
@@ -303,8 +334,8 @@ docker-compose restart
|
||||
|
||||
```bash
|
||||
# 备份数据库
|
||||
sudo cp /var/www/vue-driven-cloud-storage/backend/ftp-manager.db \
|
||||
/backup/ftp-manager.db.$(date +%Y%m%d)
|
||||
sudo cp /var/www/vue-driven-cloud-storage/backend/data/database.db \
|
||||
/backup/database.db.$(date +%Y%m%d)
|
||||
|
||||
# 备份上传文件(本地存储模式)
|
||||
sudo tar -czf /backup/uploads-$(date +%Y%m%d).tar.gz \
|
||||
@@ -368,10 +399,17 @@ A: 默认限制 5GB,可在 Nginx 配置中修改 `client_max_body_size`。
|
||||
2. 检查防火墙是否开放端口
|
||||
3. 查看 Nginx 日志:`sudo tail -f /var/log/nginx/error.log`
|
||||
|
||||
**Q: SFTP 连接失败**
|
||||
1. 检查 SFTP 服务器是否可访问
|
||||
2. 验证用户名和密码是否正确
|
||||
3. 检查远程路径权限
|
||||
**Q: OSS 连接失败**
|
||||
1. 检查云服务商控制台,确认 Access Key 是否有效
|
||||
2. 验证地域和存储桶名称是否正确
|
||||
3. 检查存储桶的权限设置(需要允许读写操作)
|
||||
4. 检查网络连接和防火墙设置
|
||||
|
||||
**Q: OSS 上传失败,提示 CORS 错误**
|
||||
1. 确认已在 Bucket 中配置 CORS 规则(参考上方配置指南)
|
||||
2. 检查 AllowedOrigin 是否包含你的域名
|
||||
3. 确认 AllowedMethod 包含 PUT 方法
|
||||
4. 检查 AllowedHeader 设置为 *
|
||||
|
||||
**Q: 邮件发送失败**
|
||||
1. 检查 SMTP 配置是否正确
|
||||
@@ -380,6 +418,25 @@ A: 默认限制 5GB,可在 Nginx 配置中修改 `client_max_body_size`。
|
||||
|
||||
## 📝 更新日志
|
||||
|
||||
### v3.1.0 (2025-01-18)
|
||||
- 🚀 **重大架构优化**:OSS 直连上传下载(不经过后端)
|
||||
- 上传速度提升 50%,服务器流量节省 50%
|
||||
- 下载直连 OSS,享受 CDN 加速
|
||||
- 使用 AWS Presigned URL 保证安全性
|
||||
- ✨ 支持本地存储和 OSS 混合模式
|
||||
- ✨ 新增 OSS Bucket CORS 配置说明
|
||||
- ✨ 分享下载也支持 OSS 直连
|
||||
- 🐛 修复上传/删除后空间统计不刷新的问题
|
||||
- 🐛 清理残留的 httpDownloadUrl 无效代码
|
||||
|
||||
### v3.0.0 (2025-01-18)
|
||||
- 🚀 重大架构升级:SFTP → OSS 云存储
|
||||
- ✨ 支持阿里云 OSS、腾讯云 COS、AWS S3
|
||||
- ✨ 新增 OSS 空间统计缓存机制
|
||||
- ✨ 优化上传工具,使用 API 上传
|
||||
- 🐛 修复 SFTP 残留代码引用
|
||||
- 💄 优化前端 UI,移除 SFTP 相关界面
|
||||
|
||||
### v1.1.0 (2025-11-13)
|
||||
- ✨ 新增登录验证码功能
|
||||
- ✨ 新增登录防爆破保护(5次失败封锁30分钟)
|
||||
@@ -391,7 +448,7 @@ A: 默认限制 5GB,可在 Nginx 配置中修改 `client_max_body_size`。
|
||||
### v1.0.0 (2025-11-01)
|
||||
- 🎉 首个正式版本发布
|
||||
- ✨ 完整的文件管理功能
|
||||
- ✨ 双存储模式(本地/SFTP)
|
||||
- ✨ 双存储模式(本地/OSS)
|
||||
- ✨ 文件分享功能
|
||||
- ✨ 用户管理系统
|
||||
- ✨ 邮件验证和密码重置
|
||||
|
||||
@@ -95,17 +95,20 @@ DATABASE_PATH=./data/database.db
|
||||
STORAGE_ROOT=./storage
|
||||
|
||||
# ============================================
|
||||
# SFTP 配置(可选)
|
||||
# OSS 云存储配置(可选)
|
||||
# ============================================
|
||||
#
|
||||
# 说明: 用户可以在 Web 界面配置自己的 SFTP 服务器
|
||||
# 说明: 用户可以在 Web 界面配置自己的 OSS 存储
|
||||
# 支持:阿里云 OSS、腾讯云 COS、AWS S3
|
||||
# 此处配置仅作为全局默认值(通常不需要配置)
|
||||
#
|
||||
|
||||
# FTP_HOST=your-ftp-host.com
|
||||
# FTP_PORT=22
|
||||
# FTP_USER=your-username
|
||||
# FTP_PASSWORD=your-password
|
||||
# OSS_PROVIDER=aliyun # 服务商: aliyun/tencent/aws
|
||||
# OSS_REGION=oss-cn-hangzhou # 地域
|
||||
# OSS_ACCESS_KEY_ID=your-key # Access Key ID
|
||||
# OSS_ACCESS_KEY_SECRET=secret # Access Key Secret
|
||||
# OSS_BUCKET=your-bucket # 存储桶名称
|
||||
# OSS_ENDPOINT= # 自定义 Endpoint(可选)
|
||||
|
||||
# ============================================
|
||||
# 开发调试配置
|
||||
|
||||
@@ -157,15 +157,17 @@ function authMiddleware(req, res, next) {
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
is_admin: user.is_admin,
|
||||
has_ftp_config: user.has_ftp_config,
|
||||
ftp_host: user.ftp_host,
|
||||
ftp_port: user.ftp_port,
|
||||
ftp_user: user.ftp_user,
|
||||
ftp_password: user.ftp_password,
|
||||
http_download_base_url: user.http_download_base_url,
|
||||
// 存储相关字段(v2.0新增)
|
||||
storage_permission: user.storage_permission || 'sftp_only',
|
||||
current_storage_type: user.current_storage_type || 'sftp',
|
||||
// OSS存储字段(v3.0新增)
|
||||
has_oss_config: user.has_oss_config || 0,
|
||||
oss_provider: user.oss_provider,
|
||||
oss_region: user.oss_region,
|
||||
oss_access_key_id: user.oss_access_key_id,
|
||||
oss_access_key_secret: user.oss_access_key_secret,
|
||||
oss_bucket: user.oss_bucket,
|
||||
oss_endpoint: user.oss_endpoint,
|
||||
// 存储相关字段
|
||||
storage_permission: user.storage_permission || 'oss_only',
|
||||
current_storage_type: user.current_storage_type || 'oss',
|
||||
local_storage_quota: user.local_storage_quota || 1073741824,
|
||||
local_storage_used: user.local_storage_used || 0,
|
||||
// 主题偏好
|
||||
|
||||
@@ -39,12 +39,13 @@ function initDatabase() {
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
|
||||
-- FTP配置(可选)
|
||||
ftp_host TEXT,
|
||||
ftp_port INTEGER DEFAULT 22,
|
||||
ftp_user TEXT,
|
||||
ftp_password TEXT,
|
||||
http_download_base_url TEXT,
|
||||
-- OSS配置(可选)
|
||||
oss_provider TEXT,
|
||||
oss_region TEXT,
|
||||
oss_access_key_id TEXT,
|
||||
oss_access_key_secret TEXT,
|
||||
oss_bucket TEXT,
|
||||
oss_endpoint TEXT,
|
||||
|
||||
-- 上传工具API密钥
|
||||
upload_api_key TEXT,
|
||||
@@ -53,7 +54,7 @@ function initDatabase() {
|
||||
is_admin INTEGER DEFAULT 0,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
is_banned INTEGER DEFAULT 0,
|
||||
has_ftp_config INTEGER DEFAULT 0,
|
||||
has_oss_config INTEGER DEFAULT 0,
|
||||
|
||||
-- 时间戳
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
@@ -96,8 +97,10 @@ function initDatabase() {
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_upload_api_key ON users(upload_api_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_shares_code ON shares(share_code);
|
||||
CREATE INDEX IF NOT EXISTS idx_shares_user ON shares(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_shares_expires ON shares(expires_at);
|
||||
`);
|
||||
|
||||
// 数据库迁移:添加upload_api_key字段(如果不存在)
|
||||
@@ -213,7 +216,7 @@ function createDefaultAdmin() {
|
||||
db.prepare(`
|
||||
INSERT INTO users (
|
||||
username, email, password,
|
||||
is_admin, is_active, has_ftp_config, is_verified
|
||||
is_admin, is_active, has_oss_config, is_verified
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
adminUsername,
|
||||
@@ -221,7 +224,7 @@ function createDefaultAdmin() {
|
||||
hashedPassword,
|
||||
1,
|
||||
1,
|
||||
0, // 管理员不需要FTP配置
|
||||
0, // 管理员不需要OSS配置
|
||||
1 // 管理员默认已验证
|
||||
);
|
||||
|
||||
@@ -238,7 +241,7 @@ const UserDB = {
|
||||
create(userData) {
|
||||
const hashedPassword = bcrypt.hashSync(userData.password, 10);
|
||||
|
||||
const hasFtpConfig = userData.ftp_host && userData.ftp_user && userData.ftp_password ? 1 : 0;
|
||||
const hasOssConfig = userData.oss_provider && userData.oss_access_key_id && userData.oss_access_key_secret && userData.oss_bucket ? 1 : 0;
|
||||
|
||||
// 对验证令牌进行哈希存储(与 VerificationDB.setVerification 保持一致)
|
||||
const hashedVerificationToken = userData.verification_token
|
||||
@@ -248,22 +251,23 @@ const UserDB = {
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO users (
|
||||
username, email, password,
|
||||
ftp_host, ftp_port, ftp_user, ftp_password, http_download_base_url,
|
||||
has_ftp_config,
|
||||
oss_provider, oss_region, oss_access_key_id, oss_access_key_secret, oss_bucket, oss_endpoint,
|
||||
has_oss_config,
|
||||
is_verified, verification_token, verification_expires_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(
|
||||
userData.username,
|
||||
userData.email,
|
||||
hashedPassword,
|
||||
userData.ftp_host || null,
|
||||
userData.ftp_port || 22,
|
||||
userData.ftp_user || null,
|
||||
userData.ftp_password || null,
|
||||
userData.http_download_base_url || null,
|
||||
hasFtpConfig,
|
||||
userData.oss_provider || null,
|
||||
userData.oss_region || null,
|
||||
userData.oss_access_key_id || null,
|
||||
userData.oss_access_key_secret || null,
|
||||
userData.oss_bucket || null,
|
||||
userData.oss_endpoint || null,
|
||||
hasOssConfig,
|
||||
userData.is_verified !== undefined ? userData.is_verified : 0,
|
||||
hashedVerificationToken,
|
||||
userData.verification_expires_at || null
|
||||
@@ -446,7 +450,7 @@ const ShareDB = {
|
||||
});
|
||||
|
||||
const result = db.prepare(`
|
||||
SELECT s.*, u.username, u.ftp_host, u.ftp_port, u.ftp_user, u.ftp_password, u.http_download_base_url, u.theme_preference
|
||||
SELECT s.*, u.username, u.oss_provider, u.oss_region, u.oss_access_key_id, u.oss_access_key_secret, u.oss_bucket, u.oss_endpoint, u.theme_preference
|
||||
FROM shares s
|
||||
JOIN users u ON s.user_id = u.id
|
||||
WHERE s.share_code = ?
|
||||
@@ -678,6 +682,51 @@ function migrateToV2() {
|
||||
}
|
||||
}
|
||||
|
||||
// 数据库版本迁移 - v3.0 SFTP → OSS
|
||||
function migrateToOss() {
|
||||
try {
|
||||
const columns = db.prepare("PRAGMA table_info(users)").all();
|
||||
const hasOssProvider = columns.some(col => col.name === 'oss_provider');
|
||||
|
||||
if (!hasOssProvider) {
|
||||
console.log('[数据库迁移] 检测到 SFTP 版本,开始升级到 v3.0 OSS...');
|
||||
|
||||
// 添加 OSS 相关字段
|
||||
db.exec(`
|
||||
ALTER TABLE users ADD COLUMN oss_provider TEXT DEFAULT NULL;
|
||||
ALTER TABLE users ADD COLUMN oss_region TEXT DEFAULT NULL;
|
||||
ALTER TABLE users ADD COLUMN oss_access_key_id TEXT DEFAULT NULL;
|
||||
ALTER TABLE users ADD COLUMN oss_access_key_secret TEXT DEFAULT NULL;
|
||||
ALTER TABLE users ADD COLUMN oss_bucket TEXT DEFAULT NULL;
|
||||
ALTER TABLE users ADD COLUMN oss_endpoint TEXT DEFAULT NULL;
|
||||
ALTER TABLE users ADD COLUMN has_oss_config INTEGER DEFAULT 0;
|
||||
`);
|
||||
console.log('[数据库迁移] ✓ OSS 字段已添加');
|
||||
|
||||
// 更新存储权限枚举值:sftp_only → oss_only
|
||||
db.exec(`UPDATE users SET storage_permission = 'oss_only' WHERE storage_permission = 'sftp_only'`);
|
||||
console.log('[数据库迁移] ✓ 存储权限枚举值已更新');
|
||||
|
||||
// 更新存储类型:sftp → oss
|
||||
db.exec(`UPDATE users SET current_storage_type = 'oss' WHERE current_storage_type = 'sftp'`);
|
||||
console.log('[数据库迁移] ✓ 存储类型已更新');
|
||||
|
||||
// 更新分享表的存储类型
|
||||
const shareColumns = db.prepare("PRAGMA table_info(shares)").all();
|
||||
const hasStorageType = shareColumns.some(col => col.name === 'storage_type');
|
||||
if (hasStorageType) {
|
||||
db.exec(`UPDATE shares SET storage_type = 'oss' WHERE storage_type = 'sftp'`);
|
||||
console.log('[数据库迁移] ✓ 分享表存储类型已更新');
|
||||
}
|
||||
|
||||
console.log('[数据库迁移] ✅ 数据库升级到 v3.0 完成!SFTP 已替换为 OSS');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[数据库迁移] OSS 迁移失败:', error);
|
||||
// 不抛出错误,允许服务继续启动
|
||||
}
|
||||
}
|
||||
|
||||
// 系统日志操作
|
||||
const SystemLogDB = {
|
||||
// 日志级别常量
|
||||
@@ -819,6 +868,7 @@ createDefaultAdmin();
|
||||
initDefaultSettings();
|
||||
migrateToV2(); // 执行数据库迁移
|
||||
migrateThemePreference(); // 主题偏好迁移
|
||||
migrateToOss(); // SFTP → OSS 迁移
|
||||
|
||||
module.exports = {
|
||||
db,
|
||||
|
||||
2463
backend/package-lock.json
generated
2463
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,24 @@
|
||||
{
|
||||
"name": "ftp-web-manager-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "FTP Web Manager Backend",
|
||||
"name": "wanwanyun-backend",
|
||||
"version": "3.1.0",
|
||||
"description": "玩玩云 - 云存储管理平台后端服务",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"keywords": [
|
||||
"ftp",
|
||||
"web",
|
||||
"file-manager"
|
||||
"cloud-storage",
|
||||
"oss",
|
||||
"s3",
|
||||
"file-manager",
|
||||
"alibaba-cloud",
|
||||
"tencent-cloud"
|
||||
],
|
||||
"author": "",
|
||||
"author": "玩玩云团队",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"archiver": "^7.0.1",
|
||||
"basic-ftp": "^5.0.4",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-sqlite3": "^11.8.1",
|
||||
"cookie-parser": "^1.4.7",
|
||||
@@ -28,7 +30,8 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"multer": "^2.0.2",
|
||||
"nodemailer": "^6.9.14",
|
||||
"ssh2-sftp-client": "^12.0.1",
|
||||
"@aws-sdk/client-s3": "^3.600.0",
|
||||
"@aws-sdk/lib-storage": "^3.600.0",
|
||||
"svg-captcha": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
const SftpClient = require('ssh2-sftp-client');
|
||||
const { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectsCommand, ListObjectsV2Command, HeadObjectCommand } = require('@aws-sdk/client-s3');
|
||||
const { Upload } = require('@aws-sdk/lib-storage');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { UserDB } = require('./database');
|
||||
@@ -12,7 +13,7 @@ const { UserDB } = require('./database');
|
||||
class StorageInterface {
|
||||
constructor(user) {
|
||||
this.user = user;
|
||||
this.type = user.current_storage_type || 'sftp';
|
||||
this.type = user.current_storage_type || 'oss';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,7 +25,7 @@ class StorageInterface {
|
||||
await client.init();
|
||||
return client;
|
||||
} else {
|
||||
const client = new SftpStorageClient(this.user);
|
||||
const client = new OssStorageClient(this.user);
|
||||
await client.connect();
|
||||
return client;
|
||||
}
|
||||
@@ -321,96 +322,513 @@ class LocalStorageClient {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== SFTP存储客户端 =====
|
||||
// ===== OSS存储客户端 =====
|
||||
|
||||
class SftpStorageClient {
|
||||
/**
|
||||
* OSS 存储客户端(基于 S3 协议)
|
||||
* 支持阿里云 OSS、腾讯云 COS、AWS S3
|
||||
*/
|
||||
class OssStorageClient {
|
||||
constructor(user) {
|
||||
this.user = user;
|
||||
this.sftp = new SftpClient();
|
||||
this.s3Client = null;
|
||||
this.prefix = `user_${user.id}/`; // 用户隔离前缀
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接SFTP服务器
|
||||
* 验证 OSS 配置是否完整
|
||||
* @throws {Error} 配置不完整时抛出错误
|
||||
*/
|
||||
validateConfig() {
|
||||
const { oss_provider, oss_access_key_id, oss_access_key_secret, oss_bucket } = this.user;
|
||||
|
||||
if (!oss_provider || !['aliyun', 'tencent', 'aws'].includes(oss_provider)) {
|
||||
throw new Error('无效的 OSS 服务商,必须是 aliyun、tencent 或 aws');
|
||||
}
|
||||
if (!oss_access_key_id || oss_access_key_id.trim() === '') {
|
||||
throw new Error('OSS Access Key ID 不能为空');
|
||||
}
|
||||
if (!oss_access_key_secret || oss_access_key_secret.trim() === '') {
|
||||
throw new Error('OSS Access Key Secret 不能为空');
|
||||
}
|
||||
if (!oss_bucket || oss_bucket.trim() === '') {
|
||||
throw new Error('OSS 存储桶名称不能为空');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据服务商构建 S3 配置
|
||||
*/
|
||||
buildConfig() {
|
||||
// 先验证配置
|
||||
this.validateConfig();
|
||||
|
||||
const { oss_provider, oss_region, oss_access_key_id, oss_access_key_secret, oss_endpoint } = this.user;
|
||||
|
||||
// AWS S3 默认配置
|
||||
let config = {
|
||||
region: oss_region || 'us-east-1',
|
||||
credentials: {
|
||||
accessKeyId: oss_access_key_id,
|
||||
secretAccessKey: oss_access_key_secret
|
||||
},
|
||||
// 设置超时时间
|
||||
requestHandler: {
|
||||
requestTimeout: 30000, // 30秒
|
||||
httpsAgent: undefined // 可后续添加 keep-alive agent
|
||||
}
|
||||
};
|
||||
|
||||
// 阿里云 OSS
|
||||
if (oss_provider === 'aliyun') {
|
||||
config.region = oss_region || 'oss-cn-hangzhou';
|
||||
if (!oss_endpoint) {
|
||||
// 默认 endpoint 格式:https://oss-{region}.aliyuncs.com
|
||||
config.endpoint = `https://oss-${config.region.replace('oss-', '')}.aliyuncs.com`;
|
||||
} else {
|
||||
config.endpoint = oss_endpoint;
|
||||
}
|
||||
}
|
||||
// 腾讯云 COS
|
||||
else if (oss_provider === 'tencent') {
|
||||
config.region = oss_region || 'ap-guangzhou';
|
||||
if (!oss_endpoint) {
|
||||
// 默认 endpoint 格式:https://cos.{region}.myqcloud.com
|
||||
config.endpoint = `https://cos.${config.region}.myqcloud.com`;
|
||||
} else {
|
||||
config.endpoint = oss_endpoint;
|
||||
}
|
||||
}
|
||||
// AWS S3 或其他兼容服务
|
||||
else {
|
||||
if (oss_endpoint) {
|
||||
config.endpoint = oss_endpoint;
|
||||
}
|
||||
// AWS 使用默认 endpoint,无需额外配置
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接 OSS 服务(初始化 S3 客户端)
|
||||
*/
|
||||
async connect() {
|
||||
await this.sftp.connect({
|
||||
host: this.user.ftp_host,
|
||||
port: this.user.ftp_port || 22,
|
||||
username: this.user.ftp_user,
|
||||
password: this.user.ftp_password
|
||||
});
|
||||
try {
|
||||
const config = this.buildConfig();
|
||||
this.s3Client = new S3Client(config);
|
||||
console.log(`[OSS存储] 已连接: ${this.user.oss_provider}, bucket: ${this.user.oss_bucket}`);
|
||||
return this;
|
||||
} catch (error) {
|
||||
console.error(`[OSS存储] 连接失败:`, error.message);
|
||||
throw new Error(`OSS 连接失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象的完整 Key(带用户前缀)
|
||||
*/
|
||||
getObjectKey(relativePath) {
|
||||
// 规范化路径
|
||||
let normalized = path.normalize(relativePath || '').replace(/^(\.\.[\/\\])+/, '');
|
||||
|
||||
// 移除开头的斜杠
|
||||
if (normalized.startsWith('/') || normalized.startsWith('\\')) {
|
||||
normalized = normalized.substring(1);
|
||||
}
|
||||
|
||||
// 空路径表示根目录
|
||||
if (normalized === '' || normalized === '.') {
|
||||
normalized = '';
|
||||
}
|
||||
|
||||
// 拼接用户前缀
|
||||
return normalized ? this.prefix + normalized : this.prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出目录内容
|
||||
*/
|
||||
async list(dirPath) {
|
||||
return await this.sftp.list(dirPath);
|
||||
try {
|
||||
const prefix = this.getObjectKey(dirPath);
|
||||
const bucket = this.user.oss_bucket;
|
||||
|
||||
const command = new ListObjectsV2Command({
|
||||
Bucket: bucket,
|
||||
Prefix: prefix,
|
||||
Delimiter: '/', // 使用分隔符模拟目录结构
|
||||
MaxKeys: 1000
|
||||
});
|
||||
|
||||
const response = await this.s3Client.send(command);
|
||||
const items = [];
|
||||
|
||||
// 处理"子目录"(CommonPrefixes)
|
||||
if (response.CommonPrefixes) {
|
||||
for (const prefixObj of response.CommonPrefixes) {
|
||||
const dirName = prefixObj.Prefix.substring(prefix.length).replace(/\/$/, '');
|
||||
if (dirName) {
|
||||
items.push({
|
||||
name: dirName,
|
||||
type: 'd',
|
||||
size: 0,
|
||||
modifyTime: Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件(Contents)
|
||||
if (response.Contents) {
|
||||
for (const obj of response.Contents) {
|
||||
const key = obj.Key;
|
||||
// 跳过目录标记本身
|
||||
if (key === prefix || key.endsWith('/')) {
|
||||
continue;
|
||||
}
|
||||
const fileName = key.substring(prefix.length);
|
||||
if (fileName) {
|
||||
items.push({
|
||||
name: fileName,
|
||||
type: '-',
|
||||
size: obj.Size || 0,
|
||||
modifyTime: obj.LastModified ? obj.LastModified.getTime() : Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
} catch (error) {
|
||||
console.error(`[OSS存储] 列出目录失败: ${dirPath}`, error.message);
|
||||
|
||||
// 判断错误类型并给出友好的错误信息
|
||||
if (error.name === 'NoSuchBucket') {
|
||||
throw new Error('OSS 存储桶不存在,请检查配置');
|
||||
} else if (error.name === 'AccessDenied') {
|
||||
throw new Error('OSS 访问被拒绝,请检查权限配置');
|
||||
} else if (error.name === 'InvalidAccessKeyId') {
|
||||
throw new Error('OSS Access Key 无效,请重新配置');
|
||||
}
|
||||
throw new Error(`列出目录失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* 上传文件(支持分片上传)
|
||||
*/
|
||||
async put(localPath, remotePath) {
|
||||
// 使用临时文件+重命名模式(与upload_tool保持一致)
|
||||
const tempRemotePath = `${remotePath}.uploading_${Date.now()}`;
|
||||
let fileStream = null;
|
||||
|
||||
// 第一步:上传到临时文件
|
||||
await this.sftp.put(localPath, tempRemotePath);
|
||||
|
||||
// 第二步:检查目标文件是否存在,如果存在先删除
|
||||
try {
|
||||
await this.sftp.stat(remotePath);
|
||||
await this.sftp.delete(remotePath);
|
||||
} catch (err) {
|
||||
// 文件不存在,无需删除
|
||||
const key = this.getObjectKey(remotePath);
|
||||
const bucket = this.user.oss_bucket;
|
||||
const fileSize = fs.statSync(localPath).size;
|
||||
|
||||
// 创建文件读取流
|
||||
fileStream = fs.createReadStream(localPath);
|
||||
|
||||
// 使用 AWS SDK 的 Upload 类处理分片上传
|
||||
const upload = new Upload({
|
||||
client: this.s3Client,
|
||||
params: {
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
Body: fileStream
|
||||
},
|
||||
queueSize: 3, // 并发分片数
|
||||
partSize: 5 * 1024 * 1024 // 5MB 分片
|
||||
});
|
||||
|
||||
// 监听上传进度(可选)
|
||||
upload.on('httpUploadProgress', (progress) => {
|
||||
if (progress && progress.loaded && progress.total) {
|
||||
const percent = Math.round((progress.loaded / progress.total) * 100);
|
||||
// 只在较大文件时打印进度(避免日志过多)
|
||||
if (progress.total > 10 * 1024 * 1024 || percent % 20 === 0) {
|
||||
console.log(`[OSS存储] 上传进度: ${percent}% (${key})`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await upload.done();
|
||||
console.log(`[OSS存储] 上传成功: ${key} (${this.formatSize(fileSize)})`);
|
||||
|
||||
// 上传成功后,手动关闭流
|
||||
if (fileStream && !fileStream.destroyed) {
|
||||
fileStream.destroy();
|
||||
}
|
||||
} catch (error) {
|
||||
// 确保流被关闭,防止泄漏
|
||||
if (fileStream && !fileStream.destroyed) {
|
||||
fileStream.destroy();
|
||||
}
|
||||
|
||||
// 第三步:重命名临时文件为目标文件
|
||||
await this.sftp.rename(tempRemotePath, remotePath);
|
||||
console.error(`[OSS存储] 上传失败: ${remotePath}`, error.message);
|
||||
|
||||
// 判断错误类型并给出友好的错误信息
|
||||
if (error.name === 'NoSuchBucket') {
|
||||
throw new Error('OSS 存储桶不存在,请检查配置');
|
||||
} else if (error.name === 'AccessDenied') {
|
||||
throw new Error('OSS 访问被拒绝,请检查权限配置');
|
||||
} else if (error.name === 'EntityTooLarge') {
|
||||
throw new Error('文件过大,超过了 OSS 允许的最大大小');
|
||||
}
|
||||
throw new Error(`文件上传失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* 删除文件或文件夹
|
||||
*/
|
||||
async delete(filePath) {
|
||||
return await this.sftp.delete(filePath);
|
||||
try {
|
||||
const key = this.getObjectKey(filePath);
|
||||
const bucket = this.user.oss_bucket;
|
||||
|
||||
// 检查是文件还是目录(忽略不存在的文件)
|
||||
let statResult;
|
||||
try {
|
||||
statResult = await this.stat(filePath);
|
||||
} catch (statError) {
|
||||
if (statError.message && statError.message.includes('不存在')) {
|
||||
console.warn(`[OSS存储] 文件不存在,跳过删除: ${key}`);
|
||||
return; // 文件不存在,直接返回
|
||||
}
|
||||
throw statError; // 其他错误继续抛出
|
||||
}
|
||||
|
||||
if (statResult.isDirectory) {
|
||||
// 删除目录:列出所有对象并批量删除
|
||||
const listCommand = new ListObjectsV2Command({
|
||||
Bucket: bucket,
|
||||
Prefix: key
|
||||
});
|
||||
|
||||
const listResponse = await this.s3Client.send(listCommand);
|
||||
|
||||
if (listResponse.Contents && listResponse.Contents.length > 0) {
|
||||
// 分批删除(AWS S3 单次最多删除 1000 个对象)
|
||||
const deleteCommand = new DeleteObjectsCommand({
|
||||
Bucket: bucket,
|
||||
Delete: {
|
||||
Objects: listResponse.Contents.map(obj => ({ Key: obj.Key })),
|
||||
Quiet: false
|
||||
}
|
||||
});
|
||||
|
||||
const deleteResult = await this.s3Client.send(deleteCommand);
|
||||
|
||||
// 检查删除结果
|
||||
if (deleteResult.Errors && deleteResult.Errors.length > 0) {
|
||||
console.warn(`[OSS存储] 部分对象删除失败:`, deleteResult.Errors);
|
||||
}
|
||||
|
||||
console.log(`[OSS存储] 删除目录: ${key} (${listResponse.Contents.length} 个对象)`);
|
||||
}
|
||||
} else {
|
||||
// 删除单个文件
|
||||
const command = new DeleteObjectsCommand({
|
||||
Bucket: bucket,
|
||||
Delete: {
|
||||
Objects: [{ Key: key }],
|
||||
Quiet: false
|
||||
}
|
||||
});
|
||||
|
||||
await this.s3Client.send(command);
|
||||
console.log(`[OSS存储] 删除文件: ${key}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[OSS存储] 删除失败: ${filePath}`, error.message);
|
||||
|
||||
// 判断错误类型并给出友好的错误信息
|
||||
if (error.name === 'NoSuchBucket') {
|
||||
throw new Error('OSS 存储桶不存在,请检查配置');
|
||||
} else if (error.name === 'AccessDenied') {
|
||||
throw new Error('OSS 访问被拒绝,请检查权限配置');
|
||||
}
|
||||
throw new Error(`删除文件失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名文件
|
||||
* 重命名文件(OSS 不支持直接重命名,需要复制后删除)
|
||||
*/
|
||||
async rename(oldPath, newPath) {
|
||||
return await this.sftp.rename(oldPath, newPath);
|
||||
try {
|
||||
const oldKey = this.getObjectKey(oldPath);
|
||||
const newKey = this.getObjectKey(newPath);
|
||||
const bucket = this.user.oss_bucket;
|
||||
|
||||
// 先复制
|
||||
const copyCommand = new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: newKey,
|
||||
CopySource: `${bucket}/${oldKey}`
|
||||
});
|
||||
|
||||
await this.s3Client.send(copyCommand);
|
||||
|
||||
// 再删除原文件
|
||||
await this.delete(oldPath);
|
||||
|
||||
console.log(`[OSS存储] 重命名: ${oldKey} -> ${newKey}`);
|
||||
} catch (error) {
|
||||
console.error(`[OSS存储] 重命名失败: ${oldPath} -> ${newPath}`, error.message);
|
||||
|
||||
// 判断错误类型并给出友好的错误信息
|
||||
if (error.name === 'NoSuchBucket') {
|
||||
throw new Error('OSS 存储桶不存在,请检查配置');
|
||||
} else if (error.name === 'AccessDenied') {
|
||||
throw new Error('OSS 访问被拒绝,请检查权限配置');
|
||||
}
|
||||
throw new Error(`重命名文件失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件信息
|
||||
*/
|
||||
async stat(filePath) {
|
||||
return await this.sftp.stat(filePath);
|
||||
const key = this.getObjectKey(filePath);
|
||||
const bucket = this.user.oss_bucket;
|
||||
|
||||
try {
|
||||
const command = new HeadObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key
|
||||
});
|
||||
|
||||
const response = await this.s3Client.send(command);
|
||||
return {
|
||||
size: response.ContentLength || 0,
|
||||
modifyTime: response.LastModified ? response.LastModified.getTime() : Date.now(),
|
||||
isDirectory: false
|
||||
};
|
||||
} catch (error) {
|
||||
if (error.name === 'NotFound' || error.$metadata?.httpStatusCode === 404) {
|
||||
// 可能是目录,尝试列出前缀
|
||||
const listCommand = new ListObjectsV2Command({
|
||||
Bucket: bucket,
|
||||
Prefix: key.endsWith('/') ? key : key + '/',
|
||||
MaxKeys: 1
|
||||
});
|
||||
|
||||
try {
|
||||
const listResponse = await this.s3Client.send(listCommand);
|
||||
if (listResponse.Contents && listResponse.Contents.length > 0) {
|
||||
return { isDirectory: true, size: 0, modifyTime: Date.now() };
|
||||
}
|
||||
} catch (listError) {
|
||||
// 忽略列表错误
|
||||
}
|
||||
}
|
||||
throw new Error(`对象不存在: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文件读取流
|
||||
* 创建文件读取流(异步方法)
|
||||
* @returns {Promise<Readable>} 返回可读流 Promise
|
||||
*/
|
||||
createReadStream(filePath) {
|
||||
return this.sftp.createReadStream(filePath);
|
||||
async createReadStream(filePath) {
|
||||
const key = this.getObjectKey(filePath);
|
||||
const bucket = this.user.oss_bucket;
|
||||
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await this.s3Client.send(command);
|
||||
// AWS SDK v3 返回的 Body 是一个 IncomingMessage 类型的流
|
||||
return response.Body;
|
||||
} catch (error) {
|
||||
console.error(`[OSS存储] 创建读取流失败: ${key}`, error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭连接
|
||||
* 创建文件夹(通过创建空对象模拟)
|
||||
* OSS 中文件夹实际上是一个以斜杠结尾的空对象
|
||||
*/
|
||||
async mkdir(dirPath) {
|
||||
try {
|
||||
const key = this.getObjectKey(dirPath);
|
||||
const bucket = this.user.oss_bucket;
|
||||
|
||||
// OSS 中文件夹通过以斜杠结尾的空对象模拟
|
||||
const folderKey = key.endsWith('/') ? key : `${key}/`;
|
||||
|
||||
const { PutObjectCommand } = require('@aws-sdk/client-s3');
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: folderKey,
|
||||
Body: '', // 空内容
|
||||
ContentType: 'application/x-directory'
|
||||
});
|
||||
|
||||
await this.s3Client.send(command);
|
||||
console.log(`[OSS存储] 创建文件夹: ${folderKey}`);
|
||||
} catch (error) {
|
||||
console.error(`[OSS存储] 创建文件夹失败: ${dirPath}`, error.message);
|
||||
if (error.name === 'AccessDenied') {
|
||||
throw new Error('OSS 访问被拒绝,请检查权限配置');
|
||||
}
|
||||
throw new Error(`创建文件夹失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下载 URL(用于分享链接)
|
||||
*/
|
||||
getSignedUrl(filePath, expiresIn = 3600) {
|
||||
const key = this.getObjectKey(filePath);
|
||||
const bucket = this.user.oss_bucket;
|
||||
|
||||
// 简单的公开 URL 拼接(如果 bucket 是公共读)
|
||||
const endpoint = this.s3Client.config.endpoint;
|
||||
const region = this.s3Client.config.region;
|
||||
|
||||
let baseUrl;
|
||||
if (endpoint) {
|
||||
baseUrl = endpoint.href || endpoint.toString();
|
||||
} else if (this.user.oss_provider === 'aliyun') {
|
||||
baseUrl = `https://${bucket}.${this.user.oss_region || 'oss-cn-hangzhou'}.aliyuncs.com`;
|
||||
} else if (this.user.oss_provider === 'tencent') {
|
||||
baseUrl = `https://${bucket}.cos.${this.user.oss_region || 'ap-guangzhou'}.myqcloud.com`;
|
||||
} else {
|
||||
baseUrl = `https://${bucket}.s3.${region}.amazonaws.com`;
|
||||
}
|
||||
|
||||
return `${baseUrl}/${key}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭连接(S3Client 无需显式关闭)
|
||||
*/
|
||||
async end() {
|
||||
if (this.sftp) {
|
||||
await this.sftp.end();
|
||||
this.s3Client = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
*/
|
||||
formatSize(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
StorageInterface,
|
||||
LocalStorageClient,
|
||||
SftpStorageClient
|
||||
OssStorageClient
|
||||
};
|
||||
|
||||
@@ -1322,7 +1322,7 @@
|
||||
<button v-if="storageType === 'local'" class="btn btn-primary" @click="showCreateFolderModal = true">
|
||||
<i class="fas fa-folder-plus"></i> 新建文件夹
|
||||
</button>
|
||||
<!-- SFTP存储:显示下载上传工具按钮 -->
|
||||
<!-- OSS存储:显示下载上传工具按钮 -->
|
||||
<button v-else class="btn btn-primary" @click="downloadUploadTool" :disabled="downloadingTool">
|
||||
<i :class="downloadingTool ? 'fas fa-spinner fa-spin' : 'fas fa-download'"></i>
|
||||
{{ downloadingTool ? '生成中...' : '下载上传工具' }}
|
||||
@@ -1617,83 +1617,83 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SFTP 配置引导弹窗 -->
|
||||
<div v-if="showSftpGuideModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showSftpGuideModal')">
|
||||
<!-- OSS 配置引导弹窗 -->
|
||||
<div v-if="showOssGuideModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showOssGuideModal')">
|
||||
<div class="modal-content" @click.stop style="max-width: 520px; border-radius: 16px; overflow: hidden;">
|
||||
<div style="background: linear-gradient(135deg,#667eea,#764ba2); color: white; padding: 18px;">
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<i class="fas fa-server" style="font-size: 20px;"></i>
|
||||
<h3 style="margin: 0; font-size: 20px;">切换到 SFTP 存储</h3>
|
||||
<i class="fas fa-cloud" style="font-size: 20px;"></i>
|
||||
<h3 style="margin: 0; font-size: 20px;">切换到 OSS 存储</h3>
|
||||
</div>
|
||||
<p style="margin: 8px 0 0 0; opacity: 0.9; font-size: 14px;">先配置连接信息,再切换到你的专属 SFTP 空间。</p>
|
||||
<p style="margin: 8px 0 0 0; opacity: 0.9; font-size: 14px;">先配置云服务信息,再切换到你的专属 OSS 空间。</p>
|
||||
</div>
|
||||
<div style="padding: 18px;">
|
||||
<p style="color: var(--text-secondary); line-height: 1.6; margin-bottom: 16px;">
|
||||
我们会在你填写完成后再切换,确保过程平滑无干扰。
|
||||
支持阿里云 OSS、腾讯云 COS、AWS S3 等兼容 S3 协议的云存储服务。
|
||||
</p>
|
||||
<div style="display: flex; gap: 10px; justify-content: flex-end;">
|
||||
<button class="btn btn-secondary" @click="closeSftpGuideModal">稍后再说</button>
|
||||
<button class="btn btn-primary" @click="proceedSftpGuide">
|
||||
<i class="fas fa-tools"></i> 去配置 SFTP
|
||||
<button class="btn btn-secondary" @click="closeOssGuideModal">稍后再说</button>
|
||||
<button class="btn btn-primary" @click="proceedOssGuide">
|
||||
<i class="fas fa-tools"></i> 去配置 OSS
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SFTP 配置弹窗 -->
|
||||
<div v-if="showSftpConfigModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showSftpConfigModal')">
|
||||
<!-- OSS 配置弹窗 -->
|
||||
<div v-if="showOssConfigModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showOssConfigModal')">
|
||||
<div class="modal-content" @click.stop style="max-width: 720px; border-radius: 16px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
||||
<div>
|
||||
<h3 style="margin: 0 0 6px 0;">配置 SFTP 存储</h3>
|
||||
<p style="margin: 0; color: var(--text-muted); font-size: 13px;">填写连接信息或导入 .inf 配置,保存后即可切换到 SFTP 模式。</p>
|
||||
<h3 style="margin: 0 0 6px 0;">配置 OSS 存储</h3>
|
||||
<p style="margin: 0; color: var(--text-muted); font-size: 13px;">填写云服务配置信息,保存后即可切换到 OSS 模式。</p>
|
||||
</div>
|
||||
<button class="btn btn-secondary" style="padding: 6px 10px;" @click="closeSftpConfigModal">
|
||||
<button class="btn btn-secondary" style="padding: 6px 10px;" @click="closeOssConfigModal">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr; gap: 14px;">
|
||||
<div style="border: 1px dashed var(--glass-border); border-radius: 12px; padding: 16px; background: rgba(255,255,255,0.03); text-align: center; cursor: pointer; transition: all .3s;"
|
||||
@click="$refs.configFileInput.click()"
|
||||
@dragover.prevent="$event.currentTarget.style.background='#eef2ff'"
|
||||
@dragleave.prevent="$event.currentTarget.style.background='#f8fafc'"
|
||||
@drop.prevent="handleConfigFileDrop">
|
||||
<i class="fas fa-cloud-upload-alt" style="font-size: 36px; color: #667eea; margin-bottom: 8px;"></i>
|
||||
<div style="font-weight: 600; color: var(--text-primary);">导入配置文件</div>
|
||||
<div style="color: var(--text-muted); font-size: 13px; margin-top: 4px;">点击选择或拖拽 .inf 文件</div>
|
||||
<input type="file" accept=".inf" @change="handleConfigFileUpload" ref="configFileInput" style="display: none;">
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="updateFtpConfig" style="display: grid; gap: 12px;">
|
||||
<form @submit.prevent="updateOssConfig" style="display: grid; gap: 12px;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">主机地址</label>
|
||||
<input type="text" class="form-input" v-model="ftpConfigForm.ftp_host" required>
|
||||
<label class="form-label">云服务商</label>
|
||||
<select class="form-input" v-model="ossConfigForm.oss_provider" required style="cursor: pointer;">
|
||||
<option value="aliyun">阿里云 OSS</option>
|
||||
<option value="tencent">腾讯云 COS</option>
|
||||
<option value="aws">AWS S3</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">端口</label>
|
||||
<input type="number" class="form-input" v-model="ftpConfigForm.ftp_port" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">用户名</label>
|
||||
<input type="text" class="form-input" v-model="ftpConfigForm.ftp_user" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">密码 (留空保留现有密码)</label>
|
||||
<input type="password" class="form-input" v-model="ftpConfigForm.ftp_password" placeholder="留空保留现有密码">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">HTTP下载基础URL (可选)</label>
|
||||
<input type="text" class="form-input" v-model="ftpConfigForm.http_download_base_url" placeholder="例如: http://example.com/files">
|
||||
<label class="form-label">地域</label>
|
||||
<input type="text" class="form-input" v-model="ossConfigForm.oss_region" placeholder="如: oss-cn-hangzhou / ap-guangzhou / us-east-1" required>
|
||||
<small style="color: var(--text-secondary); font-size: 12px; margin-top: 5px; display: block;">
|
||||
配置后可通过 HTTP 直接下载,例如: 基础URL/文件路径。
|
||||
阿里云: oss-cn-hangzhou, 腾讯云: ap-guangzhou, AWS: us-east-1
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Access Key ID</label>
|
||||
<input type="text" class="form-input" v-model="ossConfigForm.oss_access_key_id" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Access Key Secret (留空保留现有密钥)</label>
|
||||
<input type="password" class="form-input" v-model="ossConfigForm.oss_access_key_secret" placeholder="留空保留现有密钥">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">存储桶名称</label>
|
||||
<input type="text" class="form-input" v-model="ossConfigForm.oss_bucket" placeholder="如: my-storage-bucket" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">自定义 Endpoint (可选)</label>
|
||||
<input type="text" class="form-input" v-model="ossConfigForm.oss_endpoint" placeholder="兼容 S3 的服务可填写自定义地址">
|
||||
<small style="color: var(--text-secondary); font-size: 12px; margin-top: 5px; display: block;">
|
||||
一般不需要填写,仅在使用自定义 S3 兼容服务时需要。
|
||||
</small>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 4px;">
|
||||
<button type="button" class="btn btn-secondary" @click="closeSftpConfigModal">取消</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> 保存配置
|
||||
<button type="button" class="btn btn-secondary" @click="closeOssConfigModal">取消</button>
|
||||
<button type="submit" class="btn btn-primary" :disabled="ossConfigSaving" :style="{ opacity: ossConfigSaving ? 0.7 : 1 }">
|
||||
<i class="fas" :class="ossConfigSaving ? 'fa-spinner fa-spin' : 'fa-save'"></i>
|
||||
{{ ossConfigSaving ? '保存中...' : '保存配置' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1730,9 +1730,9 @@
|
||||
</div>
|
||||
<div v-if="storageSwitching" style="color: #4b5fc9; font-weight: 600; display: inline-flex; align-items: center; gap: 8px;">
|
||||
<i class="fas fa-sync-alt fa-spin"></i>
|
||||
正在切换到 {{ storageSwitchTarget === 'sftp' ? 'SFTP 存储' : '本地存储' }}...
|
||||
正在切换到 {{ storageSwitchTarget === 'oss' ? 'OSS 存储' : '本地存储' }}...
|
||||
</div>
|
||||
<div v-else style="color: var(--text-secondary); font-size: 13px;">本地存储适合快速读写,SFTP 适合独立服务器空间</div>
|
||||
<div v-else style="color: var(--text-secondary); font-size: 13px;">本地存储适合快速读写,OSS 适合云存储扩展</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 16px; background: var(--bg-secondary); border-radius: 12px; padding: 12px; border: 1px solid var(--glass-border);">
|
||||
@@ -1782,53 +1782,53 @@
|
||||
<div style="background: var(--bg-secondary); border: 1px solid var(--glass-border); border-radius: 12px; padding: 16px; box-shadow: 0 6px 20px rgba(0,0,0,0.15); display: flex; flex-direction: column; height: 100%;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
<div style="font-weight: 700; color: var(--text-primary); display: flex; gap: 8px; align-items: center;">
|
||||
<i class="fas fa-server" style="color: var(--accent-1);"></i> SFTP 存储
|
||||
<i class="fas fa-cloud" style="color: var(--accent-1);"></i> OSS 存储
|
||||
</div>
|
||||
<span v-if="storageType === 'sftp'" style="font-size: 12px; color: var(--accent-1); background: rgba(102,126,234,0.15); padding: 4px 8px; border-radius: 999px;">当前</span>
|
||||
<span v-if="storageType === 'oss'" style="font-size: 12px; color: var(--accent-1); background: rgba(102,126,234,0.15); padding: 4px 8px; border-radius: 999px;">当前</span>
|
||||
</div>
|
||||
<div style="color: var(--text-secondary); font-size: 13px; margin-bottom: 10px;">使用你自己的服务器空间,独立存储更灵活。</div>
|
||||
<div v-if="user?.has_ftp_config" style="font-size: 13px; color: var(--text-primary); margin-bottom: 10px;">
|
||||
已配置: {{ user.ftp_host }}:{{ user.ftp_port }}
|
||||
<div style="color: var(--text-secondary); font-size: 13px; margin-bottom: 10px;">使用云存储服务,安全可靠扩展性强。</div>
|
||||
<div v-if="user?.has_oss_config" style="font-size: 13px; color: var(--text-primary); margin-bottom: 10px;">
|
||||
已配置: {{ user.oss_provider }} / {{ user.oss_bucket }}
|
||||
</div>
|
||||
<div v-else style="font-size: 13px; color: #f59e0b; background: rgba(245, 158, 11, 0.1); border: 1px dashed rgba(245,158,11,0.4); padding: 10px; border-radius: 8px; margin-bottom: 10px;">
|
||||
<i class="fas fa-exclamation-circle"></i> 先填写 SFTP 连接信息再切换
|
||||
<i class="fas fa-exclamation-circle"></i> 先填写 OSS 配置信息再切换
|
||||
</div>
|
||||
<!-- SFTP空间使用统计(user_choice模式) -->
|
||||
<div v-if="user?.has_ftp_config" style="margin-bottom: 10px; padding: 10px; background: rgba(255,255,255,0.03); border-radius: 8px; border: 1px solid var(--glass-border);">
|
||||
<!-- OSS空间使用统计(user_choice模式) -->
|
||||
<div v-if="user?.has_oss_config" style="margin-bottom: 10px; padding: 10px; background: rgba(255,255,255,0.03); border-radius: 8px; border: 1px solid var(--glass-border);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||||
<span style="font-size: 12px; color: var(--text-muted);">空间统计</span>
|
||||
<button
|
||||
style="background: none; border: none; color: #4b5fc9; cursor: pointer; font-size: 12px; padding: 2px 6px;"
|
||||
@click.stop="loadSftpUsage()"
|
||||
:disabled="sftpUsageLoading">
|
||||
<i :class="sftpUsageLoading ? 'fas fa-sync-alt fa-spin' : 'fas fa-sync-alt'"></i>
|
||||
@click.stop="loadOssUsage()"
|
||||
:disabled="ossUsageLoading">
|
||||
<i :class="ossUsageLoading ? 'fas fa-sync-alt fa-spin' : 'fas fa-sync-alt'"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="sftpUsageLoading && !sftpUsage" style="text-align: center; color: #667eea; font-size: 12px;">
|
||||
<div v-if="ossUsageLoading && !ossUsage" style="text-align: center; color: #667eea; font-size: 12px;">
|
||||
<i class="fas fa-spinner fa-spin"></i> 统计中...
|
||||
</div>
|
||||
<div v-else-if="sftpUsageError" style="font-size: 12px; color: #ef4444;">
|
||||
<i class="fas fa-exclamation-triangle"></i> {{ sftpUsageError }}
|
||||
<div v-else-if="ossUsageError" style="font-size: 12px; color: #ef4444;">
|
||||
<i class="fas fa-exclamation-triangle"></i> {{ ossUsageError }}
|
||||
</div>
|
||||
<div v-else-if="sftpUsage" style="font-size: 13px; font-weight: 600; color: #4b5fc9;">
|
||||
{{ sftpUsage.totalSizeFormatted }}
|
||||
<span style="font-weight: 400; color: var(--text-muted); font-size: 12px;">({{ sftpUsage.fileCount }} 文件)</span>
|
||||
<div v-else-if="ossUsage" style="font-size: 13px; font-weight: 600; color: #4b5fc9;">
|
||||
{{ ossUsage.totalSizeFormatted }}
|
||||
<span style="font-weight: 400; color: var(--text-muted); font-size: 12px;">({{ ossUsage.fileCount }} 文件)</span>
|
||||
</div>
|
||||
<div v-else style="font-size: 12px; color: var(--text-muted);">点击刷新查看</div>
|
||||
</div>
|
||||
<div style="margin-top: auto;">
|
||||
<button
|
||||
class="btn"
|
||||
:class="user?.has_ftp_config ? 'btn-primary' : 'btn-secondary'"
|
||||
:class="user?.has_oss_config ? 'btn-primary' : 'btn-secondary'"
|
||||
style="width: 100%; border-radius: 10px;"
|
||||
:disabled="storageType === 'sftp' || storageSwitching"
|
||||
@click="switchStorage('sftp')">
|
||||
:disabled="storageType === 'oss' || storageSwitching"
|
||||
@click="switchStorage('oss')">
|
||||
<i class="fas fa-random"></i>
|
||||
{{ user?.has_ftp_config ? '切到 SFTP 存储' : '去配置 SFTP' }}
|
||||
{{ user?.has_oss_config ? '切到 OSS 存储' : '去配置 OSS' }}
|
||||
</button>
|
||||
<div style="margin-top: 8px; text-align: center;">
|
||||
<a style="color: #4b5fc9; font-size: 13px; text-decoration: none; cursor: pointer;" @click.prevent="openSftpConfigModal">
|
||||
<i class="fas fa-tools"></i> 配置 / 修改 SFTP
|
||||
<a style="color: #4b5fc9; font-size: 13px; text-decoration: none; cursor: pointer;" @click.prevent="openOssConfigModal">
|
||||
<i class="fas fa-tools"></i> 配置 / 修改 OSS
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1837,7 +1837,7 @@
|
||||
|
||||
<div style="margin-top: 12px; padding: 10px 12px; background: rgba(255,255,255,0.05); border-radius: 10px; font-size: 13px; color: var(--text-secondary);">
|
||||
<i class="fas fa-info-circle" style="color: #4b5fc9;"></i>
|
||||
本地存储速度快但受配额限制;SFTP 需先配置连接,切换过程中可继续查看文件列表。
|
||||
本地存储速度快但受配额限制;OSS 支持多家云服务商,切换过程中可继续查看文件列表。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1872,44 +1872,44 @@
|
||||
|
||||
<div style="padding: 10px; background: rgba(59, 130, 246, 0.15); border-left: 4px solid #3b82f6; border-radius: 6px; font-size: 13px; color: #93c5fd;">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>说明:</strong> 管理员已将您的存储权限设置为"仅本地存储",您的文件存储在服务器本地,速度快但有配额限制。如需使用SFTP存储,请联系管理员修改权限设置。
|
||||
<strong>说明:</strong> 管理员已将您的存储权限设置为"仅本地存储",您的文件存储在服务器本地,速度快但有配额限制。如需使用OSS存储,请联系管理员修改权限设置。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SFTP 概览 / 配置入口 - 仅SFTP权限 -->
|
||||
<div v-if="user && !user.is_admin && storagePermission === 'sftp_only'" style="margin-bottom: 40px;">
|
||||
<!-- OSS 概览 / 配置入口 - 仅OSS权限 -->
|
||||
<div v-if="user && !user.is_admin && storagePermission === 'oss_only'" style="margin-bottom: 40px;">
|
||||
<h3 style="margin-bottom: 20px;">
|
||||
<i class="fas fa-server"></i> SFTP存储
|
||||
<i class="fas fa-cloud"></i> OSS存储
|
||||
</h3>
|
||||
<div style="background: rgba(255,255,255,0.03); padding: 20px; border-radius: 12px; border: 1px solid var(--glass-border);">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 12px;">
|
||||
<div>
|
||||
<div style="font-weight: 700; color: var(--text-primary); display: flex; gap: 8px; align-items: center;">
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
仅 SFTP 模式
|
||||
仅 OSS 模式
|
||||
</div>
|
||||
<div style="color: var(--text-secondary); font-size: 13px; margin-top: 6px;">
|
||||
{{ user.has_ftp_config ? '已配置服务器,可正常使用 SFTP 存储。' : '还未配置 SFTP,请先填写连接信息。' }}
|
||||
{{ user.has_oss_config ? '已配置云服务,可正常使用 OSS 存储。' : '还未配置 OSS,请先填写配置信息。' }}
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="openSftpConfigModal()" style="border-radius: 10px;">
|
||||
<i class="fas fa-tools"></i> 配置 / 修改 SFTP
|
||||
<button class="btn btn-primary" @click="openOssConfigModal()" style="border-radius: 10px;">
|
||||
<i class="fas fa-tools"></i> 配置 / 修改 OSS
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 服务器信息 -->
|
||||
<div v-if="user.has_ftp_config" style="margin-bottom: 12px; padding: 12px; background: var(--bg-secondary); border-radius: 10px; border: 1px solid var(--glass-border);">
|
||||
<!-- OSS服务器信息 -->
|
||||
<div v-if="user.has_oss_config" style="margin-bottom: 12px; padding: 12px; background: var(--bg-secondary); border-radius: 10px; border: 1px solid var(--glass-border);">
|
||||
<div style="font-weight: 600; color: var(--text-primary); margin-bottom: 8px;">
|
||||
<i class="fas fa-server" style="color: var(--accent-1);"></i> 服务器信息
|
||||
<i class="fas fa-cloud" style="color: var(--accent-1);"></i> 云服务信息
|
||||
</div>
|
||||
<div style="color: var(--text-secondary); font-size: 14px;">
|
||||
{{ user.ftp_host }}:{{ user.ftp_port }}
|
||||
{{ user.oss_provider }} / {{ user.oss_bucket }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SFTP空间使用统计 -->
|
||||
<div v-if="user.has_ftp_config" style="margin-bottom: 12px; padding: 12px; background: var(--bg-secondary); border-radius: 10px; border: 1px solid var(--glass-border);">
|
||||
<!-- OSS空间使用统计 -->
|
||||
<div v-if="user.has_oss_config" style="margin-bottom: 12px; padding: 12px; background: var(--bg-secondary); border-radius: 10px; border: 1px solid var(--glass-border);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
<div style="font-weight: 600; color: var(--text-primary);">
|
||||
<i class="fas fa-chart-pie" style="color: #667eea;"></i> 空间使用统计
|
||||
@@ -1917,51 +1917,47 @@
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
style="padding: 4px 10px; font-size: 12px; border-radius: 6px;"
|
||||
@click="loadSftpUsage()"
|
||||
:disabled="sftpUsageLoading">
|
||||
<i :class="sftpUsageLoading ? 'fas fa-sync-alt fa-spin' : 'fas fa-sync-alt'"></i>
|
||||
{{ sftpUsageLoading ? '统计中...' : '刷新' }}
|
||||
@click="loadOssUsage()"
|
||||
:disabled="ossUsageLoading">
|
||||
<i :class="ossUsageLoading ? 'fas fa-sync-alt fa-spin' : 'fas fa-sync-alt'"></i>
|
||||
{{ ossUsageLoading ? '统计中...' : '刷新' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<div v-if="sftpUsageLoading && !sftpUsage" style="text-align: center; padding: 20px; color: #667eea;">
|
||||
<div v-if="ossUsageLoading && !ossUsage" style="text-align: center; padding: 20px; color: #667eea;">
|
||||
<i class="fas fa-spinner fa-spin" style="font-size: 24px;"></i>
|
||||
<div style="margin-top: 8px; font-size: 13px;">正在统计 SFTP 空间使用情况...</div>
|
||||
<div style="margin-top: 8px; font-size: 13px;">正在统计 OSS 空间使用情况...</div>
|
||||
<div style="margin-top: 4px; font-size: 12px; color: var(--text-muted);">(文件较多时可能需要一些时间)</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div v-else-if="sftpUsageError" style="padding: 12px; background: rgba(239, 68, 68, 0.1); border-radius: 8px; color: #ef4444; font-size: 13px;">
|
||||
<i class="fas fa-exclamation-triangle"></i> {{ sftpUsageError }}
|
||||
<div v-else-if="ossUsageError" style="padding: 12px; background: rgba(239, 68, 68, 0.1); border-radius: 8px; color: #ef4444; font-size: 13px;">
|
||||
<i class="fas fa-exclamation-triangle"></i> {{ ossUsageError }}
|
||||
</div>
|
||||
|
||||
<!-- 统计结果 -->
|
||||
<div v-else-if="sftpUsage" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px;">
|
||||
<div v-else-if="ossUsage" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px;">
|
||||
<div style="text-align: center; padding: 12px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 10px; color: white;">
|
||||
<div style="font-size: 20px; font-weight: 700;">{{ sftpUsage.totalSizeFormatted }}</div>
|
||||
<div style="font-size: 20px; font-weight: 700;">{{ ossUsage.totalSizeFormatted }}</div>
|
||||
<div style="font-size: 12px; opacity: 0.9; margin-top: 4px;">总使用空间</div>
|
||||
</div>
|
||||
<div style="text-align: center; padding: 12px; background: rgba(59, 130, 246, 0.1); border-radius: 10px; border: 1px solid rgba(59,130,246,0.2);">
|
||||
<div style="font-size: 20px; font-weight: 700; color: #3b82f6;">{{ sftpUsage.fileCount }}</div>
|
||||
<div style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">文件数</div>
|
||||
</div>
|
||||
<div style="text-align: center; padding: 12px; background: rgba(245, 158, 11, 0.1); border-radius: 10px; border: 1px solid rgba(245,158,11,0.3);">
|
||||
<div style="font-size: 20px; font-weight: 700; color: #f59e0b;">{{ sftpUsage.dirCount }}</div>
|
||||
<div style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">文件夹数</div>
|
||||
<div style="font-size: 20px; font-weight: 700; color: #3b82f6;">{{ ossUsage.fileCount }}</div>
|
||||
<div style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">对象数</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 未统计提示 -->
|
||||
<div v-else style="text-align: center; padding: 16px; color: var(--text-muted); font-size: 13px;">
|
||||
<i class="fas fa-database" style="font-size: 24px; color: var(--text-muted); margin-bottom: 8px; display: block;"></i>
|
||||
点击"刷新"按钮统计 SFTP 空间使用情况
|
||||
点击"刷新"按钮统计 OSS 空间使用情况
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 10px; background: rgba(102, 126, 234, 0.1); border-radius: 10px; color: var(--text-secondary); font-size: 13px;">
|
||||
<i class="fas fa-info-circle" style="color: #4b5fc9;"></i>
|
||||
数据存储在你的 SFTP 服务器上,如需切换回本地请联系管理员调整权限。
|
||||
数据存储在云服务上,安全可靠扩展性强。如需切换回本地请联系管理员调整权限。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2701,7 +2697,7 @@
|
||||
<td style="padding: 10px; font-size: 12px; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" :title="u.email">{{ u.email }}</td>
|
||||
<td style="padding: 10px; text-align: center; font-size: 12px;">
|
||||
<span v-if="u.storage_permission === 'local_only'" style="background: #667eea; color: white; padding: 3px 8px; border-radius: 4px;">仅本地</span>
|
||||
<span v-else-if="u.storage_permission === 'sftp_only'" style="background: #6c757d; color: white; padding: 3px 8px; border-radius: 4px;">仅SFTP</span>
|
||||
<span v-else-if="u.storage_permission === 'oss_only'" style="background: #6c757d; color: white; padding: 3px 8px; border-radius: 4px;">仅OSS</span>
|
||||
<span v-else style="background: #22c55e; color: white; padding: 3px 8px; border-radius: 4px;">用户选择</span>
|
||||
</td>
|
||||
<td style="padding: 10px; text-align: center; font-size: 12px;">
|
||||
@@ -2709,7 +2705,7 @@
|
||||
<i class="fas fa-hard-drive"></i> 本地
|
||||
</span>
|
||||
<span v-else style="color: #6c757d;">
|
||||
<i class="fas fa-server"></i> SFTP
|
||||
<i class="fas fa-cloud"></i> OSS
|
||||
</span>
|
||||
</td>
|
||||
<td style="padding: 10px; text-align: center; font-size: 12px;">
|
||||
@@ -2737,7 +2733,7 @@
|
||||
<button v-else class="btn" style="background: #22c55e; color: white; font-size: 11px; padding: 5px 10px;" @click="banUser(u.id, false)">
|
||||
<i class="fas fa-check"></i> 解封
|
||||
</button>
|
||||
<button v-if="u.has_ftp_config" class="btn" style="background: #3b82f6; color: white; font-size: 11px; padding: 5px 10px;" @click="openFileInspection(u)">
|
||||
<button v-if="u.has_oss_config" class="btn" style="background: #3b82f6; color: white; font-size: 11px; padding: 5px 10px;" @click="openFileInspection(u)">
|
||||
<i class="fas fa-folder-open"></i> 文件
|
||||
</button>
|
||||
<button class="btn" style="background: #ef4444; color: white; font-size: 11px; padding: 5px 10px;" @click="deleteUser(u.id)">
|
||||
@@ -3066,11 +3062,11 @@
|
||||
<label class="form-label">存储权限</label>
|
||||
<select class="form-input" v-model="editStorageForm.storage_permission">
|
||||
<option value="local_only">仅本地存储</option>
|
||||
<option value="sftp_only">仅SFTP存储</option>
|
||||
<option value="oss_only">仅OSS存储</option>
|
||||
<option value="user_choice">用户选择</option>
|
||||
</select>
|
||||
<small style="color: var(--text-secondary); font-size: 12px; margin-top: 5px; display: block;">
|
||||
仅本地:用户只能使用本地存储 | 仅SFTP:用户只能使用SFTP | 用户选择:用户可自由切换
|
||||
仅本地:用户只能使用本地存储 | 仅OSS:用户只能使用OSS | 用户选择:用户可自由切换
|
||||
</small>
|
||||
</div>
|
||||
|
||||
@@ -3094,7 +3090,7 @@
|
||||
• 默认配额: 1GB<br>
|
||||
• 当前配额: {{ editStorageForm.local_storage_quota_value }} {{ editStorageForm.quota_unit }}
|
||||
({{ editStorageForm.quota_unit === 'GB' ? (editStorageForm.local_storage_quota_value * 1024).toFixed(0) : editStorageForm.local_storage_quota_value }} MB)<br>
|
||||
• 配额仅影响本地存储,SFTP存储不受此限制
|
||||
• 配额仅影响本地存储,OSS存储不受此限制
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
542
frontend/app.js
542
frontend/app.js
@@ -48,15 +48,17 @@ createApp({
|
||||
showCaptcha: false,
|
||||
captchaUrl: '',
|
||||
|
||||
// SFTP配置表单
|
||||
ftpConfigForm: {
|
||||
ftp_host: '',
|
||||
ftp_port: 22,
|
||||
ftp_user: '',
|
||||
ftp_password: '',
|
||||
http_download_base_url: ''
|
||||
// OSS配置表单
|
||||
ossConfigForm: {
|
||||
oss_provider: 'aliyun',
|
||||
oss_region: '',
|
||||
oss_access_key_id: '',
|
||||
oss_access_key_secret: '',
|
||||
oss_bucket: '',
|
||||
oss_endpoint: ''
|
||||
},
|
||||
showFtpConfigModal: false,
|
||||
showOssConfigModal: false,
|
||||
ossConfigSaving: false, // OSS 配置保存中状态
|
||||
|
||||
// 修改密码表单
|
||||
changePasswordForm: {
|
||||
@@ -211,8 +213,8 @@ createApp({
|
||||
verifyMessage: '',
|
||||
|
||||
// 存储相关
|
||||
storageType: 'sftp', // 当前使用的存储类型
|
||||
storagePermission: 'sftp_only', // 存储权限
|
||||
storageType: 'oss', // 当前使用的存储类型
|
||||
storagePermission: 'oss_only', // 存储权限
|
||||
localQuota: 0, // 本地存储配额(字节)
|
||||
localUsed: 0, // 本地存储已使用(字节)
|
||||
|
||||
@@ -242,7 +244,7 @@ createApp({
|
||||
editStorageForm: {
|
||||
userId: null,
|
||||
username: '',
|
||||
storage_permission: 'sftp_only',
|
||||
storage_permission: 'oss_only',
|
||||
local_storage_quota_value: 1, // 配额数值
|
||||
quota_unit: 'GB' // 配额单位:MB 或 GB
|
||||
},
|
||||
@@ -271,14 +273,14 @@ createApp({
|
||||
suppressStorageToast: false,
|
||||
profileInitialized: false,
|
||||
|
||||
// SFTP配置引导弹窗
|
||||
showSftpGuideModal: false,
|
||||
showSftpConfigModal: false,
|
||||
// OSS配置引导弹窗
|
||||
showOssGuideModal: false,
|
||||
showOssConfigModal: false,
|
||||
|
||||
// SFTP空间使用统计
|
||||
sftpUsage: null, // { totalSize, totalSizeFormatted, fileCount, dirCount }
|
||||
sftpUsageLoading: false,
|
||||
sftpUsageError: null,
|
||||
// OSS空间使用统计
|
||||
ossUsage: null, // { totalSize, totalSizeFormatted, fileCount, dirCount }
|
||||
ossUsageLoading: false,
|
||||
ossUsageError: null,
|
||||
|
||||
// 主题设置
|
||||
currentTheme: 'dark', // 当前生效的主题: 'dark' 或 'light'
|
||||
@@ -309,7 +311,7 @@ createApp({
|
||||
|
||||
// 存储类型显示文本
|
||||
storageTypeText() {
|
||||
return this.storageType === 'local' ? '本地存储' : 'SFTP存储';
|
||||
return this.storageType === 'local' ? '本地存储' : 'OSS存储';
|
||||
},
|
||||
|
||||
// 分享筛选+排序后的列表
|
||||
@@ -613,17 +615,17 @@ handleDragLeave(e) {
|
||||
this.startTokenRefresh(expiresIn);
|
||||
|
||||
// 直接从登录响应中获取存储信息
|
||||
this.storagePermission = this.user.storage_permission || 'sftp_only';
|
||||
this.storageType = this.user.current_storage_type || 'sftp';
|
||||
this.storagePermission = this.user.storage_permission || 'oss_only';
|
||||
this.storageType = this.user.current_storage_type || 'oss';
|
||||
this.localQuota = this.user.local_storage_quota || 0;
|
||||
this.localUsed = this.user.local_storage_used || 0;
|
||||
|
||||
console.log('[登录] 存储权限:', this.storagePermission, '存储类型:', this.storageType, 'SFTP配置:', this.user.has_ftp_config);
|
||||
console.log('[登录] 存储权限:', this.storagePermission, '存储类型:', this.storageType, 'OSS配置:', this.user.has_oss_config);
|
||||
|
||||
// 智能存储类型修正:如果当前是SFTP但未配置,且用户有本地存储权限,自动切换到本地
|
||||
if (this.storageType === 'sftp' && !this.user.has_ftp_config) {
|
||||
// 智能存储类型修正:如果当前是OSS但未配置,且用户有本地存储权限,自动切换到本地
|
||||
if (this.storageType === 'oss' && !this.user.has_oss_config) {
|
||||
if (this.storagePermission === 'local_only' || this.storagePermission === 'user_choice') {
|
||||
console.log('[登录] SFTP未配置但用户有本地存储权限,自动切换到本地存储');
|
||||
console.log('[登录] OSS未配置但用户有本地存储权限,自动切换到本地存储');
|
||||
this.storageType = 'local';
|
||||
// 异步更新到后端(不等待,避免阻塞登录流程)
|
||||
axios.post(`${this.apiBase}/api/user/switch-storage`, { storage_type: 'local' })
|
||||
@@ -646,15 +648,15 @@ handleDragLeave(e) {
|
||||
this.currentView = 'files';
|
||||
this.loadFiles('/');
|
||||
}
|
||||
// 如果仅SFTP模式,需要检查是否配置了SFTP
|
||||
else if (this.storagePermission === 'sftp_only') {
|
||||
if (this.user.has_ftp_config) {
|
||||
// 如果仅OSS模式,需要检查是否配置了OSS
|
||||
else if (this.storagePermission === 'oss_only') {
|
||||
if (this.user.has_oss_config) {
|
||||
this.currentView = 'files';
|
||||
this.loadFiles('/');
|
||||
} else {
|
||||
this.currentView = 'settings';
|
||||
alert('欢迎!请先配置您的SFTP服务器');
|
||||
this.openSftpConfigModal();
|
||||
alert('欢迎!请先配置您的OSS服务');
|
||||
this.openOssConfigModal();
|
||||
}
|
||||
} else {
|
||||
// 默认行为:跳转到文件页面
|
||||
@@ -808,44 +810,56 @@ handleDragLeave(e) {
|
||||
}
|
||||
},
|
||||
|
||||
async updateFtpConfig() {
|
||||
async updateOssConfig() {
|
||||
// 防止重复提交
|
||||
if (this.ossConfigSaving) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ossConfigSaving = true;
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${this.apiBase}/api/user/update-ftp`,
|
||||
this.ftpConfigForm,
|
||||
`${this.apiBase}/api/user/update-oss`,
|
||||
this.ossConfigForm,
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
alert('SFTP配置已保存!');
|
||||
// 更新用户信息
|
||||
this.user.has_ftp_config = 1;
|
||||
this.user.has_oss_config = 1;
|
||||
|
||||
// 如果用户有 user_choice 权限,自动切换到 SFTP 存储
|
||||
if (this.storagePermission === 'user_choice' || this.storagePermission === 'sftp_only') {
|
||||
// 如果用户有 user_choice 权限,自动切换到 OSS 存储
|
||||
if (this.storagePermission === 'user_choice' || this.storagePermission === 'oss_only') {
|
||||
try {
|
||||
const switchResponse = await axios.post(
|
||||
`${this.apiBase}/api/user/switch-storage`,
|
||||
{ storage_type: 'sftp' },
|
||||
{ storage_type: 'oss' },
|
||||
);
|
||||
|
||||
if (switchResponse.data.success) {
|
||||
this.storageType = 'sftp';
|
||||
console.log('[SFTP配置] 已自动切换到SFTP存储模式');
|
||||
this.storageType = 'oss';
|
||||
console.log('[OSS配置] 已自动切换到OSS存储模式');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[SFTP配置] 自动切换存储模式失败:', err);
|
||||
console.error('[OSS配置] 自动切换存储模式失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭配置弹窗
|
||||
this.showSftpConfigModal = false;
|
||||
this.showOssConfigModal = false;
|
||||
|
||||
// 刷新到文件页面
|
||||
this.currentView = 'files';
|
||||
this.loadFiles('/');
|
||||
|
||||
// 显示成功提示
|
||||
this.showToast('success', '配置成功', 'OSS存储配置已保存!');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('配置失败: ' + (error.response?.data?.message || error.message));
|
||||
console.error('OSS配置保存失败:', error);
|
||||
this.showToast('error', '配置失败', error.response?.data?.message || error.message || '请检查配置信息后重试');
|
||||
} finally {
|
||||
this.ossConfigSaving = false;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -905,7 +919,7 @@ handleDragLeave(e) {
|
||||
}
|
||||
},
|
||||
|
||||
async loadFtpConfig() {
|
||||
async loadOssConfig() {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${this.apiBase}/api/user/profile`,
|
||||
@@ -913,105 +927,20 @@ handleDragLeave(e) {
|
||||
|
||||
if (response.data.success && response.data.user) {
|
||||
const user = response.data.user;
|
||||
// 填充SFTP配置表单(密码不回显)
|
||||
this.ftpConfigForm.ftp_host = user.ftp_host || '';
|
||||
this.ftpConfigForm.ftp_port = user.ftp_port || 22;
|
||||
this.ftpConfigForm.ftp_user = user.ftp_user || '';
|
||||
this.ftpConfigForm.ftp_password = ''; // 密码不回显
|
||||
this.ftpConfigForm.http_download_base_url = user.http_download_base_url || '';
|
||||
// 填充OSS配置表单(密钥不回显)
|
||||
this.ossConfigForm.oss_provider = user.oss_provider || 'aliyun';
|
||||
this.ossConfigForm.oss_region = user.oss_region || '';
|
||||
this.ossConfigForm.oss_access_key_id = user.oss_access_key_id || '';
|
||||
this.ossConfigForm.oss_access_key_secret = ''; // 密钥不回显
|
||||
this.ossConfigForm.oss_bucket = user.oss_bucket || '';
|
||||
this.ossConfigForm.oss_endpoint = user.oss_endpoint || '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载SFTP配置失败:', error);
|
||||
console.error('加载OSS配置失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 处理配置文件上传
|
||||
handleConfigFileUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
this.processConfigFile(file);
|
||||
|
||||
// 清空文件选择,允许重复选择同一文件
|
||||
event.target.value = '';
|
||||
},
|
||||
|
||||
// 处理配置文件拖拽
|
||||
handleConfigFileDrop(event) {
|
||||
const file = event.dataTransfer.files[0];
|
||||
if (!file) return;
|
||||
|
||||
// 检查文件扩展名
|
||||
if (!file.name.toLowerCase().endsWith('.inf')) {
|
||||
this.showToast('error', '错误', '只支持 .inf 格式的配置文件');
|
||||
return;
|
||||
}
|
||||
|
||||
this.processConfigFile(file);
|
||||
|
||||
// 恢复背景色
|
||||
event.currentTarget.style.background = '#f8f9ff';
|
||||
},
|
||||
|
||||
// 处理配置文件
|
||||
async processConfigFile(file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
try {
|
||||
const content = e.target.result;
|
||||
const config = this.parseConfigFile(content);
|
||||
|
||||
if (config) {
|
||||
// 填充表单
|
||||
this.ftpConfigForm.ftp_host = config.ip || '';
|
||||
this.ftpConfigForm.ftp_port = config.port || 22;
|
||||
this.ftpConfigForm.ftp_user = config.id || '';
|
||||
this.ftpConfigForm.ftp_password = config.pw || '';
|
||||
this.ftpConfigForm.http_download_base_url = config.arr || '';
|
||||
|
||||
// 提示用户配置已导入,需要确认后保存
|
||||
this.showToast('success', '成功', '配置文件已导入!请检查并确认信息后点击"保存配置"按钮');
|
||||
} else {
|
||||
this.showToast('error', '错误', '配置文件格式不正确,请检查文件内容');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析配置文件失败:', error);
|
||||
this.showToast('error', '错误', '解析配置文件失败: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
},
|
||||
|
||||
// 解析INI格式的配置文件
|
||||
parseConfigFile(content) {
|
||||
const lines = content.split('\n');
|
||||
const config = {};
|
||||
|
||||
for (let line of lines) {
|
||||
line = line.trim();
|
||||
|
||||
// 跳过空行和注释
|
||||
if (!line || line.startsWith('#') || line.startsWith(';') || line.startsWith('[')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 解析 key=value 格式
|
||||
const equalsIndex = line.indexOf('=');
|
||||
if (equalsIndex > 0) {
|
||||
const key = line.substring(0, equalsIndex).trim();
|
||||
const value = line.substring(equalsIndex + 1).trim();
|
||||
config[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 验证必需字段
|
||||
if (config.ip && config.id && config.pw && config.port) {
|
||||
return config;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
// 上传工具配置引导已移除(OSS 不需要配置文件导入)
|
||||
|
||||
async updateUsername() {
|
||||
if (!this.usernameForm.newUsername || this.usernameForm.newUsername.length < 3) {
|
||||
@@ -1107,8 +1036,8 @@ handleDragLeave(e) {
|
||||
localStorage.setItem('user', JSON.stringify(this.user));
|
||||
|
||||
// 从最新的用户信息初始化存储相关字段
|
||||
this.storagePermission = this.user.storage_permission || 'sftp_only';
|
||||
this.storageType = this.user.current_storage_type || 'sftp';
|
||||
this.storagePermission = this.user.storage_permission || 'oss_only';
|
||||
this.storageType = this.user.current_storage_type || 'oss';
|
||||
this.localQuota = this.user.local_storage_quota || 0;
|
||||
this.localUsed = this.user.local_storage_used || 0;
|
||||
|
||||
@@ -1129,7 +1058,7 @@ handleDragLeave(e) {
|
||||
targetView = savedView;
|
||||
} else if (this.user.is_admin) {
|
||||
targetView = 'admin';
|
||||
} else if (this.storagePermission === 'sftp_only' && !this.user.has_ftp_config) {
|
||||
} else if (this.storagePermission === 'oss_only' && !this.user.has_oss_config) {
|
||||
targetView = 'settings';
|
||||
} else {
|
||||
targetView = 'files';
|
||||
@@ -1311,17 +1240,42 @@ handleDragLeave(e) {
|
||||
downloadFile(file) {
|
||||
console.log("[DEBUG] 下载文件:", file);
|
||||
|
||||
// SFTP存储且有HTTP直链,新窗口打开直接下载(避免Mixed Content问题)
|
||||
if (file.httpDownloadUrl) {
|
||||
window.open(file.httpDownloadUrl, '_blank');
|
||||
return;
|
||||
}
|
||||
// 构建文件路径
|
||||
const filePath = this.currentPath === '/' ? `/${file.name}` : `${this.currentPath}/${file.name}`;
|
||||
|
||||
// 本地存储,使用隐藏链接触发下载
|
||||
const url = `${this.apiBase}/api/files/download?path=${encodeURIComponent(this.currentPath === '/' ? `/${file.name}` : `${this.currentPath}/${file.name}`)}`;
|
||||
// OSS 模式:使用签名 URL 直连下载(不经过后端)
|
||||
if (this.storageType === 'oss' && this.user?.has_oss_config) {
|
||||
this.downloadFromOSS(filePath);
|
||||
} else {
|
||||
// 本地存储模式:通过后端下载
|
||||
this.downloadFromLocal(filePath);
|
||||
}
|
||||
},
|
||||
|
||||
// OSS 直连下载
|
||||
async downloadFromOSS(filePath) {
|
||||
try {
|
||||
// 获取签名 URL
|
||||
const { data } = await axios.get(`${this.apiBase}/api/files/download-url`, {
|
||||
params: { path: filePath }
|
||||
});
|
||||
|
||||
if (data.success) {
|
||||
// 直连 OSS 下载
|
||||
window.open(data.downloadUrl, '_blank');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取下载链接失败:', error);
|
||||
this.showToast('error', '错误', '获取下载链接失败');
|
||||
}
|
||||
},
|
||||
|
||||
// 本地存储下载
|
||||
downloadFromLocal(filePath) {
|
||||
const url = `${this.apiBase}/api/files/download?path=${encodeURIComponent(filePath)}`;
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', file.name);
|
||||
link.setAttribute('download', '');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
@@ -1385,6 +1339,7 @@ handleDragLeave(e) {
|
||||
this.showCreateFolderModal = false;
|
||||
this.createFolderForm.folderName = '';
|
||||
await this.loadFiles(this.currentPath); // 刷新文件列表
|
||||
await this.refreshStorageUsage(); // 刷新空间统计(OSS会增加空对象)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[创建文件夹失败]', error);
|
||||
@@ -1540,18 +1495,26 @@ handleDragLeave(e) {
|
||||
|
||||
// ===== 媒体预览功能 =====
|
||||
|
||||
// 获取媒体文件URL
|
||||
getMediaUrl(file) {
|
||||
// 获取媒体文件URL(OSS直连或后端代理)
|
||||
async getMediaUrl(file) {
|
||||
const filePath = this.currentPath === '/'
|
||||
? `/${file.name}`
|
||||
: `${this.currentPath}/${file.name}`;
|
||||
|
||||
// SFTP存储且配置了HTTP下载URL,使用HTTP直接访问;否则使用API下载
|
||||
if (file.httpDownloadUrl) {
|
||||
return file.httpDownloadUrl;
|
||||
// OSS 模式:返回签名 URL(用于媒体预览)
|
||||
if (this.storageType === 'oss' && this.user?.has_oss_config) {
|
||||
try {
|
||||
const { data } = await axios.get(`${this.apiBase}/api/files/download-url`, {
|
||||
params: { path: filePath }
|
||||
});
|
||||
return data.success ? data.downloadUrl : null;
|
||||
} catch (error) {
|
||||
console.error('获取媒体URL失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 本地存储或未配置HTTP URL,使用API下载(同域 Cookie 验证)
|
||||
// 本地存储模式:通过后端 API
|
||||
return `${this.apiBase}/api/files/download?path=${encodeURIComponent(filePath)}`;
|
||||
},
|
||||
|
||||
@@ -1634,7 +1597,9 @@ handleDragLeave(e) {
|
||||
|
||||
if (response.data.success) {
|
||||
this.showToast('success', '成功', '文件已删除');
|
||||
this.loadFiles(this.currentPath);
|
||||
// 刷新文件列表和空间统计
|
||||
await this.loadFiles(this.currentPath);
|
||||
await this.refreshStorageUsage();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
@@ -1764,7 +1729,7 @@ handleDragLeave(e) {
|
||||
},
|
||||
|
||||
async uploadFile(file) {
|
||||
// 文件大小限制预检查(在上传前检查,避免用户等待上传完才发现超限)
|
||||
// 文件大小限制预检查
|
||||
if (file.size > this.maxUploadSize) {
|
||||
const fileSizeMB = Math.round(file.size / (1024 * 1024));
|
||||
const maxSizeMB = Math.round(this.maxUploadSize / (1024 * 1024));
|
||||
@@ -1776,63 +1741,19 @@ handleDragLeave(e) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 本地存储配额预检查
|
||||
if (this.storageType === 'local') {
|
||||
const estimatedUsage = this.localUsed + file.size;
|
||||
if (estimatedUsage > this.localQuota) {
|
||||
this.showToast(
|
||||
'error',
|
||||
'配额不足',
|
||||
`文件大小 ${this.formatBytes(file.size)},剩余配额 ${this.formatBytes(this.localQuota - this.localUsed)},无法上传`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果使用率将超过90%,给出警告
|
||||
const willExceed90 = (estimatedUsage / this.localQuota) > 0.9;
|
||||
if (willExceed90) {
|
||||
const confirmed = confirm(
|
||||
`警告:上传此文件后将使用 ${Math.round((estimatedUsage / this.localQuota) * 100)}% 的配额。是否继续?`
|
||||
);
|
||||
if (!confirmed) return;
|
||||
}
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('path', this.currentPath);
|
||||
|
||||
try {
|
||||
// 设置上传文件名和进度
|
||||
// 设置上传状态
|
||||
this.uploadingFileName = file.name;
|
||||
this.uploadProgress = 0;
|
||||
this.uploadedBytes = 0;
|
||||
this.totalBytes = 0;
|
||||
this.totalBytes = file.size;
|
||||
|
||||
const response = await axios.post(`${this.apiBase}/api/upload`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
timeout: 30 * 60 * 1000, // 30分钟超时,支持大文件上传
|
||||
onUploadProgress: (progressEvent) => {
|
||||
this.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||
this.uploadedBytes = progressEvent.loaded;
|
||||
this.totalBytes = progressEvent.total;
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
// 显示成功提示
|
||||
this.showToast('success', '上传成功', `文件 ${file.name} 已上传`);
|
||||
|
||||
// 重置上传进度
|
||||
this.uploadProgress = 0;
|
||||
this.uploadedBytes = 0;
|
||||
this.totalBytes = 0;
|
||||
this.uploadingFileName = '';
|
||||
|
||||
// 自动刷新文件列表
|
||||
await this.loadFiles(this.currentPath);
|
||||
try {
|
||||
if (this.storageType === 'oss' && this.user?.has_oss_config) {
|
||||
// ===== OSS 直连上传(不经过后端) =====
|
||||
await this.uploadToOSSDirect(file);
|
||||
} else {
|
||||
// ===== 本地存储上传(经过后端) =====
|
||||
await this.uploadToLocal(file);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error);
|
||||
@@ -1843,32 +1764,106 @@ handleDragLeave(e) {
|
||||
this.totalBytes = 0;
|
||||
this.uploadingFileName = '';
|
||||
|
||||
// 处理文件大小超限错误
|
||||
if (error.response?.status === 413) {
|
||||
const errorData = error.response.data;
|
||||
this.showToast('error', '上传失败', error.message || '上传失败,请重试');
|
||||
}
|
||||
},
|
||||
|
||||
// 判断响应是JSON还是HTML(Nginx返回HTML,Backend返回JSON)
|
||||
if (typeof errorData === 'object' && errorData.maxSize && errorData.fileSize) {
|
||||
// Backend返回的JSON响应
|
||||
const maxSizeMB = Math.round(errorData.maxSize / (1024 * 1024));
|
||||
const fileSizeMB = Math.round(errorData.fileSize / (1024 * 1024));
|
||||
// OSS 直连上传
|
||||
async uploadToOSSDirect(file) {
|
||||
try {
|
||||
// 1. 获取签名 URL
|
||||
const { data: signData } = await axios.get(`${this.apiBase}/api/files/upload-signature`, {
|
||||
params: {
|
||||
filename: file.name,
|
||||
contentType: file.type || 'application/octet-stream'
|
||||
}
|
||||
});
|
||||
|
||||
if (!signData.success) {
|
||||
throw new Error(signData.message || '获取上传签名失败');
|
||||
}
|
||||
|
||||
// 2. 直连 OSS 上传(不经过后端!)
|
||||
await axios.put(signData.uploadUrl, file, {
|
||||
headers: {
|
||||
'Content-Type': file.type || 'application/octet-stream'
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
this.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||
this.uploadedBytes = progressEvent.loaded;
|
||||
this.totalBytes = progressEvent.total;
|
||||
},
|
||||
timeout: 30 * 60 * 1000 // 30分钟超时
|
||||
});
|
||||
|
||||
// 3. 通知后端上传完成
|
||||
await axios.post(`${this.apiBase}/api/files/upload-complete`, {
|
||||
objectKey: signData.objectKey,
|
||||
size: file.size,
|
||||
path: this.currentPath
|
||||
});
|
||||
|
||||
// 4. 显示成功提示
|
||||
this.showToast('success', '上传成功', `文件 ${file.name} 已上传到 OSS`);
|
||||
|
||||
// 5. 重置上传进度
|
||||
this.uploadProgress = 0;
|
||||
this.uploadedBytes = 0;
|
||||
this.totalBytes = 0;
|
||||
this.uploadingFileName = '';
|
||||
|
||||
// 6. 刷新文件列表和空间统计
|
||||
await this.loadFiles(this.currentPath);
|
||||
await this.refreshStorageUsage();
|
||||
|
||||
} catch (error) {
|
||||
// 处理 CORS 错误
|
||||
if (error.message?.includes('CORS') || error.message?.includes('Cross-Origin')) {
|
||||
throw new Error('OSS 跨域配置错误,请联系管理员检查 Bucket CORS 设置');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 本地存储上传(经过后端)
|
||||
async uploadToLocal(file) {
|
||||
// 本地存储配额预检查
|
||||
const estimatedUsage = this.localUsed + file.size;
|
||||
if (estimatedUsage > this.localQuota) {
|
||||
this.showToast(
|
||||
'error',
|
||||
'文件超过上传限制',
|
||||
`文件大小 ${fileSizeMB}MB 超过限制 ${maxSizeMB}MB`
|
||||
);
|
||||
} else {
|
||||
// Nginx返回的HTML响应,显示通用消息
|
||||
const fileSizeMB = Math.round(file.size / (1024 * 1024));
|
||||
this.showToast(
|
||||
'error',
|
||||
'文件超过上传限制',
|
||||
`文件大小 ${fileSizeMB}MB 超过系统限制,请联系管理员`
|
||||
'配额不足',
|
||||
`文件大小 ${this.formatBytes(file.size)},剩余配额 ${this.formatBytes(this.localQuota - this.localUsed)}`
|
||||
);
|
||||
this.uploadProgress = 0;
|
||||
this.uploadedBytes = 0;
|
||||
this.totalBytes = 0;
|
||||
this.uploadingFileName = '';
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.showToast('error', '上传失败', error.response?.data?.message || error.message);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('path', this.currentPath);
|
||||
|
||||
const response = await axios.post(`${this.apiBase}/api/upload`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
timeout: 30 * 60 * 1000,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
this.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||
this.uploadedBytes = progressEvent.loaded;
|
||||
this.totalBytes = progressEvent.total;
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
this.showToast('success', '上传成功', `文件 ${file.name} 已上传`);
|
||||
this.uploadProgress = 0;
|
||||
this.uploadedBytes = 0;
|
||||
this.totalBytes = 0;
|
||||
this.uploadingFileName = '';
|
||||
await this.loadFiles(this.currentPath);
|
||||
await this.refreshStorageUsage();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2266,14 +2261,14 @@ handleDragLeave(e) {
|
||||
|
||||
if (response.data.success && response.data.user) {
|
||||
const user = response.data.user;
|
||||
// 同步用户信息(含 has_ftp_config)
|
||||
// 同步用户信息(含 has_oss_config)
|
||||
this.user = { ...(this.user || {}), ...user };
|
||||
|
||||
// 检测存储配置是否被管理员更改
|
||||
const oldStorageType = this.storageType;
|
||||
const oldStoragePermission = this.storagePermission;
|
||||
const newStorageType = user.current_storage_type || 'sftp';
|
||||
const newStoragePermission = user.storage_permission || 'sftp_only';
|
||||
const newStorageType = user.current_storage_type || 'oss';
|
||||
const newStoragePermission = user.storage_permission || 'oss_only';
|
||||
|
||||
// 更新本地数据
|
||||
this.localQuota = user.local_storage_quota || 0;
|
||||
@@ -2293,7 +2288,7 @@ handleDragLeave(e) {
|
||||
console.log('[存储配置更新] 旧权限:', oldStoragePermission, '新权限:', newStoragePermission);
|
||||
|
||||
if (!this.suppressStorageToast) {
|
||||
this.showToast('info', '存储配置已更新', `管理员已将您的存储方式更改为${newStorageType === 'local' ? '本地存储' : 'SFTP存储'}`);
|
||||
this.showToast('info', '存储配置已更新', `管理员已将您的存储方式更改为${newStorageType === 'local' ? '本地存储' : 'OSS存储'}`);
|
||||
} else {
|
||||
this.suppressStorageToast = false;
|
||||
}
|
||||
@@ -2309,30 +2304,41 @@ handleDragLeave(e) {
|
||||
}
|
||||
},
|
||||
|
||||
// 加载SFTP空间使用统计
|
||||
async loadSftpUsage() {
|
||||
// 仅在用户已配置SFTP时才加载
|
||||
if (!this.user?.has_ftp_config) {
|
||||
this.sftpUsage = null;
|
||||
// 加载OSS空间使用统计
|
||||
async loadOssUsage() {
|
||||
// 仅在用户已配置OSS时才加载
|
||||
if (!this.user?.has_oss_config) {
|
||||
this.ossUsage = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.sftpUsageLoading = true;
|
||||
this.sftpUsageError = null;
|
||||
this.ossUsageLoading = true;
|
||||
this.ossUsageError = null;
|
||||
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${this.apiBase}/api/user/sftp-usage`,
|
||||
`${this.apiBase}/api/user/oss-usage`,
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
this.sftpUsage = response.data.usage;
|
||||
this.ossUsage = response.data.usage;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取SFTP空间使用情况失败:', error);
|
||||
this.sftpUsageError = error.response?.data?.message || '获取失败';
|
||||
console.error('获取OSS空间使用情况失败:', error);
|
||||
this.ossUsageError = error.response?.data?.message || '获取失败';
|
||||
} finally {
|
||||
this.sftpUsageLoading = false;
|
||||
this.ossUsageLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 刷新存储空间使用统计(根据当前存储类型)
|
||||
async refreshStorageUsage() {
|
||||
if (this.storageType === 'oss' && this.user?.has_oss_config) {
|
||||
// 刷新 OSS 空间统计
|
||||
await this.loadOssUsage();
|
||||
} else if (this.storageType === 'local') {
|
||||
// 刷新本地存储统计(通过重新获取用户信息)
|
||||
await this.loadUserProfile();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2368,9 +2374,9 @@ handleDragLeave(e) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 切到SFTP但还未配置,引导弹窗
|
||||
if (type === 'sftp' && (!this.user?.has_ftp_config)) {
|
||||
this.showSftpGuideModal = true;
|
||||
// 切到OSS但还未配置,引导弹窗
|
||||
if (type === 'oss' && (!this.user?.has_oss_config)) {
|
||||
this.showOssGuideModal = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2387,7 +2393,7 @@ handleDragLeave(e) {
|
||||
this.storageType = type;
|
||||
// 用户主动切换后,下一次配置同步不提示管理员修改
|
||||
this.suppressStorageToast = true;
|
||||
this.showToast('success', '成功', `已切换到${type === 'local' ? '本地存储' : 'SFTP存储'}`);
|
||||
this.showToast('success', '成功', `已切换到${type === 'local' ? '本地存储' : 'OSS存储'}`);
|
||||
|
||||
// 重新加载文件列表
|
||||
if (this.currentView === 'files') {
|
||||
@@ -2403,33 +2409,33 @@ handleDragLeave(e) {
|
||||
}
|
||||
},
|
||||
|
||||
ensureSftpConfigSection() {
|
||||
this.openSftpConfigModal();
|
||||
ensureOssConfigSection() {
|
||||
this.openOssConfigModal();
|
||||
},
|
||||
|
||||
openSftpGuideModal() {
|
||||
this.showSftpGuideModal = true;
|
||||
openOssGuideModal() {
|
||||
this.showOssGuideModal = true;
|
||||
},
|
||||
|
||||
closeSftpGuideModal() {
|
||||
this.showSftpGuideModal = false;
|
||||
closeOssGuideModal() {
|
||||
this.showOssGuideModal = false;
|
||||
},
|
||||
|
||||
proceedSftpGuide() {
|
||||
this.showSftpGuideModal = false;
|
||||
this.ensureSftpConfigSection();
|
||||
proceedOssGuide() {
|
||||
this.showOssGuideModal = false;
|
||||
this.ensureOssConfigSection();
|
||||
},
|
||||
|
||||
openSftpConfigModal() {
|
||||
this.showSftpGuideModal = false;
|
||||
this.showSftpConfigModal = true;
|
||||
openOssConfigModal() {
|
||||
this.showOssGuideModal = false;
|
||||
this.showOssConfigModal = true;
|
||||
if (this.user && !this.user.is_admin) {
|
||||
this.loadFtpConfig();
|
||||
this.loadOssConfig();
|
||||
}
|
||||
},
|
||||
|
||||
closeSftpConfigModal() {
|
||||
this.showSftpConfigModal = false;
|
||||
closeOssConfigModal() {
|
||||
this.showOssConfigModal = false;
|
||||
},
|
||||
|
||||
// 检查视图权限
|
||||
@@ -2487,7 +2493,7 @@ handleDragLeave(e) {
|
||||
openEditStorageModal(user) {
|
||||
this.editStorageForm.userId = user.id;
|
||||
this.editStorageForm.username = user.username;
|
||||
this.editStorageForm.storage_permission = user.storage_permission || 'sftp_only';
|
||||
this.editStorageForm.storage_permission = user.storage_permission || 'oss_only';
|
||||
|
||||
// 智能识别配额单位
|
||||
const quotaBytes = user.local_storage_quota || 1073741824;
|
||||
@@ -3082,8 +3088,8 @@ handleDragLeave(e) {
|
||||
this.loadSystemSettings();
|
||||
this.loadServerStorageStats();
|
||||
} else if (newView === 'settings' && this.user && !this.user.is_admin) {
|
||||
// 普通用户进入设置页面时加载SFTP配置
|
||||
this.loadFtpConfig();
|
||||
// 普通用户进入设置页面时加载OSS配置
|
||||
this.loadOssConfig();
|
||||
}
|
||||
|
||||
// 记住最后停留的视图(需合法且已登录)
|
||||
|
||||
@@ -564,7 +564,7 @@
|
||||
现代化<br><span class="gradient-text">云存储平台</span>
|
||||
</h1>
|
||||
<p class="hero-desc">
|
||||
简单、安全、高效的文件管理解决方案。支持 SFTP 远程连接和服务器本地存储双模式,随时随地管理和分享你的文件。
|
||||
简单、安全、高效的文件管理解决方案。支持 OSS 云存储和服务器本地存储双模式,随时随地管理和分享你的文件。
|
||||
</p>
|
||||
<div class="hero-buttons">
|
||||
<a href="app.html?action=register" class="btn btn-primary btn-large">
|
||||
@@ -596,10 +596,10 @@
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-server"></i>
|
||||
<i class="fas fa-cloud"></i>
|
||||
</div>
|
||||
<h3 class="feature-title">SFTP 连接</h3>
|
||||
<p class="feature-desc">连接你自己的服务器,数据完全自主掌控</p>
|
||||
<h3 class="feature-title">OSS 云存储</h3>
|
||||
<p class="feature-desc">支持阿里云、腾讯云、AWS S3,数据完全自主掌控</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
|
||||
@@ -336,7 +336,9 @@
|
||||
if (data.success && data.theme === 'light') {
|
||||
document.body.classList.add('light-theme');
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
console.warn('[主题加载] 失败,使用默认主题:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取URL参数
|
||||
|
||||
@@ -918,22 +918,9 @@
|
||||
this.viewingFile = null;
|
||||
},
|
||||
|
||||
downloadFile(file) {
|
||||
async downloadFile(file) {
|
||||
console.log("[分享下载] 文件:", file);
|
||||
|
||||
// 记录下载次数(异步,不等待)
|
||||
axios.post(`${this.apiBase}/api/share/${this.shareCode}/download`)
|
||||
.catch(err => console.error('记录下载次数失败:', err));
|
||||
|
||||
if (file.httpDownloadUrl) {
|
||||
// 如果配置了HTTP下载URL,新窗口打开直接下载(避免Mixed Content问题)
|
||||
console.log("[分享下载] 使用HTTP下载:", file.httpDownloadUrl);
|
||||
window.open(file.httpDownloadUrl, '_blank');
|
||||
return;
|
||||
} else {
|
||||
// 如果没有配置HTTP URL,通过后端SFTP下载
|
||||
console.log("[分享下载] 使用SFTP下载");
|
||||
|
||||
// 构建文件路径
|
||||
let filePath;
|
||||
if (this.shareInfo.share_type === 'file') {
|
||||
@@ -945,15 +932,33 @@
|
||||
filePath = basePath === '/' ? `/${file.name}` : `${basePath}/${file.name}`;
|
||||
}
|
||||
|
||||
// 使用分享下载API(公开API,不需要认证)
|
||||
let downloadUrl = `${this.apiBase}/api/share/${this.shareCode}/download-file?path=${encodeURIComponent(filePath)}`;
|
||||
|
||||
// 如果有密码,附加密码参数
|
||||
try {
|
||||
// 获取下载 URL(OSS 直连或后端代理)
|
||||
const params = { path: filePath };
|
||||
if (this.password) {
|
||||
downloadUrl += `&password=${encodeURIComponent(this.password)}`;
|
||||
params.password = this.password;
|
||||
}
|
||||
|
||||
this.triggerDownload(downloadUrl, file.name);
|
||||
const { data } = await axios.get(`${this.apiBase}/api/share/${this.shareCode}/download-url`, { params });
|
||||
|
||||
if (data.success) {
|
||||
// 记录下载次数(异步,不等待)
|
||||
axios.post(`${this.apiBase}/api/share/${this.shareCode}/download`)
|
||||
.catch(err => console.error('记录下载次数失败:', err));
|
||||
|
||||
if (data.direct) {
|
||||
// OSS 直连下载:新窗口打开
|
||||
console.log("[分享下载] OSS 直连下载");
|
||||
window.open(data.downloadUrl, '_blank');
|
||||
} else {
|
||||
// 本地存储:通过后端下载
|
||||
console.log("[分享下载] 后端代理下载");
|
||||
this.triggerDownload(data.downloadUrl, file.name);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[分享下载] 获取下载链接失败:', error);
|
||||
alert('获取下载链接失败: ' + (error.response?.data?.message || error.message));
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -210,7 +210,9 @@
|
||||
if (data.success && data.theme === 'light') {
|
||||
document.body.classList.add('light-theme');
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
console.warn('[主题加载] 失败,使用默认主题:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取URL参数
|
||||
|
||||
@@ -2266,7 +2266,7 @@ build_upload_tool() {
|
||||
fi
|
||||
else
|
||||
print_warning "未找到wget或curl,无法下载上传工具"
|
||||
print_info "用户仍可使用网页上传(本地存储)或SFTP客户端"
|
||||
print_info "用户仍可使用网页上传(本地存储/OSS云存储)"
|
||||
echo ""
|
||||
return 0
|
||||
fi
|
||||
@@ -2290,7 +2290,7 @@ build_upload_tool() {
|
||||
print_error "下载的文件大小异常(${FILE_SIZE}字节),可能下载不完整"
|
||||
rm -f "dist/${TOOL_FILENAME}"
|
||||
print_warning "可手动下载: ${TOOL_DOWNLOAD_URL}"
|
||||
print_info "用户仍可使用网页上传(本地存储)或SFTP客户端"
|
||||
print_info "用户仍可使用网页上传(本地存储/OSS云存储)"
|
||||
echo ""
|
||||
fi
|
||||
else
|
||||
@@ -2300,7 +2300,7 @@ build_upload_tool() {
|
||||
echo " 2. CDN链接不可访问: ${TOOL_DOWNLOAD_URL}"
|
||||
echo " 3. 防火墙拦截HTTP连接"
|
||||
print_info "您可以稍后手动下载并放置到: ${PROJECT_DIR}/upload-tool/dist/"
|
||||
print_info "用户仍可使用网页上传(本地存储)或SFTP客户端"
|
||||
print_info "用户仍可使用网页上传(本地存储/OSS云存储)"
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
============================================
|
||||
玩玩云上传工具 v2.0 使用说明
|
||||
玩玩云上传工具 v3.0 使用说明
|
||||
============================================
|
||||
|
||||
【新版本特性】
|
||||
✨ 支持多文件上传
|
||||
✨ 支持文件夹上传(递归扫描所有文件)
|
||||
✨ 支持阿里云 OSS、腾讯云 COS、AWS S3
|
||||
✨ 通过服务器 API 上传,自动识别存储类型
|
||||
✨ 支持多文件和文件夹上传
|
||||
✨ 智能上传队列管理
|
||||
✨ 自动检测可写目录(容错机制)
|
||||
✨ 实时显示队列状态
|
||||
✨ 实时显示存储类型和空间使用情况
|
||||
|
||||
【功能介绍】
|
||||
本工具用于快速上传文件到您的SFTP服务器。
|
||||
新版本支持批量上传和文件夹上传,大大提升工作效率!
|
||||
本工具用于快速上传文件到您的玩玩云存储。
|
||||
支持本地存储和 OSS 云存储双模式,自动适配!
|
||||
|
||||
【使用方法】
|
||||
1. 双击运行"玩玩云上传工具.exe"
|
||||
2. 等待程序连接服务器并测试上传目录
|
||||
- 程序会自动测试多个目录的可写性
|
||||
- 显示绿色✓表示连接成功
|
||||
- 显示当前使用的上传目录
|
||||
2. 等待程序连接服务器
|
||||
- 程序会自动检测服务器配置
|
||||
- 显示当前存储类型(本地存储/OSS)
|
||||
- OSS 模式会显示存储桶信息
|
||||
3. 拖拽文件或文件夹到窗口中
|
||||
- 可以一次拖拽多个文件
|
||||
- 可以拖拽整个文件夹(自动扫描所有文件)
|
||||
@@ -30,38 +30,46 @@
|
||||
- 每个文件都有独立的进度显示
|
||||
- 日志区域显示详细的上传信息
|
||||
|
||||
【目录容错机制】
|
||||
程序会按以下优先级自动测试并选择可写目录:
|
||||
1. /(根目录)
|
||||
2. /upload
|
||||
3. /uploads
|
||||
4. /files
|
||||
5. /home
|
||||
6. /tmp
|
||||
【存储类型说明】
|
||||
|
||||
如果根目录没有写权限,程序会自动切换到其他可用目录。
|
||||
本地存储模式:
|
||||
- 文件存储在服务器本地磁盘
|
||||
- 适合小文件和内网环境
|
||||
- 由服务器管理员管理配额
|
||||
|
||||
OSS 云存储模式:
|
||||
- 支持阿里云 OSS、腾讯云 COS、AWS S3
|
||||
- 文件直接存储到云存储桶
|
||||
- 适合大文件和外网访问
|
||||
- 无限存储空间(由云服务商决定)
|
||||
|
||||
【注意事项】
|
||||
- 文件夹上传会递归扫描所有子文件夹
|
||||
- 同名文件会被覆盖
|
||||
- 上传大量文件时请确保网络稳定
|
||||
- 所有文件会按顺序依次上传
|
||||
- 上传目录会在启动时自动检测并显示
|
||||
- OSS 模式下大文件会自动分片上传
|
||||
|
||||
【界面说明】
|
||||
- 状态显示:显示连接状态和存储类型
|
||||
- 拖拽区域:显示"支持多文件和文件夹"
|
||||
- 队列状态:显示等待上传的文件数量
|
||||
- 进度条:显示当前文件的上传进度
|
||||
- 日志区域:显示详细的操作记录
|
||||
|
||||
【版本更新】
|
||||
v2.0 (2025-11-09)
|
||||
- ✅ 新增多文件上传支持
|
||||
- ✅ 新增文件夹上传支持
|
||||
- ✅ 新增上传队列管理
|
||||
- ✅ 新增目录容错机制
|
||||
v3.0 (2025-01-18)
|
||||
- 🚀 架构升级:SFTP → OSS 云存储
|
||||
- ✅ 支持阿里云 OSS、腾讯云 COS、AWS S3
|
||||
- ✅ 使用服务器 API 上传,自动识别存储类型
|
||||
- ✅ 新增存储类型显示
|
||||
- ✅ 优化界面显示
|
||||
- ✅ 优化日志输出
|
||||
- ✅ 优化错误提示
|
||||
|
||||
v2.0 (2025-11-09)
|
||||
- 新增多文件上传支持
|
||||
- 新增文件夹上传支持
|
||||
- 新增上传队列管理
|
||||
|
||||
v1.0
|
||||
- 基础单文件上传功能
|
||||
@@ -74,14 +82,14 @@ A: 理论上无限制,所有文件会加入队列依次上传
|
||||
Q: 文件夹上传包括子文件夹吗?
|
||||
A: 是的,会递归扫描所有子文件夹中的文件
|
||||
|
||||
Q: 上传目录是哪里?
|
||||
A: 程序启动时会自动检测并显示在界面上
|
||||
Q: 如何切换存储类型?
|
||||
A: 存储类型由用户配置决定,请在网页端设置
|
||||
|
||||
Q: 提示"API密钥无效或已过期"怎么办?
|
||||
A: 请重新从网站下载最新的上传工具
|
||||
Q: 提示"API密钥无效"怎么办?
|
||||
A: 请在网页端重新生成上传 API 密钥
|
||||
|
||||
Q: 上传速度慢怎么办?
|
||||
A: 速度取决于您的网络和SFTP服务器性能
|
||||
A: 速度取决于您的网络和服务器/云存储性能
|
||||
|
||||
Q: 可以中途取消上传吗?
|
||||
A: 当前版本暂不支持取消,请等待队列完成
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
PyQt5==5.15.9
|
||||
paramiko==3.4.0
|
||||
requests==2.31.0
|
||||
|
||||
@@ -1,155 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
玩玩云上传工具 v3.0
|
||||
支持本地存储和 OSS 云存储
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
import paramiko
|
||||
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 TestDirectoryThread(QThread):
|
||||
"""测试目录可写性线程"""
|
||||
result = pyqtSignal(bool, str) # 成功/失败,目录路径或错误信息
|
||||
|
||||
def __init__(self, test_dir, sftp_config):
|
||||
super().__init__()
|
||||
self.test_dir = test_dir
|
||||
self.sftp_config = sftp_config
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
transport = paramiko.Transport((
|
||||
self.sftp_config['host'],
|
||||
self.sftp_config['port']
|
||||
))
|
||||
transport.connect(
|
||||
username=self.sftp_config['username'],
|
||||
password=self.sftp_config['password']
|
||||
)
|
||||
sftp = paramiko.SFTPClient.from_transport(transport)
|
||||
|
||||
# 测试文件名
|
||||
test_file = f"{self.test_dir}/.wwy_test_{os.getpid()}"
|
||||
if not self.test_dir.endswith('/'):
|
||||
test_file = f"{self.test_dir}/.wwy_test_{os.getpid()}"
|
||||
else:
|
||||
test_file = f"{self.test_dir}.wwy_test_{os.getpid()}"
|
||||
|
||||
# 尝试创建测试文件
|
||||
try:
|
||||
with sftp.open(test_file, 'w') as f:
|
||||
f.write('test')
|
||||
|
||||
# 删除测试文件
|
||||
try:
|
||||
sftp.remove(test_file)
|
||||
except:
|
||||
pass
|
||||
|
||||
sftp.close()
|
||||
transport.close()
|
||||
self.result.emit(True, self.test_dir)
|
||||
|
||||
except Exception as e:
|
||||
sftp.close()
|
||||
transport.close()
|
||||
self.result.emit(False, str(e))
|
||||
|
||||
except Exception as e:
|
||||
self.result.emit(False, str(e))
|
||||
|
||||
class UploadThread(QThread):
|
||||
"""上传线程"""
|
||||
"""上传线程 - 支持 OSS 和本地存储"""
|
||||
progress = pyqtSignal(int, str) # 进度,状态信息
|
||||
finished = pyqtSignal(bool, str) # 成功/失败,消息
|
||||
|
||||
def __init__(self, sftp_config, file_path, remote_dir):
|
||||
def __init__(self, api_config, file_path, remote_path):
|
||||
super().__init__()
|
||||
self.sftp_config = sftp_config
|
||||
self.api_config = api_config
|
||||
self.file_path = file_path
|
||||
self.remote_dir = remote_dir
|
||||
self.remote_path = remote_path
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
# 连接SFTP
|
||||
self.progress.emit(10, f'正在连接服务器...')
|
||||
transport = paramiko.Transport((
|
||||
self.sftp_config['host'],
|
||||
self.sftp_config['port']
|
||||
))
|
||||
transport.connect(
|
||||
username=self.sftp_config['username'],
|
||||
password=self.sftp_config['password']
|
||||
)
|
||||
sftp = paramiko.SFTPClient.from_transport(transport)
|
||||
|
||||
self.progress.emit(30, f'连接成功,开始上传...')
|
||||
|
||||
# 获取文件名
|
||||
filename = os.path.basename(self.file_path)
|
||||
# 构建远程路径
|
||||
if self.remote_dir.endswith('/'):
|
||||
remote_path = f'{self.remote_dir}{filename}'
|
||||
else:
|
||||
remote_path = f'{self.remote_dir}/{filename}'
|
||||
self.progress.emit(10, f'正在准备上传: {filename}')
|
||||
|
||||
# 上传文件(带进度)- 使用临时文件避免.fuse_hidden问题
|
||||
file_size = os.path.getsize(self.file_path)
|
||||
# 使用服务器 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
|
||||
|
||||
# 使用临时文件名
|
||||
import time
|
||||
temp_remote_path = f'{remote_path}.uploading_{int(time.time() * 1000)}'
|
||||
# 使用 multipart/form-data 上传
|
||||
files = {
|
||||
'file': (filename, file_data)
|
||||
}
|
||||
data = {
|
||||
'path': self.remote_path
|
||||
}
|
||||
headers = {
|
||||
'X-API-Key': api_key
|
||||
}
|
||||
|
||||
def callback(transferred, total):
|
||||
nonlocal uploaded
|
||||
uploaded = transferred
|
||||
percent = int((transferred / total) * 100) if total > 0 else 0
|
||||
# 进度从30%到90%
|
||||
progress_value = 30 + int(percent * 0.6)
|
||||
self.progress.emit(30, f'开始上传...')
|
||||
|
||||
# 计算速度
|
||||
size_mb = transferred / (1024 * 1024)
|
||||
self.progress.emit(progress_value, f'上传中: {filename} ({size_mb:.2f} MB / {total/(1024*1024):.2f} MB)')
|
||||
# 带进度的上传
|
||||
response = requests.post(
|
||||
f"{api_base_url}/api/upload",
|
||||
files=files,
|
||||
data=data,
|
||||
headers=headers,
|
||||
timeout=300 # 5分钟超时
|
||||
)
|
||||
|
||||
# 第一步:上传到临时文件
|
||||
sftp.put(self.file_path, temp_remote_path, callback=callback)
|
||||
|
||||
# 第二步:删除旧文件(如果存在)
|
||||
try:
|
||||
sftp.stat(remote_path)
|
||||
sftp.remove(remote_path)
|
||||
except:
|
||||
pass # 文件不存在,无需删除
|
||||
|
||||
# 第三步:重命名临时文件为目标文件
|
||||
sftp.rename(temp_remote_path, remote_path)
|
||||
|
||||
# 关闭连接
|
||||
sftp.close()
|
||||
transport.close()
|
||||
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.sftp_config = None
|
||||
self.remote_dir = '/' # 默认上传目录
|
||||
self.server_config = None
|
||||
self.remote_path = '/' # 默认上传目录
|
||||
self.upload_queue = [] # 上传队列
|
||||
self.is_uploading = False # 是否正在上传
|
||||
self.initUI()
|
||||
self.get_sftp_config()
|
||||
self.check_config()
|
||||
|
||||
def load_config(self):
|
||||
"""加载配置文件"""
|
||||
@@ -176,87 +173,58 @@ class UploadWindow(QMainWindow):
|
||||
QMessageBox.critical(None, '错误', f'加载配置失败:\n{str(e)}')
|
||||
sys.exit(1)
|
||||
|
||||
def get_sftp_config(self):
|
||||
"""从服务器获取SFTP配置"""
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{self.config['api_base_url']}/api/upload/get-config",
|
||||
json={'api_key': self.config['api_key']},
|
||||
timeout=10
|
||||
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()
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
if data['success']:
|
||||
self.sftp_config = data['sftp_config']
|
||||
# 自动测试并设置上传目录
|
||||
self.test_and_set_upload_directory()
|
||||
else:
|
||||
self.show_error(data.get('message', '获取配置失败'))
|
||||
else:
|
||||
self.show_error(f'服务器错误: {response.status_code}')
|
||||
|
||||
except Exception as e:
|
||||
self.show_error(f'无法连接到服务器: {str(e)}')
|
||||
|
||||
def test_and_set_upload_directory(self):
|
||||
"""测试并设置上传目录"""
|
||||
if not self.sftp_config:
|
||||
return
|
||||
|
||||
self.log('开始测试上传目录...')
|
||||
self.status_label.setText(
|
||||
f'<h2>玩玩云上传工具 v2.0</h2>'
|
||||
f'<p style="color: orange;">正在测试上传目录...</p>'
|
||||
)
|
||||
|
||||
# 按优先级测试目录
|
||||
self.test_dirs = ['/', '/upload', '/uploads', '/files', '/home', '/tmp']
|
||||
self.current_test_index = 0
|
||||
|
||||
self.test_next_directory()
|
||||
|
||||
def test_next_directory(self):
|
||||
"""测试下一个目录"""
|
||||
if self.current_test_index >= len(self.test_dirs):
|
||||
self.log('✗ 所有目录都无法写入,请检查SFTP权限')
|
||||
self.show_error('无法找到可写入的目录,请检查SFTP权限')
|
||||
return
|
||||
|
||||
test_dir = self.test_dirs[self.current_test_index]
|
||||
self.log(f'测试目录: {test_dir}')
|
||||
|
||||
self.test_thread = TestDirectoryThread(test_dir, self.sftp_config)
|
||||
self.test_thread.result.connect(self.on_test_result)
|
||||
self.test_thread.start()
|
||||
|
||||
def on_test_result(self, success, message):
|
||||
"""处理测试结果"""
|
||||
def on_config_result(self, success, message, config):
|
||||
"""处理配置检查结果"""
|
||||
if success:
|
||||
self.remote_dir = message
|
||||
self.log(f'✓ 已设置上传目录为: {message}')
|
||||
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>玩玩云上传工具 v2.0</h2>'
|
||||
f'<p style="color: green;">✓ 已连接 - 用户: {self.config["username"]}</p>'
|
||||
f'<p style="color: #666; font-size: 14px;">拖拽文件/文件夹到此处上传</p>'
|
||||
f'<p style="color: #999; font-size: 12px;">上传目录: {self.remote_dir}</p>'
|
||||
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.log(f'✗ 目录 {self.test_dirs[self.current_test_index]} 不可写: {message}')
|
||||
self.current_test_index += 1
|
||||
self.test_next_directory()
|
||||
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>玩玩云上传工具 v2.0</h2>'
|
||||
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('玩玩云上传工具 v2.0')
|
||||
self.setWindowTitle('玩玩云上传工具 v3.0')
|
||||
self.setGeometry(300, 300, 500, 450)
|
||||
|
||||
# 设置接受拖拽
|
||||
@@ -339,7 +307,7 @@ class UploadWindow(QMainWindow):
|
||||
|
||||
central_widget.setLayout(layout)
|
||||
|
||||
self.log('程序已启动 - 版本 v2.0')
|
||||
self.log('程序已启动 - 版本 v3.0 (支持OSS云存储)')
|
||||
|
||||
def log(self, message):
|
||||
"""添加日志"""
|
||||
@@ -395,8 +363,9 @@ class UploadWindow(QMainWindow):
|
||||
}
|
||||
""")
|
||||
|
||||
if not self.sftp_config:
|
||||
self.log('错误: 未获取到SFTP配置')
|
||||
if not self.server_config:
|
||||
self.log('错误: 未连接到服务器,请等待连接完成')
|
||||
self.show_error('服务器未连接,请稍后重试')
|
||||
return
|
||||
|
||||
paths = [url.toLocalFile() for url in event.mimeData().urls()]
|
||||
@@ -453,7 +422,15 @@ class UploadWindow(QMainWindow):
|
||||
|
||||
def upload_file(self, file_path):
|
||||
"""上传文件"""
|
||||
self.log(f'开始上传: {os.path.basename(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)
|
||||
@@ -462,7 +439,11 @@ class UploadWindow(QMainWindow):
|
||||
self.progress_label.setText('准备上传...')
|
||||
|
||||
# 创建上传线程
|
||||
self.upload_thread = UploadThread(self.sftp_config, file_path, self.remote_dir)
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user