Tauri文件系统操作:桌面应用的核心能力(入门系列四)

今天我们来聊聊Tauri中一个超级重要的功能 - 文件系统操作。这可是Web应用和桌面应用最大的区别之一。在浏览器里,出于安全考虑,我们对文件系统的访问被限制得死死的。但在Tauri桌面应用中,我们可以安全地访问用户的文件系统,这简直打开了新世界的大门!

Tauri文件系统API:两种方式任你选

Tauri给我们提供了两种处理文件系统的方式:

  1. JavaScript API:通过@tauri-apps/plugin-fs提供的前端API直接操作文件,这种方式对前端开发者来说特别友好
  2. Rust命令:通过在Rust后端定义命令,然后从前端调用,这种方式更灵活但需要写点Rust代码

作为前端开发者,第一种方式用起来更顺手,但了解两种方式都很有必要,这样你就能根据具体需求选择最合适的方案。

权限配置:安全第一

在使用文件系统API之前,我们需要在tauri.conf.json中配置相应的权限。在Tauri 2.0+中,权限配置使用了全新的capabilities系统,让我们来看看如何正确配置:

配置文件系统权限

Tauri 2.0+引入了capabilities系统来精确控制应用能访问哪些路径以及允许哪些操作。这是更安全的权限管理方式。

tauri.conf.jsonapp.security.capabilities部分,你需要这样配置文件系统权限:

{"app": {"security": {"capabilities": [{"name": "path","path": {"allow": ["$APP/*", "$DOCUMENT/myapp/*","$HOME/Desktop/myapp-files/*"],"deny": ["$HOME/Desktop/myapp-files/sensitive-data/*"],"requireLiteralLeadingDot": false}},{"name": "fs","fs": {"scope": ["$APP/*", "$DOCUMENT/myapp/*"],"allow": ["read", "write", "readDir", "copyFile", "createDir", "removeDir", "removeFile", "renameFile", "exists"]}},{"name": "dialog","dialog": {"open": true,"save": true}}]}}
}
路径变量详解

path权限中的allowdeny配置支持以下变量,用于定义允许或拒绝访问的路径:

变量描述示例
$APP应用数据目录Windows: C:\Users\用户名\AppData\Roaming\应用名\
$DOCUMENT文档目录Windows: C:\Users\用户名\Documents\
$HOME用户主目录Windows: C:\Users\用户名\
$TEMP临时文件目录Windows: C:\Users\用户名\AppData\Local\Temp\
$AUDIO音频目录Windows: C:\Users\用户名\Music\
$VIDEO视频目录Windows: C:\Users\用户名\Videos\
$PICTURE图片目录Windows: C:\Users\用户名\Pictures\
$DOWNLOAD下载目录Windows: C:\Users\用户名\Downloads\
$DESKTOP桌面目录Windows: C:\Users\用户名\Desktop\
$CONFIG配置目录Windows: C:\Users\用户名\AppData\Roaming\
$DATA数据目录Windows: C:\Users\用户名\AppData\Roaming\
$CACHE缓存目录Windows: C:\Users\用户名\AppData\Local\Cache\
$LOG日志目录Windows: C:\Users\用户名\AppData\Local\Logs\
$RESOURCE应用资源目录打包后的资源目录

注意: 使用/*表示匹配该目录下的所有文件,而*则是通配符。

文件系统操作权限

fs权限中的allow数组可以包含以下操作权限:

操作名称描述对应API
read读取文件权限readTextFile, readBinaryFile
write写入文件权限writeTextFile, writeBinaryFile
readDir读取目录内容权限readDir
createDir创建目录权限createDir
removeDir删除目录权限removeDir
copyFile复制文件权限copyFile
removeFile删除文件权限removeFile
renameFile重命名文件/目录权限renameFile
exists检查文件/目录是否存在权限exists
metadata获取文件/目录元数据权限metadata

权限设置最佳实践

  1. 最小权限原则

    只启用应用实际需要的权限,不要授予应用不必要的权限。例如,如果应用只需要读取文件而不需要写入,就只启用read权限。

  2. 合理设置路径范围

    使用path权限精确控制应用可以访问的路径,避免不必要的安全风险:

    "path": {"allow": ["$APP/*","$DOCUMENT/myapp/*"],"deny": ["$DOCUMENT/myapp/sensitive/*"]
    }
    
  3. 使用拒绝列表进一步限制权限

    deny列表可以在allow列表的基础上进一步限制特定路径的访问。

  4. 对于用户选择的文件,使用文件对话框

    当需要用户选择任意位置的文件时,使用dialog插件的文件选择对话框,这样即使没有全局文件系统权限,也能访问用户选择的文件:

    import { open } from '@tauri-apps/plugin-dialog';async function openUserSelectedFile() {try {const selected = await open({multiple: false,filters: [{name: 'Text',extensions: ['txt', 'md']}]});if (selected) {// 即使没有全局文件系统权限,也可以读取用户选择的文件const content = await readTextFile(selected);console.log('文件内容:', content);}} catch (error) {console.error('打开文件失败:', error);}
    }
    
  5. 使用相对路径

    优先使用相对于权限范围的路径,而不是绝对路径:

    // 推荐
    await writeTextFile('config/settings.json', JSON.stringify(settings));// 不推荐
    await writeTextFile('/Users/username/AppData/Roaming/myapp/config/settings.json', JSON.stringify(settings));
    

    使用相对路径时,Tauri会基于应用的根目录解析路径,更符合安全最佳实践。

正确配置文件系统权限不仅可以增强应用安全性,还能提供更好的用户体验。用户会更愿意信任只请求必要权限的应用,而不是要求完全访问文件系统的应用。

读写文件:基础操作

读取文件:文本和二进制

import { readTextFile, readBinaryFile } from '@tauri-apps/plugin-fs';// 读取文本文件(比如配置文件)
async function loadConfig() {try {const configText = await readTextFile('config.json');return JSON.parse(configText);} catch (error) {console.error('加载配置文件失败:', error);return null;}
}// 读取二进制文件(比如图片)
async function loadImage() {try {const imageData = await readBinaryFile('assets/image.png');// 创建Blob对象,用于显示图片const blob = new Blob([imageData]);const imageUrl = URL.createObjectURL(blob);return imageUrl;} catch (error) {console.error('加载图片失败:', error);return null;}
}// 读取大文件(分块读取)
async function readLargeFile(path, chunkSize = 1024 * 1024) {try {const fileSize = await getFileSize(path);const chunks = [];for (let offset = 0; offset < fileSize; offset += chunkSize) {const chunk = await readBinaryFile(path, {offset,length: Math.min(chunkSize, fileSize - offset)});chunks.push(chunk);}return new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0));} catch (error) {console.error('读取大文件失败:', error);return null;}
}

写入文件:文本和二进制

import { writeTextFile, writeBinaryFile } from '@tauri-apps/plugin-fs';// 写入文本文件
async function saveConfig(config) {try {const configText = JSON.stringify(config, null, 2);await writeTextFile('config.json', configText);return true;} catch (error) {console.error('保存配置文件失败:', error);return false;}
}// 写入二进制文件
async function saveImage(imageBlob) {try {const arrayBuffer = await imageBlob.arrayBuffer();await writeBinaryFile('output.png', new Uint8Array(arrayBuffer));return true;} catch (error) {console.error('保存图片失败:', error);return false;}
}// 追加写入文件
async function appendToLog(message) {try {const timestamp = new Date().toISOString();const logMessage = `[${timestamp}] ${message}\n`;await writeTextFile('app.log', logMessage, { append: true });return true;} catch (error) {console.error('写入日志失败:', error);return false;}
}

目录操作:管理文件和文件夹

读取目录内容

import { readDir } from '@tauri-apps/plugin-fs';// 读取目录内容
async function listDirectory(path) {try {const entries = await readDir(path, { recursive: false });return entries.map(entry => ({name: entry.name ?? '未知文件',path: entry.path,isDirectory: entry.children !== undefined,size: entry.size ?? 0,modified: entry.modified ?? new Date()}));} catch (error) {console.error(`读取目录失败 ${path}:`, error);return [];}
}// 获取目录统计信息
async function getDirectoryStats(path) {try {const entries = await readDir(path, { recursive: true });let totalFiles = 0;let totalSize = 0;function countFiles(entries) {for (const entry of entries) {if (entry.children) {countFiles(entry.children);} else {totalFiles++;totalSize += entry.size ?? 0;}}}countFiles(entries);return { totalFiles, totalSize };} catch (error) {console.error(`获取目录统计信息失败 ${path}:`, error);return { totalFiles: 0, totalSize: 0 };}
}

递归遍历目录

// 查找所有文件
async function findAllFiles(directory) {const files = [];async function traverse(dir) {const entries = await readDir(dir);for (const entry of entries) {if (entry.children) {await traverse(entry.path);} else if (entry.name) {files.push({path: entry.path,name: entry.name,size: entry.size ?? 0,modified: entry.modified ?? new Date()});}}}await traverse(directory);return files;
}// 按扩展名查找文件
async function findFilesByExtension(directory, extensions) {const allFiles = await findAllFiles(directory);return allFiles.filter(file => {const ext = file.name.split('.').pop()?.toLowerCase();return ext && extensions.includes(ext);});
}

创建和删除目录

import { createDir, removeDir } from '@tauri-apps/plugin-fs';// 创建目录
async function createDirectory(path) {try {await createDir(path, { recursive: true });console.log(`目录创建成功: ${path}`);return true;} catch (error) {console.error(`创建目录失败 ${path}:`, error);return false;}
}// 删除目录
async function removeDirectory(path) {try {await removeDir(path, { recursive: true });console.log(`目录删除成功: ${path}`);return true;} catch (error) {console.error(`删除目录失败 ${path}:`, error);return false;}
}// 清空目录
async function clearDirectory(path) {try {const entries = await readDir(path);for (const entry of entries) {if (entry.children) {await removeDirectory(entry.path);} else {await removeFile(entry.path);}}console.log(`目录清空成功: ${path}`);return true;} catch (error) {console.error(`清空目录失败 ${path}:`, error);return false;}
}

文件对话框:让用户选择文件

Tauri提供了原生的文件对话框,让用户选择文件或保存位置,这比浏览器里的文件选择器好用多了!

打开文件对话框

import { open } from '@tauri-apps/plugin-dialog';// 选择单个文件
async function selectFile() {try {const selected = await open({multiple: false,filters: [{name: '图片文件',extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp']}, {name: '所有文件',extensions: ['*']}],title: '请选择文件',defaultPath: 'C:/Users/Downloads'  // 默认打开的目录});if (selected === null) {console.log('用户取消了选择');return null;}return selected;} catch (error) {console.error('打开文件对话框失败:', error);return null;}
}// 选择多个文件
async function selectMultipleFiles() {try {const selected = await open({multiple: true,filters: [{name: '文档文件',extensions: ['pdf', 'doc', 'docx', 'txt', 'md']}],title: '请选择多个文件',defaultPath: 'C:/Users/Documents'});if (selected === null) {return [];}// 选择多个文件时,结果是字符串数组return Array.isArray(selected) ? selected : [selected];} catch (error) {console.error('打开文件对话框失败:', error);return [];}
}// 选择目录
async function selectDirectory() {try {const selected = await open({directory: true,multiple: false,title: '请选择目录',defaultPath: 'C:/Users'});if (selected === null) {console.log('用户取消了选择');return null;}return selected;} catch (error) {console.error('打开目录对话框失败:', error);return null;}
}

保存文件对话框

import { save } from '@tauri-apps/plugin-dialog';// 保存文件
async function saveFileAs(defaultPath) {try {const filePath = await save({defaultPath,filters: [{name: '文本文件',extensions: ['txt', 'md']}, {name: '所有文件',extensions: ['*']}],title: '保存文件',defaultPath: 'C:/Users/Documents/untitled.txt'});if (filePath === null) {console.log('用户取消了保存');return null;}return filePath;} catch (error) {console.error('打开保存对话框失败:', error);return null;}
}// 保存图片
async function saveImageAs(imageData, defaultName = 'image.png') {try {const filePath = await save({defaultPath: `C:/Users/Pictures/${defaultName}`,filters: [{name: '图片文件',extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp']}],title: '保存图片'});if (filePath === null) {return null;}// 保存图片数据await writeBinaryFile(filePath, imageData);return filePath;} catch (error) {console.error('保存图片失败:', error);return null;}
}

文件拖放:让用户轻松上传文件

Tauri支持标准的HTML5拖放API,这意味着你可以轻松实现文件拖放功能,让用户直接把文件拖到你的应用里!

import { readBinaryFile } from '@tauri-apps/plugin-fs';// 设置文件拖放区域
function setupFileDrop(dropElement) {// 添加拖放区域的样式dropElement.classList.add('drop-zone');// 拖拽进入区域dropElement.addEventListener('dragover', (e) => {e.preventDefault();e.stopPropagation();dropElement.classList.add('drag-over');});// 拖拽离开区域dropElement.addEventListener('dragleave', () => {dropElement.classList.remove('drag-over');});// 放置文件dropElement.addEventListener('drop', async (e) => {e.preventDefault();e.stopPropagation();dropElement.classList.remove('drag-over');// 获取拖放的文件const files = Array.from(e.dataTransfer.files);for (const file of files) {try {// 在Tauri中,File对象会有path属性const filePath = file.path;if (filePath) {// 读取文件内容const content = await readBinaryFile(filePath);console.log(`成功加载文件: ${file.name}, 大小: ${content.byteLength} 字节`);// 处理文件内容handleDroppedFile(file, content);}} catch (error) {console.error(`处理文件失败 ${file.name}:`, error);}}});
}// 处理拖放的文件
async function handleDroppedFile(file, content) {// 根据文件类型处理if (file.type.startsWith('image/')) {// 处理图片const blob = new Blob([content]);const imageUrl = URL.createObjectURL(blob);displayImage(imageUrl);} else if (file.type.startsWith('text/')) {// 处理文本文件const text = new TextDecoder().decode(content);displayText(text);} else {// 其他类型文件console.log(`不支持的文件类型: ${file.type}`);}
}// 显示图片
function displayImage(imageUrl) {const img = document.createElement('img');img.src = imageUrl;img.style.maxWidth = '100%';img.style.maxHeight = '300px';document.getElementById('preview').appendChild(img);
}// 显示文本
function displayText(text) {const pre = document.createElement('pre');pre.textContent = text;document.getElementById('preview').appendChild(pre);
}// 添加拖放区域的样式
const style = document.createElement('style');
style.textContent = `.drop-zone {border: 2px dashed #ccc;border-radius: 4px;padding: 20px;text-align: center;transition: all 0.3s;}.drag-over {border-color: #409EFF;background-color: rgba(64, 158, 255, 0.1);}
`;
document.head.appendChild(style);

监控文件变化:实时追踪文件变动

监控文件或目录的变化是桌面应用中常见的需求,比如自动刷新、实时预览等功能。在Tauri中,我们需要通过Rust后端来实现这个功能。

Rust后端实现文件监控

use notify::{RecommendedWatcher, RecursiveMode, Watcher, Config};
use std::sync::mpsc::channel;
use std::path::Path;
use std::collections::HashMap;
use std::sync::Mutex;
use tauri::State;// 存储活动的监控器
struct WatcherRegistry(Mutex<HashMap<String, RecommendedWatcher>>);#[tauri::command]
async fn watch_path(path: String, window: tauri::Window,watcher_registry: State<'_, WatcherRegistry>
) -> Result<(), String> {let (tx, rx) = channel();// 创建监控器let mut watcher = notify::recommended_watcher(tx).map_err(|e| e.to_string())?;// 开始监控watcher.watch(Path::new(&path), RecursiveMode::Recursive).map_err(|e| e.to_string())?;// 将监控器保存到注册表中{let mut registry = watcher_registry.0.lock().unwrap();registry.insert(path.clone(), watcher);}// 在后台线程中处理事件tauri::async_runtime::spawn(async move {for res in rx {match res {Ok(event) => {// 发送事件到前端window.emit("file-change", event).ok();}Err(error) => {window.emit("file-watch-error", error.to_string()).ok();}}}});Ok(())
}#[tauri::command]
fn stop_watching(path: String,watcher_registry: State<'_, WatcherRegistry>
) -> Result<(), String> {let mut registry = watcher_registry.0.lock().unwrap();registry.remove(&path);Ok(())
}fn main() {tauri::Builder::default().manage(WatcherRegistry(Mutex::new(HashMap::new()))).invoke_handler(tauri::generate_handler![watch_path, stop_watching]).run(tauri::generate_context!()).expect("error while running tauri application");
}

前端调用文件监控

import { invoke } from '@tauri-apps/api/tauri';
import { listen } from '@tauri-apps/api/event';// 开始监控
async function startWatching(path) {try {await invoke('watch_path', { path });console.log(`开始监控: ${path}`);return true;} catch (error) {console.error(`监控失败 ${path}:`, error);return false;}
}// 停止监控
async function stopWatching(path) {try {await invoke('stop_watching', { path });console.log(`停止监控: ${path}`);return true;} catch (error) {console.error(`停止监控失败 ${path}:`, error);return false;}
}// 监听文件变化事件
function setupFileChangeListener(callback) {return listen('file-change', (event) => {console.log('检测到文件变化:', event.payload);callback(event.payload);});
}// 监听错误事件
function setupErrorListener(callback) {return listen('file-watch-error', (event) => {console.error('文件监控错误:', event.payload);callback(event.payload);});
}// 使用示例
async function setupFileWatcher() {// 选择要监控的目录const directory = await selectDirectory();if (!directory) return;// 开始监控const success = await startWatching(directory);if (!success) return;// 设置事件监听器const unlistenChange = await setupFileChangeListener((event) => {// 处理文件变化事件handleFileChange(event);});const unlistenError = await setupErrorListener((error) => {// 处理错误showError(error);});// 返回清理函数return () => {unlistenChange();unlistenError();stopWatching(directory);};
}// 处理文件变化
function handleFileChange(event) {// 根据事件类型处理switch (event.kind) {case 'create':console.log(`新文件创建: ${event.paths[0]}`);break;case 'modify':console.log(`文件修改: ${event.paths[0]}`);break;case 'remove':console.log(`文件删除: ${event.paths[0]}`);break;default:console.log('未知的文件变化类型:', event);}
}// 显示错误
function showError(error) {// 在实际应用中,你可能想要显示一个错误提示alert(`文件监控错误: ${error}`);
}

处理路径:跨平台无忧

Tauri提供了一套处理文件路径的工具,帮助我们解决跨平台路径问题。不同操作系统使用不同的路径分隔符(Windows用\,Unix用/),有了这些工具,我们就不用担心这些差异了!

import { appDataDir, appConfigDir,appCacheDir,appLogDir,join, normalize,dirname,basename,extname,resolve,sep
} from '@tauri-apps/api/path';// 获取应用数据目录
async function getAppDataPath() {try {const appData = await appDataDir();console.log('应用数据目录:', appData);return appData;} catch (error) {console.error('获取应用数据目录失败:', error);return null;}
}// 获取应用配置目录
async function getAppConfigPath() {try {const configDir = await appConfigDir();console.log('应用配置目录:', configDir);return configDir;} catch (error) {console.error('获取应用配置目录失败:', error);return null;}
}// 拼接路径
async function joinPaths(...paths) {try {const result = await join(...paths);console.log('拼接后的路径:', result);return result;} catch (error) {console.error('路径拼接失败:', error);return null;}
}// 标准化路径
async function normalizePath(path) {try {const normalized = await normalize(path);console.log('标准化后的路径:', normalized);return normalized;} catch (error) {console.error('路径标准化失败:', error);return null;}
}// 获取文件信息
async function getFileInfo(path) {try {const dir = await dirname(path);const name = await basename(path);const ext = await extname(path);return {directory: dir,filename: name,extension: ext,fullPath: path};} catch (error) {console.error('获取文件信息失败:', error);return null;}
}// 解析相对路径
async function resolvePath(basePath, relativePath) {try {const absolutePath = await resolve(basePath, relativePath);console.log('解析后的绝对路径:', absolutePath);return absolutePath;} catch (error) {console.error('路径解析失败:', error);return null;}
}// 获取路径分隔符
function getPathSeparator() {return sep;
}// 使用示例:构建应用目录结构
async function setupAppPaths() {// 获取应用数据目录const appData = await getAppDataPath();if (!appData) return;// 创建配置目录const configDir = await joinPaths(appData, 'config');await createDirectory(configDir);// 创建日志目录const logDir = await joinPaths(appData, 'logs');await createDirectory(logDir);// 创建缓存目录const cacheDir = await joinPaths(appData, 'cache');await createDirectory(cacheDir);return {configDir,logDir,cacheDir};
}// 示例:处理用户选择的文件路径
async function handleUserFilePath(userPath) {// 标准化路径const normalizedPath = await normalizePath(userPath);if (!normalizedPath) return null;// 获取文件信息const fileInfo = await getFileInfo(normalizedPath);if (!fileInfo) return null;// 检查文件扩展名const allowedExtensions = ['.txt', '.md', '.json'];if (!allowedExtensions.includes(fileInfo.extension.toLowerCase())) {console.error('不支持的文件类型:', fileInfo.extension);return null;}return fileInfo;
}

各个路径API详解

Tauri的路径API主要包含两类:

1. 系统特殊目录获取

这些API让你获取跨平台的特殊目录路径,避免硬编码路径:

API描述示例结果 (Windows)示例结果 (macOS)
appDataDir()应用数据目录C:\Users\用户名\AppData\Roaming\应用名\/Users/用户名/Library/Application Support/应用名/
appConfigDir()应用配置目录C:\Users\用户名\AppData\Roaming\应用名\config\/Users/用户名/Library/Application Support/应用名/
appCacheDir()应用缓存目录C:\Users\用户名\AppData\Local\应用名\cache\/Users/用户名/Library/Caches/应用名/
appLogDir()应用日志目录C:\Users\用户名\AppData\Local\应用名\logs\/Users/用户名/Library/Logs/应用名/

这些特殊目录函数非常重要,因为:

  • 它们是跨平台的,不需要为不同操作系统写不同代码
  • 它们尊重各操作系统的约定,遵循最佳实践
  • 它们自动使用应用名称创建适当的子目录
  • 它们返回的路径已经获得了权限(如果你在tauri.conf.json中配置了scope
2. 路径操作函数

这些API用于操作路径字符串,帮助你处理路径而不必担心平台差异:

API描述示例
join(...paths)拼接多个路径段join('folder', 'file.txt')'folder/file.txt'
normalize(path)标准化路径,解析 ...normalize('folder/../other/./file.txt')'other/file.txt'
dirname(path)获取路径的目录部分dirname('/folder/file.txt')'/folder'
basename(path)获取路径的文件名部分basename('/folder/file.txt')'file.txt'
extname(path)获取路径的扩展名extname('/folder/file.txt')'.txt'
resolve(basePath, relativePath)将相对路径解析为绝对路径resolve('/base', '../other')'/other'
sep当前平台的路径分隔符Windows: '\\', Unix: '/'

最佳实践

  1. 永远使用异步API

    所有Tauri路径API都是异步的,返回Promise,务必使用await

    // 正确
    const configDir = await appConfigDir();// 错误
    const configDir = appConfigDir(); // 这会得到一个Promise,而不是实际路径
    
  2. 构建合理的应用目录结构

    使用特殊目录API构建清晰的应用目录结构:

    async function initializeAppDirectories() {const dataDir = await appDataDir();const configDir = await appConfigDir();const cacheDir = await appCacheDir();const logDir = await appLogDir();// 确保这些目录存在await createDir(configDir, { recursive: true });await createDir(join(dataDir, 'userData'), { recursive: true });await createDir(join(cacheDir, 'images'), { recursive: true });await createDir(logDir, { recursive: true });return { dataDir, configDir, cacheDir, logDir };
    }
    
  3. 总是使用路径API来操作路径,不要手动拼接

    // 正确
    const filePath = await join(configDir, 'settings.json');// 错误(不跨平台)
    const filePath = configDir + '/settings.json'; // 在Windows上会出问题
    
  4. 处理URL和路径的转换

    在Web和桌面应用混合开发中,经常需要处理URL和文件路径的转换:

    // 文件路径转URL(用于在应用中显示本地文件)
    function pathToUrl(filePath) {return `file://${filePath.replace(/\\/g, '/')}`;
    }// 从URL提取路径部分
    function urlToPath(url) {return url.replace('file://', '').replace(/\//g, sep);
    }
    

通过这些路径API,你可以构建跨平台的应用,而无需担心Windows和Unix之间的路径差异,大大简化了开发过程。

实际应用:打造一个现代化的文件浏览器

让我们把前面学到的知识综合起来,创建一个功能完整的文件浏览器组件。这个组件将包含以下功能:

  • 浏览文件和目录
  • 显示文件信息
  • 支持文件预览
  • 支持文件操作(复制、移动、删除等)
<template><div class="file-browser"><!-- 顶部工具栏 --><div class="toolbar"><el-button-group><el-button @click="navigateUp" :disabled="currentPath === '/'"><el-icon><ArrowUp /></el-icon>上级目录</el-button><el-button @click="refresh"><el-icon><Refresh /></el-icon>刷新</el-button><el-button @click="createNewFolder"><el-icon><FolderAdd /></el-icon>新建文件夹</el-button></el-button-group><div class="path-input"><el-input v-model="currentPath" @keyup.enter="navigateTo"placeholder="输入路径或按回车跳转"><template #append><el-button @click="navigateTo">跳转</el-button></template></el-input></div></div><!-- 文件列表 --><div class="file-list"><div v-if="loading" class="loading"><el-spinner /><span>加载中...</span></div><template v-else><!-- 父目录 --><div v-if="currentPath !== '/'" class="file-item" @click="navigateUp"><el-icon><ArrowUp /></el-icon><span>上级目录</span></div><!-- 目录和文件 --><div v-for="item in items" :key="item.path" class="file-item"@click="item.isDirectory ? navigateTo(item.path) : openFile(item.path)"@contextmenu.prevent="showContextMenu($event, item)"><el-icon><Folder v-if="item.isDirectory" /><Document v-else /></el-icon><span class="name">{{ item.name }}</span><span class="size">{{ formatSize(item.size) }}</span><span class="modified">{{ formatDate(item.modified) }}</span></div></template></div><!-- 右键菜单 --><el-dropdown-menu v-if="contextMenu.show" :style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"@click="handleContextMenuClick"><el-dropdown-item v-if="contextMenu.item" :command="'open'">打开</el-dropdown-item><el-dropdown-item :command="'copy'">复制</el-dropdown-item><el-dropdown-item :command="'cut'">剪切</el-dropdown-item><el-dropdown-item :command="'delete'">删除</el-dropdown-item><el-dropdown-item :command="'rename'">重命名</el-dropdown-item></el-dropdown-menu><!-- 文件预览对话框 --><el-dialogv-model="previewDialog.visible":title="previewDialog.title"width="80%"><div v-if="previewDialog.type === 'text'" class="preview-text"><pre>{{ previewDialog.content }}</pre></div><div v-else-if="previewDialog.type === 'image'" class="preview-image"><img :src="previewDialog.content" alt="预览图片" /></div><div v-else class="preview-unsupported">不支持预览此类型的文件</div></el-dialog></div>
</template><script setup>
import { ref, onMounted } from 'vue';
import { readDir, readTextFile, readBinaryFile,removeFile,renameFile,copyFile
} from '@tauri-apps/plugin-fs';
import { join, dirname, basename } from '@tauri-apps/api/path';
import { open } from '@tauri-apps/plugin-dialog';
import { ElMessage, ElMessageBox } from 'element-plus';const currentPath = ref('/');
const items = ref([]);
const loading = ref(false);
const contextMenu = ref({show: false,x: 0,y: 0,item: null
});
const previewDialog = ref({visible: false,title: '',type: '',content: ''
});// 加载目录内容
async function loadDirectory(path) {loading.value = true;try {const entries = await readDir(path);currentPath.value = path;items.value = entries.map(entry => ({name: entry.name || '未知文件',path: entry.path,isDirectory: entry.children !== undefined,size: entry.size ?? 0,modified: entry.modified ?? new Date()})).sort((a, b) => {// 目录排在前面if (a.isDirectory && !b.isDirectory) return -1;if (!a.isDirectory && b.isDirectory) return 1;return a.name.localeCompare(b.name);});} catch (error) {console.error(`读取目录失败: ${path}`, error);ElMessage.error('读取目录失败');items.value = [];} finally {loading.value = false;}
}// 导航到指定路径
async function navigateTo(path) {if (!path) return;await loadDirectory(typeof path === 'string' ? path : currentPath.value);
}// 返回上级目录
async function navigateUp() {const parent = await dirname(currentPath.value);await loadDirectory(parent);
}// 刷新当前目录
async function refresh() {await loadDirectory(currentPath.value);
}// 创建新文件夹
async function createNewFolder() {try {const name = await ElMessageBox.prompt('请输入文件夹名称', '新建文件夹', {confirmButtonText: '确定',cancelButtonText: '取消',inputPattern: /^[^\\/:*?"<>|]+$/,inputErrorMessage: '文件夹名称不能包含特殊字符'});if (name.value) {const newPath = await join(currentPath.value, name.value);await createDirectory(newPath);await refresh();}} catch (error) {if (error !== 'cancel') {console.error('创建文件夹失败:', error);ElMessage.error('创建文件夹失败');}}
}// 打开文件
async function openFile(path) {try {const name = await basename(path);const ext = path.split('.').pop()?.toLowerCase();// 根据文件类型处理if (['txt', 'md', 'json', 'js', 'ts', 'html', 'css'].includes(ext)) {// 文本文件预览const content = await readTextFile(path);previewDialog.value = {visible: true,title: name,type: 'text',content};} else if (['png', 'jpg', 'jpeg', 'gif', 'webp'].includes(ext)) {// 图片预览const content = await readBinaryFile(path);const blob = new Blob([content]);const url = URL.createObjectURL(blob);previewDialog.value = {visible: true,title: name,type: 'image',content: url};} else {ElMessage.info('不支持预览此类型的文件');}} catch (error) {console.error(`打开文件失败: ${path}`, error);ElMessage.error('打开文件失败');}
}// 显示右键菜单
function showContextMenu(event, item) {contextMenu.value = {show: true,x: event.clientX,y: event.clientY,item};// 点击其他地方关闭菜单document.addEventListener('click', () => {contextMenu.value.show = false;}, { once: true });
}// 处理右键菜单点击
async function handleContextMenuClick(command) {const item = contextMenu.value.item;if (!item) return;try {switch (command) {case 'open':if (item.isDirectory) {await navigateTo(item.path);} else {await openFile(item.path);}break;case 'copy':// 实现复制功能break;case 'cut':// 实现剪切功能break;case 'delete':await ElMessageBox.confirm(`确定要删除 ${item.name} 吗?`,'删除确认',{confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'});if (item.isDirectory) {await removeDirectory(item.path);} else {await removeFile(item.path);}await refresh();break;case 'rename':const newName = await ElMessageBox.prompt('请输入新名称','重命名',{confirmButtonText: '确定',cancelButtonText: '取消',inputValue: item.name});if (newName.value) {const newPath = await join(await dirname(item.path), newName.value);await renameFile(item.path, newPath);await refresh();}break;}} catch (error) {if (error !== 'cancel') {console.error('操作失败:', error);ElMessage.error('操作失败');}}
}// 格式化文件大小
function formatSize(bytes) {if (bytes === 0) return '0 B';const k = 1024;const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];const i = Math.floor(Math.log(bytes) / Math.log(k));return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}// 格式化日期
function formatDate(date) {return new Date(date).toLocaleString();
}onMounted(async () => {// 初始加载用户主目录const homeDir = await appDataDir();await loadDirectory(homeDir);
});
</script>## 小结:文件系统操作全掌握通过这篇文章,我们已经全面了解了Tauri中的文件系统操作。作为前端开发者,Tauri的文件系统API为我们提供了几乎所有需要的文件操作能力,同时保持了安全性和易用性的平衡。### 主要功能回顾1. **文件读写**- 读取文本和二进制文件- 写入文本和二进制文件- 追加写入文件- 大文件分块读取2. **目录操作**- 创建和删除目录- 读取目录内容- 递归遍历目录- 获取目录统计信息3. **文件对话框**- 打开文件对话框- 保存文件对话框- 选择目录对话框- 自定义文件过滤器4. **文件拖放**- 设置拖放区域- 处理拖放事件- 预览拖放的文件- 自定义拖放样式5. **文件监控**- 监控文件变化- 处理文件事件- 错误处理- 清理监控器6. **路径处理**- 跨平台路径处理- 路径拼接和解析- 获取文件信息- 应用目录管理### 实际应用示例我们还创建了一个功能完整的文件浏览器组件,展示了如何将这些功能组合起来创建一个实用的桌面应用。这个组件包含了:- 现代化的用户界面
- 完整的文件操作功能
- 文件预览功能
- 右键菜单操作
- 错误处理和用户提示### 最佳实践建议1. **安全性**- 只启用必要的文件系统权限- 使用`scope`来限制文件系统访问范围- 避免使用`"all": true`,这是安全大忌- 定期审查和更新权限配置2. **性能**- 使用分块读取处理大文件- 异步处理文件操作- 合理使用文件缓存- 及时释放资源3. **用户体验**- 提供清晰的操作反馈- 实现文件预览功能- 支持拖放操作- 添加错误提示### 下一步学习在下一篇文章中,我们将探讨Tauri的窗口管理功能,学习如何:- 创建和管理应用窗口
- 自定义窗口样式
- 实现窗口间通信
- 处理窗口事件

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/78484.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python解析地址中省市区街道

Python解析地址中省市区街道 1、效果 输入&#xff1a;海珠区沙园街道西基村 输出&#xff1a; 2、导入库 pip install jionlp3、示例代码 import jionlp as jiotext 海珠区沙园街道西基村 res jio.parse_location(text, town_villageTrue) print(res)

基于Node+HeadlessBrowser的浏览器自动化方案

基于NodeHeadlessBrowser的浏览器自动化方案 什么是无头浏览器(Headless Browser)&#xff1f; 无头浏览器&#xff0c;就像是一个没有用户界面的浏览器程序。你可以想象它就是一个“隐形”的浏览器&#xff0c;只不过它没有图形界面&#xff0c;但能做我们用普通浏览器所能做…

AEB法规升级后的市场预测与分析:技术迭代、政策驱动与产业变革

文章目录 一、政策驱动&#xff1a;全球法规升级倒逼市场扩容二、技术迭代&#xff1a;从“基础防护”到“场景全覆盖”三、市场格局&#xff1a;竞争加剧与生态重构四、挑战与未来展望五、投资建议结语 近年来&#xff0c;全球汽车安全法规的加速升级正深刻重塑AEB&#xff08…

【Docker项目实战】使用Docker部署Caddy+vaultwarden密码管理工具(详细教程)

【Docker项目实战】使用Docker部署vaultwarden密码管理工具 前言一、vaultwarden介绍1.1 vaultwarden简介1.2 主要特点二、本次实践规划2.1 本地环境规划2.2 本次实践介绍三、本地环境检查3.1 检查Docker服务状态3.2 检查Docker版本3.3 检查docker compose 版本四、拉取镜像五、…

第十六届蓝桥杯大赛软件赛省赛第二场

第十六届蓝桥杯大赛软件赛省赛第二场 大家好。最近参加了第十六届蓝桥杯大赛软件赛省赛第二场 Python 大学 B 组的比赛&#xff0c;现在来和大家分享一下我的解题思路和代码实现。以下内容是我自己写的&#xff0c;可能对也可能错&#xff0c;欢迎大家交流讨论。 试题 A&…

硬件须知的基本问题2

目录 1、典型电路 1. DC5V 转 DC3.3V 电路 2. 通信电路 2、STM32F103RCT6 最小系统如何设计搭建电路 1. 电源电路 2. 复位电路 3. 时钟电路 4. 下载电路 5. 单片机连接连接 3、请列举你所知道的二极管型号&#xff1f; 1. 整流二极管 2. 小信号二极管 3. 肖特基二极管 4. 超…

力扣HOT100——102.二叉树层序遍历

给你二叉树的根节点 root &#xff0c;返回其节点值的 层序遍历 。 &#xff08;即逐层地&#xff0c;从左到右访问所有节点&#xff09;。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;[[3],[9,20],[15,7]] /*** Definition for a bi…

CSS 定位学习笔记

一、定位概述 CSS 定位是控制 HTML 元素在页面中位置的核心技术&#xff0c;允许元素脱离正常文档流&#xff0c;实现复杂布局效果。 二、定位类型对比 定位类型属性值参考基准是否脱离文档流常用场景静态定位static无否默认布局相对定位relative自身原位置否元素微调绝对定…

Threejs中顶视图截图

Threejs中顶视图截图 一般项目中的每个模型&#xff0c;都需要有一张对应的图片&#xff0c;一般是顶视图&#xff0c;在对应的2D场景场景中展示。以下分享一个实现方式&#xff0c;先将清空模型材质的纹理&#xff0c;把颜色设置为白色&#xff0c;使用正交相机截取顶视图&am…

深度探索:DeepSeek赋能WPS图表绘制

一、研究背景 在当今数字化信息爆炸的时代&#xff0c;数据处理与可视化分析已成为众多领域研究和决策的关键环节。随着数据量的急剧增长和数据维度的不断丰富&#xff0c;传统的数据可视化工具在应对复杂数据时逐渐显露出局限性。Excel作为广泛应用的电子表格软件&#xff0c;…

第11章 面向分类任务的表示模型微调

​​​​​​第1章 对大型语言模型的介绍第2章 分词和嵌入第3章 解析大型语言模型的内部机制第4章 文本分类第5章 文本聚类与主题建模第6章 提示工程第7章 高级文本生成技术与工具第8章 语义搜索与检索增强生成第9章 多模态大语言模型第10章 构建文本嵌入模型第12章 微调生成模…

4.换行和续写

一.FileOutputStream写出数据的两个小问题&#xff1a; 问题一&#xff1a;换行 假设在本地文件中要输出数据aweihaoshuai 666&#xff0c;在输出这个数据时要换行写出&#xff0c;如下图&#xff1a; 问题二&#xff1a;续写 假设在一个文本文件中已经存在数据aweihaoshuai…

联易融受邀参加上海审计局金融审计处专题交流座谈

近日&#xff0c;联易融科技集团受邀出席了由上海市审计局金融审计处组织的专题交流座谈&#xff0c;凭借其在供应链金融领域的深厚积累和创新实践&#xff0c;联易融为与会人员带来了精彩的分享&#xff0c;进一步加深现场对供应链金融等金融发展前沿领域的理解。 在交流座谈…

SOC估算:开路电压修正的安时积分法

SOC估算&#xff1a;开路电压修正的安时积分法 基本概念 开路电压修正的安时积分法是一种结合了两种SOC估算方法的混合技术&#xff1a; 安时积分法&#xff08;库仑计数法&#xff09; - 通过电流积分计算SOC变化 开路电压法 - 通过电池电压与SOC的关系曲线进行校准 方法原…

代码随想录打卡|Day27(合并区间、单调递增的数字、监控二叉树)

贪心算法 Part05 合并区间 力扣题目链接 代码随想录链接 视频讲解链接 题目描述&#xff1a; 以数组 intervals 表示若干个区间的集合&#xff0c;其中单个区间为 intervals[i] [starti, endi] 。请你合并所有重叠的区间&#xff0c;并返回 一个不重叠的区间数组&#xff0…

PostgreSQL的扩展 pg_cron

PostgreSQL的扩展 pg_cron pg_cron 是 PostgreSQL 的一个开源扩展&#xff0c;它允许在数据库内部使用 cron 语法调度定期任务&#xff0c;是最接近 Oracle DBMS_SCHEDULER 的解决方案。 一 安装与配置 1 安装方法 下载路径&#xff1a; https://github.com/citusdata/pg_…

卷积神经网络迁移学习:原理与实践指南

引言 在深度学习领域&#xff0c;卷积神经网络(CNN)已经在计算机视觉任务中取得了巨大成功。然而&#xff0c;从头开始训练一个高性能的CNN模型需要大量标注数据和计算资源。迁移学习(Transfer Learning)技术为我们提供了一种高效解决方案&#xff0c;它能够将预训练模型的知识…

图论---朴素Prim(稠密图)

O( n ^2 ) 题目通常会提示数据范围&#xff1a; 若 V ≤ 500&#xff0c;两种方法均可&#xff08;朴素Prim更稳&#xff09;。 若 V ≤ 1e5&#xff0c;必须用优先队列Prim vector 存图。 // 最小生成树 —朴素Prim #include<cstring> #include<iostream> #i…

Spring-Cache替换Keys为Scan—负优化?

背景 使用ORM工具是往往会配合缓存框架实现三级缓存提高查询效率&#xff0c;spring-cache配合redis是非常常规的实现方案&#xff0c;如未做特殊配置&#xff0c;CacheEvict(allEntries true) 的批量驱逐方式&#xff0c;默认使用keys的方式查询历史缓存列表而后delete&…

【N8N】Docker Desktop + WSL 安装过程(Docker Desktop - WSL update Failed解决方法)

背景说明&#xff1a; 因为要用n8n&#xff0c;官网推荐这个就下载了&#xff0c;然后又是一堆卡的安装问题记录过程。 1. 下载安装包 直接去官网Get Docker | Docker Docs下载 下载的是第一个windows - x86_64. &#xff08;*下面那个beta的感觉是测试版&#xff09; PS&am…