src/db.rs 4.2 K raw
1
use nanoid::nanoid;
2
use rusqlite::{Connection, params};
3
use serde::{Deserialize, Serialize};
4
use std::fmt;
5
use std::sync::{Arc, Mutex};
6
7
pub type Db = Arc<Mutex<Connection>>;
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
32
#[derive(Serialize, Deserialize)]
33
pub struct Snippet {
34
    pub id: i64,
35
    pub short_id: String,
36
    pub content: String,
37
    pub name: String,
38
}
39
40
fn generate_short_id() -> String {
41
    nanoid!(10)
42
}
43
44
pub fn db_path() -> String {
45
    std::env::var("SIPP_DB_PATH").unwrap_or_else(|_| "sipp.sqlite".to_string())
46
}
47
48
pub fn init_db() -> Result<Db, DbError> {
49
    let conn = Connection::open(db_path())?;
50
    conn.execute(
51
        "CREATE TABLE IF NOT EXISTS snippets (
52
            id INTEGER PRIMARY KEY AUTOINCREMENT,
53
            short_id TEXT NOT NULL UNIQUE,
54
            content TEXT NOT NULL,
55
            name TEXT NOT NULL
56
        )",
57
        [],
58
    )?;
59
    Ok(Arc::new(Mutex::new(conn)))
60
}
61
62
pub fn create_snippet(db: &Db, name: &str, content: &str) -> Result<Snippet, DbError> {
63
    let conn = db.lock().map_err(|_| DbError::LockPoisoned)?;
64
    let short_id = generate_short_id();
65
    conn.execute(
66
        "INSERT INTO snippets (short_id, content, name) VALUES (?1, ?2, ?3)",
67
        params![short_id, content, name],
68
    )?;
69
    let id = conn.last_insert_rowid();
70
    Ok(Snippet {
71
        id,
72
        short_id,
73
        content: content.to_string(),
74
        name: name.to_string(),
75
    })
76
}
77
78
pub fn get_snippet_by_short_id(db: &Db, short_id: &str) -> Result<Option<Snippet>, DbError> {
79
    let conn = db.lock().map_err(|_| DbError::LockPoisoned)?;
80
    match conn.query_row(
81
        "SELECT id, short_id, content, name FROM snippets WHERE short_id = ?1",
82
        params![short_id],
83
        |row| {
84
            Ok(Snippet {
85
                id: row.get(0)?,
86
                short_id: row.get(1)?,
87
                content: row.get(2)?,
88
                name: row.get(3)?,
89
            })
90
        },
91
    ) {
92
        Ok(snippet) => Ok(Some(snippet)),
93
        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
94
        Err(e) => Err(DbError::Sqlite(e)),
95
    }
96
}
97
98
pub fn get_all_snippets(db: &Db) -> Result<Vec<Snippet>, DbError> {
99
    let conn = db.lock().map_err(|_| DbError::LockPoisoned)?;
100
    let mut stmt = conn
101
        .prepare("SELECT id, short_id, content, name FROM snippets ORDER BY id DESC")?;
102
    let snippets = stmt.query_map([], |row| {
103
        Ok(Snippet {
104
            id: row.get(0)?,
105
            short_id: row.get(1)?,
106
            content: row.get(2)?,
107
            name: row.get(3)?,
108
        })
109
    })?
110
    .filter_map(|r| r.ok())
111
    .collect();
112
    Ok(snippets)
113
}
114
115
pub fn delete_snippet_by_short_id(db: &Db, short_id: &str) -> Result<bool, DbError> {
116
    let conn = db.lock().map_err(|_| DbError::LockPoisoned)?;
117
    let rows_affected = conn.execute(
118
        "DELETE FROM snippets WHERE short_id = ?1",
119
        params![short_id],
120
    )?;
121
    Ok(rows_affected > 0)
122
}
123
124
pub fn update_snippet_by_short_id(
125
    db: &Db,
126
    short_id: &str,
127
    name: &str,
128
    content: &str,
129
) -> Result<Option<Snippet>, DbError> {
130
    let conn = db.lock().map_err(|_| DbError::LockPoisoned)?;
131
    let rows_affected = conn.execute(
132
        "UPDATE snippets SET name = ?1, content = ?2 WHERE short_id = ?3",
133
        params![name, content, short_id],
134
    )?;
135
    if rows_affected == 0 {
136
        return Ok(None);
137
    }
138
    match conn.query_row(
139
        "SELECT id, short_id, content, name FROM snippets WHERE short_id = ?1",
140
        params![short_id],
141
        |row| {
142
            Ok(Snippet {
143
                id: row.get(0)?,
144
                short_id: row.get(1)?,
145
                content: row.get(2)?,
146
                name: row.get(3)?,
147
            })
148
        },
149
    ) {
150
        Ok(snippet) => Ok(Some(snippet)),
151
        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
152
        Err(e) => Err(DbError::Sqlite(e)),
153
    }
154
}