Skip to main content

rustbasic_cli/
database.rs

1use std::fs;
2use base64::{Engine as _, engine::general_purpose};
3
4pub struct LocalDbConfig {
5    pub db_connection: String,
6    pub db_host: String,
7    pub db_port: u16,
8    pub db_database: String,
9    pub db_username: String,
10    pub db_password: String,
11}
12
13impl LocalDbConfig {
14    pub fn load() -> Self {
15        use std::env;
16        Self {
17            db_connection: env::var("DB_CONNECTION").unwrap_or_else(|_| "sqlite".to_string()),
18            db_host: env::var("DB_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()),
19            db_port: env::var("DB_PORT")
20                .unwrap_or_else(|_| "3306".to_string())
21                .parse()
22                .unwrap_or(3306),
23            db_database: env::var("DB_DATABASE").unwrap_or_else(|_| "rustbasic".to_string()),
24            db_username: env::var("DB_USERNAME").unwrap_or_else(|_| "root".to_string()),
25            db_password: env::var("DB_PASSWORD").unwrap_or_default(),
26        }
27    }
28
29    pub fn db_backend(&self) -> sea_orm::DbBackend {
30        if self.db_connection == "mysql" {
31            sea_orm::DbBackend::MySql
32        } else {
33            sea_orm::DbBackend::Sqlite
34        }
35    }
36}
37
38pub async fn connect() -> sea_orm::DatabaseConnection {
39    let cfg = LocalDbConfig::load();
40    let db_url = if cfg.db_connection == "mysql" {
41        format!(
42            "mysql://{}:{}@{}:{}/{}",
43            cfg.db_username, cfg.db_password, cfg.db_host, cfg.db_port, cfg.db_database
44        )
45    } else {
46        format!("sqlite:database/{}.sqlite?mode=rwc", cfg.db_database)
47    };
48
49    if cfg.db_connection != "mysql" {
50        let _ = std::fs::create_dir_all("database");
51    }
52
53    let mut opt = sea_orm::ConnectOptions::new(db_url);
54    opt.max_connections(20)
55       .min_connections(5)
56       .connect_timeout(std::time::Duration::from_secs(8))
57       .idle_timeout(std::time::Duration::from_secs(8))
58       .max_lifetime(std::time::Duration::from_secs(8))
59       .sqlx_logging(true);
60
61    match sea_orm::Database::connect(opt.clone()).await {
62        Ok(conn) => conn,
63        Err(e) => {
64            let err_msg = e.to_string();
65            if (err_msg.contains("1049") || err_msg.contains("Unknown database")) && cfg.db_connection == "mysql" {
66                println!("{}", "โš ๏ธ  Database tidak ditemukan. Mencoba membuat database baru...".yellow());
67                let root_url = format!(
68                    "mysql://{}:{}@{}:{}",
69                    cfg.db_username, cfg.db_password, cfg.db_host, cfg.db_port
70                );
71                if let Ok(root_db) = sea_orm::Database::connect(root_url).await {
72                    let create_query = format!("CREATE DATABASE IF NOT EXISTS `{}`", cfg.db_database);
73                    if root_db.execute(sea_orm::Statement::from_string(sea_orm::DbBackend::MySql, create_query)).await.is_ok() {
74                        println!("โœ… Database '{}' berhasil dibuat.", cfg.db_database.green());
75                        return sea_orm::Database::connect(opt).await.expect("Gagal terhubung setelah membuat database");
76                    }
77                }
78            }
79            panic!("Gagal terhubung ke database: {:?}", e);
80        }
81    }
82}
83use rand::Rng;
84use regex::Regex;
85use colored::*;
86use sea_orm::ConnectionTrait;
87
88pub async fn clear_cache() {
89    println!("\n{}", "๐Ÿงน Cleaning Cache & Logs...".magenta().bold());
90
91    // 1. Clear Logs
92    let log_dir = "storage/logs";
93    if let Ok(entries) = fs::read_dir(log_dir) {
94        let mut count = 0;
95        for entry in entries.flatten() {
96            let path = entry.path();
97            if path.is_file() {
98                let _ = fs::OpenOptions::new()
99                    .write(true)
100                    .truncate(true)
101                    .open(&path);
102                count += 1;
103            }
104        }
105        println!("   {} Folder storage/logs telah dikosongkan. ({} file dibersihkan)", "โœ… Logs:".green(), count);
106    } else {
107        println!("   {} Folder storage/logs tidak ditemukan.", "โš ๏ธ  Logs:".yellow());
108    }
109
110    // 2. Clear Sessions in DB
111    let cfg = LocalDbConfig::load();
112    let db = connect().await;
113    
114    let truncate_sql = if cfg.db_connection == "mysql" {
115        "TRUNCATE TABLE sessions"
116    } else {
117        "DELETE FROM sessions"
118    };
119
120    match db.execute(sea_orm::Statement::from_string(cfg.db_backend(), truncate_sql.to_string())).await {
121        Ok(_) => println!("   {} Tabel sessions telah dikosongkan.", "โœ… Sessions:".green()),
122        Err(e) => println!("   {} Gagal membersihkan tabel sessions. ({})", "โŒ Error:".red(), e),
123    }
124
125    println!("\n{}", "โœจ Cache berhasil dibersihkan!".green().bold());
126}
127
128pub fn generate_app_key() {
129    println!("\n{}", "๐Ÿ”‘ Generating Application Key...".magenta().bold());
130
131    let mut key = [0u8; 32];
132    rand::rng().fill_bytes(&mut key);
133    
134    let encoded = general_purpose::STANDARD.encode(key);
135    let key_str = format!("base64:{}", encoded);
136    
137    let env_path = ".env";
138    match fs::read_to_string(env_path) {
139        Ok(content) => {
140            let re = Regex::new(r"(?m)^APP_KEY=.*").unwrap();
141            let new_content = if re.is_match(&content) {
142                re.replace(&content, &format!("APP_KEY={}", key_str)).to_string()
143            } else {
144                format!("{}\nAPP_KEY={}", content.trim_end(), key_str)
145            };
146
147            if let Err(e) = fs::write(env_path, new_content) {
148                println!("{} Gagal menulis ke file .env: {}", "โŒ Error:".red(), e);
149            } else {
150                println!("{} {}", "โœ… Application key set successfully:".green(), key_str.cyan());
151                println!("{}", "๐Ÿ’ก Pastikan untuk tidak membagikan APP_KEY ini ke publik!".dimmed());
152            }
153        }
154        Err(_) => {
155            println!("{} File .env tidak ditemukan.", "โŒ Error:".red());
156        }
157    }
158}
159
160pub async fn ensure_session() {
161    if !std::path::Path::new(".env").exists() {
162        return;
163    }
164
165    let _ = dotenvy::dotenv();
166    let cfg = LocalDbConfig::load();
167
168    // 1. Hubungkan ke database dengan aman (tanpa panik jika gagal)
169    let db_url = if cfg.db_connection == "mysql" {
170        format!(
171            "mysql://{}:{}@{}:{}/{}",
172            cfg.db_username, cfg.db_password, cfg.db_host, cfg.db_port, cfg.db_database
173        )
174    } else {
175        format!("sqlite:database/{}.sqlite?mode=rwc", cfg.db_database)
176    };
177
178    // Pastikan folder database ada untuk sqlite
179    if cfg.db_connection != "mysql" {
180        let _ = std::fs::create_dir_all("database");
181    }
182
183    let mut opt = sea_orm::ConnectOptions::new(db_url);
184    opt.max_connections(5)
185       .connect_timeout(std::time::Duration::from_secs(3))
186       .sqlx_logging(false);
187
188    let db = match sea_orm::Database::connect(opt).await {
189        Ok(conn) => conn,
190        Err(_) => {
191            // Abaikan jika database belum siap/tidak bisa terhubung (agar perintah seperti migrate/help/new tidak crash)
192            return;
193        }
194    };
195
196    // 2. Pastikan tabel sessions ada di database
197    let create_table_sql = if cfg.db_connection == "mysql" {
198        "CREATE TABLE IF NOT EXISTS sessions (
199            id VARCHAR(255) PRIMARY KEY,
200            payload TEXT NOT NULL,
201            last_activity BIGINT NOT NULL,
202            ip_address VARCHAR(45)
203        )"
204    } else {
205        "CREATE TABLE IF NOT EXISTS sessions (
206            id TEXT PRIMARY KEY,
207            payload TEXT NOT NULL,
208            last_activity BIGINT NOT NULL,
209            ip_address TEXT
210        )"
211    };
212
213    let stmt = sea_orm::Statement::from_string(db.get_database_backend(), create_table_sql.to_string());
214    if db.execute(stmt).await.is_err() {
215        return;
216    }
217
218    // 3. Baca dan verifikasi session ID
219    let session_file = ".rustbasic_session";
220    let mut session_id = String::new();
221    let mut need_to_write = false;
222
223    if let Ok(content) = fs::read_to_string(session_file) {
224        session_id = content.trim().to_string();
225    }
226
227    let mut session_exists = false;
228
229    if !session_id.is_empty() {
230        let check_sql = format!("SELECT id FROM sessions WHERE id = '{}'", session_id);
231        let stmt = sea_orm::Statement::from_string(db.get_database_backend(), check_sql);
232        if let Ok(Some(_)) = db.query_one(stmt).await {
233            session_exists = true;
234        }
235    }
236
237    if !session_exists {
238        let mut bytes = [0u8; 32];
239        rand::rng().fill_bytes(&mut bytes);
240        session_id = general_purpose::STANDARD.encode(bytes);
241        need_to_write = true;
242
243        let now = chrono::Utc::now().timestamp();
244        let insert_sql = format!(
245            "INSERT INTO sessions (id, payload, last_activity, ip_address) VALUES ('{}', '{}', {}, '{}')",
246            session_id, "{}", now, "127.0.0.1"
247        );
248        let stmt = sea_orm::Statement::from_string(db.get_database_backend(), insert_sql);
249        if db.execute(stmt).await.is_ok() {
250            println!("โœจ {} ({})", "Session baru berhasil dibuat dan disimpan di database.".green().bold(), session_id.cyan());
251        }
252    } else {
253        let now = chrono::Utc::now().timestamp();
254        let update_sql = format!(
255            "UPDATE sessions SET last_activity = {} WHERE id = '{}'",
256            now, session_id
257        );
258        let stmt = sea_orm::Statement::from_string(db.get_database_backend(), update_sql);
259        let _ = db.execute(stmt).await;
260    }
261
262    if need_to_write {
263        let _ = fs::write(session_file, &session_id);
264    }
265}
266