From 3621d1952794478e4ac55f2385f2e63519d936ee Mon Sep 17 00:00:00 2001 From: Jorylee <1747358809@qq.com> Date: Thu, 23 Jan 2025 22:44:18 +0800 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=83=A8?= =?UTF-8?q?=E5=88=86=E5=9F=BA=E7=A1=80=E6=96=87=E4=BB=B6=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E6=A1=86=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/storage/FileSystemFactory.ts | 15 ++++++ src/core/storage/IFileSystem.ts | 24 +++++++++ src/core/storage/TauriFileSystem.ts.tmp | 66 +++++++++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 src/core/storage/FileSystemFactory.ts create mode 100644 src/core/storage/IFileSystem.ts create mode 100644 src/core/storage/TauriFileSystem.ts.tmp diff --git a/src/core/storage/FileSystemFactory.ts b/src/core/storage/FileSystemFactory.ts new file mode 100644 index 00000000..75bac99b --- /dev/null +++ b/src/core/storage/FileSystemFactory.ts @@ -0,0 +1,15 @@ +import { IFileSystem } from "./IFileSystem"; +import { TauriFileSystem } from "./TauriFileSystem"; + +export type StorageType = "tauri" | "memory"; + +export class FileSystemFactory { + static create(storageType: StorageType = "tauri"): IFileSystem { + switch (storageType) { + case "tauri": + return new TauriFileSystem(); + default: + throw new Error(`Unsupported storage type: ${storageType}`); + } + } +} diff --git a/src/core/storage/IFileSystem.ts b/src/core/storage/IFileSystem.ts new file mode 100644 index 00000000..b137d773 --- /dev/null +++ b/src/core/storage/IFileSystem.ts @@ -0,0 +1,24 @@ +export interface FileStats { + isFile: boolean; + isDirectory: boolean; + size: number; + modified: Date; +} + +export interface DirectoryEntry { + name: string; + isFile: boolean; + isDirectory: boolean; +} + +export interface IFileSystem { + readFile(path: string): Promise; + writeFile(path: string, content: Uint8Array | string): Promise; + readDir(path: string): Promise; + mkdir(path: string, recursive?: boolean): Promise; + stat(path: string): Promise; + rename(oldPath: string, newPath: string): Promise; + deleteFile(path: string): Promise; + deleteDirectory(path: string): Promise; + exists(path: string): Promise; +} diff --git a/src/core/storage/TauriFileSystem.ts.tmp b/src/core/storage/TauriFileSystem.ts.tmp new file mode 100644 index 00000000..10a93815 --- /dev/null +++ b/src/core/storage/TauriFileSystem.ts.tmp @@ -0,0 +1,66 @@ +import { + readBinaryFile, + writeFile, + readDir, + createDir, + renameFile, + removeFile, + removeDir, + exists as tauriExists, + type FileEntry, +} from "@tauri-apps/plugin-fs"; +import { metadata } from "@tauri-apps/api/fs"; +import { DirectoryEntry, FileStats } from "./IFileSystem"; + +export class TauriFileSystem implements IFileSystem { + async readFile(path: string): Promise { + return await readBinaryFile(path); + } + + async writeFile(path: string, content: Uint8Array | string): Promise { + const options = + content instanceof Uint8Array + ? { contents: content } + : { contents: content }; + await writeFile({ path, ...options }); + } + + async readDir(dirPath: string): Promise { + const entries = await readDir(dirPath); + return entries.map((entry) => ({ + name: entry.name || "", + isFile: entry.children === undefined, + isDirectory: entry.children !== undefined, + })); + } + + async mkdir(dirPath: string, recursive = false): Promise { + await createDir(dirPath, { recursive }); + } + + async stat(filePath: string): Promise { + const md = await metadata(filePath); + return { + isFile: md.isFile, + isDirectory: md.isDir, + size: md.size || 0, + modified: new Date(md.modifiedAt || 0), + }; + } + + async rename(oldPath: string, newPath: string): Promise { + await renameFile(oldPath, newPath); + } + + async deleteFile(filePath: string): Promise { + await removeFile(filePath); + } + + async deleteDirectory(dirPath: string): Promise { + await removeDir(dirPath, { recursive: true }); + } + + async exists(path: string): Promise { + return await tauriExists(path); + } +} From 507cc231021530016607dc9f26141b274b43ffb2 Mon Sep 17 00:00:00 2001 From: Jorylee <1747358809@qq.com> Date: Sat, 25 Jan 2025 22:09:42 +0800 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=86=85?= =?UTF-8?q?=E9=83=A8indexedDB=E5=AD=98=E5=82=A8=E4=BB=A5=E5=8F=8Atauri?= =?UTF-8?q?=E5=AD=98=E5=82=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/lib.rs | 141 ++++--- src/core/storage/IFileSystem.ts | 24 -- src/core/storage/TauriFileSystem.ts.tmp | 66 ---- .../storage => utils/fs}/FileSystemFactory.ts | 0 src/utils/fs/IFileSystem.ts | 28 ++ src/utils/fs/IndexedDBFileSystem.ts | 351 ++++++++++++++++++ src/utils/fs/TauriFileSystem.ts | 111 ++++++ 7 files changed, 578 insertions(+), 143 deletions(-) delete mode 100644 src/core/storage/IFileSystem.ts delete mode 100644 src/core/storage/TauriFileSystem.ts.tmp rename src/{core/storage => utils/fs}/FileSystemFactory.ts (100%) create mode 100644 src/utils/fs/IFileSystem.ts create mode 100644 src/utils/fs/IndexedDBFileSystem.ts create mode 100644 src/utils/fs/TauriFileSystem.ts diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b6eba25b..3c4b1efe 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,77 +1,111 @@ -use std::env; -use std::io::Read; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::Path; +use std::time::UNIX_EPOCH; +use tauri::Manager; -use base64::engine::general_purpose; -use base64::Engine; +#[derive(Debug, Serialize, Deserialize)] +struct FileStats { + name: String, + #[serde(rename = "isDir")] + is_directory: bool, + size: u64, + modified: i64, // milliseconds since epoch +} -use tauri::Manager; +#[derive(Debug, Serialize, Deserialize)] +struct DirectoryEntry { + name: String, + #[serde(rename = "isDir")] + is_directory: bool, +} -/// 判断文件是否存在 #[tauri::command] -fn exists(path: String) -> bool { - std::path::Path::new(&path).exists() +async fn read_file(path: String) -> Result, String> { + std::fs::read(&path).map_err(|e| e.to_string()) } -/// 读取文件,返回字符串 #[tauri::command] -fn read_text_file(path: String) -> String { - let mut file = std::fs::File::open(path).unwrap(); - let mut contents = String::new(); - file.read_to_string(&mut contents).unwrap(); - contents +async fn write_file(path: String, content: Vec) -> Result<(), String> { + fs::write(path, content).map_err(|e| e.to_string()) } -/// 读取文件,返回base64 #[tauri::command] -fn read_file_base64(path: String) -> Result { - Ok(general_purpose::STANDARD - .encode(&std::fs::read(path).map_err(|e| format!("无法读取文件: {}", e))?)) +async fn read_dir(path: String) -> Result, String> { + let entries = fs::read_dir(path).map_err(|e| e.to_string())?; + let mut result = Vec::new(); + + for entry in entries { + let entry = entry.map_err(|e| e.to_string())?; + let metadata = entry.metadata().map_err(|e| e.to_string())?; + + result.push(DirectoryEntry { + name: entry.file_name().to_string_lossy().to_string(), + is_directory: metadata.is_dir(), + }); + } + + Ok(result) } -/// 写入文件 #[tauri::command] -fn write_text_file(path: String, content: String) -> Result<(), String> { - std::fs::write(path, content).map_err(|e| e.to_string())?; - Ok(()) +async fn mkdir(path: String, recursive: bool) -> Result<(), String> { + if recursive { + fs::create_dir_all(path).map_err(|e| e.to_string()) + } else { + fs::create_dir(path).map_err(|e| e.to_string()) + } +} + +#[tauri::command] +async fn stat(path: String) -> Result { + let metadata = fs::metadata(&path).map_err(|e| e.to_string())?; + + // 获取文件名 + let name = Path::new(&path) + .file_name() + .and_then(|n| n.to_str()) + .map(|s| s.to_string()) + .ok_or_else(|| "无法解析文件名".to_string())?; + + // 转换修改时间 + let modified = metadata + .modified() + .map_err(|e| e.to_string())? + .duration_since(UNIX_EPOCH) + .map_err(|e| e.to_string())? + .as_millis() as i64; + + Ok(FileStats { + name, // 新增的name字段 + is_directory: metadata.is_dir(), + size: metadata.len(), + modified, + }) } -/// 写入文件,base64字符串 #[tauri::command] -fn write_file_base64(content: String, path: String) -> Result<(), String> { - std::fs::write( - &path, - &general_purpose::STANDARD - .decode(content) - .map_err(|e| format!("解码失败: {}", e))?, - ) - .map_err(|e| { - eprintln!("写入文件失败: {}", e); - return e.to_string(); - })?; - Ok(()) +async fn rename(old_path: String, new_path: String) -> Result<(), String> { + fs::rename(old_path, new_path).map_err(|e| e.to_string()) } #[tauri::command] -fn write_stdout(content: String) { - println!("{}", content); +async fn delete_file(path: String) -> Result<(), String> { + fs::remove_file(path).map_err(|e| e.to_string()) } #[tauri::command] -fn write_stderr(content: String) { - eprintln!("{}", content); +async fn delete_directory(path: String) -> Result<(), String> { + fs::remove_dir_all(path).map_err(|e| e.to_string()) } #[tauri::command] -fn exit(code: i32) { - std::process::exit(code); +async fn exists(path: String) -> Result { + Ok(fs::metadata(path).is_ok()) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - // 在 Linux 上禁用 DMA-BUF 渲染器 - // 否则无法在 Linux 上运行 - // 相同的bug: https://github.com/tauri-apps/tauri/issues/10702 - // 解决方案来源: https://github.com/clash-verge-rev/clash-verge-rev/blob/ae5b2cfb79423c7e76a281725209b812774367fa/src-tauri/src/lib.rs#L27-L28 #[cfg(target_os = "linux")] std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); @@ -98,14 +132,15 @@ pub fn run() { Ok(()) }) .invoke_handler(tauri::generate_handler![ - read_text_file, - write_text_file, - exists, - read_file_base64, - write_file_base64, - write_stdout, - write_stderr, - exit + read_file, + write_file, + read_dir, + mkdir, + stat, + rename, + delete_file, + delete_directory, + exists ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/core/storage/IFileSystem.ts b/src/core/storage/IFileSystem.ts deleted file mode 100644 index b137d773..00000000 --- a/src/core/storage/IFileSystem.ts +++ /dev/null @@ -1,24 +0,0 @@ -export interface FileStats { - isFile: boolean; - isDirectory: boolean; - size: number; - modified: Date; -} - -export interface DirectoryEntry { - name: string; - isFile: boolean; - isDirectory: boolean; -} - -export interface IFileSystem { - readFile(path: string): Promise; - writeFile(path: string, content: Uint8Array | string): Promise; - readDir(path: string): Promise; - mkdir(path: string, recursive?: boolean): Promise; - stat(path: string): Promise; - rename(oldPath: string, newPath: string): Promise; - deleteFile(path: string): Promise; - deleteDirectory(path: string): Promise; - exists(path: string): Promise; -} diff --git a/src/core/storage/TauriFileSystem.ts.tmp b/src/core/storage/TauriFileSystem.ts.tmp deleted file mode 100644 index 10a93815..00000000 --- a/src/core/storage/TauriFileSystem.ts.tmp +++ /dev/null @@ -1,66 +0,0 @@ -import { - readBinaryFile, - writeFile, - readDir, - createDir, - renameFile, - removeFile, - removeDir, - exists as tauriExists, - type FileEntry, -} from "@tauri-apps/plugin-fs"; -import { metadata } from "@tauri-apps/api/fs"; -import { DirectoryEntry, FileStats } from "./IFileSystem"; - -export class TauriFileSystem implements IFileSystem { - async readFile(path: string): Promise { - return await readBinaryFile(path); - } - - async writeFile(path: string, content: Uint8Array | string): Promise { - const options = - content instanceof Uint8Array - ? { contents: content } - : { contents: content }; - await writeFile({ path, ...options }); - } - - async readDir(dirPath: string): Promise { - const entries = await readDir(dirPath); - return entries.map((entry) => ({ - name: entry.name || "", - isFile: entry.children === undefined, - isDirectory: entry.children !== undefined, - })); - } - - async mkdir(dirPath: string, recursive = false): Promise { - await createDir(dirPath, { recursive }); - } - - async stat(filePath: string): Promise { - const md = await metadata(filePath); - return { - isFile: md.isFile, - isDirectory: md.isDir, - size: md.size || 0, - modified: new Date(md.modifiedAt || 0), - }; - } - - async rename(oldPath: string, newPath: string): Promise { - await renameFile(oldPath, newPath); - } - - async deleteFile(filePath: string): Promise { - await removeFile(filePath); - } - - async deleteDirectory(dirPath: string): Promise { - await removeDir(dirPath, { recursive: true }); - } - - async exists(path: string): Promise { - return await tauriExists(path); - } -} diff --git a/src/core/storage/FileSystemFactory.ts b/src/utils/fs/FileSystemFactory.ts similarity index 100% rename from src/core/storage/FileSystemFactory.ts rename to src/utils/fs/FileSystemFactory.ts diff --git a/src/utils/fs/IFileSystem.ts b/src/utils/fs/IFileSystem.ts new file mode 100644 index 00000000..ad283757 --- /dev/null +++ b/src/utils/fs/IFileSystem.ts @@ -0,0 +1,28 @@ +export interface FileStats { + name: string; + isDir: boolean; + size: number; + modified: Date; +} + +export interface DirectoryEntry { + name: string; + isDir: boolean; +} + +// 强制使用 `/` 作为分隔符 +export abstract class IFileSystem { + abstract readFile(path: string): Promise; + abstract writeFile(path: string, content: Uint8Array | string): Promise; + abstract readDir(path: string): Promise; + abstract mkdir(path: string, recursive?: boolean): Promise; + abstract stat(path: string): Promise; + abstract rename(oldPath: string, newPath: string): Promise; + abstract deleteFile(path: string): Promise; + abstract deleteDirectory(path: string): Promise; + abstract exists(path: string): Promise; + async readTextFile(path: string) { + const content = await this.readFile(path); + return new TextDecoder("utf-8").decode(content); + } +} diff --git a/src/utils/fs/IndexedDBFileSystem.ts b/src/utils/fs/IndexedDBFileSystem.ts new file mode 100644 index 00000000..c8fc4518 --- /dev/null +++ b/src/utils/fs/IndexedDBFileSystem.ts @@ -0,0 +1,351 @@ +import { + IFileSystem, + type FileStats, + type DirectoryEntry, +} from "./IFileSystem"; + +const DB_VERSION = 1; + +export class IndexedDBFileSystem extends IFileSystem { + private db: IDBDatabase | null = null; + private DIR_STORE_NAME: string; + constructor( + private DB_NAME: string, + private STORE_NAME: string, + ) { + super(); + this.DIR_STORE_NAME = STORE_NAME + "_DIR"; + this.initDB(); + } + + private async initDB(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(this.DB_NAME, DB_VERSION); + + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + console.log( + this, + db.objectStoreNames.contains(this.STORE_NAME), + db.objectStoreNames.contains(this.DIR_STORE_NAME), + ); + if (!db.objectStoreNames.contains(this.STORE_NAME)) { + db.createObjectStore(this.STORE_NAME, { keyPath: "path" }); + } + if (!db.objectStoreNames.contains(this.DIR_STORE_NAME)) { + db.createObjectStore(this.DIR_STORE_NAME, { keyPath: "path" }); + } + }; + + request.onsuccess = (event) => { + this.db = (event.target as IDBOpenDBRequest).result; + resolve(); + }; + + request.onerror = (event) => { + reject((event.target as IDBOpenDBRequest).error); + }; + }); + } + + private async getTransaction( + storeNames: string | string[], + mode: IDBTransactionMode = "readonly", + ): Promise { + if (!this.db) { + await this.initDB(); + } + return this.db!.transaction(storeNames, mode); + } + + private async getStore( + storeName: string, + mode: IDBTransactionMode = "readonly", + ): Promise { + const transaction = await this.getTransaction(storeName, mode); + return transaction.objectStore(storeName); + } + + async exists(path: string): Promise { + if (!this.db) { + await this.initDB(); + } + const transaction = await this.getTransaction( + [this.STORE_NAME, this.DIR_STORE_NAME], + "readwrite", + ); + const filesStore = transaction.objectStore(this.STORE_NAME); + const dirsStore = transaction.objectStore(this.DIR_STORE_NAME); + + return new Promise((resolve) => { + const request = filesStore.get(path); + request.onsuccess = () => { + if (request.result) { + resolve(true); + } else { + const dirRequest = dirsStore.get(path); + dirRequest.onsuccess = () => resolve(!!dirRequest.result); + dirRequest.onerror = () => resolve(false); + } + }; + request.onerror = () => resolve(false); + }); + } + + async readFile(path: string): Promise { + const store = await this.getStore(this.STORE_NAME); + return new Promise((resolve, reject) => { + const request = store.get(path); + request.onsuccess = () => { + if (request.result) { + resolve(request.result.content); + } else { + reject(new Error(`File not found: ${path}`)); + } + }; + request.onerror = () => reject(request.error); + }); + } + + async writeFile(path: string, content: Uint8Array | string): Promise { + const store = await this.getStore(this.STORE_NAME, "readwrite"); + const data = + typeof content === "string" ? new TextEncoder().encode(content) : content; + + return new Promise((resolve, reject) => { + const request = store.put({ path, content: data }); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + } + + async readDir(path: string): Promise { + const store = await this.getStore(this.DIR_STORE_NAME); + return new Promise((resolve, reject) => { + const request = store.get(path); + request.onsuccess = () => { + if (request.result) { + resolve(request.result.entries); + } else { + resolve([]); + } + }; + request.onerror = () => reject(request.error); + }); + } + + async mkdir(path: string, recursive = false): Promise { + if (!recursive) { + const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); + return new Promise((resolve, reject) => { + const request = store.put({ path, entries: [] }); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + } + + // 递归创建父目录 + const parts = path.split("/"); + let currentPath = ""; + const pathsToCreate: string[] = []; + + // 收集需要创建的路径 + for (const part of parts) { + if (!part) continue; + currentPath = currentPath ? `${currentPath}/${part}` : part; + if (!(await this.exists(currentPath))) { + pathsToCreate.push(currentPath); + } + } + + // 在单个事务中创建所有目录 + if (pathsToCreate.length > 0) { + const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); + return new Promise((resolve, reject) => { + let completed = 0; + let error: IDBRequest | null = null; + + for (const path of pathsToCreate) { + const request = store.put({ path, entries: [] }); + request.onsuccess = () => { + completed++; + if (completed === pathsToCreate.length && !error) { + resolve(); + } + }; + request.onerror = () => { + if (!error) { + error = request; + reject(request.error); + } + }; + } + }); + } + } + + async stat(path: string): Promise { + const filesStore = await this.getStore(this.STORE_NAME); + const dirsStore = await this.getStore(this.DIR_STORE_NAME); + + return new Promise((resolve, reject) => { + const fileRequest = filesStore.get(path); + fileRequest.onsuccess = () => { + if (fileRequest.result) { + resolve({ + name: path.split("/").pop() || "", + isDir: false, + size: fileRequest.result.content.byteLength, + modified: new Date(), + }); + } else { + const dirRequest = dirsStore.get(path); + dirRequest.onsuccess = () => { + if (dirRequest.result) { + resolve({ + name: path.split("/").pop() || "", + isDir: true, + size: 0, + modified: new Date(), + }); + } else { + reject(new Error(`Path not found: ${path}`)); + } + }; + } + }; + }); + } + + async rename(oldPath: string, newPath: string): Promise { + const filesStore = await this.getStore(this.STORE_NAME, "readwrite"); + const dirsStore = await this.getStore(this.DIR_STORE_NAME, "readwrite"); + + return new Promise((resolve, reject) => { + const moveFile = () => { + const request = filesStore.get(oldPath); + request.onsuccess = () => { + if (request.result) { + const content = request.result.content; + const deleteRequest = filesStore.delete(oldPath); + deleteRequest.onsuccess = () => { + const putRequest = filesStore.put({ path: newPath, content }); + putRequest.onsuccess = () => resolve(); + putRequest.onerror = () => reject(putRequest.error); + }; + } else { + moveDir(); + } + }; + }; + + const moveDir = () => { + const request = dirsStore.get(oldPath); + request.onsuccess = () => { + if (request.result) { + const entries = request.result.entries; + const deleteRequest = dirsStore.delete(oldPath); + deleteRequest.onsuccess = () => { + const putRequest = dirsStore.put({ path: newPath, entries }); + putRequest.onsuccess = () => resolve(); + putRequest.onerror = () => reject(putRequest.error); + }; + } else { + reject(new Error(`Path not found: ${oldPath}`)); + } + }; + }; + + moveFile(); + }); + } + + async deleteFile(path: string): Promise { + const store = await this.getStore(this.STORE_NAME, "readwrite"); + return new Promise((resolve, reject) => { + const request = store.delete(path); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + } + + async deleteDirectory(path: string): Promise { + const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); + return new Promise((resolve, reject) => { + const request = store.delete(path); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + } + + /** + * 验证文件内容是否匹配 + * @param fs 文件系统实例 + * @param path 文件路径 + * @param expectedContent 预期内容 + */ + private static async verifyFileContent( + fs: IndexedDBFileSystem, + path: string, + expectedContent: string, + ): Promise { + const content = await fs.readFile(path); + const actualContent = new TextDecoder("utf-8").decode(content); + if (actualContent !== expectedContent) { + throw new Error( + `File content verification failed at ${path}\n` + + `Expected: ${expectedContent}\n` + + `Actual: ${actualContent}`, + ); + } + } + + /** + * 测试IndexedDB文件系统功能 + * @param dbName 测试数据库名称 + * @param storeName 测试存储名称 + */ + static async testFileSystem( + dbName: string, + storeName: string, + ): Promise { + const fs = new IndexedDBFileSystem(dbName, storeName); + + // 初始化数据库 + await fs.initDB(); + + // 测试目录操作 + const testDirPath = "/test-dir"; + await fs.mkdir(testDirPath, true); + console.log(`Created directory: ${testDirPath}`); + + // 测试文件操作 + const testFilePath = `${testDirPath}/test.txt`; + const testContent = "Hello IndexedDB File System!"; + + // 写入前验证文件不存在 + if (await fs.exists(testFilePath)) { + throw new Error(`File already exists: ${testFilePath}`); + } + + // 写入文件 + await fs.writeFile(testFilePath, testContent); + console.log(`Wrote file: ${testFilePath}`); + + // 写入后验证内容 + await this.verifyFileContent(fs, testFilePath, testContent); + console.log(`Verified file content: ${testFilePath}`); + + // 删除文件 + await fs.deleteFile(testFilePath); + console.log(`Deleted file: ${testFilePath}`); + + // 删除目录 + await fs.deleteDirectory(testDirPath); + console.log(`Deleted directory: ${testDirPath}`); + + // 清理测试数据库 + indexedDB.deleteDatabase(dbName); + console.log(`Cleaned up test database: ${dbName}`); + } +} diff --git a/src/utils/fs/TauriFileSystem.ts b/src/utils/fs/TauriFileSystem.ts new file mode 100644 index 00000000..a7435f31 --- /dev/null +++ b/src/utils/fs/TauriFileSystem.ts @@ -0,0 +1,111 @@ +import { invoke } from "@tauri-apps/api/core"; +import { IFileSystem, FileStats, DirectoryEntry } from "./IFileSystem"; + +/** + * Tauri 文件系统工具类 + */ +export class TauriFileSystem extends IFileSystem { + async exists(path: string): Promise { + return invoke("exists", { path }); + } + + async readFile(path: string): Promise { + return new Uint8Array(await invoke("read_file", { path })); + } + + async writeFile(path: string, content: Uint8Array | string): Promise { + let data: Uint8Array; + if (typeof content === "string") { + data = new TextEncoder().encode(content); + } else { + data = content; + } + return invoke("write_file", { + path, + content: Array.from(data), + }); + } + + async mkdir(path: string, recursive = false): Promise { + return invoke("mkdir", { path, recursive }); + } + + async stat(path: string): Promise { + return invoke("stat", { path }); + } + + async readDir(path: string): Promise { + return invoke("read_dir", { path }); + } + + async rename(oldPath: string, newPath: string): Promise { + return invoke("rename", { oldPath, newPath }); + } + + async deleteFile(path: string): Promise { + return invoke("delete_file", { path }); + } + + async deleteDirectory(path: string): Promise { + return invoke("delete_directory", { path }); + } + + /** + * 验证文件内容是否匹配 + * @param fs 文件系统实例 + * @param path 文件路径 + * @param expectedContent 预期内容 + */ + private static async verifyFileContent( + fs: TauriFileSystem, + path: string, + expectedContent: string, + ): Promise { + const content = await fs.readFile(path); + const actualContent = new TextDecoder("utf-8").decode(content); + if (actualContent !== expectedContent) { + throw new Error( + `File content verification failed at ${path}\n` + + `Expected: ${expectedContent}\n` + + `Actual: ${actualContent}`, + ); + } + } + + /** + * 测试Tauri文件系统功能 + * @param dirPath 要测试的目录路径 + */ + static async testFileSystem(dirPath: string): Promise { + const fs = new TauriFileSystem(); + + // 测试目录操作 + await fs.mkdir(dirPath, true); + console.log(`Created directory: ${dirPath}`); + + // 测试文件操作 + const testFilePath = `${dirPath}/test.txt`; + const testContent = "Hello Tauri File System!"; + + // 写入前验证文件不存在 + if (await fs.exists(testFilePath)) { + throw new Error(`File already exists: ${testFilePath}`); + } + + // 写入文件 + await fs.writeFile(testFilePath, testContent); + console.log(`Wrote file: ${testFilePath}`); + + // 写入后验证内容 + await this.verifyFileContent(fs, testFilePath, testContent); + console.log(`Verified file content: ${testFilePath}`); + + // 删除文件 + await fs.deleteFile(testFilePath); + console.log(`Deleted file: ${testFilePath}`); + + // 删除目录 + await fs.deleteDirectory(dirPath); + console.log(`Deleted directory: ${dirPath}`); + } +} From 8356e70b752978450e58453a5e92eb654391d2a1 Mon Sep 17 00:00:00 2001 From: Jorylee <1747358809@qq.com> Date: Wed, 29 Jan 2025 20:34:41 +0800 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E8=87=B3VFileS?= =?UTF-8?q?ystem?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/lib.rs | 27 ++- src/cli.tsx | 4 +- src/core/service/RecentFileManager.tsx | 2 +- src/core/service/SoundService.tsx | 2 +- src/core/service/VFileSystem.tsx | 97 +++++++++ .../controller/concrete/ControllerCopy.tsx | 37 ++-- .../concrete/ControllerDragFile.tsx | 8 +- src/core/stage/StageSaveManager.tsx | 20 +- .../stage/stageObject/entity/ImageNode.tsx | 8 +- src/main.tsx | 5 +- src/pages/_app_menu.tsx | 20 +- src/pages/_start_file_panel.tsx | 8 +- src/pages/_toolbar.tsx | 6 +- src/pages/index.tsx | 6 + ...SystemFactory.ts => FileSystemFactory.tsx} | 0 src/utils/fs/IFileSystem.ts | 28 --- src/utils/fs/IFileSystem.tsx | 117 ++++++++++ ...BFileSystem.ts => IndexedDBFileSystem.tsx} | 203 +++++++++++------- ...TauriFileSystem.ts => TauriFileSystem.tsx} | 46 ++-- src/utils/fs/WebFileApiSystem.tsx | 192 +++++++++++++++++ src/utils/{fs.tsx => fs/com.tsx} | 136 ++++++------ src/utils/platform.tsx | 4 + 22 files changed, 721 insertions(+), 255 deletions(-) create mode 100644 src/core/service/VFileSystem.tsx rename src/utils/fs/{FileSystemFactory.ts => FileSystemFactory.tsx} (100%) delete mode 100644 src/utils/fs/IFileSystem.ts create mode 100644 src/utils/fs/IFileSystem.tsx rename src/utils/fs/{IndexedDBFileSystem.ts => IndexedDBFileSystem.tsx} (63%) rename src/utils/fs/{TauriFileSystem.ts => TauriFileSystem.tsx} (62%) create mode 100644 src/utils/fs/WebFileApiSystem.tsx rename src/utils/{fs.tsx => fs/com.tsx} (52%) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3c4b1efe..58006a36 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,16 +1,21 @@ use serde::{Deserialize, Serialize}; use std::fs; -use std::path::Path; +use std::path::{Path, MAIN_SEPARATOR}; use std::time::UNIX_EPOCH; use tauri::Manager; +// 新增路径规范化函数 +fn normalize_path(path: &str) -> String { + path.replace('/', &MAIN_SEPARATOR.to_string()) +} + #[derive(Debug, Serialize, Deserialize)] struct FileStats { name: String, #[serde(rename = "isDir")] is_directory: bool, size: u64, - modified: i64, // milliseconds since epoch + modified: i64, } #[derive(Debug, Serialize, Deserialize)] @@ -22,16 +27,19 @@ struct DirectoryEntry { #[tauri::command] async fn read_file(path: String) -> Result, String> { + let path = normalize_path(&path); std::fs::read(&path).map_err(|e| e.to_string()) } #[tauri::command] async fn write_file(path: String, content: Vec) -> Result<(), String> { + let path = normalize_path(&path); fs::write(path, content).map_err(|e| e.to_string()) } #[tauri::command] async fn read_dir(path: String) -> Result, String> { + let path = normalize_path(&path); let entries = fs::read_dir(path).map_err(|e| e.to_string())?; let mut result = Vec::new(); @@ -50,6 +58,7 @@ async fn read_dir(path: String) -> Result, String> { #[tauri::command] async fn mkdir(path: String, recursive: bool) -> Result<(), String> { + let path = normalize_path(&path); if recursive { fs::create_dir_all(path).map_err(|e| e.to_string()) } else { @@ -59,16 +68,15 @@ async fn mkdir(path: String, recursive: bool) -> Result<(), String> { #[tauri::command] async fn stat(path: String) -> Result { - let metadata = fs::metadata(&path).map_err(|e| e.to_string())?; + let normalized_path = normalize_path(&path); + let metadata = fs::metadata(&normalized_path).map_err(|e| e.to_string())?; - // 获取文件名 - let name = Path::new(&path) + let name = Path::new(&normalized_path) .file_name() .and_then(|n| n.to_str()) .map(|s| s.to_string()) .ok_or_else(|| "无法解析文件名".to_string())?; - // 转换修改时间 let modified = metadata .modified() .map_err(|e| e.to_string())? @@ -77,7 +85,7 @@ async fn stat(path: String) -> Result { .as_millis() as i64; Ok(FileStats { - name, // 新增的name字段 + name, is_directory: metadata.is_dir(), size: metadata.len(), modified, @@ -86,21 +94,26 @@ async fn stat(path: String) -> Result { #[tauri::command] async fn rename(old_path: String, new_path: String) -> Result<(), String> { + let old_path = normalize_path(&old_path); + let new_path = normalize_path(&new_path); fs::rename(old_path, new_path).map_err(|e| e.to_string()) } #[tauri::command] async fn delete_file(path: String) -> Result<(), String> { + let path = normalize_path(&path); fs::remove_file(path).map_err(|e| e.to_string()) } #[tauri::command] async fn delete_directory(path: String) -> Result<(), String> { + let path = normalize_path(&path); fs::remove_dir_all(path).map_err(|e| e.to_string()) } #[tauri::command] async fn exists(path: String) -> Result { + let path = normalize_path(&path); Ok(fs::metadata(path).is_ok()) } diff --git a/src/cli.tsx b/src/cli.tsx index efdd7019..511f560b 100644 --- a/src/cli.tsx +++ b/src/cli.tsx @@ -1,7 +1,7 @@ import { CliMatches } from "@tauri-apps/plugin-cli"; import { StageDumperSvg } from "./core/stage/StageDumperSvg"; -import { writeTextFile } from "./utils/fs"; import { writeStdout } from "./utils/otherApi"; +import { TauriBaseFS } from "./utils/fs/TauriFileSystem"; export async function runCli(matches: CliMatches) { if (matches.args.output?.occurrences > 0) { @@ -13,7 +13,7 @@ export async function runCli(matches: CliMatches) { if (outputPath === "-") { writeStdout(result); } else { - await writeTextFile(outputPath, result); + await TauriBaseFS.writeTextFile(outputPath, result); } } else { throw new Error("Invalid output format. Only SVG format is supported."); diff --git a/src/core/service/RecentFileManager.tsx b/src/core/service/RecentFileManager.tsx index a4f577b9..03dc4886 100644 --- a/src/core/service/RecentFileManager.tsx +++ b/src/core/service/RecentFileManager.tsx @@ -1,7 +1,7 @@ import { Store } from "@tauri-apps/plugin-store"; // import { exists } from "@tauri-apps/plugin-fs"; // 导入文件相关函数 import { Serialized } from "../../types/node"; -import { exists, readTextFile } from "../../utils/fs"; +import { exists, readTextFile } from "../../utils/fs/com"; import { createStore } from "../../utils/store"; import { Camera } from "../stage/Camera"; import { Stage } from "../stage/Stage"; diff --git a/src/core/service/SoundService.tsx b/src/core/service/SoundService.tsx index 64bb9fc5..a397395b 100644 --- a/src/core/service/SoundService.tsx +++ b/src/core/service/SoundService.tsx @@ -2,7 +2,7 @@ // @tauri-apps/plugin-fs 只能读取文本文件,不能强行读取流文件并强转为ArrayBuffer // import { readTextFile } from "@tauri-apps/plugin-fs"; -import { readFile } from "../../utils/fs"; +import { readFile } from "../../utils/fs/com"; import { StringDict } from "../dataStruct/StringDict"; import { Settings } from "./Settings"; diff --git a/src/core/service/VFileSystem.tsx b/src/core/service/VFileSystem.tsx new file mode 100644 index 00000000..f6751cd3 --- /dev/null +++ b/src/core/service/VFileSystem.tsx @@ -0,0 +1,97 @@ +import JSZip, * as jszip from "jszip"; +import { IndexedDBFileSystem } from "../../utils/fs/IndexedDBFileSystem"; +import { readFile } from "../../utils/fs/com"; + +export enum FSType { + Tauri = "Tauri", + WebFS = "WebFS", + IndexedDB = "IndexedDB", +} +export namespace VFileSystem { + const fs = new IndexedDBFileSystem("PG", "Project"); + export async function getMetaData() { + return fs.readTextFile("/meta.json"); + } + export async function setMetaData(content: string) { + return fs.writeTextFile("/meta.json", content); + } + export async function loadFromPath(path: string) { + const data = await readFile(path); + const zip = await jszip.loadAsync(data); + const entries = zip.files; + + const operations: Promise[] = []; + + for (const [rawPath, file] of Object.entries(entries)) { + // 标准化路径:替换多个斜杠为单个,并移除末尾斜杠 + const normalizedPath = rawPath.replace(/\/+/g, "/").replace(/\/$/, ""); + + if (file.dir) { + await fs.mkdir(normalizedPath, true); + } else { + // 处理文件 + operations.push( + (async () => { + try { + // 分离目录和文件名 + const lastSlashIndex = normalizedPath.lastIndexOf("/"); + const parentDir = + lastSlashIndex >= 0 + ? normalizedPath.slice(0, lastSlashIndex) + : ""; + + // 创建父目录(如果存在) + if (parentDir) { + await fs.mkdir(parentDir, true); + } + + // 写入文件内容 + const content = await file.async("uint8array"); + await fs.writeFile(normalizedPath, content); + } catch (error) { + console.error(`Process file failed: ${normalizedPath}`, error); + } + })(), + ); + } + } + + await Promise.all(operations); + } + export async function exportZipData(): Promise { + const zip = new JSZip(); + + // 递归添加目录和文件到zip + async function addToZip(zipParent: jszip, path: string) { + console.log(zipParent, path); + const entries = await fs.readDir(path); + + for (const entry of entries) { + const fullPath = path ? `${path}/${entry.name}` : entry.name; + + if (entry.isDir) { + // 创建目录节点并递归处理子项 + const dirZip = zipParent.folder(entry.name); + await addToZip(dirZip!, fullPath); + } else { + // 添加文件内容到zip + const content = await fs.readFile(fullPath); + zipParent.file(entry.name, content); + } + } + } + + // 从根目录开始处理 + await addToZip(zip, "/"); + + // 生成zip文件内容 + return zip.generateAsync({ + type: "uint8array", + compression: "DEFLATE", // 使用压缩 + compressionOptions: { level: 6 }, + }); + } + export function getFS() { + return fs; + } +} diff --git a/src/core/service/controller/concrete/ControllerCopy.tsx b/src/core/service/controller/concrete/ControllerCopy.tsx index 3ee499aa..3fc3554f 100644 --- a/src/core/service/controller/concrete/ControllerCopy.tsx +++ b/src/core/service/controller/concrete/ControllerCopy.tsx @@ -1,6 +1,5 @@ import { v4 as uuidv4 } from "uuid"; import { Dialog } from "../../../../utils/dialog"; -import { writeFileBase64 } from "../../../../utils/fs"; import { PathString } from "../../../../utils/pathString"; import { Rectangle } from "../../../dataStruct/shape/Rectangle"; import { Vector } from "../../../dataStruct/Vector"; @@ -15,6 +14,7 @@ import { UrlNode } from "../../../stage/stageObject/entity/UrlNode"; import { Entity } from "../../../stage/stageObject/StageObject"; import { Controller } from "../Controller"; import { ControllerClass } from "../ControllerClass"; +import { VFileSystem } from "../../VFileSystem"; /** * 关于复制相关的功能 @@ -124,13 +124,14 @@ async function readClipboardItems(mouseLocation: Vector) { } const blob = await item.getType(item.types[0]); // 获取 Blob 对象 const imageUUID = uuidv4(); - const folder = PathString.dirPath(Stage.Path.getFilePath()); - const imagePath = `${folder}${Stage.Path.getSep()}${imageUUID}.png`; // 2024.12.31 测试发现这样的写法会导致读取时base64解码失败 // writeFile(imagePath, new Uint8Array(await blob.arrayBuffer())); // 下面这样的写法是没有问题的 - writeFileBase64(imagePath, await convertBlobToBase64(blob)); + VFileSystem.getFS().writeFile( + `/picture/${imageUUID}.png`, + await blob.bytes(), + ); // 要延迟一下,等待保存完毕 setTimeout(() => { @@ -190,17 +191,17 @@ function blobToText(blob: Blob): Promise { }); } -async function convertBlobToBase64(blob: Blob): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onloadend = () => { - if (typeof reader.result === "string") { - resolve(reader.result.split(",")[1]); // 去掉"data:image/png;base64,"前缀 - } else { - reject(new Error("Invalid result type")); - } - }; - reader.onerror = reject; - reader.readAsDataURL(blob); - }); -} +// async function convertBlobToBase64(blob: Blob): Promise { +// return new Promise((resolve, reject) => { +// const reader = new FileReader(); +// reader.onloadend = () => { +// if (typeof reader.result === "string") { +// resolve(reader.result.split(",")[1]); // 去掉"data:image/png;base64,"前缀 +// } else { +// reject(new Error("Invalid result type")); +// } +// }; +// reader.onerror = reject; +// reader.readAsDataURL(blob); +// }); +// } diff --git a/src/core/service/controller/concrete/ControllerDragFile.tsx b/src/core/service/controller/concrete/ControllerDragFile.tsx index 517d9160..f538538a 100644 --- a/src/core/service/controller/concrete/ControllerDragFile.tsx +++ b/src/core/service/controller/concrete/ControllerDragFile.tsx @@ -1,6 +1,4 @@ import { v4 as uuidv4 } from "uuid"; -import { writeFileBase64 } from "../../../../utils/fs"; -import { PathString } from "../../../../utils/pathString"; import { Color } from "../../../dataStruct/Color"; import { Vector } from "../../../dataStruct/Vector"; import { Renderer } from "../../../render/canvas2d/renderer"; @@ -12,6 +10,7 @@ import { TextNode } from "../../../stage/stageObject/entity/TextNode"; import { TextRiseEffect } from "../../effectEngine/concrete/TextRiseEffect"; import { ViewFlashEffect } from "../../effectEngine/concrete/ViewFlashEffect"; import { ControllerClassDragFile } from "../ControllerClassDragFile"; +import { VFileSystem } from "../../VFileSystem"; // import { listen } from "@tauri-apps/api/event"; // listen("tauri://file-drop", (event) => { @@ -195,9 +194,8 @@ function dealPngFileDrop(file: File, mouseWorldLocation: Vector) { // data:image/png;base64,iVBORw0KGgoAAAANS... // 在这里处理读取到的内容 const imageUUID = uuidv4(); - const folderPath = PathString.dirPath(Stage.Path.getFilePath()); - writeFileBase64( - `${folderPath}${Stage.Path.getSep()}${imageUUID}.png`, + VFileSystem.getFS().writeFileBase64( + `/picture/${imageUUID}.png`, fileContent.split(",")[1], ); const imageNode = new ImageNode({ diff --git a/src/core/stage/StageSaveManager.tsx b/src/core/stage/StageSaveManager.tsx index df0f6e99..c8129bc2 100644 --- a/src/core/stage/StageSaveManager.tsx +++ b/src/core/stage/StageSaveManager.tsx @@ -1,7 +1,8 @@ import { Serialized } from "../../types/node"; -import { exists, writeTextFile } from "../../utils/fs"; +import { exists, writeFile, writeTextFile } from "../../utils/fs/com"; import { PathString } from "../../utils/pathString"; import { ViewFlashEffect } from "../service/effectEngine/concrete/ViewFlashEffect"; +import { VFileSystem } from "../service/VFileSystem"; import { Stage } from "./Stage"; import { StageHistoryManager } from "./stageManager/StageHistoryManager"; import { StageManager } from "./stageManager/StageManager"; @@ -20,7 +21,8 @@ export namespace StageSaveManager { * @param errorCallback */ export async function saveHandle(path: string, data: Serialized.File) { - await writeTextFile(path, JSON.stringify(data)); + await VFileSystem.setMetaData(JSON.stringify(data)); + await writeFile(path, await VFileSystem.exportZipData()); Stage.effectMachine.addEffect(ViewFlashEffect.SaveFile()); StageHistoryManager.reset(data); // 重置历史 isCurrentSaved = true; @@ -41,7 +43,11 @@ export namespace StageSaveManager { if (Stage.Path.isDraft()) { throw new Error("当前文档的状态为草稿,请您先保存为文件"); } - await writeTextFile(Stage.Path.getFilePath(), JSON.stringify(data)); + await VFileSystem.setMetaData(JSON.stringify(data)); + await writeFile( + Stage.Path.getFilePath(), + await VFileSystem.exportZipData(), + ); if (addFlashEffect) { Stage.effectMachine.addEffect(ViewFlashEffect.SaveFile()); } @@ -65,8 +71,8 @@ export namespace StageSaveManager { if (!isExists) { throw new Error("备份文件路径错误:" + backupFolderPath); } - - await writeTextFile(path, JSON.stringify(data)); + await VFileSystem.setMetaData(JSON.stringify(data)); + await writeFile(path, await VFileSystem.exportZipData()); Stage.effectMachine.addEffect(ViewFlashEffect.SaveFile()); } /** @@ -89,8 +95,8 @@ export namespace StageSaveManager { const dateTime = PathString.getTime(); const backupPath = `${Stage.Path.getFilePath()}.${dateTime}.backup`; - - await writeTextFile(backupPath, JSON.stringify(data)); + await VFileSystem.setMetaData(JSON.stringify(data)); + await writeFile(backupPath, await VFileSystem.exportZipData()); if (addFlashEffect) { Stage.effectMachine.addEffect(ViewFlashEffect.SaveFile()); } diff --git a/src/core/stage/stageObject/entity/ImageNode.tsx b/src/core/stage/stageObject/entity/ImageNode.tsx index 195f0e0e..89f974f6 100644 --- a/src/core/stage/stageObject/entity/ImageNode.tsx +++ b/src/core/stage/stageObject/entity/ImageNode.tsx @@ -1,12 +1,11 @@ -import { join } from "@tauri-apps/api/path"; import { Serialized } from "../../../../types/node"; -import { readFileBase64 } from "../../../../utils/fs"; import { PathString } from "../../../../utils/pathString"; import { Rectangle } from "../../../dataStruct/shape/Rectangle"; import { Vector } from "../../../dataStruct/Vector"; import { Stage } from "../../Stage"; import { CollisionBox } from "../collisionBox/collisionBox"; import { ConnectableEntity } from "../StageObject"; +import { VFileSystem } from "../../../service/VFileSystem"; /** * 一个图片节点 @@ -117,13 +116,14 @@ export class ImageNode extends ConnectableEntity { * @param folderPath 工程文件所在路径文件夹,不加尾部斜杠 * @returns */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars public updateBase64StringByPath(folderPath: string) { if (this.path === "") { return; } - join(folderPath, this.path) - .then((path) => readFileBase64(path)) + VFileSystem.getFS() + .readFileBase64(`/picture/${this.path}`) .then((res) => { // 获取base64String成功 diff --git a/src/main.tsx b/src/main.tsx index b8e640ce..580668da 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -36,11 +36,13 @@ import "./index.pcss"; import { ColorPanel } from "./pages/_toolbar"; import "./polyfills/roundRect"; import { Dialog } from "./utils/dialog"; -import { exists } from "./utils/fs"; +import { exists } from "./utils/fs/com"; import { exit, openDevtools, writeStderr, writeStdout } from "./utils/otherApi"; import { getCurrentWindow, isDesktop, isWeb } from "./utils/platform"; import { Popup } from "./utils/popup"; import { InputElement } from "./core/render/domElement/inputElement"; +// import { VFileSystem } from "./core/service/VFileSystem"; +import { IndexedDBFileSystem } from "./utils/fs/IndexedDBFileSystem"; const router = createMemoryRouter(routes); const Routes = () => ; @@ -52,6 +54,7 @@ const el = document.getElementById("root")!; (async () => { const matches = !isWeb && isDesktop ? await getMatches() : null; const isCliMode = isDesktop && matches?.args.output?.occurrences === 1; + IndexedDBFileSystem.testFileSystem("A", "B"); await Promise.all([ Settings.init(), RecentFileManager.init(), diff --git a/src/pages/_app_menu.tsx b/src/pages/_app_menu.tsx index 4e20c640..d221fbb7 100644 --- a/src/pages/_app_menu.tsx +++ b/src/pages/_app_menu.tsx @@ -152,7 +152,7 @@ export default function AppMenu({ ? [ { name: "Project Graph", - extensions: ["json"], + extensions: ["gp"], }, ] : [], @@ -160,9 +160,9 @@ export default function AppMenu({ if (!path) { return; } - if (isDesktop && !path.endsWith(".json")) { + if (isDesktop && !path.endsWith(".gp")) { Dialog.show({ - title: "请选择一个JSON文件", + title: "请选择一个gp文件", type: "error", }); return; @@ -173,7 +173,7 @@ export default function AppMenu({ setFile(path); } catch (e) { Dialog.show({ - title: "请选择正确的JSON文件", + title: "请选择正确的gp文件", content: String(e), type: "error", }); @@ -194,7 +194,8 @@ export default function AppMenu({ // await writeTextFile(path, JSON.stringify(data, null, 2)); // 将数据写入文件 try { await StageSaveManager.saveHandle(path_, data); - } catch { + } catch (e) { + console.error(e); await Dialog.show({ title: "保存失败", content: "保存失败,请重试", @@ -204,14 +205,14 @@ export default function AppMenu({ const onSaveNew = async () => { const path = isWeb - ? "file.json" + ? "file.gp" : await saveFileDialog({ title: "另存为", - defaultPath: "新文件.json", // 提供一个默认的文件名 + defaultPath: "新文件.gp", // 提供一个默认的文件名 filters: [ { name: "Project Graph", - extensions: ["json"], + extensions: ["gp"], }, ], }); @@ -224,7 +225,8 @@ export default function AppMenu({ try { await StageSaveManager.saveHandle(path, data); setFile(path); - } catch { + } catch (e) { + console.error(e); await Dialog.show({ title: "保存失败", content: "保存失败,请重试", diff --git a/src/pages/_start_file_panel.tsx b/src/pages/_start_file_panel.tsx index b1ea08d4..67642536 100644 --- a/src/pages/_start_file_panel.tsx +++ b/src/pages/_start_file_panel.tsx @@ -74,7 +74,7 @@ export default function StartFilePanel({ open = false }: { open: boolean }) { ? [ { name: "Project Graph", - extensions: ["json"], + extensions: ["gp"], }, ] : [], @@ -82,9 +82,9 @@ export default function StartFilePanel({ open = false }: { open: boolean }) { if (!path) { return; } - if (isDesktop && !path.endsWith(".json")) { + if (isDesktop && !path.endsWith(".gp")) { Dialog.show({ - title: "请选择一个JSON文件", + title: "请选择一个gp文件", type: "error", }); return; @@ -101,7 +101,7 @@ export default function StartFilePanel({ open = false }: { open: boolean }) { updateStartFiles(); } catch (e) { Dialog.show({ - title: "请选择正确的JSON文件", + title: "请选择正确的gp文件", content: String(e), type: "error", }); diff --git a/src/pages/_toolbar.tsx b/src/pages/_toolbar.tsx index f2957176..8733b534 100644 --- a/src/pages/_toolbar.tsx +++ b/src/pages/_toolbar.tsx @@ -40,7 +40,7 @@ import { StageManager } from "../core/stage/stageManager/StageManager"; import { cn } from "../utils/cn"; // import { StageSaveManager } from "../core/stage/StageSaveManager"; import { Dialog } from "../utils/dialog"; -import { writeTextFile } from "../utils/fs"; +import { writeTextFile } from "../utils/fs/com"; import { Popup } from "../utils/popup"; // import { PathString } from "../utils/pathString"; import { ColorManager } from "../core/service/ColorManager"; @@ -500,11 +500,11 @@ export default function Toolbar({ className = "" }: { className?: string }) { const onSaveSelectedNew = async () => { const path = await saveFileDialog({ title: "另存为", - defaultPath: "新文件.json", // 提供一个默认的文件名 + defaultPath: "新文件.gp", // 提供一个默认的文件名 filters: [ { name: "Project Graph", - extensions: ["json"], + extensions: ["gp"], }, ], }); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index d4704369..bf68bcc2 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -11,6 +11,7 @@ import DetailsEditSidePanel from "./_details_edit_side_panel"; import HintText from "./_hint_text"; import SearchingNodePanel from "./_searching_node_panel"; import Toolbar from "./_toolbar"; +// import { WebFileApiSystem } from "../utils/fs/WebFileApiSystem"; export default function Home() { const canvasRef: React.RefObject = useRef(null); @@ -51,6 +52,11 @@ export default function Home() { }); } + // window.addEventListener("click", () => { + // const p = window.showDirectoryPicker(); + // const sys = new WebFileApiSystem(p); + // sys.mkdir("/a/b/c/d/e/f", true); + // }); window.addEventListener("resize", handleResize); window.addEventListener("focus", handleFocus); window.addEventListener("blur", handleBlur); diff --git a/src/utils/fs/FileSystemFactory.ts b/src/utils/fs/FileSystemFactory.tsx similarity index 100% rename from src/utils/fs/FileSystemFactory.ts rename to src/utils/fs/FileSystemFactory.tsx diff --git a/src/utils/fs/IFileSystem.ts b/src/utils/fs/IFileSystem.ts deleted file mode 100644 index ad283757..00000000 --- a/src/utils/fs/IFileSystem.ts +++ /dev/null @@ -1,28 +0,0 @@ -export interface FileStats { - name: string; - isDir: boolean; - size: number; - modified: Date; -} - -export interface DirectoryEntry { - name: string; - isDir: boolean; -} - -// 强制使用 `/` 作为分隔符 -export abstract class IFileSystem { - abstract readFile(path: string): Promise; - abstract writeFile(path: string, content: Uint8Array | string): Promise; - abstract readDir(path: string): Promise; - abstract mkdir(path: string, recursive?: boolean): Promise; - abstract stat(path: string): Promise; - abstract rename(oldPath: string, newPath: string): Promise; - abstract deleteFile(path: string): Promise; - abstract deleteDirectory(path: string): Promise; - abstract exists(path: string): Promise; - async readTextFile(path: string) { - const content = await this.readFile(path); - return new TextDecoder("utf-8").decode(content); - } -} diff --git a/src/utils/fs/IFileSystem.tsx b/src/utils/fs/IFileSystem.tsx new file mode 100644 index 00000000..4f8ac636 --- /dev/null +++ b/src/utils/fs/IFileSystem.tsx @@ -0,0 +1,117 @@ +export interface FileStats { + name: string; + isDir: boolean; + size: number; + modified: Date; +} + +export interface DirectoryEntry { + name: string; + isDir: boolean; +} +export function base64ToUint8Array(base64String: string) { + const padding = "=".repeat((4 - (base64String.length % 4)) % 4); + const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/"); + + const sliceSize = 572; + const bytes = new Uint8Array((base64.length / 4) * 3); + let byteIndex = 0; + + for (let offset = 0; offset < base64.length; offset += sliceSize) { + const slice = base64.slice(offset, offset + sliceSize); + + const byteChars = atob(slice); + + for (let i = 0; i < byteChars.length; i++) { + bytes[byteIndex++] = byteChars.charCodeAt(i); + } + } + + return bytes.subarray(0, byteIndex); +} + +export function uint8ArrayToBase64(u8Arr: Uint8Array): string { + let binaryStr = ""; + for (let i = 0; i < u8Arr.length; i++) { + binaryStr += String.fromCharCode(u8Arr[i]); + } + return btoa(binaryStr); +} + +// 强制使用 `/` 作为分隔符 +export abstract class IFileSystem { + // 私有方法:统一规范化路径格式 + static normalizePath(path: string): string { + return path.replace(/[\\/]+/g, "/"); // 替换所有分隔符为 / + } + + // 抽象原始方法(带下划线版本) + abstract _readFile(path: string): Promise; + abstract _writeFile( + path: string, + content: Uint8Array | string, + ): Promise; + abstract _readDir(path: string): Promise; + abstract _mkdir(path: string, recursive?: boolean): Promise; + abstract _stat(path: string): Promise; + abstract _rename(oldPath: string, newPath: string): Promise; + abstract _deleteFile(path: string): Promise; + abstract _deleteDirectory(path: string): Promise; + abstract _exists(path: string): Promise; + + // 公共方法(自动处理路径分隔符) + readFile(path: string) { + return this._readFile(IFileSystem.normalizePath(path)); + } + + writeFile(path: string, content: Uint8Array | string) { + return this._writeFile(IFileSystem.normalizePath(path), content); + } + + readDir(path: string) { + return this._readDir(IFileSystem.normalizePath(path)); + } + + mkdir(path: string, recursive?: boolean) { + return this._mkdir(IFileSystem.normalizePath(path), recursive); + } + + stat(path: string) { + return this._stat(IFileSystem.normalizePath(path)); + } + + rename(oldPath: string, newPath: string) { + return this._rename( + IFileSystem.normalizePath(oldPath), + IFileSystem.normalizePath(newPath), + ); + } + + deleteFile(path: string) { + return this._deleteFile(IFileSystem.normalizePath(path)); + } + + deleteDirectory(path: string) { + return this._deleteDirectory(IFileSystem.normalizePath(path)); + } + + exists(path: string) { + return this._exists(IFileSystem.normalizePath(path)); + } + + async readTextFile(path: string) { + const content = await this.readFile(path); // 注意这里调用的是处理后的公共方法 + return new TextDecoder("utf-8").decode(content); + } + async writeTextFile(path: string, content: string) { + const text = new TextEncoder().encode(content); + return this.writeFile(path, text); // 注意这里调用的是处理后的公共方法 + } + + async readFileBase64(path: string) { + return uint8ArrayToBase64(await this.readFile(path)); + } + async writeFileBase64(path: string, str: string) { + return this.writeFile(path, base64ToUint8Array(str)); + } +} diff --git a/src/utils/fs/IndexedDBFileSystem.ts b/src/utils/fs/IndexedDBFileSystem.tsx similarity index 63% rename from src/utils/fs/IndexedDBFileSystem.ts rename to src/utils/fs/IndexedDBFileSystem.tsx index c8fc4518..fc329743 100644 --- a/src/utils/fs/IndexedDBFileSystem.ts +++ b/src/utils/fs/IndexedDBFileSystem.tsx @@ -65,8 +65,53 @@ export class IndexedDBFileSystem extends IFileSystem { const transaction = await this.getTransaction(storeName, mode); return transaction.objectStore(storeName); } + private normalizePath(path: string): string { + let normalized = path.replace(/\/+/g, "/").replace(/\/$/, ""); + if (normalized === "") return "/"; + if (!normalized.startsWith("/")) normalized = "/" + normalized; + return normalized; + } + + private getParentPath(path: string): string | null { + const normalized = this.normalizePath(path); + if (normalized === "/") return null; + const parts = normalized.split("/").filter((p) => p !== ""); + if (parts.length === 0) return null; + parts.pop(); + return parts.length === 0 ? "/" : `/${parts.join("/")}`; + } + + private async addEntryToParent( + childPath: string, + isDir: boolean, + ): Promise { + const parentPath = this.getParentPath(childPath); + if (!parentPath) return; + + const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); + const parentDir = await new Promise((resolve, reject) => { + const req = store.get(parentPath); + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); + + if (!parentDir) throw new Error(`Parent directory ${parentPath} not found`); + + const childName = childPath.split("/").pop()!; + const exists = parentDir.entries.some( + (e: DirectoryEntry) => e.name === childName, + ); + if (!exists) { + parentDir.entries.push({ name: childName, isDir }); + await new Promise((resolve, reject) => { + const req = store.put(parentDir); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); + }); + } + } - async exists(path: string): Promise { + async _exists(path: string): Promise { if (!this.db) { await this.initDB(); } @@ -76,7 +121,6 @@ export class IndexedDBFileSystem extends IFileSystem { ); const filesStore = transaction.objectStore(this.STORE_NAME); const dirsStore = transaction.objectStore(this.DIR_STORE_NAME); - return new Promise((resolve) => { const request = filesStore.get(path); request.onsuccess = () => { @@ -92,7 +136,7 @@ export class IndexedDBFileSystem extends IFileSystem { }); } - async readFile(path: string): Promise { + async _readFile(path: string): Promise { const store = await this.getStore(this.STORE_NAME); return new Promise((resolve, reject) => { const request = store.get(path); @@ -107,86 +151,77 @@ export class IndexedDBFileSystem extends IFileSystem { }); } - async writeFile(path: string, content: Uint8Array | string): Promise { - const store = await this.getStore(this.STORE_NAME, "readwrite"); - const data = - typeof content === "string" ? new TextEncoder().encode(content) : content; - - return new Promise((resolve, reject) => { - const request = store.put({ path, content: data }); - request.onsuccess = () => resolve(); - request.onerror = () => reject(request.error); - }); - } - - async readDir(path: string): Promise { - const store = await this.getStore(this.DIR_STORE_NAME); - return new Promise((resolve, reject) => { - const request = store.get(path); - request.onsuccess = () => { - if (request.result) { - resolve(request.result.entries); - } else { - resolve([]); - } - }; - request.onerror = () => reject(request.error); - }); - } - - async mkdir(path: string, recursive = false): Promise { + async _mkdir(path: string, recursive = false): Promise { + path = this.normalizePath(path); if (!recursive) { + const parentPath = this.getParentPath(path); + if (parentPath && !(await this._exists(parentPath))) { + throw new Error(`Parent directory does not exist: ${parentPath}`); + } const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); - return new Promise((resolve, reject) => { - const request = store.put({ path, entries: [] }); - request.onsuccess = () => resolve(); - request.onerror = () => reject(request.error); + await new Promise((resolve, reject) => { + const r = store.put({ path, entries: [] }); + r.onsuccess = () => resolve(); + r.onerror = () => reject(); }); + if (parentPath) await this.addEntryToParent(path, true); + return; } - // 递归创建父目录 - const parts = path.split("/"); + const parts = path.split("/").filter((p) => p !== ""); let currentPath = ""; const pathsToCreate: string[] = []; - - // 收集需要创建的路径 for (const part of parts) { - if (!part) continue; - currentPath = currentPath ? `${currentPath}/${part}` : part; - if (!(await this.exists(currentPath))) { - pathsToCreate.push(currentPath); - } + currentPath = currentPath ? `${currentPath}/${part}` : `/${part}`; + currentPath = this.normalizePath(currentPath); + if (!(await this._exists(currentPath))) pathsToCreate.push(currentPath); } - // 在单个事务中创建所有目录 if (pathsToCreate.length > 0) { const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); - return new Promise((resolve, reject) => { - let completed = 0; - let error: IDBRequest | null = null; - - for (const path of pathsToCreate) { - const request = store.put({ path, entries: [] }); - request.onsuccess = () => { - completed++; - if (completed === pathsToCreate.length && !error) { - resolve(); - } - }; - request.onerror = () => { - if (!error) { - error = request; - reject(request.error); - } - }; - } - }); + for (const p of pathsToCreate) { + await new Promise((resolve, reject) => { + const r = store.put({ path: p, entries: [] }); + r.onsuccess = () => resolve(); + r.onerror = () => reject(); + }); + const parentPath = this.getParentPath(p); + if (parentPath) await this.addEntryToParent(p, true); + } } } - async stat(path: string): Promise { - const filesStore = await this.getStore(this.STORE_NAME); - const dirsStore = await this.getStore(this.DIR_STORE_NAME); + async _writeFile(path: string, content: Uint8Array | string): Promise { + path = this.normalizePath(path); + const store = await this.getStore(this.STORE_NAME, "readwrite"); + const data = + typeof content === "string" ? new TextEncoder().encode(content) : content; + await new Promise((resolve, reject) => { + const r = store.put({ path, content: data }); + r.onsuccess = () => resolve(); + r.onerror = () => reject(); + }); + const parentPath = this.getParentPath(path); + if (parentPath) await this.addEntryToParent(path, false); + } + + async _readDir(path: string): Promise { + path = this.normalizePath(path); + const store = await this.getStore(this.DIR_STORE_NAME); + return new Promise((resolve, reject) => { + const req = store.get(path); + req.onsuccess = () => resolve(req.result?.entries || []); + req.onerror = () => reject(req.error); + }); + } + + async _stat(path: string): Promise { + const transaction = await this.getTransaction( + [this.STORE_NAME, this.DIR_STORE_NAME], + "readwrite", + ); + const filesStore = transaction.objectStore(this.STORE_NAME); + const dirsStore = transaction.objectStore(this.DIR_STORE_NAME); return new Promise((resolve, reject) => { const fileRequest = filesStore.get(path); @@ -217,9 +252,13 @@ export class IndexedDBFileSystem extends IFileSystem { }); } - async rename(oldPath: string, newPath: string): Promise { - const filesStore = await this.getStore(this.STORE_NAME, "readwrite"); - const dirsStore = await this.getStore(this.DIR_STORE_NAME, "readwrite"); + async _rename(oldPath: string, newPath: string): Promise { + const transaction = await this.getTransaction( + [this.STORE_NAME, this.DIR_STORE_NAME], + "readwrite", + ); + const filesStore = transaction.objectStore(this.STORE_NAME); + const dirsStore = transaction.objectStore(this.DIR_STORE_NAME); return new Promise((resolve, reject) => { const moveFile = () => { @@ -260,7 +299,7 @@ export class IndexedDBFileSystem extends IFileSystem { }); } - async deleteFile(path: string): Promise { + async _deleteFile(path: string): Promise { const store = await this.getStore(this.STORE_NAME, "readwrite"); return new Promise((resolve, reject) => { const request = store.delete(path); @@ -269,7 +308,7 @@ export class IndexedDBFileSystem extends IFileSystem { }); } - async deleteDirectory(path: string): Promise { + async _deleteDirectory(path: string): Promise { const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); return new Promise((resolve, reject) => { const request = store.delete(path); @@ -278,6 +317,24 @@ export class IndexedDBFileSystem extends IFileSystem { }); } + async clear() { + const transaction = await this.getTransaction( + [this.STORE_NAME, this.DIR_STORE_NAME], + "readwrite", + ); + const filesStore = transaction.objectStore(this.STORE_NAME); + const dirsStore = transaction.objectStore(this.DIR_STORE_NAME); + return new Promise((resolve, reject) => { + const r0 = filesStore.clear(); + r0.onsuccess = () => { + const r2 = dirsStore.clear(); + r2.onsuccess = () => resolve(); + r2.onerror = () => reject(); + }; + r0.onerror = () => reject(); + }); + } + /** * 验证文件内容是否匹配 * @param fs 文件系统实例 diff --git a/src/utils/fs/TauriFileSystem.ts b/src/utils/fs/TauriFileSystem.tsx similarity index 62% rename from src/utils/fs/TauriFileSystem.ts rename to src/utils/fs/TauriFileSystem.tsx index a7435f31..b56a42b0 100644 --- a/src/utils/fs/TauriFileSystem.ts +++ b/src/utils/fs/TauriFileSystem.tsx @@ -5,15 +5,20 @@ import { IFileSystem, FileStats, DirectoryEntry } from "./IFileSystem"; * Tauri 文件系统工具类 */ export class TauriFileSystem extends IFileSystem { - async exists(path: string): Promise { - return invoke("exists", { path }); + constructor(private basePath: string = "") { + super(); + } + async _exists(path: string): Promise { + return invoke("exists", { path: this.basePath + path }); } - async readFile(path: string): Promise { - return new Uint8Array(await invoke("read_file", { path })); + async _readFile(path: string): Promise { + return new Uint8Array( + await invoke("read_file", { path: this.basePath + path }), + ); } - async writeFile(path: string, content: Uint8Array | string): Promise { + async _writeFile(path: string, content: Uint8Array | string): Promise { let data: Uint8Array; if (typeof content === "string") { data = new TextEncoder().encode(content); @@ -21,33 +26,36 @@ export class TauriFileSystem extends IFileSystem { data = content; } return invoke("write_file", { - path, + path: this.basePath + path, content: Array.from(data), }); } - async mkdir(path: string, recursive = false): Promise { - return invoke("mkdir", { path, recursive }); + async _mkdir(path: string, recursive = false): Promise { + return invoke("mkdir", { path: this.basePath + path, recursive }); } - async stat(path: string): Promise { - return invoke("stat", { path }); + async _stat(path: string): Promise { + return invoke("stat", { path: this.basePath + path }); } - async readDir(path: string): Promise { - return invoke("read_dir", { path }); + async _readDir(path: string): Promise { + return invoke("read_dir", { path: this.basePath + path }); } - async rename(oldPath: string, newPath: string): Promise { - return invoke("rename", { oldPath, newPath }); + async _rename(oldPath: string, newPath: string): Promise { + return invoke("rename", { + oldPath: this.basePath + oldPath, + newPath: this.basePath + newPath, + }); } - async deleteFile(path: string): Promise { - return invoke("delete_file", { path }); + async _deleteFile(path: string): Promise { + return invoke("delete_file", { path: this.basePath + path }); } - async deleteDirectory(path: string): Promise { - return invoke("delete_directory", { path }); + async _deleteDirectory(path: string): Promise { + return invoke("delete_directory", { path: this.basePath + path }); } /** @@ -109,3 +117,5 @@ export class TauriFileSystem extends IFileSystem { console.log(`Deleted directory: ${dirPath}`); } } + +export const TauriBaseFS = new TauriFileSystem(); diff --git a/src/utils/fs/WebFileApiSystem.tsx b/src/utils/fs/WebFileApiSystem.tsx new file mode 100644 index 00000000..8379d678 --- /dev/null +++ b/src/utils/fs/WebFileApiSystem.tsx @@ -0,0 +1,192 @@ +import { + IFileSystem, + type FileStats, + type DirectoryEntry, +} from "./IFileSystem"; + +type FSAPHandle = FileSystemDirectoryHandle; + +export class WebFileApiSystem extends IFileSystem { + constructor(private rootHandle: FSAPHandle) { + super(); + } + + private async resolvePathComponents(path: string): Promise { + return IFileSystem.normalizePath(path) + .split("/") + .filter((p) => p !== ""); + } + + private async resolveHandle(path: string): Promise { + const parts = await this.resolvePathComponents(path); + let currentHandle: FileSystemHandle = this.rootHandle; + + for (const part of parts) { + if (currentHandle.kind !== "directory") { + throw new Error(`Cannot traverse into non-directory at: ${part}`); + } + + try { + currentHandle = await ( + currentHandle as FileSystemDirectoryHandle + ).getDirectoryHandle(part); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (dirError) { + try { + currentHandle = await ( + currentHandle as FileSystemDirectoryHandle + ).getFileHandle(part); + // 提前终止检查:文件节点不能在路径中间 + if (part !== parts[parts.length - 1]) { + throw new Error(`File node cannot be in path middle: ${path}`); + } + } catch (fileError) { + throw new Error(`Path resolution failed: ${path} (${fileError})`); + } + } + } + return currentHandle; + } + + async _readFile(path: string): Promise { + const handle = await this.resolveHandle(path); + if (handle.kind !== "file") { + throw new Error(`Path is not a file: ${path}`); + } + const file = await (handle as FileSystemFileHandle).getFile(); + return new Uint8Array(await file.arrayBuffer()); + } + + async _writeFile(path: string, content: Uint8Array | string): Promise { + const buffer = + typeof content === "string" ? new TextEncoder().encode(content) : content; + + const parts = await this.resolvePathComponents(path); + const fileName = parts.pop()!; + const parentHandle = await this.ensureDirectoryPath(parts); + + const fileHandle = await parentHandle.getFileHandle(fileName, { + create: true, + }); + const writable = await fileHandle.createWritable(); + await writable.write(buffer); + await writable.close(); + } + + private async ensureDirectoryPath( + parts: string[], + recursive = false, + ): Promise { + let currentHandle = this.rootHandle; + + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + currentHandle = await currentHandle.getDirectoryHandle(part, { + create: recursive, + }); + } + return currentHandle; + } + + async _readDir(path: string): Promise { + const handle = await this.resolveHandle(path); + if (handle.kind !== "directory") { + throw new Error(`Path is not a directory: ${path}`); + } + + const entries: DirectoryEntry[] = []; + + for await (const [name, entry] of (handle as FileSystemDirectoryHandle) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + .entries()) { + entries.push({ name, isDir: entry.kind === "directory" }); + } + return entries; + } + + async _mkdir(path: string, recursive = false): Promise { + const parts = await this.resolvePathComponents(path); + await this.ensureDirectoryPath(parts, recursive); + } + + async _stat(path: string): Promise { + const handle = await this.resolveHandle(path); + let size = 0; + if (handle.kind === "file") { + const file = await (handle as FileSystemFileHandle).getFile(); + size = file.size; + } + return { + name: path.split("/").pop() || "", + isDir: handle.kind === "directory", + size, + modified: new Date(), // 使用当前时间作为替代方案 + }; + } + + async _rename(oldPath: string, newPath: string): Promise { + // 递归复制函数 + const copyRecursive = async ( + srcHandle: FileSystemHandle, + destDir: FileSystemDirectoryHandle, + newName: string, + ) => { + if (srcHandle.kind === "file") { + const file = await (srcHandle as FileSystemFileHandle).getFile(); + const newFile = await destDir.getFileHandle(newName, { create: true }); + const writable = await newFile.createWritable(); + await writable.write(await file.arrayBuffer()); + await writable.close(); + } else { + const newDir = await destDir.getDirectoryHandle(newName, { + create: true, + }); + const srcDir = srcHandle as FileSystemDirectoryHandle; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + for await (const [name, entry] of srcDir.entries()) { + await copyRecursive(entry, newDir, name); + } + } + }; + + const oldHandle = await this.resolveHandle(oldPath); + const newParts = await this.resolvePathComponents(newPath); + const newName = newParts.pop()!; + const newDirHandle = await this.ensureDirectoryPath(newParts, true); + + await copyRecursive(oldHandle, newDirHandle, newName); + await this._delete(oldHandle.kind, oldPath); + } + + private async _delete( + kind: "file" | "directory", + path: string, + ): Promise { + const parts = await this.resolvePathComponents(path); + const targetName = parts.pop()!; + const parentHandle = await this.ensureDirectoryPath(parts); + + await parentHandle.removeEntry(targetName, { + recursive: kind === "directory", + }); + } + + async _deleteFile(path: string): Promise { + await this._delete("file", path); + } + + async _deleteDirectory(path: string): Promise { + await this._delete("directory", path); + } + + async _exists(path: string): Promise { + try { + await this.resolveHandle(path); + return true; + } catch { + return false; + } + } +} diff --git a/src/utils/fs.tsx b/src/utils/fs/com.tsx similarity index 52% rename from src/utils/fs.tsx rename to src/utils/fs/com.tsx index bfc448a4..5630d479 100644 --- a/src/utils/fs.tsx +++ b/src/utils/fs/com.tsx @@ -1,6 +1,6 @@ -import { invoke } from "@tauri-apps/api/core"; -import { PathString } from "./pathString"; -import { isWeb } from "./platform"; +import { isWeb } from "../platform"; +import { TauriBaseFS } from "./TauriFileSystem"; +import { PathString } from "../pathString"; /** * 检查一个文件是否存在 @@ -11,7 +11,7 @@ export async function exists(path: string): Promise { if (isWeb) { return true; } else { - return invoke("exists", { path }); + return TauriBaseFS.exists(path); } } @@ -40,7 +40,7 @@ export async function readTextFile(path: string): Promise { input.click(); }); } else { - return invoke("read_text_file", { path }); + return TauriBaseFS.readTextFile(path); } } @@ -70,50 +70,44 @@ export async function readFile(path: string): Promise { input.click(); }); } else { - const base64 = await invoke("read_file_base64", { path }); - const binary = atob(base64); - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) { - bytes[i] = binary.charCodeAt(i); - } - return bytes; + return TauriBaseFS.readFile(path); } } -/** - * 读取文件并以 base64 编码返回 - * @param path 文件路径 - * @returns 文件的 base64 编码 - */ -export async function readFileBase64(path: string): Promise { - if (isWeb) { - return new Promise((resolve, reject) => { - const input = document.createElement("input"); - input.type = "file"; - input.onchange = () => { - const file = input.files?.item(0); - if (file) { - const reader = new FileReader(); - reader.onload = () => { - const content = reader.result as string; - resolve(content); - }; - reader.onerror = reject; - reader.readAsDataURL(file); - } - }; - input.click(); - }); - } else { - return invoke("read_file_base64", { path }); - } -} +// /** +// * 读取文件并以 base64 编码返回 +// * @param path 文件路径 +// * @returns 文件的 base64 编码 +// */ +// export async function readFileBase64(path: string): Promise { +// if (isWeb) { +// return new Promise((resolve, reject) => { +// const input = document.createElement("input"); +// input.type = "file"; +// input.onchange = () => { +// const file = input.files?.item(0); +// if (file) { +// const reader = new FileReader(); +// reader.onload = () => { +// const content = reader.result as string; +// resolve(content); +// }; +// reader.onerror = reject; +// reader.readAsDataURL(file); +// } +// }; +// input.click(); +// }); +// } else { +// return invoke("read_file_base64", { path }); +// } +// } -/** - * 将内容写入文本文件 - * @param path 文件路径 - * @param content 文件内容 - */ +// /** +// * 将内容写入文本文件 +// * @param path 文件路径 +// * @param content 文件内容 +// */ export async function writeTextFile( path: string, content: string, @@ -127,7 +121,7 @@ export async function writeTextFile( a.click(); URL.revokeObjectURL(url); } else { - return invoke("write_text_file", { path, content }); + return TauriBaseFS.writeFile(path, content); } } @@ -149,34 +143,28 @@ export async function writeFile( a.click(); URL.revokeObjectURL(url); } else { - const base64url = await new Promise((resolve) => { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result as string); - reader.readAsDataURL(new Blob([content])); - }); - const base64 = btoa(base64url.split(",")[1]); - return invoke("write_file_base64", { path, content: base64 }); + return TauriBaseFS.writeFile(path, content); } } -/** - * 将内容以 base64 编码写入文件 - * @param path 文件路径 - * @param content 文件的 base64 编码内容 - */ -export async function writeFileBase64( - path: string, - content: string, -): Promise { - if (isWeb) { - const blob = new Blob([content], { type: "text/plain" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = PathString.absolute2file(path); - a.click(); - URL.revokeObjectURL(url); - } else { - return invoke("write_file_base64", { path, content }); - } -} +// /** +// * 将内容以 base64 编码写入文件 +// * @param path 文件路径 +// * @param content 文件的 base64 编码内容 +// */ +// export async function writeFileBase64( +// path: string, +// content: string, +// ): Promise { +// if (isWeb) { +// const blob = new Blob([content], { type: "text/plain" }); +// const url = URL.createObjectURL(blob); +// const a = document.createElement("a"); +// a.href = url; +// a.download = PathString.absolute2file(path); +// a.click(); +// URL.revokeObjectURL(url); +// } else { +// return invoke("write_file_base64", { path, content }); +// } +// } diff --git a/src/utils/platform.tsx b/src/utils/platform.tsx index 6de91a0d..f0b5cfcb 100644 --- a/src/utils/platform.tsx +++ b/src/utils/platform.tsx @@ -12,6 +12,10 @@ export const isDesktop = !isMobile; export const isMac = !isWeb && platform() === "macos"; export const appScale = isMobile ? 0.5 : 1; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +//@ts-ignore +export const webFileApiSupport = !!window.showDirectoryPicker; + export function family() { if (isWeb) { // 从userAgent判断unix|windows From a9bc72584c0fbc7cf47fe7fde6ba077fe4b18f59 Mon Sep 17 00:00:00 2001 From: Jorylee <1747358809@qq.com> Date: Sat, 1 Feb 2025 22:03:45 +0800 Subject: [PATCH 04/11] =?UTF-8?q?build(deps):=20=E6=B7=BB=E5=8A=A0=20jszip?= =?UTF-8?q?=20=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 package.json 中添加 jszip 依赖,版本为 ^3.10.1 - jszip 是一个用于处理 ZIP 文件的 JavaScript 库,可能用于文件压缩或解压功能 --- package.json | 1 + pnpm-lock.yaml | 134 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) diff --git a/package.json b/package.json index 9fc25d51..ae570bd7 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@tauri-apps/plugin-store": "^2.2.0", "@tauri-apps/plugin-updater": "~2", "i18next": "^24.2.0", + "jszip": "^3.10.1", "lucide-react": "^0.469.0", "markdown-it": "^14.1.0", "react": "^19.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 844f7ba2..0803d719 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,6 +46,9 @@ importers: i18next: specifier: ^24.2.0 version: 24.2.0(typescript@5.7.2) + jszip: + specifier: ^3.10.1 + version: 3.10.1 lucide-react: specifier: ^0.469.0 version: 0.469.0(react@19.0.0) @@ -1307,6 +1310,7 @@ packages: } cpu: [arm] os: [linux] + libc: [glibc] "@rollup/rollup-linux-arm-musleabihf@4.29.1": resolution: @@ -1315,6 +1319,7 @@ packages: } cpu: [arm] os: [linux] + libc: [musl] "@rollup/rollup-linux-arm64-gnu@4.29.1": resolution: @@ -1323,6 +1328,7 @@ packages: } cpu: [arm64] os: [linux] + libc: [glibc] "@rollup/rollup-linux-arm64-musl@4.29.1": resolution: @@ -1331,6 +1337,7 @@ packages: } cpu: [arm64] os: [linux] + libc: [musl] "@rollup/rollup-linux-loongarch64-gnu@4.29.1": resolution: @@ -1339,6 +1346,7 @@ packages: } cpu: [loong64] os: [linux] + libc: [glibc] "@rollup/rollup-linux-powerpc64le-gnu@4.29.1": resolution: @@ -1347,6 +1355,7 @@ packages: } cpu: [ppc64] os: [linux] + libc: [glibc] "@rollup/rollup-linux-riscv64-gnu@4.29.1": resolution: @@ -1355,6 +1364,7 @@ packages: } cpu: [riscv64] os: [linux] + libc: [glibc] "@rollup/rollup-linux-s390x-gnu@4.29.1": resolution: @@ -1363,6 +1373,7 @@ packages: } cpu: [s390x] os: [linux] + libc: [glibc] "@rollup/rollup-linux-x64-gnu@4.29.1": resolution: @@ -1371,6 +1382,7 @@ packages: } cpu: [x64] os: [linux] + libc: [glibc] "@rollup/rollup-linux-x64-musl@4.29.1": resolution: @@ -1379,6 +1391,7 @@ packages: } cpu: [x64] os: [linux] + libc: [musl] "@rollup/rollup-win32-arm64-msvc@4.29.1": resolution: @@ -1579,6 +1592,7 @@ packages: engines: { node: ">=10" } cpu: [arm64] os: [linux] + libc: [glibc] "@swc/core-linux-arm64-musl@1.10.1": resolution: @@ -1588,6 +1602,7 @@ packages: engines: { node: ">=10" } cpu: [arm64] os: [linux] + libc: [musl] "@swc/core-linux-x64-gnu@1.10.1": resolution: @@ -1597,6 +1612,7 @@ packages: engines: { node: ">=10" } cpu: [x64] os: [linux] + libc: [glibc] "@swc/core-linux-x64-musl@1.10.1": resolution: @@ -1606,6 +1622,7 @@ packages: engines: { node: ">=10" } cpu: [x64] os: [linux] + libc: [musl] "@swc/core-win32-arm64-msvc@1.10.1": resolution: @@ -1699,6 +1716,7 @@ packages: engines: { node: ">= 10" } cpu: [arm64] os: [linux] + libc: [glibc] "@tauri-apps/cli-linux-arm64-musl@2.1.0": resolution: @@ -1708,6 +1726,7 @@ packages: engines: { node: ">= 10" } cpu: [arm64] os: [linux] + libc: [musl] "@tauri-apps/cli-linux-x64-gnu@2.1.0": resolution: @@ -1717,6 +1736,7 @@ packages: engines: { node: ">= 10" } cpu: [x64] os: [linux] + libc: [glibc] "@tauri-apps/cli-linux-x64-musl@2.1.0": resolution: @@ -1726,6 +1746,7 @@ packages: engines: { node: ">= 10" } cpu: [x64] os: [linux] + libc: [musl] "@tauri-apps/cli-win32-arm64-msvc@2.1.0": resolution: @@ -2629,6 +2650,12 @@ packages: } engines: { node: ">=12.13" } + core-util-is@1.0.3: + resolution: + { + integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==, + } + cosmiconfig@8.3.6: resolution: { @@ -3506,6 +3533,12 @@ packages: engines: { node: ">=0.10.0" } hasBin: true + immediate@3.0.6: + resolution: + { + integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==, + } + immutable@5.0.3: resolution: { @@ -3526,6 +3559,12 @@ packages: } engines: { node: ">=0.8.19" } + inherits@2.0.4: + resolution: + { + integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==, + } + internal-slot@1.1.0: resolution: { @@ -3768,6 +3807,12 @@ packages: } engines: { node: ">=12.13" } + isarray@1.0.0: + resolution: + { + integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==, + } + isarray@2.0.5: resolution: { @@ -3894,6 +3939,12 @@ packages: } engines: { node: ">=4.0" } + jszip@3.10.1: + resolution: + { + integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==, + } + keyv@4.5.4: resolution: { @@ -3922,6 +3973,12 @@ packages: } engines: { node: ">= 0.8.0" } + lie@3.3.0: + resolution: + { + integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==, + } + lilconfig@3.1.3: resolution: { @@ -4354,6 +4411,12 @@ packages: integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==, } + pako@1.0.11: + resolution: + { + integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==, + } + parent-module@1.0.1: resolution: { @@ -4645,6 +4708,12 @@ packages: engines: { node: ">=14" } hasBin: true + process-nextick-args@2.0.1: + resolution: + { + integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==, + } + prop-types@15.8.1: resolution: { @@ -4749,6 +4818,12 @@ packages: integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==, } + readable-stream@2.3.8: + resolution: + { + integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==, + } + readdirp@3.6.0: resolution: { @@ -4869,6 +4944,12 @@ packages: } engines: { node: ">=0.4" } + safe-buffer@5.1.2: + resolution: + { + integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==, + } + safe-regex-test@1.1.0: resolution: { @@ -5144,6 +5225,12 @@ packages: } engines: { node: ">= 0.4" } + setimmediate@1.0.5: + resolution: + { + integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==, + } + shebang-command@2.0.0: resolution: { @@ -5332,6 +5419,12 @@ packages: } engines: { node: ">= 0.4" } + string_decoder@1.1.1: + resolution: + { + integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==, + } + stringify-entities@4.0.4: resolution: { @@ -7643,6 +7736,8 @@ snapshots: dependencies: is-what: 4.1.16 + core-util-is@1.0.3: {} + cosmiconfig@8.3.6(typescript@5.7.2): dependencies: import-fresh: 3.3.0 @@ -8284,6 +8379,8 @@ snapshots: image-size@0.5.5: optional: true + immediate@3.0.6: {} + immutable@5.0.3: {} import-fresh@3.3.0: @@ -8293,6 +8390,8 @@ snapshots: imurmurhash@0.1.4: {} + inherits@2.0.4: {} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -8421,6 +8520,8 @@ snapshots: is-what@4.1.16: {} + isarray@1.0.0: {} + isarray@2.0.5: {} isexe@2.0.0: {} @@ -8505,6 +8606,13 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -8530,6 +8638,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -8791,6 +8903,8 @@ snapshots: package-json-from-dist@1.0.1: {} + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -8897,6 +9011,8 @@ snapshots: prettier@3.4.2: {} + process-nextick-args@2.0.1: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -8952,6 +9068,16 @@ snapshots: dependencies: pify: 2.3.0 + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -9052,6 +9178,8 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 + safe-buffer@5.1.2: {} + safe-regex-test@1.1.0: dependencies: call-bound: 1.0.3 @@ -9193,6 +9321,8 @@ snapshots: functions-have-names: 1.2.3 has-property-descriptors: 1.0.2 + setimmediate@1.0.5: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -9334,6 +9464,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 From a7ac33519fe7ff05a13d2eadb4237e31678d0efd Mon Sep 17 00:00:00 2001 From: Jorylee <1747358809@qq.com> Date: Sat, 1 Feb 2025 22:04:04 +0800 Subject: [PATCH 05/11] =?UTF-8?q?feat(fs):=20=E4=BC=98=E5=8C=96=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E7=B3=BB=E7=BB=9F=E5=8A=9F=E8=83=BD=E5=B9=B6=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=9B=AE=E5=BD=95=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构 IndexedDBFileSystem 类,优化文件和目录操作逻辑 - 新增目录创建、读取、重命名和删除功能 - 改进文件重命名和删除,支持自动更新父目录条目 - 优化文件下载时的文件名处理,支持保留文件后缀 --- src/utils/fs/IndexedDBFileSystem.tsx | 353 ++++++++++++++------------- src/utils/fs/com.tsx | 4 +- 2 files changed, 180 insertions(+), 177 deletions(-) diff --git a/src/utils/fs/IndexedDBFileSystem.tsx b/src/utils/fs/IndexedDBFileSystem.tsx index fc329743..4eed1982 100644 --- a/src/utils/fs/IndexedDBFileSystem.tsx +++ b/src/utils/fs/IndexedDBFileSystem.tsx @@ -6,6 +6,13 @@ import { const DB_VERSION = 1; +function asPromise(i: IDBRequest) { + return new Promise((resolve, reject) => { + i.onsuccess = () => resolve(i.result); + i.onerror = () => reject(i.error); + }); +} + export class IndexedDBFileSystem extends IFileSystem { private db: IDBDatabase | null = null; private DIR_STORE_NAME: string; @@ -19,33 +26,18 @@ export class IndexedDBFileSystem extends IFileSystem { } private async initDB(): Promise { - return new Promise((resolve, reject) => { - const request = indexedDB.open(this.DB_NAME, DB_VERSION); - - request.onupgradeneeded = (event) => { - const db = (event.target as IDBOpenDBRequest).result; - console.log( - this, - db.objectStoreNames.contains(this.STORE_NAME), - db.objectStoreNames.contains(this.DIR_STORE_NAME), - ); - if (!db.objectStoreNames.contains(this.STORE_NAME)) { - db.createObjectStore(this.STORE_NAME, { keyPath: "path" }); - } - if (!db.objectStoreNames.contains(this.DIR_STORE_NAME)) { - db.createObjectStore(this.DIR_STORE_NAME, { keyPath: "path" }); - } - }; + const request = indexedDB.open(this.DB_NAME, DB_VERSION); - request.onsuccess = (event) => { - this.db = (event.target as IDBOpenDBRequest).result; - resolve(); - }; - - request.onerror = (event) => { - reject((event.target as IDBOpenDBRequest).error); - }; - }); + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + if (!db.objectStoreNames.contains(this.STORE_NAME)) { + db.createObjectStore(this.STORE_NAME, { keyPath: "path" }); + } + if (!db.objectStoreNames.contains(this.DIR_STORE_NAME)) { + db.createObjectStore(this.DIR_STORE_NAME, { keyPath: "path" }); + } + }; + this.db = await asPromise(request); } private async getTransaction( @@ -65,6 +57,7 @@ export class IndexedDBFileSystem extends IFileSystem { const transaction = await this.getTransaction(storeName, mode); return transaction.objectStore(storeName); } + private normalizePath(path: string): string { let normalized = path.replace(/\/+/g, "/").replace(/\/$/, ""); if (normalized === "") return "/"; @@ -88,82 +81,68 @@ export class IndexedDBFileSystem extends IFileSystem { const parentPath = this.getParentPath(childPath); if (!parentPath) return; + // 确保父目录存在(递归创建) + if (!(await this._exists(parentPath))) { + await this._mkdir(parentPath, true); + } + const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); - const parentDir = await new Promise((resolve, reject) => { - const req = store.get(parentPath); - req.onsuccess = () => resolve(req.result); - req.onerror = () => reject(req.error); - }); + const parentDir = await asPromise(store.get(parentPath)); + const childName = childPath.split("/").pop()!; - if (!parentDir) throw new Error(`Parent directory ${parentPath} not found`); + if (!parentDir) { + throw new Error(`Parent directory ${parentPath} not found`); + } - const childName = childPath.split("/").pop()!; const exists = parentDir.entries.some( (e: DirectoryEntry) => e.name === childName, ); if (!exists) { parentDir.entries.push({ name: childName, isDir }); - await new Promise((resolve, reject) => { - const req = store.put(parentDir); - req.onsuccess = () => resolve(); - req.onerror = () => reject(req.error); - }); + await asPromise(store.put(parentDir)); } } async _exists(path: string): Promise { - if (!this.db) { - await this.initDB(); - } - const transaction = await this.getTransaction( + path = this.normalizePath(path); + const trans = await this.getTransaction( [this.STORE_NAME, this.DIR_STORE_NAME], - "readwrite", + "readonly", ); - const filesStore = transaction.objectStore(this.STORE_NAME); - const dirsStore = transaction.objectStore(this.DIR_STORE_NAME); - return new Promise((resolve) => { - const request = filesStore.get(path); - request.onsuccess = () => { - if (request.result) { - resolve(true); - } else { - const dirRequest = dirsStore.get(path); - dirRequest.onsuccess = () => resolve(!!dirRequest.result); - dirRequest.onerror = () => resolve(false); - } - }; - request.onerror = () => resolve(false); - }); + const fileExists = !!(await asPromise( + trans.objectStore(this.STORE_NAME).get(path), + )); + if (fileExists) return true; + return !!(await asPromise( + trans.objectStore(this.DIR_STORE_NAME).get(path), + )); } async _readFile(path: string): Promise { const store = await this.getStore(this.STORE_NAME); - return new Promise((resolve, reject) => { - const request = store.get(path); - request.onsuccess = () => { - if (request.result) { - resolve(request.result.content); - } else { - reject(new Error(`File not found: ${path}`)); - } - }; - request.onerror = () => reject(request.error); - }); + const result = await asPromise(store.get(path)); + if (result) return result.content; + throw new Error(`File not found: ${path}`); } async _mkdir(path: string, recursive = false): Promise { path = this.normalizePath(path); + if (path === "/") { + const exists = await this._exists("/"); + if (!exists) { + const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); + await asPromise(store.put({ path: "/", entries: [] })); + } + return; + } + if (!recursive) { const parentPath = this.getParentPath(path); if (parentPath && !(await this._exists(parentPath))) { throw new Error(`Parent directory does not exist: ${parentPath}`); } const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); - await new Promise((resolve, reject) => { - const r = store.put({ path, entries: [] }); - r.onsuccess = () => resolve(); - r.onerror = () => reject(); - }); + await asPromise(store.put({ path, entries: [] })); if (parentPath) await this.addEntryToParent(path, true); return; } @@ -174,85 +153,69 @@ export class IndexedDBFileSystem extends IFileSystem { for (const part of parts) { currentPath = currentPath ? `${currentPath}/${part}` : `/${part}`; currentPath = this.normalizePath(currentPath); - if (!(await this._exists(currentPath))) pathsToCreate.push(currentPath); + if (!(await this._exists(currentPath))) { + pathsToCreate.push(currentPath); + } } - if (pathsToCreate.length > 0) { - const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); - for (const p of pathsToCreate) { - await new Promise((resolve, reject) => { - const r = store.put({ path: p, entries: [] }); - r.onsuccess = () => resolve(); - r.onerror = () => reject(); - }); - const parentPath = this.getParentPath(p); - if (parentPath) await this.addEntryToParent(p, true); - } + const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); + for (const p of pathsToCreate) { + await asPromise(store.put({ path: p, entries: [] })); + await this.addEntryToParent(p, true); } } async _writeFile(path: string, content: Uint8Array | string): Promise { path = this.normalizePath(path); + const parentPath = this.getParentPath(path); + if (parentPath) { + await this._mkdir(parentPath, true); + } + const store = await this.getStore(this.STORE_NAME, "readwrite"); const data = typeof content === "string" ? new TextEncoder().encode(content) : content; - await new Promise((resolve, reject) => { - const r = store.put({ path, content: data }); - r.onsuccess = () => resolve(); - r.onerror = () => reject(); - }); - const parentPath = this.getParentPath(path); - if (parentPath) await this.addEntryToParent(path, false); + await asPromise(store.put({ path, content: data })); + await this.addEntryToParent(path, false); } async _readDir(path: string): Promise { path = this.normalizePath(path); const store = await this.getStore(this.DIR_STORE_NAME); - return new Promise((resolve, reject) => { - const req = store.get(path); - req.onsuccess = () => resolve(req.result?.entries || []); - req.onerror = () => reject(req.error); - }); + const dirEntry = await asPromise(store.get(path)); + if (!dirEntry) { + throw new Error(`Directory not found: ${path}`); + } + return dirEntry.entries; } async _stat(path: string): Promise { - const transaction = await this.getTransaction( - [this.STORE_NAME, this.DIR_STORE_NAME], - "readwrite", - ); - const filesStore = transaction.objectStore(this.STORE_NAME); - const dirsStore = transaction.objectStore(this.DIR_STORE_NAME); - - return new Promise((resolve, reject) => { - const fileRequest = filesStore.get(path); - fileRequest.onsuccess = () => { - if (fileRequest.result) { - resolve({ - name: path.split("/").pop() || "", - isDir: false, - size: fileRequest.result.content.byteLength, - modified: new Date(), - }); - } else { - const dirRequest = dirsStore.get(path); - dirRequest.onsuccess = () => { - if (dirRequest.result) { - resolve({ - name: path.split("/").pop() || "", - isDir: true, - size: 0, - modified: new Date(), - }); - } else { - reject(new Error(`Path not found: ${path}`)); - } - }; - } + const fileStore = await this.getStore(this.STORE_NAME); + const dirStore = await this.getStore(this.DIR_STORE_NAME); + const file = await asPromise(fileStore.get(path)); + if (file) { + return { + name: path.split("/").pop() || "", + isDir: false, + size: file.content.byteLength, + modified: new Date(), }; - }); + } + const dir = await asPromise(dirStore.get(path)); + if (dir) { + return { + name: path.split("/").pop() || "", + isDir: true, + size: 0, + modified: new Date(), + }; + } + throw new Error(`Path not found: ${path}`); } async _rename(oldPath: string, newPath: string): Promise { + oldPath = this.normalizePath(oldPath); + newPath = this.normalizePath(newPath); const transaction = await this.getTransaction( [this.STORE_NAME, this.DIR_STORE_NAME], "readwrite", @@ -260,61 +223,101 @@ export class IndexedDBFileSystem extends IFileSystem { const filesStore = transaction.objectStore(this.STORE_NAME); const dirsStore = transaction.objectStore(this.DIR_STORE_NAME); - return new Promise((resolve, reject) => { - const moveFile = () => { - const request = filesStore.get(oldPath); - request.onsuccess = () => { - if (request.result) { - const content = request.result.content; - const deleteRequest = filesStore.delete(oldPath); - deleteRequest.onsuccess = () => { - const putRequest = filesStore.put({ path: newPath, content }); - putRequest.onsuccess = () => resolve(); - putRequest.onerror = () => reject(putRequest.error); - }; - } else { - moveDir(); - } - }; - }; - - const moveDir = () => { - const request = dirsStore.get(oldPath); - request.onsuccess = () => { - if (request.result) { - const entries = request.result.entries; - const deleteRequest = dirsStore.delete(oldPath); - deleteRequest.onsuccess = () => { - const putRequest = dirsStore.put({ path: newPath, entries }); - putRequest.onsuccess = () => resolve(); - putRequest.onerror = () => reject(putRequest.error); - }; - } else { - reject(new Error(`Path not found: ${oldPath}`)); + const isDir = !!(await asPromise(dirsStore.get(oldPath))); + if (isDir) { + // 处理目录重命名及子项 + const dir = await asPromise(dirsStore.get(oldPath)); + if (!dir) throw new Error(`Directory not found: ${oldPath}`); + await asPromise(dirsStore.delete(oldPath)); + await asPromise(dirsStore.put({ ...dir, path: newPath })); + + // 更新所有子路径 + const filesCursor = await asPromise(filesStore.openCursor()); + const dirsCursor = await asPromise(dirsStore.openCursor()); + const updateEntries = async (cursor: IDBCursorWithValue | null) => { + while (cursor) { + const oldChildPath = cursor.key as string; + if (oldChildPath.startsWith(`${oldPath}/`)) { + const newChildPath = newPath + oldChildPath.slice(oldPath.length); + if (cursor.source.name === this.STORE_NAME) { + await asPromise(filesStore.delete(oldChildPath)); + await asPromise( + filesStore.put({ ...cursor.value, path: newChildPath }), + ); + } else { + await asPromise(dirsStore.delete(oldChildPath)); + await asPromise( + dirsStore.put({ ...cursor.value, path: newChildPath }), + ); + } } - }; + cursor.continue(); + } }; + await updateEntries(filesCursor); + await updateEntries(dirsCursor); + } else { + // 处理文件重命名 + const file = await asPromise(filesStore.get(oldPath)); + if (!file) throw new Error(`File not found: ${oldPath}`); + await asPromise(filesStore.delete(oldPath)); + await asPromise(filesStore.put({ ...file, path: newPath })); + } - moveFile(); - }); + // 更新父目录条目 + const oldParentPath = this.getParentPath(oldPath); + const newParentPath = this.getParentPath(newPath); + if (oldParentPath) { + const oldParentStore = await this.getStore( + this.DIR_STORE_NAME, + "readwrite", + ); + const oldParent = await asPromise(oldParentStore.get(oldParentPath)); + if (oldParent) { + const entryName = oldPath.split("/").pop()!; + oldParent.entries = oldParent.entries.filter( + (e: DirectoryEntry) => e.name !== entryName, + ); + await asPromise(oldParentStore.put(oldParent)); + } + } + if (newParentPath) { + await this.addEntryToParent(newPath, isDir); + } } async _deleteFile(path: string): Promise { const store = await this.getStore(this.STORE_NAME, "readwrite"); - return new Promise((resolve, reject) => { - const request = store.delete(path); - request.onsuccess = () => resolve(); - request.onerror = () => reject(request.error); - }); + await asPromise(store.delete(path)); + const parentPath = this.getParentPath(path); + if (parentPath) { + const parentStore = await this.getStore(this.DIR_STORE_NAME, "readwrite"); + const parent = await asPromise(parentStore.get(parentPath)); + if (parent) { + const entryName = path.split("/").pop()!; + parent.entries = parent.entries.filter( + (e: DirectoryEntry) => e.name !== entryName, + ); + await asPromise(parentStore.put(parent)); + } + } } async _deleteDirectory(path: string): Promise { const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); - return new Promise((resolve, reject) => { - const request = store.delete(path); - request.onsuccess = () => resolve(); - request.onerror = () => reject(request.error); - }); + await asPromise(store.delete(path)); + const parentPath = this.getParentPath(path); + if (parentPath) { + const parentStore = await this.getStore(this.DIR_STORE_NAME, "readwrite"); + const parent = await asPromise(parentStore.get(parentPath)); + if (parent) { + const entryName = path.split("/").pop()!; + parent.entries = parent.entries.filter( + (e: DirectoryEntry) => e.name !== entryName, + ); + await asPromise(parentStore.put(parent)); + } + } } async clear() { diff --git a/src/utils/fs/com.tsx b/src/utils/fs/com.tsx index 5630d479..baa9baee 100644 --- a/src/utils/fs/com.tsx +++ b/src/utils/fs/com.tsx @@ -117,7 +117,7 @@ export async function writeTextFile( const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; - a.download = PathString.absolute2file(path); + a.download = PathString.absolute2fileWithExt(path); a.click(); URL.revokeObjectURL(url); } else { @@ -139,7 +139,7 @@ export async function writeFile( const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; - a.download = PathString.absolute2file(path); + a.download = PathString.absolute2fileWithExt(path); a.click(); URL.revokeObjectURL(url); } else { From b55c6c5100e2870731994554994fc380671fad33 Mon Sep 17 00:00:00 2001 From: Jorylee <1747358809@qq.com> Date: Sat, 1 Feb 2025 22:04:22 +0800 Subject: [PATCH 06/11] =?UTF-8?q?feat(file):=20=E6=94=B9=E8=BF=9B=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=89=93=E5=BC=80=E5=8A=9F=E8=83=BD=E5=B9=B6=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=96=87=E4=BB=B6=E5=90=8E=E7=BC=80=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构文件打开流程,使用 VFileSystem 统一处理文件读取 - 修改文件路径处理逻辑,支持获取文件名和文件名(含后缀) - 更新文件对话框默认文件名,使用 .gp 后缀 --- src/core/service/RecentFileManager.tsx | 10 ++++++---- src/pages/_app_menu.tsx | 2 +- src/utils/pathString.tsx | 22 ++++++++++++++++------ 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/core/service/RecentFileManager.tsx b/src/core/service/RecentFileManager.tsx index 03dc4886..d671e6e6 100644 --- a/src/core/service/RecentFileManager.tsx +++ b/src/core/service/RecentFileManager.tsx @@ -1,7 +1,7 @@ import { Store } from "@tauri-apps/plugin-store"; // import { exists } from "@tauri-apps/plugin-fs"; // 导入文件相关函数 import { Serialized } from "../../types/node"; -import { exists, readTextFile } from "../../utils/fs/com"; +import { exists } from "../../utils/fs/com"; import { createStore } from "../../utils/store"; import { Camera } from "../stage/Camera"; import { Stage } from "../stage/Stage"; @@ -15,6 +15,7 @@ import { Section } from "../stage/stageObject/entity/Section"; import { TextNode } from "../stage/stageObject/entity/TextNode"; import { UrlNode } from "../stage/stageObject/entity/UrlNode"; import { ViewFlashEffect } from "./effectEngine/concrete/ViewFlashEffect"; +import { VFileSystem } from "./VFileSystem"; /** * 管理最近打开的文件列表 @@ -143,16 +144,17 @@ export namespace RecentFileManager { */ export async function openFileByPath(path: string) { StageManager.destroy(); - let content: string; try { - content = await readTextFile(path); + await VFileSystem.loadFromPath(path); } catch (e) { console.error("打开文件失败:", path); console.error(e); return; } - const data = StageLoader.validate(JSON.parse(content)); + const data = StageLoader.validate( + JSON.parse(await VFileSystem.getMetaData()), + ); loadStageByData(data); StageHistoryManager.reset(data); diff --git a/src/pages/_app_menu.tsx b/src/pages/_app_menu.tsx index d221fbb7..afaeeaf1 100644 --- a/src/pages/_app_menu.tsx +++ b/src/pages/_app_menu.tsx @@ -143,7 +143,7 @@ export default function AppMenu({ const openFileByDialogWindow = async () => { const path = isWeb - ? "file.json" + ? "file.gp" : await openFileDialog({ title: "打开文件", directory: false, diff --git a/src/utils/pathString.tsx b/src/utils/pathString.tsx index d768ff66..e676e5da 100644 --- a/src/utils/pathString.tsx +++ b/src/utils/pathString.tsx @@ -7,6 +7,21 @@ export namespace PathString { * @returns */ export function absolute2file(path: string): string { + const file = absolute2fileWithExt(path); + const parts = file.split("."); + if (parts.length > 1) { + return parts.slice(0, -1).join("."); + } else { + return file; + } + } + + /** + * 将绝对路径转换为文件名(含文件后缀) + * @param path + * @returns + */ + export function absolute2fileWithExt(path: string): string { const fam = family(); // const fam = "windows"; // vitest 测试时打开此行注释 @@ -17,12 +32,7 @@ export namespace PathString { if (!file) { throw new Error("Invalid path"); } - const parts = file.split("."); - if (parts.length > 1) { - return parts.slice(0, -1).join("."); - } else { - return file; - } + return file; } /** From c7f307be83dd47275b3f70d416267c4c457b82f5 Mon Sep 17 00:00:00 2001 From: Jorylee <1747358809@qq.com> Date: Sat, 1 Feb 2025 23:52:10 +0800 Subject: [PATCH 07/11] =?UTF-8?q?feat(core):=20=E4=BF=AE=E6=94=B9=E8=99=9A?= =?UTF-8?q?=E6=8B=9F=E6=96=87=E4=BB=B6=E7=B3=BB=E7=BB=9F=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构 VFileSystem 类,增加 pullMetaData 和 saveToPath 方法 - 更新 RecentFileManager 和 StageSaveManager,使用新的文件系统方法 - 修改 ControllerDragFile 和 CopyEngine,使用 VFileSystem 替代直接文件操作 - 更新 StageExportEngine 和 SoundService,使用新的文件读写方法 - 重构 fs/com.tsx,提供统一的文件操作接口 --- src/core/service/VFileSystem.tsx | 15 +++++--- .../concrete/ControllerDragFile.tsx | 6 +-- .../dataFileService/RecentFileManager.tsx | 9 +++-- .../dataFileService/StageSaveManager.tsx | 10 +++-- .../stageExportEngine/stageExportEngine.tsx | 2 +- .../copyEngine/copyEngine.tsx | 37 ++++++++++--------- .../service/feedbackService/SoundService.tsx | 2 +- src/utils/fs/com.tsx | 2 +- 8 files changed, 45 insertions(+), 38 deletions(-) diff --git a/src/core/service/VFileSystem.tsx b/src/core/service/VFileSystem.tsx index f6751cd3..f6e59db9 100644 --- a/src/core/service/VFileSystem.tsx +++ b/src/core/service/VFileSystem.tsx @@ -1,6 +1,7 @@ import JSZip, * as jszip from "jszip"; import { IndexedDBFileSystem } from "../../utils/fs/IndexedDBFileSystem"; -import { readFile } from "../../utils/fs/com"; +import { readFile, writeFile } from "../../utils/fs/com"; +import { StageDumper } from "../stage/StageDumper"; export enum FSType { Tauri = "Tauri", @@ -15,6 +16,9 @@ export namespace VFileSystem { export async function setMetaData(content: string) { return fs.writeTextFile("/meta.json", content); } + export async function pullMetaData() { + setMetaData(JSON.stringify(StageDumper.dump())); + } export async function loadFromPath(path: string) { const data = await readFile(path); const zip = await jszip.loadAsync(data); @@ -35,10 +39,7 @@ export namespace VFileSystem { try { // 分离目录和文件名 const lastSlashIndex = normalizedPath.lastIndexOf("/"); - const parentDir = - lastSlashIndex >= 0 - ? normalizedPath.slice(0, lastSlashIndex) - : ""; + const parentDir = lastSlashIndex >= 0 ? normalizedPath.slice(0, lastSlashIndex) : ""; // 创建父目录(如果存在) if (parentDir) { @@ -58,6 +59,10 @@ export namespace VFileSystem { await Promise.all(operations); } + export async function saveToPath(path: string) { + await setMetaData(JSON.stringify(StageDumper.dump())); + await writeFile(path, await VFileSystem.exportZipData()); + } export async function exportZipData(): Promise { const zip = new JSZip(); diff --git a/src/core/service/controlService/controller/concrete/ControllerDragFile.tsx b/src/core/service/controlService/controller/concrete/ControllerDragFile.tsx index 6ff92533..17010a2c 100644 --- a/src/core/service/controlService/controller/concrete/ControllerDragFile.tsx +++ b/src/core/service/controlService/controller/concrete/ControllerDragFile.tsx @@ -1,6 +1,4 @@ import { v4 as uuidv4 } from "uuid"; -import { writeFileBase64 } from "../../../../../utils/fs"; -import { PathString } from "../../../../../utils/pathString"; import { Color } from "../../../../dataStruct/Color"; import { Vector } from "../../../../dataStruct/Vector"; import { Renderer } from "../../../../render/canvas2d/renderer"; @@ -12,6 +10,7 @@ import { TextNode } from "../../../../stage/stageObject/entity/TextNode"; import { TextRiseEffect } from "../../../feedbackService/effectEngine/concrete/TextRiseEffect"; import { ViewFlashEffect } from "../../../feedbackService/effectEngine/concrete/ViewFlashEffect"; import { ControllerClassDragFile } from "../ControllerClassDragFile"; +import { VFileSystem } from "../../../VFileSystem"; /** * BUG: 始终无法触发文件拖入事件 @@ -169,8 +168,7 @@ function dealPngFileDrop(file: File, mouseWorldLocation: Vector) { // data:image/png;base64,iVBORw0KGgoAAAANS... // 在这里处理读取到的内容 const imageUUID = uuidv4(); - const folderPath = PathString.dirPath(Stage.path.getFilePath()); - writeFileBase64(`${folderPath}${PathString.getSep()}${imageUUID}.png`, fileContent.split(",")[1]); + VFileSystem.getFS().writeFileBase64(`/picture/${imageUUID}.png`, fileContent.split(",")[1]); const imageNode = new ImageNode({ uuid: imageUUID, location: [mouseWorldLocation.x, mouseWorldLocation.y], diff --git a/src/core/service/dataFileService/RecentFileManager.tsx b/src/core/service/dataFileService/RecentFileManager.tsx index d00d0390..b4141c0a 100644 --- a/src/core/service/dataFileService/RecentFileManager.tsx +++ b/src/core/service/dataFileService/RecentFileManager.tsx @@ -1,7 +1,7 @@ import { Store } from "@tauri-apps/plugin-store"; // import { exists } from "@tauri-apps/plugin-fs"; // 导入文件相关函数 import { Serialized } from "../../../types/node"; -import { exists, readTextFile } from "../../../utils/fs"; +import { exists } from "../../../utils/fs/com"; import { createStore } from "../../../utils/store"; import { Camera } from "../../stage/Camera"; import { Stage } from "../../stage/Stage"; @@ -16,6 +16,7 @@ import { TextNode } from "../../stage/stageObject/entity/TextNode"; import { UrlNode } from "../../stage/stageObject/entity/UrlNode"; import { ViewFlashEffect } from "../feedbackService/effectEngine/concrete/ViewFlashEffect"; import { PenStroke } from "../../stage/stageObject/entity/PenStroke"; +import { VFileSystem } from "../VFileSystem"; /** * 管理最近打开的文件列表 @@ -144,16 +145,16 @@ export namespace RecentFileManager { */ export async function openFileByPath(path: string) { StageManager.destroy(); - let content: string; + try { - content = await readTextFile(path); + await VFileSystem.loadFromPath(path); } catch (e) { console.error("打开文件失败:", path); console.error(e); return; } - const data = StageLoader.validate(JSON.parse(content)); + const data = StageLoader.validate(JSON.parse(await VFileSystem.getMetaData())); loadStageByData(data); StageHistoryManager.reset(data); diff --git a/src/core/service/dataFileService/StageSaveManager.tsx b/src/core/service/dataFileService/StageSaveManager.tsx index 3b123bba..fa87231d 100644 --- a/src/core/service/dataFileService/StageSaveManager.tsx +++ b/src/core/service/dataFileService/StageSaveManager.tsx @@ -1,9 +1,10 @@ import { Serialized } from "../../../types/node"; -import { exists, writeTextFile } from "../../../utils/fs"; +import { exists, writeTextFile } from "../../../utils/fs/com"; import { PathString } from "../../../utils/pathString"; import { Stage } from "../../stage/Stage"; import { StageHistoryManager } from "../../stage/stageManager/StageHistoryManager"; import { ViewFlashEffect } from "../feedbackService/effectEngine/concrete/ViewFlashEffect"; +import { VFileSystem } from "../VFileSystem"; /** * 管理所有和保存相关的内容 @@ -16,7 +17,7 @@ export namespace StageSaveManager { * @param errorCallback */ export async function saveHandle(path: string, data: Serialized.File) { - await writeTextFile(path, JSON.stringify(data)); + await VFileSystem.saveToPath(path); Stage.effectMachine.addEffect(ViewFlashEffect.SaveFile()); StageHistoryManager.reset(data); // 重置历史 isCurrentSaved = true; @@ -37,7 +38,7 @@ export namespace StageSaveManager { if (Stage.path.isDraft()) { throw new Error("当前文档的状态为草稿,请您先保存为文件"); } - await writeTextFile(Stage.path.getFilePath(), JSON.stringify(data)); + await VFileSystem.saveToPath(Stage.path.getFilePath()); if (addFlashEffect) { Stage.effectMachine.addEffect(ViewFlashEffect.SaveFile()); } @@ -55,6 +56,7 @@ export namespace StageSaveManager { * @param errorCallback * @returns */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars export async function backupHandle(path: string, data: Serialized.File) { const backupFolderPath = PathString.dirPath(path); const isExists = await exists(backupFolderPath); @@ -62,7 +64,7 @@ export namespace StageSaveManager { throw new Error("备份文件路径错误:" + backupFolderPath); } - await writeTextFile(path, JSON.stringify(data)); + await VFileSystem.saveToPath(path); Stage.effectMachine.addEffect(ViewFlashEffect.SaveFile()); } /** diff --git a/src/core/service/dataGenerateService/stageExportEngine/stageExportEngine.tsx b/src/core/service/dataGenerateService/stageExportEngine/stageExportEngine.tsx index 1ee9af64..222038b9 100644 --- a/src/core/service/dataGenerateService/stageExportEngine/stageExportEngine.tsx +++ b/src/core/service/dataGenerateService/stageExportEngine/stageExportEngine.tsx @@ -1,4 +1,4 @@ -import { writeTextFile } from "../../../../utils/fs"; +import { writeTextFile } from "../../../../utils/fs/com"; import { GraphMethods } from "../../../stage/stageManager/basicMethods/GraphMethods"; import { ConnectableEntity } from "../../../stage/stageObject/abstract/ConnectableEntity"; import { Entity } from "../../../stage/stageObject/abstract/StageEntity"; diff --git a/src/core/service/dataManageService/copyEngine/copyEngine.tsx b/src/core/service/dataManageService/copyEngine/copyEngine.tsx index f992126d..4d9d274b 100644 --- a/src/core/service/dataManageService/copyEngine/copyEngine.tsx +++ b/src/core/service/dataManageService/copyEngine/copyEngine.tsx @@ -1,7 +1,6 @@ import { v4 as uuidv4 } from "uuid"; import { Dialog } from "../../../../components/dialog"; import { Serialized } from "../../../../types/node"; -import { writeFileBase64 } from "../../../../utils/fs"; import { PathString } from "../../../../utils/pathString"; import { Rectangle } from "../../../dataStruct/shape/Rectangle"; import { Vector } from "../../../dataStruct/Vector"; @@ -15,6 +14,7 @@ import { ImageNode } from "../../../stage/stageObject/entity/ImageNode"; import { TextNode } from "../../../stage/stageObject/entity/TextNode"; import { UrlNode } from "../../../stage/stageObject/entity/UrlNode"; import { MouseLocation } from "../../controlService/MouseLocation"; +import { VFileSystem } from "../../VFileSystem"; /** * 专门用来管理节点复制的引擎 @@ -129,13 +129,14 @@ async function readClipboardItems(mouseLocation: Vector) { } const blob = await item.getType(item.types[0]); // 获取 Blob 对象 const imageUUID = uuidv4(); - const folder = PathString.dirPath(Stage.path.getFilePath()); - const imagePath = `${folder}${PathString.getSep()}${imageUUID}.png`; + //const folder = PathString.dirPath(Stage.path.getFilePath()); + VFileSystem.getFS().writeFile(`/picture/${imageUUID}.png`, await blob.bytes()); + //const imagePath = `${folder}${PathString.getSep()}${imageUUID}.png`; // 2024.12.31 测试发现这样的写法会导致读取时base64解码失败 // writeFile(imagePath, new Uint8Array(await blob.arrayBuffer())); // 下面这样的写法是没有问题的 - writeFileBase64(imagePath, await convertBlobToBase64(blob)); + // writeFileBase64(imagePath, await convertBlobToBase64(blob)); // 要延迟一下,等待保存完毕 setTimeout(() => { @@ -190,17 +191,17 @@ function blobToText(blob: Blob): Promise { }); } -async function convertBlobToBase64(blob: Blob): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onloadend = () => { - if (typeof reader.result === "string") { - resolve(reader.result.split(",")[1]); // 去掉"data:image/png;base64,"前缀 - } else { - reject(new Error("Invalid result type")); - } - }; - reader.onerror = reject; - reader.readAsDataURL(blob); - }); -} +// async function convertBlobToBase64(blob: Blob): Promise { +// return new Promise((resolve, reject) => { +// const reader = new FileReader(); +// reader.onloadend = () => { +// if (typeof reader.result === "string") { +// resolve(reader.result.split(",")[1]); // 去掉"data:image/png;base64,"前缀 +// } else { +// reject(new Error("Invalid result type")); +// } +// }; +// reader.onerror = reject; +// reader.readAsDataURL(blob); +// }); +// } diff --git a/src/core/service/feedbackService/SoundService.tsx b/src/core/service/feedbackService/SoundService.tsx index c4c1cfa5..6fe304f5 100644 --- a/src/core/service/feedbackService/SoundService.tsx +++ b/src/core/service/feedbackService/SoundService.tsx @@ -2,7 +2,7 @@ // @tauri-apps/plugin-fs 只能读取文本文件,不能强行读取流文件并强转为ArrayBuffer // import { readTextFile } from "@tauri-apps/plugin-fs"; -import { readFile } from "../../../utils/fs"; +import { readFile } from "../../../utils/fs/com"; import { StringDict } from "../../dataStruct/StringDict"; import { Settings } from "../Settings"; diff --git a/src/utils/fs/com.tsx b/src/utils/fs/com.tsx index c563bfde..c2af0ac1 100644 --- a/src/utils/fs/com.tsx +++ b/src/utils/fs/com.tsx @@ -156,7 +156,7 @@ export async function writeFileBase64(path: string, content: string): Promise Date: Sat, 1 Feb 2025 23:58:08 +0800 Subject: [PATCH 08/11] =?UTF-8?q?refactor(service):=20=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=20VFileSystem=20=E5=88=B0=E5=90=88=E9=80=82=E7=9A=84=E4=BD=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/concrete/ControllerDragFile.tsx | 2 +- src/core/service/dataFileService/RecentFileManager.tsx | 2 +- src/core/service/dataFileService/StageSaveManager.tsx | 2 +- src/core/service/{ => dataFileService}/VFileSystem.tsx | 6 +++--- .../service/dataManageService/copyEngine/copyEngine.tsx | 2 +- src/core/stage/stageObject/entity/ImageNode.tsx | 2 +- src/main.tsx | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) rename src/core/service/{ => dataFileService}/VFileSystem.tsx (94%) diff --git a/src/core/service/controlService/controller/concrete/ControllerDragFile.tsx b/src/core/service/controlService/controller/concrete/ControllerDragFile.tsx index 17010a2c..cc533265 100644 --- a/src/core/service/controlService/controller/concrete/ControllerDragFile.tsx +++ b/src/core/service/controlService/controller/concrete/ControllerDragFile.tsx @@ -10,7 +10,7 @@ import { TextNode } from "../../../../stage/stageObject/entity/TextNode"; import { TextRiseEffect } from "../../../feedbackService/effectEngine/concrete/TextRiseEffect"; import { ViewFlashEffect } from "../../../feedbackService/effectEngine/concrete/ViewFlashEffect"; import { ControllerClassDragFile } from "../ControllerClassDragFile"; -import { VFileSystem } from "../../../VFileSystem"; +import { VFileSystem } from "../../../dataFileService/VFileSystem"; /** * BUG: 始终无法触发文件拖入事件 diff --git a/src/core/service/dataFileService/RecentFileManager.tsx b/src/core/service/dataFileService/RecentFileManager.tsx index b4141c0a..d0f9f1dd 100644 --- a/src/core/service/dataFileService/RecentFileManager.tsx +++ b/src/core/service/dataFileService/RecentFileManager.tsx @@ -16,7 +16,7 @@ import { TextNode } from "../../stage/stageObject/entity/TextNode"; import { UrlNode } from "../../stage/stageObject/entity/UrlNode"; import { ViewFlashEffect } from "../feedbackService/effectEngine/concrete/ViewFlashEffect"; import { PenStroke } from "../../stage/stageObject/entity/PenStroke"; -import { VFileSystem } from "../VFileSystem"; +import { VFileSystem } from "./VFileSystem"; /** * 管理最近打开的文件列表 diff --git a/src/core/service/dataFileService/StageSaveManager.tsx b/src/core/service/dataFileService/StageSaveManager.tsx index fa87231d..2811a87a 100644 --- a/src/core/service/dataFileService/StageSaveManager.tsx +++ b/src/core/service/dataFileService/StageSaveManager.tsx @@ -4,7 +4,7 @@ import { PathString } from "../../../utils/pathString"; import { Stage } from "../../stage/Stage"; import { StageHistoryManager } from "../../stage/stageManager/StageHistoryManager"; import { ViewFlashEffect } from "../feedbackService/effectEngine/concrete/ViewFlashEffect"; -import { VFileSystem } from "../VFileSystem"; +import { VFileSystem } from "./VFileSystem"; /** * 管理所有和保存相关的内容 diff --git a/src/core/service/VFileSystem.tsx b/src/core/service/dataFileService/VFileSystem.tsx similarity index 94% rename from src/core/service/VFileSystem.tsx rename to src/core/service/dataFileService/VFileSystem.tsx index f6e59db9..f52282e3 100644 --- a/src/core/service/VFileSystem.tsx +++ b/src/core/service/dataFileService/VFileSystem.tsx @@ -1,7 +1,7 @@ import JSZip, * as jszip from "jszip"; -import { IndexedDBFileSystem } from "../../utils/fs/IndexedDBFileSystem"; -import { readFile, writeFile } from "../../utils/fs/com"; -import { StageDumper } from "../stage/StageDumper"; +import { IndexedDBFileSystem } from "../../../utils/fs/IndexedDBFileSystem"; +import { readFile, writeFile } from "../../../utils/fs/com"; +import { StageDumper } from "../../stage/StageDumper"; export enum FSType { Tauri = "Tauri", diff --git a/src/core/service/dataManageService/copyEngine/copyEngine.tsx b/src/core/service/dataManageService/copyEngine/copyEngine.tsx index 4d9d274b..1e811e35 100644 --- a/src/core/service/dataManageService/copyEngine/copyEngine.tsx +++ b/src/core/service/dataManageService/copyEngine/copyEngine.tsx @@ -14,7 +14,7 @@ import { ImageNode } from "../../../stage/stageObject/entity/ImageNode"; import { TextNode } from "../../../stage/stageObject/entity/TextNode"; import { UrlNode } from "../../../stage/stageObject/entity/UrlNode"; import { MouseLocation } from "../../controlService/MouseLocation"; -import { VFileSystem } from "../../VFileSystem"; +import { VFileSystem } from "../../dataFileService/VFileSystem"; /** * 专门用来管理节点复制的引擎 diff --git a/src/core/stage/stageObject/entity/ImageNode.tsx b/src/core/stage/stageObject/entity/ImageNode.tsx index 448ce14e..620f2d6f 100644 --- a/src/core/stage/stageObject/entity/ImageNode.tsx +++ b/src/core/stage/stageObject/entity/ImageNode.tsx @@ -5,7 +5,7 @@ import { Vector } from "../../../dataStruct/Vector"; import { Stage } from "../../Stage"; import { ConnectableEntity } from "../abstract/ConnectableEntity"; import { CollisionBox } from "../collisionBox/collisionBox"; -import { VFileSystem } from "../../../service/VFileSystem"; +import { VFileSystem } from "../../../service/dataFileService/VFileSystem"; /** * 一个图片节点 diff --git a/src/main.tsx b/src/main.tsx index 0805447d..985d1c83 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -42,7 +42,7 @@ import { exists } from "./utils/fs/com"; import { exit, openDevtools, writeStderr, writeStdout } from "./utils/otherApi"; import { getCurrentWindow, isDesktop, isWeb } from "./utils/platform"; import { Tourials } from "./core/service/Tourials"; -// import { VFileSystem } from "./core/service/VFileSystem"; +// import { VFileSystem } from "./core/service/dataFileService/VFileSystem"; import { IndexedDBFileSystem } from "./utils/fs/IndexedDBFileSystem"; const router = createMemoryRouter(routes); From 59fe4144aab6853656e116e5c721ad3ce55ccbb8 Mon Sep 17 00:00:00 2001 From: Jorylee <1747358809@qq.com> Date: Sun, 2 Feb 2025 00:00:23 +0800 Subject: [PATCH 09/11] =?UTF-8?q?feat(core):=20=E4=BC=98=E5=8C=96=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E7=B3=BB=E7=BB=9F=E5=B9=B6=E6=B7=BB=E5=8A=A0=E6=B8=85?= =?UTF-8?q?=E9=99=A4=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 VFileSystem 中添加 clear 方法,用于清除文件系统数据 - 优化 IndexedDBFileSystem 中的 getStore、addEntryToParent 和 clear 方法 - 删除多余的空格和括号,提高代码可读性 --- .../service/dataFileService/VFileSystem.tsx | 3 + src/utils/fs/IndexedDBFileSystem.tsx | 94 +++++-------------- 2 files changed, 24 insertions(+), 73 deletions(-) diff --git a/src/core/service/dataFileService/VFileSystem.tsx b/src/core/service/dataFileService/VFileSystem.tsx index f52282e3..37c1c70a 100644 --- a/src/core/service/dataFileService/VFileSystem.tsx +++ b/src/core/service/dataFileService/VFileSystem.tsx @@ -96,6 +96,9 @@ export namespace VFileSystem { compressionOptions: { level: 6 }, }); } + export async function clear() { + return fs.clear(); + } export function getFS() { return fs; } diff --git a/src/utils/fs/IndexedDBFileSystem.tsx b/src/utils/fs/IndexedDBFileSystem.tsx index 4eed1982..e5c49314 100644 --- a/src/utils/fs/IndexedDBFileSystem.tsx +++ b/src/utils/fs/IndexedDBFileSystem.tsx @@ -1,8 +1,4 @@ -import { - IFileSystem, - type FileStats, - type DirectoryEntry, -} from "./IFileSystem"; +import { IFileSystem, type FileStats, type DirectoryEntry } from "./IFileSystem"; const DB_VERSION = 1; @@ -50,10 +46,7 @@ export class IndexedDBFileSystem extends IFileSystem { return this.db!.transaction(storeNames, mode); } - private async getStore( - storeName: string, - mode: IDBTransactionMode = "readonly", - ): Promise { + private async getStore(storeName: string, mode: IDBTransactionMode = "readonly"): Promise { const transaction = await this.getTransaction(storeName, mode); return transaction.objectStore(storeName); } @@ -74,10 +67,7 @@ export class IndexedDBFileSystem extends IFileSystem { return parts.length === 0 ? "/" : `/${parts.join("/")}`; } - private async addEntryToParent( - childPath: string, - isDir: boolean, - ): Promise { + private async addEntryToParent(childPath: string, isDir: boolean): Promise { const parentPath = this.getParentPath(childPath); if (!parentPath) return; @@ -94,9 +84,7 @@ export class IndexedDBFileSystem extends IFileSystem { throw new Error(`Parent directory ${parentPath} not found`); } - const exists = parentDir.entries.some( - (e: DirectoryEntry) => e.name === childName, - ); + const exists = parentDir.entries.some((e: DirectoryEntry) => e.name === childName); if (!exists) { parentDir.entries.push({ name: childName, isDir }); await asPromise(store.put(parentDir)); @@ -105,17 +93,10 @@ export class IndexedDBFileSystem extends IFileSystem { async _exists(path: string): Promise { path = this.normalizePath(path); - const trans = await this.getTransaction( - [this.STORE_NAME, this.DIR_STORE_NAME], - "readonly", - ); - const fileExists = !!(await asPromise( - trans.objectStore(this.STORE_NAME).get(path), - )); + const trans = await this.getTransaction([this.STORE_NAME, this.DIR_STORE_NAME], "readonly"); + const fileExists = !!(await asPromise(trans.objectStore(this.STORE_NAME).get(path))); if (fileExists) return true; - return !!(await asPromise( - trans.objectStore(this.DIR_STORE_NAME).get(path), - )); + return !!(await asPromise(trans.objectStore(this.DIR_STORE_NAME).get(path))); } async _readFile(path: string): Promise { @@ -173,8 +154,7 @@ export class IndexedDBFileSystem extends IFileSystem { } const store = await this.getStore(this.STORE_NAME, "readwrite"); - const data = - typeof content === "string" ? new TextEncoder().encode(content) : content; + const data = typeof content === "string" ? new TextEncoder().encode(content) : content; await asPromise(store.put({ path, content: data })); await this.addEntryToParent(path, false); } @@ -216,10 +196,7 @@ export class IndexedDBFileSystem extends IFileSystem { async _rename(oldPath: string, newPath: string): Promise { oldPath = this.normalizePath(oldPath); newPath = this.normalizePath(newPath); - const transaction = await this.getTransaction( - [this.STORE_NAME, this.DIR_STORE_NAME], - "readwrite", - ); + const transaction = await this.getTransaction([this.STORE_NAME, this.DIR_STORE_NAME], "readwrite"); const filesStore = transaction.objectStore(this.STORE_NAME); const dirsStore = transaction.objectStore(this.DIR_STORE_NAME); @@ -241,14 +218,10 @@ export class IndexedDBFileSystem extends IFileSystem { const newChildPath = newPath + oldChildPath.slice(oldPath.length); if (cursor.source.name === this.STORE_NAME) { await asPromise(filesStore.delete(oldChildPath)); - await asPromise( - filesStore.put({ ...cursor.value, path: newChildPath }), - ); + await asPromise(filesStore.put({ ...cursor.value, path: newChildPath })); } else { await asPromise(dirsStore.delete(oldChildPath)); - await asPromise( - dirsStore.put({ ...cursor.value, path: newChildPath }), - ); + await asPromise(dirsStore.put({ ...cursor.value, path: newChildPath })); } } cursor.continue(); @@ -268,16 +241,11 @@ export class IndexedDBFileSystem extends IFileSystem { const oldParentPath = this.getParentPath(oldPath); const newParentPath = this.getParentPath(newPath); if (oldParentPath) { - const oldParentStore = await this.getStore( - this.DIR_STORE_NAME, - "readwrite", - ); + const oldParentStore = await this.getStore(this.DIR_STORE_NAME, "readwrite"); const oldParent = await asPromise(oldParentStore.get(oldParentPath)); if (oldParent) { const entryName = oldPath.split("/").pop()!; - oldParent.entries = oldParent.entries.filter( - (e: DirectoryEntry) => e.name !== entryName, - ); + oldParent.entries = oldParent.entries.filter((e: DirectoryEntry) => e.name !== entryName); await asPromise(oldParentStore.put(oldParent)); } } @@ -295,9 +263,7 @@ export class IndexedDBFileSystem extends IFileSystem { const parent = await asPromise(parentStore.get(parentPath)); if (parent) { const entryName = path.split("/").pop()!; - parent.entries = parent.entries.filter( - (e: DirectoryEntry) => e.name !== entryName, - ); + parent.entries = parent.entries.filter((e: DirectoryEntry) => e.name !== entryName); await asPromise(parentStore.put(parent)); } } @@ -312,30 +278,17 @@ export class IndexedDBFileSystem extends IFileSystem { const parent = await asPromise(parentStore.get(parentPath)); if (parent) { const entryName = path.split("/").pop()!; - parent.entries = parent.entries.filter( - (e: DirectoryEntry) => e.name !== entryName, - ); + parent.entries = parent.entries.filter((e: DirectoryEntry) => e.name !== entryName); await asPromise(parentStore.put(parent)); } } } async clear() { - const transaction = await this.getTransaction( - [this.STORE_NAME, this.DIR_STORE_NAME], - "readwrite", - ); - const filesStore = transaction.objectStore(this.STORE_NAME); - const dirsStore = transaction.objectStore(this.DIR_STORE_NAME); - return new Promise((resolve, reject) => { - const r0 = filesStore.clear(); - r0.onsuccess = () => { - const r2 = dirsStore.clear(); - r2.onsuccess = () => resolve(); - r2.onerror = () => reject(); - }; - r0.onerror = () => reject(); - }); + const filesStore = await this.getStore(this.STORE_NAME, "readwrite"); + await asPromise(filesStore.clear()); + const dirsStore = await this.getStore(this.DIR_STORE_NAME, "readwrite"); + await asPromise(dirsStore.clear()); } /** @@ -353,9 +306,7 @@ export class IndexedDBFileSystem extends IFileSystem { const actualContent = new TextDecoder("utf-8").decode(content); if (actualContent !== expectedContent) { throw new Error( - `File content verification failed at ${path}\n` + - `Expected: ${expectedContent}\n` + - `Actual: ${actualContent}`, + `File content verification failed at ${path}\n` + `Expected: ${expectedContent}\n` + `Actual: ${actualContent}`, ); } } @@ -365,10 +316,7 @@ export class IndexedDBFileSystem extends IFileSystem { * @param dbName 测试数据库名称 * @param storeName 测试存储名称 */ - static async testFileSystem( - dbName: string, - storeName: string, - ): Promise { + static async testFileSystem(dbName: string, storeName: string): Promise { const fs = new IndexedDBFileSystem(dbName, storeName); // 初始化数据库 From 9fec9be4af613527643a891534743c8930be45fd Mon Sep 17 00:00:00 2001 From: Jorylee <1747358809@qq.com> Date: Sun, 2 Feb 2025 00:21:54 +0800 Subject: [PATCH 10/11] =?UTF-8?q?feat(core):=20=E5=9C=A8=E5=85=B3=E9=97=AD?= =?UTF-8?q?=E6=97=B6=E6=B8=85=E7=90=86=E8=99=9A=E6=8B=9F=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E5=B9=B6=E4=BC=98=E5=8C=96=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在加载文件前,增加清除虚拟文件系统的操作 - 在多个地方添加 VFileSystem.clear() 调用,以确保文件切换时清理虚拟文件系统 - 修改文件加载逻辑,只允许加载 .gp 后缀的文件 - 更新错误提示信息,明确指出需要选择 .gp 文件 --- src/core/service/dataFileService/VFileSystem.tsx | 1 + src/pages/_app.tsx | 5 +++++ src/pages/_app_menu.tsx | 7 +++++-- src/pages/_recent_files_panel.tsx | 6 +++--- src/pages/_start_file_panel.tsx | 4 ++-- 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/core/service/dataFileService/VFileSystem.tsx b/src/core/service/dataFileService/VFileSystem.tsx index 37c1c70a..37dc1a92 100644 --- a/src/core/service/dataFileService/VFileSystem.tsx +++ b/src/core/service/dataFileService/VFileSystem.tsx @@ -20,6 +20,7 @@ export namespace VFileSystem { setMetaData(JSON.stringify(StageDumper.dump())); } export async function loadFromPath(path: string) { + await clear(); const data = await readFile(path); const zip = await jszip.loadAsync(data); const entries = zip.files; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index b8bba138..14a80f5a 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -21,6 +21,7 @@ import LogicNodePanel from "./_logic_node_panel"; import RecentFilesPanel from "./_recent_files_panel"; import StartFilePanel from "./_start_file_panel"; import TagPanel from "./_tag_panel"; +import { VFileSystem } from "../core/service/dataFileService/VFileSystem"; export default function App() { const [maxmized, setMaxmized] = React.useState(false); @@ -104,6 +105,7 @@ export default function App() { { text: "不保存", onClick: async () => { + await VFileSystem.clear(); await getCurrentWindow().destroy(); }, }, @@ -118,10 +120,12 @@ export default function App() { if (isAutoSave) { // 开启了自动保存,不弹窗 await StageSaveManager.saveHandle(file, StageDumper.dump()); + await VFileSystem.clear(); getCurrentWindow().destroy(); } else { // 没开启自动保存,逐步确认 if (StageSaveManager.isSaved()) { + await VFileSystem.clear(); getCurrentWindow().destroy(); } else { await Dialog.show({ @@ -132,6 +136,7 @@ export default function App() { text: "保存并关闭", onClick: async () => { await StageSaveManager.saveHandle(file, StageDumper.dump()); + await VFileSystem.clear(); await getCurrentWindow().destroy(); }, }, diff --git a/src/pages/_app_menu.tsx b/src/pages/_app_menu.tsx index 57e67be2..5fcbfaeb 100644 --- a/src/pages/_app_menu.tsx +++ b/src/pages/_app_menu.tsx @@ -46,6 +46,7 @@ import { Stage } from "../core/stage/Stage"; import { GraphMethods } from "../core/stage/stageManager/basicMethods/GraphMethods"; import { TextNode } from "../core/stage/stageObject/entity/TextNode"; import { PathString } from "../utils/pathString"; +import { VFileSystem } from "../core/service/dataFileService/VFileSystem"; export default function AppMenu({ className = "", open = false }: { className?: string; open: boolean }) { const navigate = useNavigate(); @@ -57,9 +58,10 @@ export default function AppMenu({ className = "", open = false }: { className?: /** * 新建草稿 */ - const onNewDraft = () => { + const onNewDraft = async () => { if (StageSaveManager.isSaved() || StageManager.isEmpty()) { StageManager.destroy(); + await VFileSystem.clear(); setFile("Project Graph"); } else { // 当前文件未保存 @@ -76,8 +78,9 @@ export default function AppMenu({ className = "", open = false }: { className?: }, { text: "丢弃当前并直接新开", - onClick: () => { + onClick: async () => { StageManager.destroy(); + await VFileSystem.clear(); setFile("Project Graph"); }, }, diff --git a/src/pages/_recent_files_panel.tsx b/src/pages/_recent_files_panel.tsx index 7cd5b18b..4e1f8eef 100644 --- a/src/pages/_recent_files_panel.tsx +++ b/src/pages/_recent_files_panel.tsx @@ -76,9 +76,9 @@ export default function RecentFilesPanel() { try { const path = file.path; setFile(decodeURIComponent(path)); - if (isDesktop && !path.endsWith(".json")) { + if (isDesktop && !path.endsWith(".gp")) { Dialog.show({ - title: "请选择一个JSON文件", + title: "请选择一个GP文件", type: "error", }); return; @@ -87,7 +87,7 @@ export default function RecentFilesPanel() { setRecentFilePanelOpen(false); } catch (error) { Dialog.show({ - title: "请选择正确的JSON文件", + title: "请选择正确的GP文件", content: String(error), type: "error", }); diff --git a/src/pages/_start_file_panel.tsx b/src/pages/_start_file_panel.tsx index 2206ffcd..fdc98bb8 100644 --- a/src/pages/_start_file_panel.tsx +++ b/src/pages/_start_file_panel.tsx @@ -157,9 +157,9 @@ export default function StartFilePanel({ open = false }: { open: boolean }) { const checkoutFile = (path: string) => { try { setFile(decodeURIComponent(path)); - if (isDesktop && !path.endsWith(".json")) { + if (isDesktop && !path.endsWith(".gp")) { Dialog.show({ - title: "请选择一个JSON文件", + title: "请选择一个GP文件", type: "error", }); return; From f9aaa3d4a220da3cd457ab6d9179f6a289d22bfd Mon Sep 17 00:00:00 2001 From: Jorylee <1747358809@qq.com> Date: Sun, 2 Feb 2025 00:25:59 +0800 Subject: [PATCH 11/11] =?UTF-8?q?fix(file):=20=E4=BF=AE=E5=A4=8Dvfs?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E6=97=B6=E4=B8=A2=E5=A4=B1await=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 dealPngFileDrop 函数中添加 async/await 以确保文件写入操作是异步执行的 - 在 readClipboardItems 函数中添加 await 以确保文件写入操作完成后再继续执行后续代码 --- .../controlService/controller/concrete/ControllerDragFile.tsx | 4 ++-- src/core/service/dataManageService/copyEngine/copyEngine.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/service/controlService/controller/concrete/ControllerDragFile.tsx b/src/core/service/controlService/controller/concrete/ControllerDragFile.tsx index cc533265..e978d2bb 100644 --- a/src/core/service/controlService/controller/concrete/ControllerDragFile.tsx +++ b/src/core/service/controlService/controller/concrete/ControllerDragFile.tsx @@ -155,7 +155,7 @@ function dealJsonFileDrop(file: File, mouseWorldLocation: Vector) { function dealPngFileDrop(file: File, mouseWorldLocation: Vector) { const reader = new FileReader(); reader.readAsDataURL(file); // 以文本格式读取文件内容 - reader.onload = (e) => { + reader.onload = async (e) => { const fileContent = e.target?.result; // 读取的文件内容 if (typeof fileContent !== "string") { @@ -168,7 +168,7 @@ function dealPngFileDrop(file: File, mouseWorldLocation: Vector) { // data:image/png;base64,iVBORw0KGgoAAAANS... // 在这里处理读取到的内容 const imageUUID = uuidv4(); - VFileSystem.getFS().writeFileBase64(`/picture/${imageUUID}.png`, fileContent.split(",")[1]); + await VFileSystem.getFS().writeFileBase64(`/picture/${imageUUID}.png`, fileContent.split(",")[1]); const imageNode = new ImageNode({ uuid: imageUUID, location: [mouseWorldLocation.x, mouseWorldLocation.y], diff --git a/src/core/service/dataManageService/copyEngine/copyEngine.tsx b/src/core/service/dataManageService/copyEngine/copyEngine.tsx index 1e811e35..185f62e9 100644 --- a/src/core/service/dataManageService/copyEngine/copyEngine.tsx +++ b/src/core/service/dataManageService/copyEngine/copyEngine.tsx @@ -130,7 +130,7 @@ async function readClipboardItems(mouseLocation: Vector) { const blob = await item.getType(item.types[0]); // 获取 Blob 对象 const imageUUID = uuidv4(); //const folder = PathString.dirPath(Stage.path.getFilePath()); - VFileSystem.getFS().writeFile(`/picture/${imageUUID}.png`, await blob.bytes()); + await VFileSystem.getFS().writeFile(`/picture/${imageUUID}.png`, await blob.bytes()); //const imagePath = `${folder}${PathString.getSep()}${imageUUID}.png`; // 2024.12.31 测试发现这样的写法会导致读取时base64解码失败