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