将整套 web 源码纳入仓库,并为 web/node_modules、构建产物及本地环境文件配置 .gitignore,同时移除占位用的 assets/.gitkeep。 Co-authored-by: Cursor <cursoragent@cursor.com>
344 lines
14 KiB
JavaScript
344 lines
14 KiB
JavaScript
// 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'
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|