Skip to main content

fslite_sqlite/
workspace.rs

1use fslite_core::{FsError, FsResult, NodeId, WorkspaceId, WorkspaceUsage};
2use rusqlite::{OptionalExtension, params};
3use tokio_rusqlite::Connection;
4
5use crate::db::{self, now_ms};
6
7/// Configurable per-workspace resource limits.
8#[non_exhaustive]
9#[derive(Clone, Copy, Debug)]
10pub struct WorkspaceOptions {
11    /// The maximum total logical bytes the workspace may hold.
12    pub max_bytes: u64,
13    /// The maximum number of active nodes the workspace may hold.
14    pub max_nodes: u64,
15    /// The maximum logical size of a single regular file.
16    pub max_file_bytes: u64,
17}
18
19impl Default for WorkspaceOptions {
20    fn default() -> Self {
21        Self {
22            max_bytes: 10 * 1024 * 1024 * 1024,
23            max_nodes: 1_000_000,
24            max_file_bytes: 1024 * 1024 * 1024,
25        }
26    }
27}
28
29/// A created workspace and its configured limits.
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31pub struct Workspace {
32    /// The stable identity of the workspace.
33    pub id: WorkspaceId,
34    /// The Unix timestamp in milliseconds when the workspace was created.
35    pub created_at_ms: i64,
36    /// The Unix timestamp in milliseconds when the workspace was last updated.
37    pub updated_at_ms: i64,
38    /// The configured logical-byte quota.
39    pub max_bytes: u64,
40    /// The configured node-count quota.
41    pub max_nodes: u64,
42    /// The configured maximum size of one regular file.
43    pub max_file_bytes: u64,
44}
45
46pub(crate) async fn create_workspace(
47    conn: &Connection,
48    options: WorkspaceOptions,
49) -> FsResult<Workspace> {
50    conn.call(move |conn| {
51        let workspace_id = WorkspaceId::new();
52        let root_id = NodeId::new();
53        let now = now_ms();
54
55        let tx = conn.transaction()?;
56        tx.execute(
57            "INSERT INTO workspaces(id, created_at_ms, updated_at_ms, change_seq, max_bytes, max_nodes, max_file_bytes) \
58             VALUES (?1, ?2, ?2, 0, ?3, ?4, ?5)",
59            params![
60                workspace_id.to_string(),
61                now,
62                options.max_bytes as i64,
63                options.max_nodes as i64,
64                options.max_file_bytes as i64,
65            ],
66        )?;
67        tx.execute(
68            "INSERT INTO nodes(id, workspace_id, parent_id, name, kind, size, revision, created_at_ms, modified_at_ms, accessed_at_ms) \
69             VALUES (?1, ?2, NULL, '', 0, 0, 1, ?3, ?3, ?3)",
70            params![root_id.to_string(), workspace_id.to_string(), now],
71        )?;
72        tx.commit()?;
73
74        Ok(Workspace {
75            id: workspace_id,
76            created_at_ms: now,
77            updated_at_ms: now,
78            max_bytes: options.max_bytes,
79            max_nodes: options.max_nodes,
80            max_file_bytes: options.max_file_bytes,
81        })
82    })
83    .await
84    .map_err(db::map_call_error)
85}
86
87pub(crate) async fn delete_workspace(conn: &Connection, workspace_id: WorkspaceId) -> FsResult<()> {
88    let workspace_id_str = workspace_id.to_string();
89    conn.call(move |conn| {
90        conn.execute(
91            "DELETE FROM workspaces WHERE id = ?1",
92            params![workspace_id_str],
93        )?;
94        Ok(())
95    })
96    .await
97    .map_err(db::map_call_error)
98}
99
100struct RawUsage {
101    active_logical_bytes: i64,
102    trashed_logical_bytes: i64,
103    staged_bytes: i64,
104    active_nodes: i64,
105    trashed_nodes: i64,
106    max_bytes: i64,
107    max_nodes: i64,
108    max_file_bytes: i64,
109}
110
111impl RawUsage {
112    fn into_usage(self, workspace_id: WorkspaceId) -> WorkspaceUsage {
113        WorkspaceUsage {
114            workspace_id,
115            active_logical_bytes: self.active_logical_bytes as u64,
116            trashed_logical_bytes: self.trashed_logical_bytes as u64,
117            staged_bytes: self.staged_bytes as u64,
118            active_nodes: self.active_nodes as u64,
119            trashed_nodes: self.trashed_nodes as u64,
120            max_logical_bytes: self.max_bytes as u64,
121            max_nodes: self.max_nodes as u64,
122            max_file_bytes: self.max_file_bytes as u64,
123        }
124    }
125}
126
127fn fetch_usage(
128    conn: &rusqlite::Connection,
129    workspace_id: &str,
130) -> rusqlite::Result<Option<RawUsage>> {
131    let workspace_row: Option<(i64, i64, i64)> = conn
132        .query_row(
133            "SELECT max_bytes, max_nodes, max_file_bytes FROM workspaces WHERE id = ?1",
134            params![workspace_id],
135            |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
136        )
137        .optional()?;
138
139    let Some((max_bytes, max_nodes, max_file_bytes)) = workspace_row else {
140        return Ok(None);
141    };
142
143    let (active_logical_bytes, active_nodes): (i64, i64) = conn.query_row(
144        "SELECT COALESCE(SUM(size), 0), COUNT(*) FROM nodes \
145         WHERE workspace_id = ?1 AND trashed_at_ms IS NULL",
146        params![workspace_id],
147        |row| Ok((row.get(0)?, row.get(1)?)),
148    )?;
149
150    let (trashed_logical_bytes, trashed_nodes): (i64, i64) = conn.query_row(
151        "SELECT COALESCE(SUM(size), 0), COUNT(*) FROM nodes \
152         WHERE workspace_id = ?1 AND trashed_at_ms IS NOT NULL",
153        params![workspace_id],
154        |row| Ok((row.get(0)?, row.get(1)?)),
155    )?;
156
157    let staged_bytes: i64 = conn.query_row(
158        "SELECT COALESCE(SUM(length), 0) FROM content_generations \
159         WHERE workspace_id = ?1 AND complete = 0",
160        params![workspace_id],
161        |row| row.get(0),
162    )?;
163
164    Ok(Some(RawUsage {
165        active_logical_bytes,
166        trashed_logical_bytes,
167        staged_bytes,
168        active_nodes,
169        trashed_nodes,
170        max_bytes,
171        max_nodes,
172        max_file_bytes,
173    }))
174}
175
176pub(crate) async fn workspace_usage(
177    conn: &Connection,
178    workspace_id: WorkspaceId,
179) -> FsResult<WorkspaceUsage> {
180    let workspace_id_str = workspace_id.to_string();
181    let raw = conn
182        .call(move |conn| Ok(fetch_usage(conn, &workspace_id_str)?))
183        .await
184        .map_err(db::map_call_error)?;
185
186    raw.map(|usage| usage.into_usage(workspace_id))
187        .ok_or_else(|| FsError::not_found(workspace_id))
188}