新增:SFTP直连+连接池+autoConnect+文件服务器端口自动回退

- SFTP模块:连接/断开/文件CRUD/系统信息采集/base64二进制写入
- 连接池:多服务器同时在线,瞬间切换profile
- autoConnect:启动时自动连接所有非本地服务器
- 端口自动回退:listenWithFallback消除TOCTOU,解决端口冲突崩溃
- 文件服务器URL集中管理:file-server.ts消除8+处硬编码端口
- Sidebar设置面板:添加服务器/自动连接/自动刷新开关
- 修复:validateFilePath越界panic、正则预编译
- 修复:注释准确性(RemoveAll/端口8073/动态端口文档)
This commit is contained in:
lxy
2026-05-04 15:33:19 +08:00
parent 6eaaa56eb6
commit 6bee55b96f
41 changed files with 2620 additions and 458 deletions
@@ -8,6 +8,8 @@ import { FILE_EXTENSIONS, FILE_SIZE_THRESHOLDS } from '@/utils/constants'
import { normalizeFilePath, getExt } from '@/utils/fileUtils'
import { detectFileTypeByContent } from '@/api/system'
import { connectionManager } from '@/api/connection-manager'
import { SftpTransport } from '@/api/sftp-transport'
import { getFileServerBaseURL } from '@/api/file-server'
import {
isImageFile, isVideoFile, isAudioFile, isPdfFile,
isHtmlFile, isMarkdownFile, isPreviewable as isPreviewableType,
@@ -28,7 +30,7 @@ export interface UseFilePreviewOptions {
}
function getLocalServerURL(): string {
return 'http://localhost:8073'
return getFileServerBaseURL()
}
function resolveFileServerBase(): string {
@@ -51,16 +53,28 @@ export function useFilePreview(options: UseFilePreviewOptions = {}) {
const currentImageDimensions = ref('')
/**
* 获取预览 URL(本地/远程自适应,每次实时计算)
* 本地: http://localhost:8073/localfs/{encoded_path}
* 远程: {baseUrl}/api/v1/proxy/localfs/{raw_path}(Cookie 自动携带认证)
* 获取预览 URL(本地/远程/SFTP 自适应,每次实时计算)
* 本地: {fileServerBaseURL}/localfs/{encoded_path}
* 远程(HTTP): {baseUrl}/api/v1/proxy/localfs/{raw_path}
* SFTP: 下载到本地临时目录 → {fileServerBaseURL}/localfs/{temp_path}
*/
const getPreviewUrl = (path: string): string => {
if (!path) return ''
const isSftp = connectionManager.isSftp()
const isRemote = connectionManager.isRemote()
// SFTP 模式:需要先下载到本地临时目录
// 注意:这里返回的是同步路径,实际下载在 updatePreviewUrl 中异步完成
// 对于 SFTP 模式,getPreviewUrl 返回的 URL 会在 updatePreviewUrl 中被覆盖为临时文件路径
if (isSftp) {
const base = getLocalServerURL()
let normalized = normalizeFilePath(path, true)
const sep = base.endsWith('/') ? '' : '/'
return `${base}${sep}localfs/${normalized}`
}
const base = resolveFileServerBase()
let normalized = normalizeFilePath(path, true)
// 远程模式去掉前导 /,避免与 URL 基础路径拼接产生双斜杠(导致 307 重定向)
if (isRemote && normalized.startsWith('/')) normalized = normalized.slice(1)
const sep = base.endsWith('/') ? '' : '/'
return `${base}${sep}${isRemote ? '' : 'localfs/'}${normalized}`
@@ -92,9 +106,25 @@ export function useFilePreview(options: UseFilePreviewOptions = {}) {
}
/**
* 更新预览 URL
* 更新预览 URL(SFTP 模式会先下载到本地临时目录)
*/
const updatePreviewUrl = async (path: string) => {
if (!path) { previewUrl.value = ''; return }
// SFTP 模式:下载到本地临时目录后用本地文件服务器预览
if (connectionManager.isSftp()) {
const transport = connectionManager.getTransport()
if (transport instanceof SftpTransport) {
try {
const tempPath = await transport.downloadForPreview(path)
previewUrl.value = getPreviewUrl(tempPath)
return
} catch {
// 下载失败,回退显示原始路径(会无法预览但不会崩溃)
}
}
}
previewUrl.value = getPreviewUrl(path)
}