aboutsummaryrefslogtreecommitdiff
path: root/src/shared.rs
blob: bbca537fe5e21d58121ad299e8766686cb77f2d6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
use std::ffi::{OsStr, OsString};
#[cfg(target_family="unix")]
use std::os::unix::ffi::OsStrExt;
#[cfg(target_family="windows")]
use std::os::windows::ffi::OsStrExt;
use std::sync::Mutex;
use std::path::Path;

use rusqlite::{Connection, params};

// in some configurations (`main.rs`), nothing actually uses id.
#[allow(dead_code)]
pub struct Project {
    pub id: u32,
    pub name: String,
    pub dir: OsString,
    pub host: Option<String>,
    pub info: Option<String>,
    pub upstream: Option<String>,
}

impl Project {
    pub fn is_here(&self) -> bool {
        let uname = rustix::system::uname();
        let me = uname.nodename();
        if let Some(host) = self.host.as_ref() {
            host.as_bytes() == me.to_bytes()
        } else {
            std::path::Path::new(&self.dir).exists()
        }
    }
}

pub struct DbCtx {
    pub conn: Mutex<Connection>,
}

impl DbCtx {
    pub fn new<P: AsRef<Path>>(db_path: P) -> Result<Self, String> {
        let conn = Connection::open(db_path)
            .map_err(|e| format!("failed to open db: {e}"))?;

        Ok(Self {
            conn: Mutex::new(conn)
        })
    }

    pub fn init<P: AsRef<Path>>(db_path: P) -> Result<(), String> {
        let conn = Connection::open(db_path)
            .map_err(|e| format!("failed to open db: {e}"))?;

        conn.execute(
            "CREATE TABLE projects (\
              id INTEGER PRIMARY KEY AUTOINCREMENT, \
              name TEXT UNIQUE NOT NULL, \
              dir BLOB NOT NULL, \
              host TEXT, \
              info TEXT, \
              upstream TEXT\
            );", params![])
            .map_err(|e| format!("failed to create projects table: {e}"))?;

        conn.execute(
            "CREATE INDEX projname on projects(name);", params![])
            .map_err(|e| format!("failed to create index?!: {e}"))?;

        Ok(())
    }

    pub fn project_add(&self, p: Project) -> Result<(), String> {
        let conn = self.conn.lock().unwrap();
        conn
            .execute(
                "insert into projects \
                    (name, dir, host, info, upstream) \
                    values (?1, ?2, ?3, ?4, ?5);",/* \
                    on conflict (name) do update \
                        set dir=excluded.dir \
                            info=excluded.info \
                            upstream=excluded.upstream;",*/
                params![p.name, p.dir.as_os_str().as_bytes(), p.host, p.info, p.upstream]
            )
            .map_err(|e| format!("failed to insert project: {e}"))?;
        Ok(())
    }

    pub fn project_update(&self, p: &Project) -> Result<(), String> {
        let conn = self.conn.lock().unwrap();
        // TODO: don't panic on name conflict..
        conn
            .execute(
                "update projects \
                    set name=?2, \
                        dir=?3, \
                        host=?4, \
                        info=?5, \
                        upstream=?6 \
                    where id=?1",
                params![p.id.to_string(), p.name, p.dir.as_os_str().as_bytes(), p.host, p.info, p.upstream]
            )
            .map_err(|e| format!("failed to update project: {e}"))?;
        Ok(())
    }

    pub fn project_forget(&self, name: &str) -> Result<(), String> {
        let conn = self.conn.lock().unwrap();
        conn
            .execute(
                "delete from projects where name=?1;",
                params![name]
            )
            .map_err(|e| format!("failed to delete project: {e}"))?;
        Ok(())
    }

    pub fn for_each_project(&self, op: impl Fn(Project)) -> Result<(), String> {
        let conn = self.conn.lock().unwrap();
        let mut stmt = conn.prepare("select * from projects").expect("can prepare");
        let mut rows = stmt.query([])
            .map_err(|e| format!("failed to select projects: {e}"))?;

        while let Some(row) = rows.next().expect("can read row") {
            let id: u32 = row.get(0).expect("typecheck");
            let dir: Box<[u8]> = row.get(2).expect("typecheck");
            let osstr: &OsStr = OsStrExt::from_bytes(&dir);
            let project = Project {
                id,
                name: row.get(1).expect("typecheck"),
                dir: osstr.to_os_string(),
                host: row.get(3).expect("typecheck"),
                info: row.get(4).expect("typecheck"),
                upstream: row.get(5).expect("typecheck"),
            };

            op(project);
        }
        Ok(())
    }

    pub fn project_by_name(&self, name: &str) -> Result<Option<Project>, String> {
        let conn = self.conn.lock().unwrap();
        let mut stmt = conn.prepare("select id, name, dir, host, info, upstream from projects where name=?1")
            .expect("can prepare");
        let mut rows = stmt.query([name])
            .map_err(|e| format!("failed to find projects: {e}"))?;

        let project = if let Some(row) = rows.next().expect("can read row") {
            let id: u32 = row.get(0).expect("typecheck");
            let dir: Box<[u8]> = row.get(2).expect("typecheck");
            let osstr: &OsStr = OsStrExt::from_bytes(&dir);
            Some(Project {
                id,
                name: row.get(1).expect("typecheck"),
                dir: osstr.to_os_string(),
                host: row.get(3).expect("typecheck"),
                info: row.get(4).expect("typecheck"),
                upstream: row.get(5).expect("typecheck"),
            })
        } else {
            None
        };

        assert!(rows.next().expect("can try getting next row").is_none());

        Ok(project)
    }
}