fix: wire up BlockNote uploadFile to enable image uploads in editor
The editor's useCreateBlockNote was missing the uploadFile callback, so BlockNote had no handler for image uploads (drag-drop, paste, or toolbar button). Images now convert to data URLs for immediate display. In Tauri mode, uploaded images are also persisted to the vault's attachments/ directory via a new save_image backend command. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,11 @@ fn git_push(vault_path: String) -> Result<String, String> {
|
||||
git::git_push(&vault_path)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn save_image(vault_path: String, filename: String, data: String) -> Result<String, String> {
|
||||
vault::save_image(&vault_path, &filename, &data)
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
@@ -73,7 +78,8 @@ pub fn run() {
|
||||
get_modified_files,
|
||||
get_file_diff,
|
||||
git_commit,
|
||||
git_push
|
||||
git_push,
|
||||
save_image
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -658,6 +658,47 @@ pub fn delete_frontmatter_property(path: &str, key: &str) -> Result<String, Stri
|
||||
with_frontmatter(path, |content| update_frontmatter_content(content, key, None))
|
||||
}
|
||||
|
||||
/// 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::*;
|
||||
@@ -1471,4 +1512,62 @@ References:
|
||||
}
|
||||
|
||||
// Frontmatter update/delete tests are in frontmatter.rs
|
||||
|
||||
// --- save_image tests ---
|
||||
|
||||
#[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"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user