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#[non_exhaustive]
9#[derive(Clone, Copy, Debug)]
10pub struct WorkspaceOptions {
11 pub max_bytes: u64,
13 pub max_nodes: u64,
15 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31pub struct Workspace {
32 pub id: WorkspaceId,
34 pub created_at_ms: i64,
36 pub updated_at_ms: i64,
38 pub max_bytes: u64,
40 pub max_nodes: u64,
42 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}