CSV ファイルの読み書き (Rust)

Recipe ID: db-009

業務システムで頻繁に利用される CSV データのインポート・エクスポート機能を、Rust バックエンドを使用して高速かつ型安全に実装する方法を解説します。
フロントエンドで巨大な CSV をパースするのは UI フリーズの原因になるため、Rust 側で処理するのが推奨されます。

前提条件

Cargo.toml の設定

src-tauri/Cargo.toml に以下の依存関係を追加します。

[dependencies]
csv = "1.4.0"
serde = { version = "1.0", features = ["derive"] }

またはコマンドで追加します。

cd src-tauri
cargo add csv serde --features serde/derive

プラグインのインストール

フロントエンドからパスを指定する場合や、権限スコープを利用するために plugin-fs が必要です。

npm run tauri add fs

Permissions (権限) の設定

ファイル操作を行うため、plugin-fs の権限が必要です。

src-tauri/capabilities/default.json:

{
  "permissions": [
    ...,
    {
      "identifier": "fs:allow-read-text-file",
      "allow": [{ "path": "$APPLOCALDATA/**" }]
    },
    {
      "identifier": "fs:allow-write-text-file",
      "allow": [{ "path": "$APPLOCALDATA/**" }]
    }
  ]
}

実装例

ユーザー情報のリストを CSV として書き出し、それを再び読み込む例です。

1. データ構造の定義

serde::Serializeserde::Deserialize を derive することで、構造体と CSV 行の相互変換が自動化されます。

// src-tauri/src/lib.rs

use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::{BufReader, BufWriter};

#[derive(Debug, Serialize, Deserialize)]
struct User {
    id: u32,
    name: String,
    email: String,
}

2. CSV 書き出し (Export)

#[tauri::command]
fn export_users_to_csv(path: String, users: Vec<User>) -> Result<(), String> {
    let file = File::create(path).map_err(|e| e.to_string())?;
    let mut writer = csv::Writer::from_writer(BufWriter::new(file));

    for user in users {
        writer.serialize(user).map_err(|e| e.to_string())?;
    }

    writer.flush().map_err(|e| e.to_string())?;
    Ok(())
}

3. CSV 読み込み (Import)

#[tauri::command]
fn import_users_from_csv(path: String) -> Result<Vec<User>, String> {
    let file = File::open(path).map_err(|e| e.to_string())?;
    let mut reader = csv::Reader::from_reader(BufReader::new(file));
    let mut users = Vec::new();

    for result in reader.deserialize() {
        // パースエラーがあればエラーとして返す(必要に応じてスキップ処理などに変更可)
        let user: User = result.map_err(|e| e.to_string())?;
        users.push(user);
    }

    Ok(users)
}

4. main関数への登録

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            export_users_to_csv,
            import_users_from_csv
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

TypeScript 側からの呼び出し

import { invoke } from '@tauri-apps/api/core';
import { appLocalDataDir, join } from '@tauri-apps/api/path';

async function demoCSV() {
  const path = await join(await appLocalDataDir(), 'users.csv');

  // 書き出し
  await invoke('export_users_to_csv', {
    path,
    users: [
      { id: 1, name: "Alice", email: "alice@example.com" },
      { id: 2, name: "Bob", email: "bob@example.com" }
    ]
  });
  console.log('Export finished');

  // 読み込み
  const users = await invoke('import_users_from_csv', { path });
  console.log('Imported users:', users);
}

注意点

* ヘッダーの扱い: csv::ReaderBuilder / WriterBuilder を使うと、ヘッダーの有無 (has_headers(false)) やデリミタ(カンマ以外、例えばタブ区切り delimiter(b'\t'))を設定できます。
* エンコーディング: csv クレートは UTF-8 を前提としています。Shift-JIS (CP932) などを扱う場合は、encoding_rs クレートなどを使用して事前にデコード/エンコードする必要があります。