Private
Public Access
1
0
Files
u-desk/web/src/utils/fileUtils.js
绝尘 72fef3e56f 优化:文件服务器安全重构+编辑器增强+搜索排序+更新面板Markdown渲染
- 路径校验提取validateFilePath+sentinel error替代字符串匹配
- requireUpdateAPI收敛7处重复nil检查
- 端口18765统一为8073,消除分散魔法数字
- CodeMirror添加搜索功能+滚动位置LRU缓存恢复
- 文件列表新增列排序+搜索过滤
- Toolbar重排:快捷访问内嵌+搜索框集成+历史改图标
- 重命名零闪烁:updateFilePath草稿迁移
- changelog用marked渲染+sanitizeHtml防XSS
- MigrateTabConfig扩展map驱动覆盖openclaw-manager→version迁移

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-21 21:53:31 +08:00

273 lines
7.6 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.
/**
* 文件工具函数集合
*
* @module utils/fileUtils
* @description 提供文件相关的通用工具函数,避免代码重复
*/
import { FILE_ICON_MAP, FILE_ICONS, BYTE_UNITS, FILE_SIZE_FORMAT } from './constants'
/**
* 路径分隔符正则(匹配 Windows 和 Unix 风格)
* @type {RegExp}
*/
const PATH_SEPARATOR_REGEX = /[/\\]/
/**
* 规范化路径分隔符(统一为正斜杠)
* @param {string} path - 文件路径
* @returns {string} 规范化后的路径
*/
export const normalizePathSeparators = (path) => {
if (!path) return ''
return path.replace(/\\/g, '/')
}
/**
* HTML 转义,防止 XSS
* @param {string} str - 原始字符串
* @returns {string} 转义后的字符串
*/
export const escapeHtml = (str) => {
if (str == null) return ''
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
/**
* 轻量 HTML 消毒(用于渲染远程 Markdown 等不可信 HTML 片段)
* 移除 script/iframe/object/embed 标签和 on* 事件属性
*/
export const sanitizeHtml = (html) => {
if (!html) return ''
return String(html)
.replace(/<script\b[^<]*(?:<\/script>|$)/gi, '')
.replace(/<iframe\b[^<]*(?:<\/iframe>|$)/gi, '')
.replace(/<object\b[^<]*(?:<\/object>|$)/gi, '')
.replace(/<embed\b[^>]*\/?>/gi, '')
.replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '')
.replace(/\s+on\w+\s*=\s*[^\s>]*/gi, '')
}
/**
* 获取文件扩展名(路径安全)
* @param {string} path - 文件路径
* @returns {string} 扩展名(小写,不含点号)
*/
export const getExt = (path) => {
if (!path) return ''
const dot = path.lastIndexOf('.')
const slash = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
if (dot === -1 || dot < slash) return ''
return path.slice(dot + 1).toLowerCase()
}
/**
* 格式化文件大小
* @param {number} bytes - 文件大小(字节)
* @returns {string} 格式化后的文件大小字符串
*
* @example
* formatBytes(1024) // "1.00 KB"
* formatBytes(1048576) // "1.00 MB"
* formatBytes(0) // "0 B"
*/
export function formatBytes(bytes) {
if (!bytes || bytes === 0) return '0 B'
if (typeof bytes !== 'number' || isNaN(bytes)) return '0 B'
const unit = FILE_SIZE_FORMAT.UNIT
const decimals = FILE_SIZE_FORMAT.DECIMAL_PLACES
if (bytes < unit) return bytes + ' B'
const exp = Math.min(Math.floor(Math.log(bytes) / Math.log(unit)), BYTE_UNITS.length - 1)
const value = bytes / Math.pow(unit, exp)
const unitSymbol = BYTE_UNITS[exp]
return value.toFixed(decimals) + ' ' + unitSymbol
}
/**
* 从文件路径中提取文件名
* @param {string} path - 文件路径
* @returns {string} 文件名
*
* @example
* getFileName('/home/user/docs/file.txt') // "file.txt"
* getFileName('C:\\Users\\user\\file.txt') // "file.txt"
* getFileName('file.txt') // "file.txt"
*/
export function getFileName(path) {
if (!path) return ''
const parts = path.split(PATH_SEPARATOR_REGEX)
return parts[parts.length - 1] || path
}
/**
* 根据文件信息获取对应的图标
* @param {Object} fileInfo - 文件信息对象
* @param {boolean} fileInfo.is_dir - 是否为目录
* @param {string} fileInfo.name - 文件名
* @returns {string} 文件图标emoji
*
* @example
* getFileIcon({ is_dir: true }) // "📁"
* getFileIcon({ is_dir: false, name: 'image.png' }) // "🖼️"
* getFileIcon({ is_dir: false, name: 'document.pdf' }) // "📕"
*/
export function getFileIcon(fileInfo) {
if (!fileInfo) return FILE_ICONS.FILE
// 如果是目录
if (fileInfo.is_dir) {
return FILE_ICONS.FOLDER
}
// 获取文件扩展名
const ext = getExt(fileInfo.name)
// 从映射表中查找图标
return FILE_ICON_MAP.get(ext) || FILE_ICONS.FILE
}
/**
* 规范化文件路径将反斜杠转换为正斜杠并进行URL编码
* @param {string} path - 原始路径
* @param {boolean} encode - 是否进行URL编码用于URL路径
* @returns {string} 规范化后的路径
*
* @example
* normalizeFilePath('C:\\Users\\user\\file.txt') // "C:/Users/user/file.txt"
* normalizeFilePath('/home/user/file.txt') // "/home/user/file.txt"
* normalizeFilePath('E:/中文路径/file.pdf', true) // "E:/%E4%B8%AD%E6%96%87%E8%B7%AF%E5%BE%84/file.pdf"
*/
export function normalizeFilePath(path, encode = false) {
if (!path) return ''
const normalized = normalizePathSeparators(path)
// 如果需要编码,则使用 encodeURIComponent
if (encode) {
const parts = normalized.split('/')
// 只对包含需要编码字符的路径段进行编码
// Windows 路径格式: E:/path/to/file第一部分是盘符如 E:),不应编码
return parts.map((segment, index) => {
// 盘符部分(如 "E:")不编码
if (index === 0 && /^[A-Za-z]:$/.test(segment)) {
return segment
}
// 检查是否需要编码包含非ASCII字符或特殊字符
if (/[^A-Za-z0-9\-_.~]/.test(segment)) {
return encodeURIComponent(segment)
}
return segment
}).join('/')
}
return normalized
}
/**
* 获取路径使用的分隔符Windows 反斜杠或 Unix 正斜杠)
* @param {string} path - 文件路径
* @returns {string} 分隔符 '\\' 或 '/'
*/
export function getPathSeparator(path) {
return path.includes('\\') ? '\\' : '/'
}
/**
* 获取父目录路径
* @param {string} path - 文件路径
* @returns {string} 父目录路径
*
* @example
* getParentPath('/home/user/docs/file.txt') // "/home/user/docs"
* getParentPath('/home/user/docs/') // "/home/user"
*/
export function getParentPath(path) {
if (!path) return ''
const normalizedPath = normalizePathSeparators(path)
const lastSlashIndex = normalizedPath.lastIndexOf('/')
if (lastSlashIndex <= 0) {
if (/^[A-Za-z]:$/.test(normalizedPath)) {
return normalizedPath + '/'
}
return normalizedPath || '/'
}
const parentPath = normalizedPath.substring(0, lastSlashIndex)
// 盘符根目录下文件E:/file.txt → E:/
if (/^[A-Za-z]:$/.test(parentPath)) {
return parentPath + '/'
}
return parentPath || '/'
}
/**
* 文件列表排序:文件夹优先,支持多字段排序
* @param {Array} fileList - 文件列表
* @param {Object} options - 排序选项 { sortBy, sortOrder }
* @returns {Array} 排序后的文件列表
*
* @example
* sortFileList(fileList, { sortBy: 'name', sortOrder: 'asc' })
*/
/**
* 格式化文件修改时间
* @param {string} t - 时间字符串
* @returns {string} 格式化后的时间,如 2026/04/11 14:30
*/
export function formatFileTime(t) {
if (!t) return ''
const d = new Date(t)
if (isNaN(d.getTime())) return t
const pad = n => String(n).padStart(2, '0')
return `${d.getFullYear()}/${pad(d.getMonth()+1)}/${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
}
export function sortFileList(fileList, options = {}) {
if (!Array.isArray(fileList)) return fileList
const { sortBy = 'name', sortOrder = 'asc' } = options
const dir = sortOrder === 'desc' ? 1 : -1
return fileList.sort((a, b) => {
// 文件夹始终排在前面
if (a.isDir !== b.isDir) {
return a.isDir ? -1 : 1
}
let cmp = 0
switch (sortBy) {
case 'size':
cmp = (a.size || 0) - (b.size || 0)
break
case 'type': {
cmp = getExt(a.name).localeCompare(getExt(b.name))
break
}
case 'modified_time': {
const ta = a.modified_time ? new Date(a.modified_time).getTime() : 0
const tb = b.modified_time ? new Date(b.modified_time).getTime() : 0
cmp = ta - tb
break
}
default: // name
cmp = a.name.localeCompare(b.name)
}
return cmp * dir
})
}