Files
xmc-Assets/web/src/api/asset/webdavDriver.js
caopeng a254aae503 feat(web): 引入 Vite 前端应用并扩展仓库忽略规则
将整套 web 源码纳入仓库,并为 web/node_modules、构建产物及本地环境文件配置 .gitignore,同时移除占位用的 assets/.gitkeep。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-17 15:22:29 +08:00

344 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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'
}
})
}
}
}