Skip to main content

romance_core/addon/
storage.rs

1use crate::addon::Addon;
2use anyhow::Result;
3use std::path::Path;
4
5pub struct StorageAddon;
6
7impl Addon for StorageAddon {
8    fn name(&self) -> &str {
9        "storage"
10    }
11
12    fn check_prerequisites(&self, project_root: &Path) -> Result<()> {
13        super::check_romance_project(project_root)
14    }
15
16    fn is_already_installed(&self, project_root: &Path) -> bool {
17        project_root.join("backend/src/storage.rs").exists()
18    }
19
20    fn install(&self, project_root: &Path) -> Result<()> {
21        install_storage(project_root)
22    }
23
24    fn uninstall(&self, project_root: &Path) -> Result<()> {
25        use colored::Colorize;
26
27        println!("{}", "Uninstalling file storage...".bold());
28
29        // Delete files
30        if super::remove_file_if_exists(&project_root.join("backend/src/storage.rs"))? {
31            println!("  {} backend/src/storage.rs", "delete".red());
32        }
33        if super::remove_file_if_exists(&project_root.join("backend/src/handlers/upload.rs"))? {
34            println!("  {} backend/src/handlers/upload.rs", "delete".red());
35        }
36        if super::remove_file_if_exists(&project_root.join("backend/src/routes/upload.rs"))? {
37            println!("  {} backend/src/routes/upload.rs", "delete".red());
38        }
39        if super::remove_file_if_exists(
40            &project_root.join("frontend/src/components/FileUpload.tsx"),
41        )? {
42            println!(
43                "  {} frontend/src/components/FileUpload.tsx",
44                "delete".red()
45            );
46        }
47
48        // Remove mod declaration from main.rs
49        super::remove_mod_from_main(project_root, "storage")?;
50
51        // Remove from handlers/mod.rs
52        super::remove_line_from_file(
53            &project_root.join("backend/src/handlers/mod.rs"),
54            "pub mod upload;",
55        )?;
56
57        // Remove from routes/mod.rs
58        super::remove_line_from_file(
59            &project_root.join("backend/src/routes/mod.rs"),
60            "pub mod upload;",
61        )?;
62        super::remove_line_from_file(
63            &project_root.join("backend/src/routes/mod.rs"),
64            ".merge(upload::router())",
65        )?;
66
67        // Remove [storage] section from romance.toml
68        super::remove_toml_section(project_root, "storage")?;
69
70        // Regenerate AI context
71        crate::ai_context::regenerate(project_root).ok();
72
73        println!();
74        println!(
75            "{}",
76            "File storage uninstalled successfully.".green().bold()
77        );
78
79        Ok(())
80    }
81}
82
83fn install_storage(project_root: &Path) -> Result<()> {
84    use crate::template::TemplateEngine;
85    use crate::utils;
86    use colored::Colorize;
87    use tera::Context;
88
89    println!("{}", "Installing file storage...".bold());
90
91    let engine = TemplateEngine::new()?;
92    let ctx = Context::new();
93
94    // Generate storage backend trait + impls
95    let content = engine.render("addon/storage/storage.rs.tera", &ctx)?;
96    utils::write_file(&project_root.join("backend/src/storage.rs"), &content)?;
97    println!("  {} backend/src/storage.rs", "create".green());
98
99    // Generate upload handler
100    let content = engine.render("addon/storage/upload_handler.rs.tera", &ctx)?;
101    utils::write_file(
102        &project_root.join("backend/src/handlers/upload.rs"),
103        &content,
104    )?;
105    println!("  {} backend/src/handlers/upload.rs", "create".green());
106
107    // Generate upload routes
108    let content = engine.render("addon/storage/upload_routes.rs.tera", &ctx)?;
109    utils::write_file(
110        &project_root.join("backend/src/routes/upload.rs"),
111        &content,
112    )?;
113    println!("  {} backend/src/routes/upload.rs", "create".green());
114
115    // Generate frontend upload component
116    let content = engine.render("addon/storage/FileUpload.tsx.tera", &ctx)?;
117    utils::write_file(
118        &project_root.join("frontend/src/components/FileUpload.tsx"),
119        &content,
120    )?;
121    println!(
122        "  {} frontend/src/components/FileUpload.tsx",
123        "create".green()
124    );
125
126    // Add mod storage to main.rs
127    super::add_mod_to_main(project_root, "storage")?;
128
129    // Register upload routes
130    let mods_marker = "// === ROMANCE:MODS ===";
131    utils::insert_at_marker(
132        &project_root.join("backend/src/handlers/mod.rs"),
133        mods_marker,
134        "pub mod upload;",
135    )?;
136    utils::insert_at_marker(
137        &project_root.join("backend/src/routes/mod.rs"),
138        mods_marker,
139        "pub mod upload;",
140    )?;
141    utils::insert_at_marker(
142        &project_root.join("backend/src/routes/mod.rs"),
143        "// === ROMANCE:ROUTES ===",
144        "        .merge(upload::router())",
145    )?;
146
147    // Add dependencies
148    crate::generator::auth::insert_cargo_dependency(
149        &project_root.join("backend/Cargo.toml"),
150        &[
151            ("axum", r#"{ version = "0.8", features = ["json", "multipart"] }"#),
152            ("mime", r#""0.3""#),
153            ("async-trait", r#""0.1""#),
154        ],
155    )?;
156
157    // Add env vars for storage configuration
158    super::append_env_var(
159        &project_root.join("backend/.env"),
160        "UPLOAD_DIR=./uploads",
161    )?;
162    super::append_env_var(
163        &project_root.join("backend/.env"),
164        "UPLOAD_URL=/uploads",
165    )?;
166    super::append_env_var(
167        &project_root.join("backend/.env"),
168        "MAX_FILE_SIZE=10MB",
169    )?;
170    super::append_env_var(
171        &project_root.join("backend/.env.example"),
172        "UPLOAD_DIR=./uploads",
173    )?;
174    super::append_env_var(
175        &project_root.join("backend/.env.example"),
176        "UPLOAD_URL=/uploads",
177    )?;
178    super::append_env_var(
179        &project_root.join("backend/.env.example"),
180        "MAX_FILE_SIZE=10MB",
181    )?;
182
183    // Create uploads directory
184    std::fs::create_dir_all(project_root.join("backend/uploads"))?;
185    println!("  {} backend/uploads/", "create".green());
186
187    // Update romance.toml
188    let config_path = project_root.join("romance.toml");
189    let content = std::fs::read_to_string(&config_path)?;
190    if !content.contains("[storage]") {
191        let new_content = format!(
192            "{}\n[storage]\nbackend = \"local\"\nupload_dir = \"./uploads\"\nmax_file_size = \"10MB\"\n",
193            content.trim_end()
194        );
195        std::fs::write(&config_path, new_content)?;
196    }
197
198    println!();
199    println!("{}", "File storage installed successfully!".green().bold());
200    println!("  Use `avatar:image` or `document:file` field types in entity generation.");
201    println!("  Configure storage in romance.toml under [storage].");
202
203    Ok(())
204}