feat: 重构VirtualFile和VirtualDirectory的实现

This commit is contained in:
539943419 committed 2025-03-17 20:56:44 +08:00
1 parent 42479f2265
commit 286dd8e386
4 files changed
+282 -166

No files matched your search

+75 -113
View File
@@ -1,10 +1,6 @@
import config from '@/config'
import generateId from '@/utils/generateId' import generateId from '@/utils/generateId'
import useMiaoFetchApi from '@/hooks/useMiaoFetchApi'
import type { stats, file, directory } from '@/types/type.ts' import type { stats, file, directory } from '@/types/type.ts'
import difference from 'lodash/difference'
const miaoFetchApi = useMiaoFetchApi()
// const vDirectoryMap = new Map<string, VirtualDirectory>() // const vDirectoryMap = new Map<string, VirtualDirectory>()
// const vFileMap = new Map<string, VirtualFile>() // const vFileMap = new Map<string, VirtualFile>()
@@ -24,65 +20,63 @@ export interface Tree {
directories: Tree[] directories: Tree[]
} }
export class VirtualFile { export abstract class VirtualFileBase {
constructor(info: file, parent: VirtualDirectory) {
this.name = info.name
this.size = info.size
this.stats = info.stats
this.parent = parent
this.type = 'file'
this.id = generateId()
// if (vFileMap.get(this.path) !== undefined) {
// throw `${this.path} is already exist`
// }
// vFileMap.set(this.path, this)
}
name: string name: string
type: 'file' type: 'file' = 'file'
size: number size: number
stats: stats stats: stats
parent: VirtualDirectory parent: VirtualDirectoryBase
id: number id: number
storage: { [key: string]: any } = {} storage: { [key: string]: any } = {}
constructor(name: string, size: number, stats: stats, parent: VirtualDirectoryBase) {
this.name = name
this.size = size
this.stats = stats
this.parent = parent
this.id = generateId()
}
get path() { get path() {
return `${this.parent.path}${this.name}` return `${this.parent.path}${this.name}`
} }
get url() { abstract get url(): string;
return `${config.api.get}${this.parent.path}${this.name}`
}
// destroy() { /**
// vFileMap.delete(this.path) * 删除文件
// } * @returns {Promise<boolean>} 删除是否成功
*/
abstract delete(): Promise<boolean>;
/**
* 重命名文件
* @param {string} newName 新文件名
* @returns {Promise<boolean>} 重命名是否成功
*/
abstract rename(newName: string): Promise<boolean>;
/**
* 移动文件到目标目录
* @param {VirtualDirectoryBase} targetDir 目标目录
* @returns {Promise<boolean>} 移动是否成功
*/
abstract moveTo(targetDir: VirtualDirectoryBase): Promise<boolean>;
} }
class VirtualDirectory { export abstract class VirtualDirectoryBase {
/**
* 除非为根节点,否则必须设置parent
*/
constructor(info: directory, parent?: VirtualDirectory) {
this.name = info.name
this.stats = info.stats
this.parent = parent ?? undefined
this.id = generateId()
// if (vDirectoryMap.get(this.path) !== undefined) {
// throw `${this.path} is already exist`
// }
// vDirectoryMap.set(this.path, this)
}
// 文件夹名 // 文件夹名
name: string name: string
// 子文件 // 子文件
files: VirtualFile[] = [] files: VirtualFileBase[] = []
// 子目录 // 子目录
directories: VirtualDirectory[] = [] directories: VirtualDirectoryBase[] = []
// 没啥意义,标注一下就是了 // 没啥意义,标注一下就是了
type: 'directory' = 'directory' type: 'directory' = 'directory'
// 文件夹创建时间等杂项属性 // 文件夹创建时间等杂项属性
stats: stats stats: stats
// 父文件夹 // 父文件夹
parent?: VirtualDirectory parent?: VirtualDirectoryBase
_isUpdated: boolean = false _isUpdated: boolean = false
@@ -91,6 +85,13 @@ class VirtualDirectory {
// 用来存储一些标记信息啥的 // 用来存储一些标记信息啥的
storage: { [key: string]: any } = {} storage: { [key: string]: any } = {}
constructor(name: string, stats: stats, parent?: VirtualDirectoryBase) {
this.name = name
this.stats = stats
this.parent = parent
this.id = generateId()
}
/** /**
* 获取相对于根目录的路径的层数 * 获取相对于根目录的路径的层数
* 用于目录下文件定位 * 用于目录下文件定位
@@ -99,6 +100,7 @@ class VirtualDirectory {
get layer(): number { get layer(): number {
return this.parent ? this.parent.layer + 1 : 0 return this.parent ? this.parent.layer + 1 : 0
} }
/** /**
* 相对于根目录的路径, 如: "/", "/123/321/" * 相对于根目录的路径, 如: "/", "/123/321/"
* 用于目录下文件定位 * 用于目录下文件定位
@@ -111,15 +113,13 @@ class VirtualDirectory {
return `${this.parent.path}${this.name}/` return `${this.parent.path}${this.name}/`
} }
get url(): string { abstract get url(): string;
return `${config.api.get}${this.path}/`
}
/** /**
* 获取从根目录开始的按顺序的文件夹 * 获取从根目录开始的按顺序的文件夹
* @returns {VirtualDirectory[]} * @returns {VirtualDirectoryBase[]}
*/ */
get getParents(): VirtualDirectory[] { get getParents(): VirtualDirectoryBase[] {
if (!this.parent) { if (!this.parent) {
return [this] return [this]
} }
@@ -136,9 +136,9 @@ class VirtualDirectory {
/** /**
* 获取 layer 级父文件夹, 如getParent(1)就返回父级 * 获取 layer 级父文件夹, 如getParent(1)就返回父级
* @param {number} layer 需要跳转的层数 * @param {number} layer 需要跳转的层数
* @returns {VirtualDirectory} * @returns {VirtualDirectoryBase}
*/ */
getParent(layer: number): VirtualDirectory { getParent(layer: number): VirtualDirectoryBase {
if (layer <= 0 || !this.parent) { if (layer <= 0 || !this.parent) {
return this return this
} }
@@ -163,77 +163,39 @@ class VirtualDirectory {
} }
} }
// 时间复杂度大爆炸 hasChild(item: VirtualDirectoryBase | VirtualFileBase): boolean {
updateContent(content: (file | directory)[]) {
this._isUpdated = true
const itemNames: string[] = content.map((v) => v.name)
// 删去远程端已不存在的文件与文件夹
const _delete = (target: (VirtualFile | VirtualDirectory)[]) => {
let index = 0
while (index < target.length) {
if (!itemNames.includes(target[index].name)) {
target.splice(index, 1)
} else {
index += 1
}
}
}
const _addFile = (item: file) => {
const nvf =
// vFileMap.get(`${this.path}${item.name}`) ??
new VirtualFile(item, this)
this.files.push(nvf)
}
const _addDirectory = (item: directory) => {
const nvd =
// vDirectoryMap.get(`${this.path}${item.name}/`) ??
new VirtualDirectory(item, this)
this.directories.push(nvd)
}
const newFileNames: string[] = difference(
content.filter((v) => v.type === 'file').map((v) => v.name),
this.files.map((v) => v.name)
)
const newDirectoryNames: string[] = difference(
content.filter((v) => v.type === 'directory').map((v) => v.name),
this.directories.map((v) => v.name)
)
_delete(this.files)
_delete(this.directories)
// 加入新的文件与文件夹
for (let item of content.filter((v) => newFileNames.includes(v.name))) {
_addFile(item as file)
}
for (let item of content.filter((v) =>
newDirectoryNames.includes(v.name)
)) {
_addDirectory(item as directory)
}
}
hasChild(item: VirtualDirectory | VirtualFile): boolean {
if (item.type === 'directory') { if (item.type === 'directory') {
return this.directories?.includes(item) ?? false return this.directories?.includes(item as VirtualDirectoryBase) ?? false
} else { } else {
return this.files?.includes(item) ?? false return this.files?.includes(item as VirtualFileBase) ?? false
} }
} }
// destroy() { abstract updateContent(content: (file | directory)[]): void;
// for(const item of [...this.files, ...this.directories]){ abstract update(): Promise<void>;
// item.destroy()
// }
// vDirectoryMap.delete(this.path)
// }
/** /**
* 调用后重新向远端拉取文件夹信息并进行更新 * 删除目录
* @returns {void} * @returns {Promise<boolean>} 删除是否成功
*/ */
async update(): Promise<void> { abstract delete(): Promise<boolean>;
const data: (file | directory)[] = await miaoFetchApi.get(this)
this.updateContent(data) /**
} * 重命名目录
* @param {string} newName 新目录名
* @returns {Promise<boolean>} 重命名是否成功
*/
abstract rename(newName: string): Promise<boolean>;
/**
* 移动目录到目标目录
* @param {VirtualDirectoryBase} targetDir 目标目录
* @returns {Promise<boolean>} 移动是否成功
*/
abstract mv(targetDir: VirtualDirectoryBase): Promise<boolean>;
} }
export abstract class VirtualFile extends VirtualFileBase {}
export abstract class VirtualDirectory extends VirtualDirectoryBase {}
export default VirtualDirectory export default VirtualDirectory
+185
View File
@@ -0,0 +1,185 @@
import { file, directory } from "@/types/type";
import { VirtualFileBase, VirtualDirectoryBase } from "./VirtualDirectory";
import useMiaoFetchApi from "@/hooks/useMiaoFetchApi";
import config from '@/config'
const miaoFetchApi = useMiaoFetchApi()
export class VirtualRemoteFile extends VirtualFileBase {
constructor(info: file, parent: VirtualDirectoryBase) {
super(info.name, info.size, info.stats, parent)
}
get url() {
return `${config.api.get}${this.parent.path}${this.name}`
}
async delete(): Promise<boolean> {
try {
const { response } = miaoFetchApi.delete(this, {
retry: 5
});
const id = (await response).eventId;
const { response: res } = miaoFetchApi.query(id);
const eventResult = await res;
return eventResult.status === 'success';
} catch (error) {
console.error('删除文件失败:', error);
return false;
}
}
async rename(newName: string): Promise<boolean> {
if (!newName) {
return false;
}
try {
const { response } = miaoFetchApi.rename(this, newName, {
retry: 5
});
const id = (await response).eventId;
const { response: res } = miaoFetchApi.query(id);
const eventResult = await res;
return eventResult.status === 'success';
} catch (error) {
console.error('重命名文件失败:', error);
return false;
}
}
async moveTo(targetDir: VirtualDirectoryBase): Promise<boolean> {
if (this.parent === targetDir) {
return false; // 已经在目标目录中
}
try {
const _from = this.parent;
const { response } = miaoFetchApi.cut(this, targetDir);
const id = (await response).eventId;
const { response: res } = miaoFetchApi.query(id);
const eventResult = await res;
const success = eventResult.status === 'success';
if (success) {
_from.update();
}
return success;
} catch (error) {
console.error('移动文件失败:', error);
return false;
}
}
}
export class VirtualRemoteDirectory extends VirtualDirectoryBase {
constructor(info: directory, parent?: VirtualDirectoryBase) {
super(info.name, info.stats, parent)
}
get url(): string {
return `${config.api.get}${this.path}/`
}
updateContent(content: (file | directory)[]) {
this._isUpdated = true
// 创建名称到内容项的映射,O(n)
const contentMap = new Map<string, file | directory>();
content.forEach(item => contentMap.set(item.name, item));
// 使用Set存储所有内容项的名称,用于O(1)查找,O(n)
const itemNamesSet = new Set(content.map(v => v.name));
// 删除不再存在的文件和目录,O(m)
this.files = this.files.filter(file => itemNamesSet.has(file.name));
this.directories = this.directories.filter(dir => itemNamesSet.has(dir.name));
// 创建现有文件和目录的名称集合,O(m)
const existingFileNames = new Set(this.files.map(file => file.name));
const existingDirNames = new Set(this.directories.map(dir => dir.name));
// 添加新文件和目录,O(n)
content.forEach(item => {
// 如果是文件且不存在于现有文件中
if (item.type === 'file' && !existingFileNames.has(item.name)) {
const nvf = new VirtualRemoteFile(item as file, this);
this.files.push(nvf);
}
// 如果是目录且不存在于现有目录中
else if (item.type === 'directory' && !existingDirNames.has(item.name)) {
const nvd = new VirtualRemoteDirectory(item as directory, this);
this.directories.push(nvd);
}
});
}
async update(): Promise<void> {
const data: (file | directory)[] = await miaoFetchApi.get(this)
this.updateContent(data)
}
async delete(): Promise<boolean> {
try {
const { response } = miaoFetchApi.delete(this, {
retry: 5
});
const id = (await response).eventId;
const { response: res } = miaoFetchApi.query(id);
const eventResult = await res;
return eventResult.status === 'success';
} catch (error) {
console.error('删除目录失败:', error);
return false;
}
}
async rename(newName: string): Promise<boolean> {
if (!newName) {
return false;
}
try {
const { response } = miaoFetchApi.rename(this, newName, {
retry: 5
});
const id = (await response).eventId;
const { response: res } = miaoFetchApi.query(id);
const eventResult = await res;
return eventResult.status === 'success';
} catch (error) {
console.error('重命名目录失败:', error);
return false;
}
}
async mv(targetDir: VirtualDirectoryBase): Promise<boolean> {
// 不能移动到自己或自己的子目录
if (targetDir === this || this.getParents.includes(targetDir as VirtualDirectoryBase)) {
return false;
}
if (this.parent === targetDir) {
return false; // 已经在目标目录中
}
try {
const _from = this.parent;
const { response } = miaoFetchApi.cut(this, targetDir);
const id = (await response).eventId;
const { response: res } = miaoFetchApi.query(id);
const eventResult = await res;
const success = eventResult.status === 'success';
if (success && _from) {
_from.update();
}
return success;
} catch (error) {
console.error('移动目录失败:', error);
return false;
}
}
}
+2 -2
View File
@@ -1,8 +1,8 @@
import VirtualDirectory from '@/class/VirtualDirectory' import { VirtualRemoteDirectory } from '@/class/VirtualRemoteDirectory'
import { reactive } from 'vue' import { reactive } from 'vue'
const root = reactive( const root = reactive(
new VirtualDirectory({ new VirtualRemoteDirectory({
name: '根目录', name: '根目录',
stats: { stats: {
atimeMs: 0, atimeMs: 0,
+20 -51
View File
@@ -441,18 +441,12 @@ const handleItemDownload = (item: VirtualFile) => {
const handleItemDelete = async (_item: VirtualDirectory | VirtualFile) => { const handleItemDelete = async (_item: VirtualDirectory | VirtualFile) => {
for (let item of uniq([...selectedItem.value, _item])) { for (let item of uniq([...selectedItem.value, _item])) {
;(async () => { (async () => {
const { response } = miaoFetchApi.delete(item, { const success = await item.delete();
retry: 5 if (success) {
}) reload();
// todo: 将删除事件统一用一个事件管理中心管理
const id = (await response).eventId
const { response: res } = miaoFetchApi.query(id)
const eventResult = await res
if (eventResult.status === 'success') {
reload()
} }
})() })();
} }
} }
@@ -483,14 +477,10 @@ const handleItemRename = async (item: VirtualDirectory | VirtualFile) => {
if (!newName) { if (!newName) {
return return
} }
const { response } = miaoFetchApi.rename(item, newName, {
retry: 5 const success = await item.rename(newName);
}) if (success) {
const id = (await response).eventId reload();
const { response: res } = miaoFetchApi.query(id)
const eventResult = await res
if (eventResult.status === 'success') {
reload()
} }
} }
@@ -520,46 +510,25 @@ const handleDropFiles = async (files: File[]) => {
} }
const handleDropVDirectory = async (vDirs: VirtualDirectory[]) => { const handleDropVDirectory = async (vDirs: VirtualDirectory[]) => {
// 不能移到自己或自己的子文件夹内 // 不能移到自己或自己的子文件夹内 - 这部分逻辑已经移到类的实现中
for (let vDir of vDirs) {
if (currentDirectory.value.getParents.includes(vDir)) {
return
}
if (vDir.parent === currentDirectory.value) {
return
}
}
for (let dir of vDirs) { for (let dir of vDirs) {
;(async () => { (async () => {
const _from = dir.parent const success = await dir.mv(currentDirectory.value);
const { response } = miaoFetchApi.cut(dir, currentDirectory.value) if (success) {
const id = (await response).eventId reload();
const { response: res } = miaoFetchApi.query(id)
const eventResult = await res
if (eventResult.status === 'success') {
reload()
_from && _from.update()
} }
})() })();
} }
} }
const handleDropVirtualFiles = (files: VirtualFile[]) => { const handleDropVirtualFiles = (files: VirtualFile[]) => {
if (files[0].parent === currentDirectory.value) {
return
}
for (let file of files) { for (let file of files) {
;(async () => { (async () => {
const _from = file.parent const success = await file.moveTo(currentDirectory.value);
const { response } = miaoFetchApi.cut(file, currentDirectory.value) if (success) {
const id = (await response).eventId reload();
const { response: res } = miaoFetchApi.query(id)
const eventResult = await res
if (eventResult.status === 'success') {
reload()
_from.update()
} }
})() })();
} }
} }