chore: added proper error handling
b1a69bfe
4 file(s) · +108 −60
| 1 | 1 | use crate::db::{self, Db, Snippet}; |
|
| 2 | 2 | use std::fmt; |
|
| 3 | 3 | ||
| 4 | + | #[derive(Debug)] |
|
| 4 | 5 | pub enum BackendError { |
|
| 5 | 6 | NotFound, |
|
| 6 | 7 | Unauthorized(String), |
|
| 7 | 8 | Network(String), |
|
| 9 | + | Database(String), |
|
| 8 | 10 | } |
|
| 9 | 11 | ||
| 10 | 12 | impl fmt::Display for BackendError { |
|
| 13 | 15 | BackendError::NotFound => write!(f, "Not found"), |
|
| 14 | 16 | BackendError::Unauthorized(msg) => write!(f, "Unauthorized: {}", msg), |
|
| 15 | 17 | BackendError::Network(msg) => write!(f, "Network error: {}", msg), |
|
| 18 | + | BackendError::Database(msg) => write!(f, "Database error: {}", msg), |
|
| 16 | 19 | } |
|
| 20 | + | } |
|
| 21 | + | } |
|
| 22 | + | ||
| 23 | + | impl std::error::Error for BackendError {} |
|
| 24 | + | ||
| 25 | + | impl From<db::DbError> for BackendError { |
|
| 26 | + | fn from(e: db::DbError) -> Self { |
|
| 27 | + | BackendError::Database(e.to_string()) |
|
| 17 | 28 | } |
|
| 18 | 29 | } |
|
| 19 | 30 | ||
| 29 | 40 | } |
|
| 30 | 41 | ||
| 31 | 42 | impl Backend { |
|
| 32 | - | pub fn local() -> Self { |
|
| 33 | - | Backend::Local { db: db::init_db() } |
|
| 43 | + | pub fn local() -> Result<Self, BackendError> { |
|
| 44 | + | Ok(Backend::Local { db: db::init_db()? }) |
|
| 34 | 45 | } |
|
| 35 | 46 | ||
| 36 | 47 | pub fn remote(base_url: String, api_key: Option<String>) -> Self { |
|
| 43 | 54 | ||
| 44 | 55 | pub fn list_snippets(&self) -> Result<Vec<Snippet>, BackendError> { |
|
| 45 | 56 | match self { |
|
| 46 | - | Backend::Local { db } => Ok(db::get_all_snippets(db)), |
|
| 57 | + | Backend::Local { db } => Ok(db::get_all_snippets(db)?), |
|
| 47 | 58 | Backend::Remote { |
|
| 48 | 59 | base_url, |
|
| 49 | 60 | api_key, |
|
| 68 | 79 | ||
| 69 | 80 | pub fn create_snippet(&self, name: &str, content: &str) -> Result<Snippet, BackendError> { |
|
| 70 | 81 | match self { |
|
| 71 | - | Backend::Local { db } => Ok(db::create_snippet(db, name, content)), |
|
| 82 | + | Backend::Local { db } => Ok(db::create_snippet(db, name, content)?), |
|
| 72 | 83 | Backend::Remote { |
|
| 73 | 84 | base_url, |
|
| 74 | 85 | api_key, |
|
| 95 | 106 | ||
| 96 | 107 | pub fn delete_snippet(&self, short_id: &str) -> Result<bool, BackendError> { |
|
| 97 | 108 | match self { |
|
| 98 | - | Backend::Local { db } => Ok(db::delete_snippet_by_short_id(db, short_id)), |
|
| 109 | + | Backend::Local { db } => Ok(db::delete_snippet_by_short_id(db, short_id)?), |
|
| 99 | 110 | Backend::Remote { |
|
| 100 | 111 | base_url, |
|
| 101 | 112 | api_key, |
|
| 1 | 1 | use rand::RngExt; |
|
| 2 | 2 | use rusqlite::{Connection, params}; |
|
| 3 | 3 | use serde::{Deserialize, Serialize}; |
|
| 4 | + | use std::fmt; |
|
| 4 | 5 | use std::sync::{Arc, Mutex}; |
|
| 5 | 6 | ||
| 6 | 7 | pub type Db = Arc<Mutex<Connection>>; |
|
| 7 | 8 | ||
| 9 | + | #[derive(Debug)] |
|
| 10 | + | pub enum DbError { |
|
| 11 | + | Sqlite(rusqlite::Error), |
|
| 12 | + | LockPoisoned, |
|
| 13 | + | } |
|
| 14 | + | ||
| 15 | + | impl fmt::Display for DbError { |
|
| 16 | + | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
|
| 17 | + | match self { |
|
| 18 | + | DbError::Sqlite(e) => write!(f, "Database error: {}", e), |
|
| 19 | + | DbError::LockPoisoned => write!(f, "Database lock poisoned"), |
|
| 20 | + | } |
|
| 21 | + | } |
|
| 22 | + | } |
|
| 23 | + | ||
| 24 | + | impl std::error::Error for DbError {} |
|
| 25 | + | ||
| 26 | + | impl From<rusqlite::Error> for DbError { |
|
| 27 | + | fn from(e: rusqlite::Error) -> Self { |
|
| 28 | + | DbError::Sqlite(e) |
|
| 29 | + | } |
|
| 30 | + | } |
|
| 31 | + | ||
| 8 | 32 | #[derive(Serialize, Deserialize)] |
|
| 9 | 33 | pub struct Snippet { |
|
| 10 | 34 | pub id: i64, |
|
| 22 | 46 | .collect() |
|
| 23 | 47 | } |
|
| 24 | 48 | ||
| 25 | - | pub fn init_db() -> Db { |
|
| 26 | - | let conn = Connection::open("sipp.sqlite").expect("Failed to open database"); |
|
| 49 | + | pub fn init_db() -> Result<Db, DbError> { |
|
| 50 | + | let conn = Connection::open("sipp.sqlite")?; |
|
| 27 | 51 | conn.execute( |
|
| 28 | 52 | "CREATE TABLE IF NOT EXISTS snippets ( |
|
| 29 | 53 | id INTEGER PRIMARY KEY AUTOINCREMENT, |
|
| 32 | 56 | name TEXT NOT NULL |
|
| 33 | 57 | )", |
|
| 34 | 58 | [], |
|
| 35 | - | ) |
|
| 36 | - | .expect("Failed to create table"); |
|
| 37 | - | Arc::new(Mutex::new(conn)) |
|
| 59 | + | )?; |
|
| 60 | + | Ok(Arc::new(Mutex::new(conn))) |
|
| 38 | 61 | } |
|
| 39 | 62 | ||
| 40 | - | pub fn create_snippet(db: &Db, name: &str, content: &str) -> Snippet { |
|
| 41 | - | let conn = db.lock().unwrap(); |
|
| 63 | + | pub fn create_snippet(db: &Db, name: &str, content: &str) -> Result<Snippet, DbError> { |
|
| 64 | + | let conn = db.lock().map_err(|_| DbError::LockPoisoned)?; |
|
| 42 | 65 | let short_id = generate_short_id(); |
|
| 43 | 66 | conn.execute( |
|
| 44 | 67 | "INSERT INTO snippets (short_id, content, name) VALUES (?1, ?2, ?3)", |
|
| 45 | 68 | params![short_id, content, name], |
|
| 46 | - | ) |
|
| 47 | - | .expect("Failed to insert snippet"); |
|
| 69 | + | )?; |
|
| 48 | 70 | let id = conn.last_insert_rowid(); |
|
| 49 | - | Snippet { |
|
| 71 | + | Ok(Snippet { |
|
| 50 | 72 | id, |
|
| 51 | 73 | short_id, |
|
| 52 | 74 | content: content.to_string(), |
|
| 53 | 75 | name: name.to_string(), |
|
| 54 | - | } |
|
| 76 | + | }) |
|
| 55 | 77 | } |
|
| 56 | 78 | ||
| 57 | - | pub fn get_snippet_by_short_id(db: &Db, short_id: &str) -> Option<Snippet> { |
|
| 58 | - | let conn = db.lock().unwrap(); |
|
| 59 | - | conn.query_row( |
|
| 79 | + | pub fn get_snippet_by_short_id(db: &Db, short_id: &str) -> Result<Option<Snippet>, DbError> { |
|
| 80 | + | let conn = db.lock().map_err(|_| DbError::LockPoisoned)?; |
|
| 81 | + | match conn.query_row( |
|
| 60 | 82 | "SELECT id, short_id, content, name FROM snippets WHERE short_id = ?1", |
|
| 61 | 83 | params![short_id], |
|
| 62 | 84 | |row| { |
|
| 67 | 89 | name: row.get(3)?, |
|
| 68 | 90 | }) |
|
| 69 | 91 | }, |
|
| 70 | - | ) |
|
| 71 | - | .ok() |
|
| 92 | + | ) { |
|
| 93 | + | Ok(snippet) => Ok(Some(snippet)), |
|
| 94 | + | Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), |
|
| 95 | + | Err(e) => Err(DbError::Sqlite(e)), |
|
| 96 | + | } |
|
| 72 | 97 | } |
|
| 73 | 98 | ||
| 74 | - | pub fn get_all_snippets(db: &Db) -> Vec<Snippet> { |
|
| 75 | - | let conn = db.lock().unwrap(); |
|
| 99 | + | pub fn get_all_snippets(db: &Db) -> Result<Vec<Snippet>, DbError> { |
|
| 100 | + | let conn = db.lock().map_err(|_| DbError::LockPoisoned)?; |
|
| 76 | 101 | let mut stmt = conn |
|
| 77 | - | .prepare("SELECT id, short_id, content, name FROM snippets ORDER BY id DESC") |
|
| 78 | - | .expect("Failed to prepare statement"); |
|
| 79 | - | stmt.query_map([], |row| { |
|
| 102 | + | .prepare("SELECT id, short_id, content, name FROM snippets ORDER BY id DESC")?; |
|
| 103 | + | let snippets = stmt.query_map([], |row| { |
|
| 80 | 104 | Ok(Snippet { |
|
| 81 | 105 | id: row.get(0)?, |
|
| 82 | 106 | short_id: row.get(1)?, |
|
| 83 | 107 | content: row.get(2)?, |
|
| 84 | 108 | name: row.get(3)?, |
|
| 85 | 109 | }) |
|
| 86 | - | }) |
|
| 87 | - | .expect("Failed to query snippets") |
|
| 110 | + | })? |
|
| 88 | 111 | .filter_map(|r| r.ok()) |
|
| 89 | - | .collect() |
|
| 112 | + | .collect(); |
|
| 113 | + | Ok(snippets) |
|
| 90 | 114 | } |
|
| 91 | 115 | ||
| 92 | - | pub fn delete_snippet_by_short_id(db: &Db, short_id: &str) -> bool { |
|
| 93 | - | let conn = db.lock().unwrap(); |
|
| 94 | - | match conn.execute( |
|
| 116 | + | pub fn delete_snippet_by_short_id(db: &Db, short_id: &str) -> Result<bool, DbError> { |
|
| 117 | + | let conn = db.lock().map_err(|_| DbError::LockPoisoned)?; |
|
| 118 | + | let rows_affected = conn.execute( |
|
| 95 | 119 | "DELETE FROM snippets WHERE short_id = ?1", |
|
| 96 | 120 | params![short_id], |
|
| 97 | - | ) { |
|
| 98 | - | Ok(rows_affected) => rows_affected > 0, |
|
| 99 | - | Err(_) => false, |
|
| 100 | - | } |
|
| 121 | + | )?; |
|
| 122 | + | Ok(rows_affected > 0) |
|
| 101 | 123 | } |
|
| 80 | 80 | Path(short_id): Path<String>, |
|
| 81 | 81 | ) -> Result<WebTemplate<SnippetTemplate>, (StatusCode, Html<String>)> { |
|
| 82 | 82 | match db::get_snippet_by_short_id(&state.db, &short_id) { |
|
| 83 | - | Some(snippet) => { |
|
| 83 | + | Ok(Some(snippet)) => { |
|
| 84 | 84 | let highlighted_content = state.highlighter.highlight(&snippet.name, &snippet.content); |
|
| 85 | 85 | Ok(WebTemplate(SnippetTemplate { |
|
| 86 | 86 | name: snippet.name, |
|
| 88 | 88 | highlighted_content, |
|
| 89 | 89 | })) |
|
| 90 | 90 | } |
|
| 91 | - | None => Err(( |
|
| 91 | + | Ok(None) => Err(( |
|
| 92 | 92 | StatusCode::NOT_FOUND, |
|
| 93 | 93 | Html("<h1>Snippet not found</h1>".to_string()), |
|
| 94 | 94 | )), |
|
| 95 | + | Err(_) => Err(( |
|
| 96 | + | StatusCode::INTERNAL_SERVER_ERROR, |
|
| 97 | + | Html("<h1>Internal server error</h1>".to_string()), |
|
| 98 | + | )), |
|
| 95 | 99 | } |
|
| 96 | 100 | } |
|
| 97 | 101 | ||
| 98 | 102 | async fn create_snippet( |
|
| 99 | 103 | State(state): State<AppState>, |
|
| 100 | 104 | Form(form): Form<CreateSnippetForm>, |
|
| 101 | - | ) -> impl IntoResponse { |
|
| 102 | - | let snippet = db::create_snippet(&state.db, &form.name, &form.content); |
|
| 103 | - | Redirect::to(&format!("/s/{}", snippet.short_id)) |
|
| 105 | + | ) -> Result<Redirect, (StatusCode, Html<String>)> { |
|
| 106 | + | match db::create_snippet(&state.db, &form.name, &form.content) { |
|
| 107 | + | Ok(snippet) => Ok(Redirect::to(&format!("/s/{}", snippet.short_id))), |
|
| 108 | + | Err(_) => Err(( |
|
| 109 | + | StatusCode::INTERNAL_SERVER_ERROR, |
|
| 110 | + | Html("<h1>Internal server error</h1>".to_string()), |
|
| 111 | + | )), |
|
| 112 | + | } |
|
| 104 | 113 | } |
|
| 105 | 114 | ||
| 106 | 115 | async fn require_api_key( |
|
| 130 | 139 | ||
| 131 | 140 | async fn api_list_snippets( |
|
| 132 | 141 | State(state): State<AppState>, |
|
| 133 | - | ) -> Json<Vec<Snippet>> { |
|
| 134 | - | Json(db::get_all_snippets(&state.db)) |
|
| 142 | + | ) -> Result<Json<Vec<Snippet>>, (StatusCode, Json<serde_json::Value>)> { |
|
| 143 | + | match db::get_all_snippets(&state.db) { |
|
| 144 | + | Ok(snippets) => Ok(Json(snippets)), |
|
| 145 | + | Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "Internal server error"})))), |
|
| 146 | + | } |
|
| 135 | 147 | } |
|
| 136 | 148 | ||
| 137 | 149 | async fn api_get_snippet( |
|
| 139 | 151 | Path(short_id): Path<String>, |
|
| 140 | 152 | ) -> Result<Json<Snippet>, (StatusCode, Json<serde_json::Value>)> { |
|
| 141 | 153 | match db::get_snippet_by_short_id(&state.db, &short_id) { |
|
| 142 | - | Some(snippet) => Ok(Json(snippet)), |
|
| 143 | - | None => Err((StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "Snippet not found"})))), |
|
| 154 | + | Ok(Some(snippet)) => Ok(Json(snippet)), |
|
| 155 | + | Ok(None) => Err((StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "Snippet not found"})))), |
|
| 156 | + | Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "Internal server error"})))), |
|
| 144 | 157 | } |
|
| 145 | 158 | } |
|
| 146 | 159 | ||
| 153 | 166 | async fn api_create_snippet( |
|
| 154 | 167 | State(state): State<AppState>, |
|
| 155 | 168 | Json(body): Json<ApiCreateSnippet>, |
|
| 156 | - | ) -> (StatusCode, Json<Snippet>) { |
|
| 157 | - | let snippet = db::create_snippet(&state.db, &body.name, &body.content); |
|
| 158 | - | (StatusCode::CREATED, Json(snippet)) |
|
| 169 | + | ) -> Result<(StatusCode, Json<Snippet>), (StatusCode, Json<serde_json::Value>)> { |
|
| 170 | + | match db::create_snippet(&state.db, &body.name, &body.content) { |
|
| 171 | + | Ok(snippet) => Ok((StatusCode::CREATED, Json(snippet))), |
|
| 172 | + | Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "Internal server error"})))), |
|
| 173 | + | } |
|
| 159 | 174 | } |
|
| 160 | 175 | ||
| 161 | 176 | async fn api_delete_snippet( |
|
| 162 | 177 | State(state): State<AppState>, |
|
| 163 | 178 | Path(short_id): Path<String>, |
|
| 164 | 179 | ) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> { |
|
| 165 | - | if db::delete_snippet_by_short_id(&state.db, &short_id) { |
|
| 166 | - | Ok(Json(serde_json::json!({"deleted": true}))) |
|
| 167 | - | } else { |
|
| 168 | - | Err((StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "Snippet not found"})))) |
|
| 180 | + | match db::delete_snippet_by_short_id(&state.db, &short_id) { |
|
| 181 | + | Ok(true) => Ok(Json(serde_json::json!({"deleted": true}))), |
|
| 182 | + | Ok(false) => Err((StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "Snippet not found"})))), |
|
| 183 | + | Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "Internal server error"})))), |
|
| 169 | 184 | } |
|
| 170 | 185 | } |
|
| 171 | 186 | ||
| 279 | 294 | } |
|
| 280 | 295 | ||
| 281 | 296 | let state = AppState { |
|
| 282 | - | db: db::init_db(), |
|
| 297 | + | db: db::init_db().expect("Failed to initialize database"), |
|
| 283 | 298 | highlighter: Arc::new(Highlighter::new()), |
|
| 284 | 299 | server_config, |
|
| 285 | 300 | }; |
|
| 288 | 288 | } |
|
| 289 | 289 | } |
|
| 290 | 290 | ||
| 291 | - | fn resolve_backend(remote: Option<String>, api_key: Option<String>) -> (Backend, bool, Option<String>) { |
|
| 291 | + | fn resolve_backend(remote: Option<String>, api_key: Option<String>) -> Result<(Backend, bool, Option<String>), Box<dyn std::error::Error>> { |
|
| 292 | 292 | if let Some(url) = remote { |
|
| 293 | - | return ( |
|
| 293 | + | return Ok(( |
|
| 294 | 294 | Backend::remote(url.clone(), api_key), |
|
| 295 | 295 | true, |
|
| 296 | 296 | Some(url), |
|
| 297 | - | ); |
|
| 297 | + | )); |
|
| 298 | 298 | } |
|
| 299 | 299 | ||
| 300 | 300 | if !std::path::Path::new("sipp.sqlite").exists() { |
|
| 301 | 301 | let cfg = config::load_config(); |
|
| 302 | 302 | let url = cfg.remote_url.unwrap_or_else(|| "http://localhost:3000".to_string()); |
|
| 303 | 303 | let api_key = api_key.or(cfg.api_key); |
|
| 304 | - | return (Backend::remote(url.clone(), api_key), true, Some(url)); |
|
| 304 | + | return Ok((Backend::remote(url.clone(), api_key), true, Some(url))); |
|
| 305 | 305 | } |
|
| 306 | 306 | ||
| 307 | - | (Backend::local(), false, Some("http://localhost:3000".to_string())) |
|
| 307 | + | Ok((Backend::local()?, false, Some("http://localhost:3000".to_string()))) |
|
| 308 | 308 | } |
|
| 309 | 309 | ||
| 310 | 310 | pub fn run_auth() -> Result<(), Box<dyn std::error::Error>> { |
|
| 340 | 340 | } |
|
| 341 | 341 | ||
| 342 | 342 | pub fn run_interactive(remote: Option<String>, api_key: Option<String>) -> Result<(), Box<dyn std::error::Error>> { |
|
| 343 | - | let (backend, is_remote, remote_url) = resolve_backend(remote, api_key); |
|
| 343 | + | let (backend, is_remote, remote_url) = resolve_backend(remote, api_key)?; |
|
| 344 | 344 | ||
| 345 | 345 | let snippets = match backend.list_snippets() { |
|
| 346 | 346 | Ok(s) => s, |
|
| 354 | 354 | } |
|
| 355 | 355 | ||
| 356 | 356 | pub fn run_file_upload(remote: Option<String>, api_key: Option<String>, file: PathBuf) -> Result<(), Box<dyn std::error::Error>> { |
|
| 357 | - | let (backend, _, remote_url) = resolve_backend(remote, api_key); |
|
| 357 | + | let (backend, _, remote_url) = resolve_backend(remote, api_key)?; |
|
| 358 | 358 | ||
| 359 | 359 | let name = file |
|
| 360 | 360 | .file_name() |
|