| use std::io::Write; |
| use std::path::PathBuf; |
| use std::time::UNIX_EPOCH; |
|
|
| use serde::Serialize; |
|
|
| const MAX_READ_BYTES: u64 = 10 * 1024 * 1024; |
| const BINARY_SNIFF_BYTES: usize = 8 * 1024; |
|
|
| #[derive(Serialize)] |
| #[serde(tag = "kind", rename_all = "lowercase")] |
| pub enum ReadResult { |
| Text { |
| content: String, |
| size: u64, |
| }, |
| Binary { |
| size: u64, |
| }, |
| |
| TooLarge { |
| size: u64, |
| limit: u64, |
| }, |
| } |
|
|
| #[derive(Serialize)] |
| #[serde(rename_all = "lowercase")] |
| pub enum StatKind { |
| File, |
| Dir, |
| Symlink, |
| } |
|
|
| #[derive(Serialize)] |
| pub struct FileStat { |
| pub size: u64, |
| pub mtime: u64, |
| pub kind: StatKind, |
| } |
|
|
| #[tauri::command] |
| pub fn fs_read_file(path: String) -> Result<ReadResult, String> { |
| let p = PathBuf::from(&path); |
| let meta = std::fs::metadata(&p).map_err(|e| { |
| log::debug!("fs_read_file stat({}) failed: {e}", p.display()); |
| e.to_string() |
| })?; |
|
|
| let size = meta.len(); |
| if size > MAX_READ_BYTES { |
| return Ok(ReadResult::TooLarge { |
| size, |
| limit: MAX_READ_BYTES, |
| }); |
| } |
|
|
| let bytes = std::fs::read(&p).map_err(|e| { |
| log::debug!("fs_read_file read({}) failed: {e}", p.display()); |
| e.to_string() |
| })?; |
|
|
| |
| |
| let sniff_len = bytes.len().min(BINARY_SNIFF_BYTES); |
| if bytes[..sniff_len].contains(&0) { |
| return Ok(ReadResult::Binary { size }); |
| } |
|
|
| match String::from_utf8(bytes) { |
| Ok(content) => Ok(ReadResult::Text { content, size }), |
| Err(_) => Ok(ReadResult::Binary { size }), |
| } |
| } |
|
|
| |
| |
| #[tauri::command] |
| pub fn fs_write_file(path: String, content: String) -> Result<(), String> { |
| let target = PathBuf::from(&path); |
| let parent = target |
| .parent() |
| .ok_or_else(|| "path has no parent".to_string())?; |
| let file_name = target |
| .file_name() |
| .and_then(|s| s.to_str()) |
| .ok_or_else(|| "path has no file name".to_string())?; |
|
|
| let tmp = parent.join(format!(".{file_name}.terax.tmp")); |
|
|
| { |
| let mut f = std::fs::File::create(&tmp).map_err(|e| { |
| log::debug!("fs_write_file create({}) failed: {e}", tmp.display()); |
| e.to_string() |
| })?; |
| f.write_all(content.as_bytes()).map_err(|e| { |
| log::debug!("fs_write_file write({}) failed: {e}", tmp.display()); |
| e.to_string() |
| })?; |
| f.sync_all().map_err(|e| e.to_string())?; |
| } |
|
|
| std::fs::rename(&tmp, &target).map_err(|e| { |
| log::warn!( |
| "fs_write_file rename({} -> {}) failed: {e}", |
| tmp.display(), |
| target.display() |
| ); |
| |
| let _ = std::fs::remove_file(&tmp); |
| e.to_string() |
| })?; |
|
|
| Ok(()) |
| } |
|
|
| #[tauri::command] |
| pub fn fs_stat(path: String) -> Result<FileStat, String> { |
| let p = PathBuf::from(&path); |
| let meta = std::fs::metadata(&p).map_err(|e| e.to_string())?; |
| let kind = if meta.is_dir() { |
| StatKind::Dir |
| } else if meta.file_type().is_symlink() { |
| StatKind::Symlink |
| } else { |
| StatKind::File |
| }; |
| let mtime = meta |
| .modified() |
| .ok() |
| .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) |
| .map(|d| d.as_millis() as u64) |
| .unwrap_or(0); |
| Ok(FileStat { |
| size: meta.len(), |
| mtime, |
| kind, |
| }) |
| } |
|
|