Files
tolaria/src-tauri/src/vault/image.rs
lucaronin 42b543992c refactor: split vault.rs into focused submodules
Break the 2338-line vault.rs into vault/ directory with 6 focused modules:
- mod.rs: core types (VaultEntry, Frontmatter), parse_md_file, scan_vault
- parsing.rs: text processing (snippet extraction, markdown stripping, date parsing)
- cache.rs: git-based incremental vault caching
- trash.rs: purge_trash with flat iterator chain
- rename.rs: note renaming with wikilink updates
- image.rs: attachment saving with filename sanitization

Also moves frontmatter update/delete wrappers to frontmatter.rs where they
belong, and converts public functions to accept &Path instead of &str.

CodeScene scores: mod.rs 10.0, parsing.rs 9.68, cache.rs 9.68,
trash.rs 9.38, rename.rs 9.68, image.rs 10.0 (all above 9.0 target).
All 169 Rust tests pass, 86.28% line coverage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 22:37:31 +01:00

106 lines
3.5 KiB
Rust

use std::fs;
use std::path::Path;
use std::time::UNIX_EPOCH;
/// Check if a character is safe for use in filenames (alphanumeric, dot, dash, underscore).
fn is_safe_filename_char(c: char) -> bool {
c.is_alphanumeric() || matches!(c, '.' | '-' | '_')
}
/// Sanitize a filename by replacing unsafe characters with underscores.
fn sanitize_filename(name: &str) -> String {
name.chars()
.map(|c| if is_safe_filename_char(c) { c } else { '_' })
.collect()
}
/// Save an uploaded image to the vault's attachments directory.
/// Returns the absolute path to the saved file.
pub fn save_image(vault_path: &str, filename: &str, data: &str) -> Result<String, String> {
use base64::Engine;
let vault = Path::new(vault_path);
let attachments_dir = vault.join("attachments");
fs::create_dir_all(&attachments_dir)
.map_err(|e| format!("Failed to create attachments directory: {}", e))?;
// Generate unique filename to avoid collisions
let timestamp = std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
let unique_name = format!("{}-{}", timestamp, sanitize_filename(filename));
let target_path = attachments_dir.join(&unique_name);
let bytes = base64::engine::general_purpose::STANDARD
.decode(data)
.map_err(|e| format!("Invalid base64 data: {}", e))?;
fs::write(&target_path, bytes).map_err(|e| format!("Failed to write image: {}", e))?;
Ok(target_path.to_string_lossy().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_sanitize_filename_safe_chars() {
assert_eq!(sanitize_filename("photo.png"), "photo.png");
assert_eq!(sanitize_filename("my-image_01.jpg"), "my-image_01.jpg");
}
#[test]
fn test_sanitize_filename_unsafe_chars() {
assert_eq!(sanitize_filename("my file (1).png"), "my_file__1_.png");
assert_eq!(sanitize_filename("path/to/img.png"), "path_to_img.png");
}
#[test]
fn test_save_image_creates_file() {
use base64::Engine;
let dir = TempDir::new().unwrap();
let vault_path = dir.path().to_str().unwrap();
let data = base64::engine::general_purpose::STANDARD.encode(b"fake image data");
let result = save_image(vault_path, "test.png", &data);
assert!(result.is_ok());
let saved_path = result.unwrap();
assert!(std::path::Path::new(&saved_path).exists());
assert!(saved_path.contains("attachments"));
assert!(saved_path.contains("test.png"));
let content = fs::read(&saved_path).unwrap();
assert_eq!(content, b"fake image data");
}
#[test]
fn test_save_image_creates_attachments_dir() {
use base64::Engine;
let dir = TempDir::new().unwrap();
let vault_path = dir.path().to_str().unwrap();
let attachments = dir.path().join("attachments");
assert!(!attachments.exists());
let data = base64::engine::general_purpose::STANDARD.encode(b"test");
save_image(vault_path, "img.png", &data).unwrap();
assert!(attachments.exists());
}
#[test]
fn test_save_image_invalid_base64() {
let dir = TempDir::new().unwrap();
let vault_path = dir.path().to_str().unwrap();
let result = save_image(vault_path, "test.png", "not-valid-base64!!!");
assert!(result.is_err());
assert!(result.unwrap_err().contains("Invalid base64"));
}
}