feat: 添加SFTP空间使用统计功能
- 新增 /api/user/sftp-usage API,递归统计SFTP服务器空间使用情况 - 返回总使用空间、文件数、文件夹数 - 在设置页面显示SFTP空间统计信息 - 支持手动刷新统计数据 - 适配"仅SFTP"和"用户可选"两种权限模式的UI 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1571,6 +1571,88 @@ app.post('/api/user/update-ftp',
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 获取SFTP存储空间使用情况
|
||||||
|
app.get('/api/user/sftp-usage', authMiddleware, async (req, res) => {
|
||||||
|
let sftp = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查用户是否配置了SFTP
|
||||||
|
if (!req.user.has_ftp_config) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '未配置SFTP服务器'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接SFTP
|
||||||
|
sftp = await connectToSFTP(req.user);
|
||||||
|
|
||||||
|
// 递归计算目录大小的函数
|
||||||
|
async function calculateDirSize(dirPath) {
|
||||||
|
let totalSize = 0;
|
||||||
|
let fileCount = 0;
|
||||||
|
let dirCount = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const list = await sftp.list(dirPath);
|
||||||
|
|
||||||
|
for (const item of list) {
|
||||||
|
// 跳过 . 和 .. 目录
|
||||||
|
if (item.name === '.' || item.name === '..') continue;
|
||||||
|
|
||||||
|
const itemPath = dirPath === '/' ? `/${item.name}` : `${dirPath}/${item.name}`;
|
||||||
|
|
||||||
|
if (item.type === 'd') {
|
||||||
|
// 是目录,递归计算
|
||||||
|
dirCount++;
|
||||||
|
const subResult = await calculateDirSize(itemPath);
|
||||||
|
totalSize += subResult.totalSize;
|
||||||
|
fileCount += subResult.fileCount;
|
||||||
|
dirCount += subResult.dirCount;
|
||||||
|
} else {
|
||||||
|
// 是文件,累加大小
|
||||||
|
fileCount++;
|
||||||
|
totalSize += item.size || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// 跳过无法访问的目录
|
||||||
|
console.warn(`[SFTP统计] 无法访问目录 ${dirPath}: ${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { totalSize, fileCount, dirCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从根目录开始计算
|
||||||
|
const result = await calculateDirSize('/');
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
usage: {
|
||||||
|
totalSize: result.totalSize,
|
||||||
|
totalSizeFormatted: formatFileSize(result.totalSize),
|
||||||
|
fileCount: result.fileCount,
|
||||||
|
dirCount: result.dirCount
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[SFTP统计] 获取失败:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '获取SFTP空间使用情况失败: ' + error.message
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
if (sftp) {
|
||||||
|
try {
|
||||||
|
await sftp.end();
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略关闭错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 修改管理员账号信息(仅管理员可修改用户名)
|
// 修改管理员账号信息(仅管理员可修改用户名)
|
||||||
app.post('/api/admin/update-profile',
|
app.post('/api/admin/update-profile',
|
||||||
authMiddleware,
|
authMiddleware,
|
||||||
|
|||||||
@@ -1281,6 +1281,29 @@
|
|||||||
<div v-else style="font-size: 13px; color: #b45309; background: #fff7ed; border: 1px dashed #fcd34d; padding: 10px; border-radius: 8px; margin-bottom: 10px;">
|
<div v-else style="font-size: 13px; color: #b45309; background: #fff7ed; border: 1px dashed #fcd34d; padding: 10px; border-radius: 8px; margin-bottom: 10px;">
|
||||||
<i class="fas fa-exclamation-circle"></i> 先填写 SFTP 连接信息再切换
|
<i class="fas fa-exclamation-circle"></i> 先填写 SFTP 连接信息再切换
|
||||||
</div>
|
</div>
|
||||||
|
<!-- SFTP空间使用统计(user_choice模式) -->
|
||||||
|
<div v-if="user?.has_ftp_config" style="margin-bottom: 10px; padding: 10px; background: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0;">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||||||
|
<span style="font-size: 12px; color: #64748b;">空间统计</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>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="sftpUsageLoading && !sftpUsage" 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: #c53030;">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i> {{ sftpUsageError }}
|
||||||
|
</div>
|
||||||
|
<div v-else-if="sftpUsage" style="font-size: 13px; font-weight: 600; color: #4b5fc9;">
|
||||||
|
{{ sftpUsage.totalSizeFormatted }}
|
||||||
|
<span style="font-weight: 400; color: #64748b; font-size: 12px;">({{ sftpUsage.fileCount }} 文件)</span>
|
||||||
|
</div>
|
||||||
|
<div v-else style="font-size: 12px; color: #94a3b8;">点击刷新查看</div>
|
||||||
|
</div>
|
||||||
<div style="margin-top: auto;">
|
<div style="margin-top: auto;">
|
||||||
<button
|
<button
|
||||||
class="btn"
|
class="btn"
|
||||||
@@ -1342,34 +1365,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- SFTP存储信息 - 仅SFTP权限 -->
|
<!-- SFTP 概览 / 配置入口 - 仅SFTP权限 -->
|
||||||
<div v-if="user && !user.is_admin && storagePermission === 'sftp_only' && user.has_ftp_config" style="margin-bottom: 40px;">
|
|
||||||
<h3 style="margin-bottom: 20px;">
|
|
||||||
<i class="fas fa-server"></i> SFTP存储
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px;">
|
|
||||||
<div style="margin-bottom: 15px;">
|
|
||||||
<span style="font-weight: 600; color: #333;">存储方式: </span>
|
|
||||||
<span style="color: #667eea; font-weight: 600;">SFTP存储</span>
|
|
||||||
<span style="margin-left: 10px; padding: 4px 12px; background: #17a2b8; color: white; border-radius: 12px; font-size: 12px;">
|
|
||||||
<i class="fas fa-lock"></i> 仅SFTP
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-bottom: 15px;">
|
|
||||||
<span style="font-weight: 600; color: #333;">服务器: </span>
|
|
||||||
<span>{{ user.ftp_host }}:{{ user.ftp_port }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="padding: 10px; background: #d1ecf1; border-left: 4px solid #0c5460; border-radius: 6px; font-size: 13px; color: #0c5460;">
|
|
||||||
<i class="fas fa-info-circle"></i>
|
|
||||||
<strong>说明:</strong> 管理员已将您的存储权限设置为"仅SFTP存储",您的文件存储在远程SFTP服务器上。如需使用本地存储,请联系管理员修改权限设置。
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- SFTP 概览 / 配置入口 -->
|
|
||||||
<div v-if="user && !user.is_admin && storagePermission === 'sftp_only'" style="margin-bottom: 40px;">
|
<div v-if="user && !user.is_admin && storagePermission === 'sftp_only'" style="margin-bottom: 40px;">
|
||||||
<h3 style="margin-bottom: 20px;">
|
<h3 style="margin-bottom: 20px;">
|
||||||
<i class="fas fa-server"></i> SFTP存储
|
<i class="fas fa-server"></i> SFTP存储
|
||||||
@@ -1389,6 +1385,68 @@
|
|||||||
<i class="fas fa-tools"></i> 配置 / 修改 SFTP
|
<i class="fas fa-tools"></i> 配置 / 修改 SFTP
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 服务器信息 -->
|
||||||
|
<div v-if="user.has_ftp_config" style="margin-bottom: 12px; padding: 12px; background: white; border-radius: 10px; border: 1px solid #e2e8f0;">
|
||||||
|
<div style="font-weight: 600; color: #333; margin-bottom: 8px;">
|
||||||
|
<i class="fas fa-server" style="color: #667eea;"></i> 服务器信息
|
||||||
|
</div>
|
||||||
|
<div style="color: #475569; font-size: 14px;">
|
||||||
|
{{ user.ftp_host }}:{{ user.ftp_port }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SFTP空间使用统计 -->
|
||||||
|
<div v-if="user.has_ftp_config" style="margin-bottom: 12px; padding: 12px; background: white; border-radius: 10px; border: 1px solid #e2e8f0;">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||||
|
<div style="font-weight: 600; color: #333;">
|
||||||
|
<i class="fas fa-chart-pie" style="color: #667eea;"></i> 空间使用统计
|
||||||
|
</div>
|
||||||
|
<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 ? '统计中...' : '刷新' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 加载中 -->
|
||||||
|
<div v-if="sftpUsageLoading && !sftpUsage" 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: 4px; font-size: 12px; color: #999;">(文件较多时可能需要一些时间)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误提示 -->
|
||||||
|
<div v-else-if="sftpUsageError" style="padding: 12px; background: #fff5f5; border-radius: 8px; color: #c53030; font-size: 13px;">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i> {{ sftpUsageError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计结果 -->
|
||||||
|
<div v-else-if="sftpUsage" 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: 12px; opacity: 0.9; margin-top: 4px;">总使用空间</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center; padding: 12px; background: #f0f9ff; border-radius: 10px; border: 1px solid #e0f2fe;">
|
||||||
|
<div style="font-size: 20px; font-weight: 700; color: #0369a1;">{{ sftpUsage.fileCount }}</div>
|
||||||
|
<div style="font-size: 12px; color: #64748b; margin-top: 4px;">文件数</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center; padding: 12px; background: #fefce8; border-radius: 10px; border: 1px solid #fef08a;">
|
||||||
|
<div style="font-size: 20px; font-weight: 700; color: #a16207;">{{ sftpUsage.dirCount }}</div>
|
||||||
|
<div style="font-size: 12px; color: #64748b; margin-top: 4px;">文件夹数</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 未统计提示 -->
|
||||||
|
<div v-else style="text-align: center; padding: 16px; color: #64748b; font-size: 13px;">
|
||||||
|
<i class="fas fa-database" style="font-size: 24px; color: #cbd5e1; margin-bottom: 8px; display: block;"></i>
|
||||||
|
点击"刷新"按钮统计 SFTP 空间使用情况
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="padding: 10px; background: #eef2ff; border-radius: 10px; color: #374151; font-size: 13px;">
|
<div style="padding: 10px; background: #eef2ff; border-radius: 10px; color: #374151; font-size: 13px;">
|
||||||
<i class="fas fa-info-circle" style="color: #4b5fc9;"></i>
|
<i class="fas fa-info-circle" style="color: #4b5fc9;"></i>
|
||||||
数据存储在你的 SFTP 服务器上,如需切换回本地请联系管理员调整权限。
|
数据存储在你的 SFTP 服务器上,如需切换回本地请联系管理员调整权限。
|
||||||
|
|||||||
@@ -220,7 +220,12 @@ createApp({
|
|||||||
|
|
||||||
// SFTP配置引导弹窗
|
// SFTP配置引导弹窗
|
||||||
showSftpGuideModal: false,
|
showSftpGuideModal: false,
|
||||||
showSftpConfigModal: false
|
showSftpConfigModal: false,
|
||||||
|
|
||||||
|
// SFTP空间使用统计
|
||||||
|
sftpUsage: null, // { totalSize, totalSizeFormatted, fileCount, dirCount }
|
||||||
|
sftpUsageLoading: false,
|
||||||
|
sftpUsageError: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1840,6 +1845,35 @@ handleDragLeave(e) {
|
|||||||
console.error('加载用户资料失败:', error);
|
console.error('加载用户资料失败:', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 加载SFTP空间使用统计
|
||||||
|
async loadSftpUsage() {
|
||||||
|
// 仅在用户已配置SFTP时才加载
|
||||||
|
if (!this.user?.has_ftp_config) {
|
||||||
|
this.sftpUsage = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sftpUsageLoading = true;
|
||||||
|
this.sftpUsageError = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(
|
||||||
|
`${this.apiBase}/api/user/sftp-usage`,
|
||||||
|
{ headers: { Authorization: `Bearer ${this.token}` } }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
this.sftpUsage = response.data.usage;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取SFTP空间使用情况失败:', error);
|
||||||
|
this.sftpUsageError = error.response?.data?.message || '获取失败';
|
||||||
|
} finally {
|
||||||
|
this.sftpUsageLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// 启动定期检查用户配置
|
// 启动定期检查用户配置
|
||||||
startProfileSync() {
|
startProfileSync() {
|
||||||
// 清除已有的定时器
|
// 清除已有的定时器
|
||||||
|
|||||||
Reference in New Issue
Block a user