feat(web): 引入 Vite 前端应用并扩展仓库忽略规则
将整套 web 源码纳入仓库,并为 web/node_modules、构建产物及本地环境文件配置 .gitignore,同时移除占位用的 assets/.gitkeep。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
343
web/src/api/asset/webdavDriver.js
Normal file
343
web/src/api/asset/webdavDriver.js
Normal file
@@ -0,0 +1,343 @@
|
||||
// WebDAV Driver for VueFinder
|
||||
import request from '../../utils/request'
|
||||
import { getApiBaseUrl } from '../../utils/config'
|
||||
|
||||
// 解析 WebDAV PROPFIND XML 响应
|
||||
export function parseWebDAVXML(xmlString) {
|
||||
try {
|
||||
// 确保输入是字符串
|
||||
if (typeof xmlString !== 'string') {
|
||||
console.error('parseWebDAVXML: 输入不是字符串类型', typeof xmlString, xmlString)
|
||||
throw new Error('XML 响应必须是字符串类型')
|
||||
}
|
||||
|
||||
// 清理和提取有效的 XML 内容
|
||||
let cleanedXml = xmlString.trim()
|
||||
|
||||
// 如果响应包含多个 XML 文档或末尾有额外内容,尝试提取第一个完整的 XML 文档
|
||||
// 查找第一个 <?xml 或 <multistatus 或 <d:multistatus 标签
|
||||
const xmlStartPattern = /(<\?xml[\s\S]*?<[^:]*:?multistatus)/i
|
||||
const match = cleanedXml.match(xmlStartPattern)
|
||||
|
||||
if (match) {
|
||||
// 找到 XML 开始位置
|
||||
const startIndex = cleanedXml.indexOf(match[1])
|
||||
// 查找对应的结束标签
|
||||
const endTagPattern = /<\/[^:]*:?multistatus>/i
|
||||
const endMatch = cleanedXml.substring(startIndex).match(endTagPattern)
|
||||
|
||||
if (endMatch) {
|
||||
// 提取完整的 XML 文档
|
||||
const endIndex = startIndex + endMatch.index + endMatch[0].length
|
||||
cleanedXml = cleanedXml.substring(startIndex, endIndex)
|
||||
// console.log('提取了有效的 XML 片段,原始长度:', xmlString.length, '清理后长度:', cleanedXml.length)
|
||||
} else {
|
||||
console.warn('未找到 XML 结束标签,使用原始响应')
|
||||
}
|
||||
}
|
||||
|
||||
// 如果仍然没有找到有效的 XML,尝试查找第一个 <multistatus> 到 </multistatus>
|
||||
if (!cleanedXml.includes('multistatus') && !cleanedXml.includes('<?xml')) {
|
||||
console.warn('响应可能不是有效的 XML 格式:', cleanedXml.substring(0, 200))
|
||||
}
|
||||
|
||||
const parser = new DOMParser()
|
||||
const xmlDoc = parser.parseFromString(cleanedXml, 'text/xml')
|
||||
|
||||
// 检查是否有解析错误
|
||||
const parserError = xmlDoc.querySelector('parsererror')
|
||||
if (parserError) {
|
||||
// 记录原始响应的一部分用于调试
|
||||
console.error('XML 解析错误详情:', {
|
||||
errorText: parserError.textContent,
|
||||
xmlPreview: cleanedXml.substring(0, 500),
|
||||
xmlLength: cleanedXml.length,
|
||||
originalLength: xmlString.length
|
||||
})
|
||||
throw new Error('XML 解析失败: ' + parserError.textContent)
|
||||
}
|
||||
|
||||
// 辅助函数:通过本地名称查找元素(处理命名空间)
|
||||
const getElementByLocalName = (parent, localName) => {
|
||||
if (!parent || !parent.getElementsByTagName) return null
|
||||
const allElements = parent.getElementsByTagName('*')
|
||||
for (let i = 0; i < allElements.length; i++) {
|
||||
const elem = allElements[i]
|
||||
const elemLocalName = elem.localName || elem.tagName?.split(':').pop() || elem.tagName
|
||||
if (elemLocalName === localName || elem.tagName === localName || elem.tagName?.endsWith(':' + localName)) {
|
||||
return elem
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const getElementTextByLocalName = (parent, localName) => {
|
||||
const element = getElementByLocalName(parent, localName)
|
||||
return element?.textContent || ''
|
||||
}
|
||||
|
||||
// 获取所有 response 节点
|
||||
const allElements = xmlDoc.getElementsByTagName('*')
|
||||
const responses = []
|
||||
for (let i = 0; i < allElements.length; i++) {
|
||||
const elem = allElements[i]
|
||||
const elemLocalName = elem.localName || elem.tagName?.split(':').pop() || elem.tagName
|
||||
if (elemLocalName === 'response' || elem.tagName === 'response' || elem.tagName?.endsWith(':response')) {
|
||||
responses.push(elem)
|
||||
}
|
||||
}
|
||||
|
||||
const fileList = []
|
||||
|
||||
responses.forEach((response) => {
|
||||
try {
|
||||
const href = getElementTextByLocalName(response, 'href')
|
||||
|
||||
// 跳过根路径
|
||||
if (href === '/' || !href) {
|
||||
return
|
||||
}
|
||||
|
||||
const propstat = getElementByLocalName(response, 'propstat')
|
||||
if (!propstat) return
|
||||
|
||||
const prop = getElementByLocalName(propstat, 'prop')
|
||||
if (!prop) return
|
||||
|
||||
// 检查是否为集合(文件夹)
|
||||
const resourcetype = getElementByLocalName(prop, 'resourcetype')
|
||||
const collectionNode = resourcetype ? getElementByLocalName(resourcetype, 'collection') : null
|
||||
const isCollection = collectionNode !== null
|
||||
|
||||
// 获取文件大小
|
||||
const getcontentlengthText = getElementTextByLocalName(prop, 'getcontentlength')
|
||||
const size = getcontentlengthText ? parseInt(getcontentlengthText, 10) : 0
|
||||
|
||||
// 获取修改时间
|
||||
const lastModified = getElementTextByLocalName(prop, 'getlastmodified')
|
||||
|
||||
// 获取内容类型
|
||||
const getcontenttypeText = getElementTextByLocalName(prop, 'getcontenttype')
|
||||
const contentType = getcontenttypeText || (isCollection ? 'directory' : 'application/octet-stream')
|
||||
|
||||
// 从路径中提取文件名和相对路径
|
||||
// href 格式可能是: /folda/ 或 /filea,可能是 URL 编码的
|
||||
// 先对 href 进行 URL 解码
|
||||
let decodedHref = href
|
||||
try {
|
||||
decodedHref = decodeURIComponent(href)
|
||||
} catch (e) {
|
||||
// 如果解码失败,使用原始 href
|
||||
decodedHref = href
|
||||
}
|
||||
|
||||
const cleanHref = decodedHref.replace(/\/$/, '') // 移除末尾斜杠
|
||||
const pathParts = cleanHref.split('/').filter(p => p)
|
||||
const name = pathParts[pathParts.length - 1] || cleanHref || 'unknown'
|
||||
|
||||
// 构建相对路径(相对于 webdav 根目录)
|
||||
// 例如: /folda/ -> folda, /filea -> filea
|
||||
// 注意:相对路径也需要解码后的值
|
||||
const relativePath = pathParts.join('/')
|
||||
|
||||
fileList.push({
|
||||
href: href,
|
||||
name: name,
|
||||
isDirectory: isCollection,
|
||||
size: size,
|
||||
lastModified: lastModified,
|
||||
contentType: contentType,
|
||||
path: relativePath, // 相对路径,例如: "folda" 或 "filea"
|
||||
})
|
||||
} catch (err) {
|
||||
console.warn('解析单个 response 失败:', err)
|
||||
}
|
||||
})
|
||||
|
||||
return fileList
|
||||
} catch (error) {
|
||||
console.error('解析 WebDAV XML 失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 将 WebDAV 文件列表转换为 VueFinder 格式
|
||||
function convertToVueFinderFormat(webdavFiles, basePath = '') {
|
||||
const items = []
|
||||
|
||||
webdavFiles.forEach(file => {
|
||||
// VueFinder 期望的格式:path应该以/开头
|
||||
let itemPath
|
||||
|
||||
if (basePath && basePath !== '' && basePath !== '/') {
|
||||
const normalizedBasePath = basePath.startsWith('/') ? basePath : '/' + basePath
|
||||
itemPath = normalizedBasePath + '/' + file.name
|
||||
} else {
|
||||
// 根目录下的文件,path应该是 /folda 或 /filea
|
||||
itemPath = '/' + file.name
|
||||
}
|
||||
|
||||
const item = {
|
||||
name: file.name,
|
||||
path: itemPath, // 绝对路径,例如: /folda 或 /filea
|
||||
type: file.isDirectory ? 'dir' : 'file',
|
||||
size: file.size || 0,
|
||||
modified: file.lastModified ? new Date(file.lastModified).getTime() : Date.now(),
|
||||
isDirectory: file.isDirectory,
|
||||
}
|
||||
|
||||
items.push(item)
|
||||
})
|
||||
|
||||
// 目录排在前面
|
||||
items.sort((a, b) => {
|
||||
if (a.type === 'dir' && b.type !== 'dir') return -1
|
||||
if (a.type !== 'dir' && b.type === 'dir') return 1
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
|
||||
// 返回的path表示当前目录路径
|
||||
// 对于根目录,path应该是空字符串;对于子目录,path应该是相对路径(不带开头的/)
|
||||
let normalizedPath = basePath || ''
|
||||
if (normalizedPath === '/') {
|
||||
normalizedPath = ''
|
||||
}
|
||||
|
||||
return {
|
||||
items: items,
|
||||
path: normalizedPath, // 当前目录路径,根目录为空字符串
|
||||
}
|
||||
}
|
||||
|
||||
// 创建 WebDAV Driver
|
||||
export function createWebDAVDriver() {
|
||||
const baseURL = getApiBaseUrl() || ''
|
||||
const basePath = `${baseURL}/webdav`
|
||||
|
||||
return {
|
||||
async list(params = {}) {
|
||||
try {
|
||||
// 构建路径,确保 URL 格式为 webdav/ 或 webdav/path/
|
||||
let path = params.path || ''
|
||||
// 移除开头的 /,因为我们要构建相对路径用于API请求
|
||||
if (path.startsWith('/')) {
|
||||
path = path.substring(1)
|
||||
}
|
||||
|
||||
// 如果 path 为空,请求 webdav/,否则请求 webdav/path/
|
||||
const url = path ? `/webdav/${path}${path.endsWith('/') ? '' : '/'}` : '/webdav/'
|
||||
|
||||
// console.log('WebDAV list 请求:', { url, path, params })
|
||||
|
||||
// 发送 PROPFIND 请求,使用 Depth: 1 请求头
|
||||
const response = await request({
|
||||
url: url,
|
||||
method: 'PROPFIND',
|
||||
headers: {
|
||||
'Depth': '1'
|
||||
}
|
||||
})
|
||||
|
||||
// console.log('WebDAV 原始响应:', response)
|
||||
|
||||
// 解析 XML 响应
|
||||
const fileList = parseWebDAVXML(response)
|
||||
// console.log('解析后的文件列表:', fileList)
|
||||
|
||||
// 转换为 VueFinder 格式
|
||||
// normalizedPath用于构建item的path,应该保持原样(不带开头的/)
|
||||
const normalizedPath = path || ''
|
||||
const vueFinderData = convertToVueFinderFormat(fileList, normalizedPath)
|
||||
// console.log('VueFinder 格式数据:', vueFinderData)
|
||||
// console.log('VueFinder items 数量:', vueFinderData.items.length)
|
||||
// console.log('VueFinder items 详情:', vueFinderData.items)
|
||||
|
||||
// 确保返回的数据格式正确
|
||||
if (!vueFinderData.items || !Array.isArray(vueFinderData.items)) {
|
||||
console.error('VueFinder 数据格式错误:', vueFinderData)
|
||||
return { items: [], path: normalizedPath }
|
||||
}
|
||||
|
||||
// 验证每个 item 的格式,确保所有必需字段都存在
|
||||
vueFinderData.items.forEach((item, index) => {
|
||||
if (!item.name || !item.path || !item.type) {
|
||||
console.warn(`Item ${index} 格式不完整:`, item)
|
||||
}
|
||||
// 确保path是字符串格式
|
||||
if (typeof item.path !== 'string') {
|
||||
item.path = String(item.path)
|
||||
}
|
||||
// 确保size是数字
|
||||
if (typeof item.size !== 'number') {
|
||||
item.size = Number(item.size) || 0
|
||||
}
|
||||
})
|
||||
|
||||
console.log('返回给 VueFinder 的数据:', JSON.stringify(vueFinderData, null, 2))
|
||||
console.log('最终返回数据:', vueFinderData)
|
||||
|
||||
return vueFinderData
|
||||
} catch (error) {
|
||||
console.error('WebDAV list 失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
async delete(params) {
|
||||
return request({
|
||||
url: `/webdav/${params.path}`,
|
||||
method: 'DELETE'
|
||||
})
|
||||
},
|
||||
|
||||
async rename(params) {
|
||||
// WebDAV MOVE 方法用于重命名
|
||||
// 对新路径进行 URL 编码(对每个路径段分别编码,保留斜杠)
|
||||
const encodedNewPath = params.newPath
|
||||
.split('/')
|
||||
.map(segment => encodeURIComponent(segment))
|
||||
.join('/')
|
||||
|
||||
return request({
|
||||
url: `/webdav/${params.path}`,
|
||||
method: 'MOVE',
|
||||
headers: {
|
||||
'Destination': `/webdav/${encodedNewPath}`
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
getDownloadUrl(params) {
|
||||
const token = localStorage.getItem('token')
|
||||
const uid = localStorage.getItem('uid')
|
||||
const path = params.path?.startsWith('/') ? params.path.substring(1) : params.path
|
||||
return `${basePath}/${path}?token=${token}&uid=${uid}`
|
||||
},
|
||||
|
||||
getPreviewUrl(params) {
|
||||
return this.getDownloadUrl(params)
|
||||
},
|
||||
|
||||
async search(params) {
|
||||
// 搜索功能可能需要遍历所有目录
|
||||
// 这里先实现简单的列表
|
||||
const result = await this.list({ path: params.path || '' })
|
||||
return result.items.filter(item =>
|
||||
item.name.toLowerCase().includes(params.query.toLowerCase())
|
||||
)
|
||||
},
|
||||
|
||||
async save(params) {
|
||||
// 上传文件使用 PUT 方法
|
||||
return request({
|
||||
url: `/webdav/${params.path}`,
|
||||
method: 'PUT',
|
||||
data: params.content,
|
||||
headers: {
|
||||
'Content-Type': params.contentType || 'application/octet-stream'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user