当前位置: 首页 > article >正文

Electron 开发者的 Tauri 2.0 实战指南:文件系统操作

作为 Electron 开发者,我们习惯了使用 Node.js 的 fs 模块来处理文件操作。在 Tauri 2.0 中,文件系统操作被重新设计,采用了 Rust 的安全特性和权限系统。本文将帮助你理解和重构这部分功能。

文件操作对比

Electron 的文件操作

在 Electron 中,我们通常这样处理文件:

// main.js
const fs = require('fs').promises
const path = require('path')

// 读取文件
async function readFile(filePath) {
  try {
    const content = await fs.readFile(filePath, 'utf8')
    return content
  } catch (error) {
    console.error('Failed to read file:', error)
    throw error
  }
}

// 写入文件
async function writeFile(filePath, content) {
  try {
    await fs.writeFile(filePath, content, 'utf8')
  } catch (error) {
    console.error('Failed to write file:', error)
    throw error
  }
}

// 列出目录内容
async function listDirectory(dirPath) {
  try {
    const files = await fs.readdir(dirPath)
    return files
  } catch (error) {
    console.error('Failed to list directory:', error)
    throw error
  }
}

主要特点:

  1. 直接访问文件系统
  2. 无权限限制
  3. 同步/异步操作
  4. 完整的 Node.js API

Tauri 的文件操作

Tauri 采用了更安全的方式:

// main.rs
use std::fs;
use tauri::api::path;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize)]
struct FileEntry {
    name: String,
    path: String,
    is_file: bool,
    size: u64,
}

#[tauri::command]
async fn read_file(path: String) -> Result<String, String> {
    fs::read_to_string(path)
        .map_err(|e| e.to_string())
}

#[tauri::command]
async fn write_file(path: String, content: String) -> Result<(), String> {
    fs::write(path, content)
        .map_err(|e| e.to_string())
}

#[tauri::command]
async fn list_directory(path: String) -> Result<Vec<FileEntry>, String> {
    let mut entries = Vec::new();

    for entry in fs::read_dir(path).map_err(|e| e.to_string())? {
        let entry = entry.map_err(|e| e.to_string())?;
        let metadata = entry.metadata().map_err(|e| e.to_string())?;

        entries.push(FileEntry {
            name: entry.file_name().to_string_lossy().into_owned(),
            path: entry.path().to_string_lossy().into_owned(),
            is_file: metadata.is_file(),
            size: metadata.len(),
        });
    }

    Ok(entries)
}
// fileSystem.ts
import { invoke } from '@tauri-apps/api/tauri'
import { BaseDirectory, createDir, readDir } from '@tauri-apps/api/fs'

interface FileEntry {
  name: string
  path: string
  isFile: boolean
  size: number
}

// 读取文件
export const readFile = async (path: string): Promise<string> => {
  try {
    return await invoke('read_file', { path })
  } catch (error) {
    console.error('Failed to read file:', error)
    throw error
  }
}

// 写入文件
export const writeFile = async (path: string, content: string): Promise<void> => {
  try {
    await invoke('write_file', { path, content })
  } catch (error) {
    console.error('Failed to write file:', error)
    throw error
  }
}

// 列出目录
export const listDirectory = async (path: string): Promise<FileEntry[]> => {
  try {
    return await invoke('list_directory', { path })
  } catch (error) {
    console.error('Failed to list directory:', error)
    throw error
  }
}

主要特点:

  1. 权限控制
  2. 类型安全
  3. 错误处理
  4. 跨平台兼容

常见文件操作场景

1. 配置文件管理

Electron 实现
// config.js
const fs = require('fs')
const path = require('path')

class ConfigManager {
  constructor() {
    this.configPath = path.join(app.getPath('userData'), 'config.json')
  }

  async load() {
    try {
      const data = await fs.promises.readFile(this.configPath, 'utf8')
      return JSON.parse(data)
    } catch (error) {
      return {}
    }
  }

  async save(config) {
    await fs.promises.writeFile(
      this.configPath,
      JSON.stringify(config, null, 2),
      'utf8'
    )
  }
}
Tauri 实现
// main.rs
use std::fs;
use tauri::api::path::app_config_dir;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Config {
    theme: String,
    language: String,
}

#[tauri::command]
async fn load_config(app: tauri::AppHandle) -> Result<Config, String> {
    let config_dir = app_config_dir(&app.config())
        .ok_or("Failed to get config directory")?;
    let config_path = config_dir.join("config.json");

    match fs::read_to_string(config_path) {
        Ok(data) => serde_json::from_str(&data)
            .map_err(|e| e.to_string()),
        Err(_) => Ok(Config {
            theme: "light".into(),
            language: "en".into(),
        })
    }
}

#[tauri::command]
async fn save_config(
    app: tauri::AppHandle,
    config: Config
) -> Result<(), String> {
    let config_dir = app_config_dir(&app.config())
        .ok_or("Failed to get config directory")?;
    fs::create_dir_all(&config_dir)
        .map_err(|e| e.to_string())?;

    let config_path = config_dir.join("config.json");
    let data = serde_json::to_string_pretty(&config)
        .map_err(|e| e.to_string())?;

    fs::write(config_path, data)
        .map_err(|e| e.to_string())
}

2. 文件监听

Electron 实现
const { watch } = require('fs')

const watcher = watch('/path/to/watch', (eventType, filename) => {
  console.log(`File ${filename} changed: ${eventType}`)
})

// 清理
watcher.close()
Tauri 实现
use notify::{Watcher, RecursiveMode, Result as NotifyResult};
use std::sync::mpsc::channel;
use std::time::Duration;

#[tauri::command]
async fn watch_directory(
    window: tauri::Window,
    path: String
) -> Result<(), String> {
    let (tx, rx) = channel();

    let mut watcher = notify::recommended_watcher(move |res: NotifyResult<notify::Event>| {
        if let Ok(event) = res {
            let _ = tx.send(event);
        }
    }).map_err(|e| e.to_string())?;

    watcher.watch(
        path.as_ref(),
        RecursiveMode::Recursive
    ).map_err(|e| e.to_string())?;

    tauri::async_runtime::spawn(async move {
        while let Ok(event) = rx.recv() {
            let _ = window.emit("file-change", event);
        }
    });

    Ok(())
}
// fileWatcher.ts
import { listen } from '@tauri-apps/api/event'

export const setupFileWatcher = async (path: string) => {
  try {
    await invoke('watch_directory', { path })

    const unlisten = await listen('file-change', (event) => {
      console.log('File changed:', event)
    })

    return unlisten
  } catch (error) {
    console.error('Failed to setup file watcher:', error)
    throw error
  }
}

3. 拖放文件处理

Electron 实现
// renderer.js
document.addEventListener('drop', (e) => {
  e.preventDefault()
  e.stopPropagation()

  for (const file of e.dataTransfer.files) {
    console.log('File path:', file.path)
  }
})

document.addEventListener('dragover', (e) => {
  e.preventDefault()
  e.stopPropagation()
})
Tauri 实现
// main.rs
#[tauri::command]
async fn handle_file_drop(
    paths: Vec<String>
) -> Result<(), String> {
    for path in paths {
        println!("Dropped file: {}", path);
    }
    Ok(())
}
// App.tsx
import { listen } from '@tauri-apps/api/event'

useEffect(() => {
  const setupDropZone = async () => {
    await listen('tauri://file-drop', async (event: any) => {
      const paths = event.payload as string[]
      await invoke('handle_file_drop', { paths })
    })
  }

  setupDropZone()
}, [])

实战案例:文件加密管理器

让我们通过一个实际的案例来综合运用这些文件操作:

// main.rs
use chacha20poly1305::{
    aead::{Aead, KeyInit},
    ChaCha20Poly1305, Nonce
};
use rand::Rng;
use serde::{Deserialize, Serialize};
use std::fs;

#[derive(Debug, Serialize, Deserialize)]
struct EncryptedFile {
    path: String,
    nonce: Vec<u8>,
    data: Vec<u8>,
}

#[tauri::command]
async fn encrypt_file(
    path: String,
    password: String
) -> Result<(), String> {
    // 读取文件
    let content = fs::read(&path)
        .map_err(|e| e.to_string())?;

    // 生成密钥
    let key = derive_key(password);
    let cipher = ChaCha20Poly1305::new(&key.into());

    // 生成随机 nonce
    let mut rng = rand::thread_rng();
    let nonce = Nonce::from_slice(&rng.gen::<[u8; 12]>());

    // 加密数据
    let encrypted_data = cipher
        .encrypt(nonce, content.as_ref())
        .map_err(|e| e.to_string())?;

    // 保存加密文件
    let encrypted_file = EncryptedFile {
        path: path.clone(),
        nonce: nonce.to_vec(),
        data: encrypted_data,
    };

    let encrypted_path = format!("{}.encrypted", path);
    let json = serde_json::to_string(&encrypted_file)
        .map_err(|e| e.to_string())?;

    fs::write(encrypted_path, json)
        .map_err(|e| e.to_string())?;

    // 删除原文件
    fs::remove_file(path)
        .map_err(|e| e.to_string())?;

    Ok(())
}

#[tauri::command]
async fn decrypt_file(
    path: String,
    password: String
) -> Result<(), String> {
    // 读取加密文件
    let json = fs::read_to_string(&path)
        .map_err(|e| e.to_string())?;
    let encrypted_file: EncryptedFile = serde_json::from_str(&json)
        .map_err(|e| e.to_string())?;

    // 生成密钥
    let key = derive_key(password);
    let cipher = ChaCha20Poly1305::new(&key.into());

    // 解密数据
    let nonce = Nonce::from_slice(&encrypted_file.nonce);
    let decrypted_data = cipher
        .decrypt(nonce, encrypted_file.data.as_ref())
        .map_err(|e| e.to_string())?;

    // 保存解密文件
    fs::write(&encrypted_file.path, decrypted_data)
        .map_err(|e| e.to_string())?;

    // 删除加密文件
    fs::remove_file(path)
        .map_err(|e| e.to_string())?;

    Ok(())
}

fn derive_key(password: String) -> [u8; 32] {
    use sha2::{Sha256, Digest};
    let mut hasher = Sha256::new();
    hasher.update(password.as_bytes());
    hasher.finalize().into()
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            encrypt_file,
            decrypt_file
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
// App.tsx
import { useState, useEffect } from 'react'
import { invoke } from '@tauri-apps/api/tauri'
import { listen } from '@tauri-apps/api/event'

function App() {
  const [password, setPassword] = useState('')
  const [status, setStatus] = useState('')

  useEffect(() => {
    const setupDropZone = async () => {
      await listen('tauri://file-drop', async (event: any) => {
        const [path] = event.payload as string[]
        if (path.endsWith('.encrypted')) {
          await handleDecrypt(path)
        } else {
          await handleEncrypt(path)
        }
      })
    }

    setupDropZone()
  }, [])

  const handleEncrypt = async (path: string) => {
    try {
      await invoke('encrypt_file', {
        path,
        password
      })
      setStatus(`Encrypted: ${path}`)
    } catch (error) {
      setStatus(`Error: ${error}`)
    }
  }

  const handleDecrypt = async (path: string) => {
    try {
      await invoke('decrypt_file', {
        path,
        password
      })
      setStatus(`Decrypted: ${path}`)
    } catch (error) {
      setStatus(`Error: ${error}`)
    }
  }

  return (
    <div className="container">
      <h1>File Encryption Manager</h1>

      <div className="password-input">
        <input
          type="password"
          placeholder="Enter password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
        />
      </div>

      <div className="drop-zone">
        <p>Drop files here to encrypt/decrypt</p>
        <p className="status">{status}</p>
      </div>
    </div>
  )
}

export default App
/* styles.css */
.container {
  padding: 20px;
  max-width: 800px;
  margin: 0 auto;
}

.password-input {
  margin: 20px 0;
}

.password-input input {
  width: 100%;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.drop-zone {
  border: 2px dashed #ddd;
  border-radius: 8px;
  padding: 40px;
  text-align: center;
  margin-top: 20px;
}

.status {
  margin-top: 20px;
  color: #666;
}

性能优化建议

  1. 缓冲区操作

    • 使用缓冲读写
    • 分块处理大文件
    • 实现流式传输
  2. 并发处理

    • 使用异步操作
    • 实现并行处理
    • 避免阻塞主线程
  3. 内存管理

    • 及时释放资源
    • 控制内存使用
    • 实现垃圾回收

安全考虑

  1. 路径验证

    • 检查路径合法性
    • 防止路径遍历
    • 限制访问范围
  2. 权限控制

    • 实现最小权限
    • 验证用户权限
    • 记录操作日志
  3. 数据保护

    • 加密敏感数据
    • 安全删除文件
    • 防止数据泄露

调试技巧

  1. 文件操作日志

    use log::{info, error};
    
    #[tauri::command]
    async fn debug_file_operation(path: String) -> Result<(), String> {
        info!("File operation on: {}", path);
        Ok(())
    }
  2. 错误处理

    fn handle_file_error(error: std::io::Error) -> String {
        match error.kind() {
            std::io::ErrorKind::NotFound => "File not found".into(),
            std::io::ErrorKind::PermissionDenied => "Permission denied".into(),
            _ => error.to_string()
        }
    }
  3. 性能监控

    use std::time::Instant;
    
    #[tauri::command]
    async fn measure_file_operation(path: String) -> Result<String, String> {
        let start = Instant::now();
        // 执行文件操作
        let duration = start.elapsed();
        Ok(format!("Operation took: {:?}", duration))
    }

小结

  1. Tauri 文件操作的优势:

    • 更安全的权限控制
    • 更好的性能表现
    • 更强的类型安全
    • 更现代的 API 设计
  2. 迁移策略:

    • 重构文件操作
    • 实现权限控制
    • 优化性能
    • 加强安全性
  3. 最佳实践:

    • 使用异步操作
    • 实现错误处理
    • 注重安全性
    • 优化性能

下一篇文章,我们将深入探讨 Tauri 2.0 的安全实践,帮助你构建更安全的桌面应用。

如果觉得这篇文章对你有帮助,别忘了点个赞 👍


http://www.kler.cn/a/505436.html

相关文章:

  • 【Go】Go Gin框架初识(一)
  • 【大数据】机器学习-----线性模型
  • 浅谈云计算12 | KVM虚拟化技术
  • Unreal Engine 5 C++ Advanced Action RPG 八章笔记
  • 逻辑测试题
  • HTTP详解——HTTP基础
  • LeetCode热题100(三十四) —— 23.合并K个升序链表
  • git报错处理
  • linux服务器 常用脚本(超全)
  • SpringBoot项目中解决CORS跨域资源共享问题
  • 比较分析:Windsurf、Cody、Cline、Roo Cline、Copilot 和 通义灵码
  • hadoop3.3和hive4.0安装——单节点
  • ubuntu如何搭建ftp
  • MAP评价指标在Faster R-CNN中的使用
  • macOS 如何终止端口占用的进程 ?
  • 【Linux网络编程】高效I/O--I/O的五种类型
  • 【机器人】复现SceneGrasp 同时支持多目标 3D物体重建、6DoF位姿估计、抓取预测
  • Blazor中Syncfusion图像编辑器组件使用方法
  • 汽车故障码U010087 EMS Communication time out 解析和处理方法
  • C 陷阱与缺陷 —— Chapter one 词法陷阱
  • docker 部署 Kafka 单机和集群
  • 七十五:握手的优化:Session缓存、Ticket票据及TLS 1.3的0-RTT
  • iOS - 关联对象的实现
  • Qt 5.14.2 学习记录 —— 십삼 QComboBox、QSpinBox、QDateTimeEdit、QDial、QSlider
  • Jmeter 简单使用、生成测试报告(一)
  • 升级 SpringBoot3 全项目讲解 — 为什么 SpringBoot3 应该抛弃 Maven,搭配 Gradle 来使用?