chore: added proper error handling b1a69bfe
Steve · 2026-02-20 00:14 4 file(s) · +108 −60
src/backend.rs +16 −5
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,
src/db.rs +53 −31
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
}
src/server.rs +32 −17
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
    };
src/tui.rs +7 −7
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()