1use serde::Serialize;
12use serde_json::json;
13use sqlx::{PgConnection, PgPool};
14use uuid::Uuid;
15
16use crate::{
17 auth::Actor,
18 domain::Role,
19 error::{AppError, AppResult},
20 services::audit,
21 util,
22};
23
24#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
26pub struct Usage {
27 pub limit: Option<i64>,
28 pub used: i64,
29}
30
31pub(crate) async fn ensure_room(
38 conn: &mut PgConnection,
39 user_id: Uuid,
40 pending: i64,
41) -> AppResult<()> {
42 let user_limit: Option<Option<i64>> =
43 sqlx::query_scalar("SELECT storage_quota_bytes FROM users WHERE id = $1 FOR NO KEY UPDATE")
44 .bind(user_id)
45 .fetch_optional(&mut *conn)
46 .await?;
47 if let Some(Some(limit)) = user_limit {
48 let used = user_used(&mut *conn, user_id).await?;
49 check(limit, used, pending, "your storage quota")?;
50 }
51 let total: Option<i64> = sqlx::query_scalar("SELECT total_bytes FROM vault_quota")
54 .fetch_optional(&mut *conn)
55 .await?
56 .flatten();
57 if total.is_some() {
58 let limit: Option<i64> =
59 sqlx::query_scalar("SELECT total_bytes FROM vault_quota FOR UPDATE")
60 .fetch_optional(&mut *conn)
61 .await?
62 .flatten();
63 if let Some(limit) = limit {
64 let used = vault_used(&mut *conn).await?;
65 check(limit, used, pending, "the storage quota of the vault")?;
66 }
67 }
68 Ok(())
69}
70
71fn check(limit: i64, used: i64, pending: i64, what: &str) -> AppResult<()> {
73 let after = used.saturating_add(pending.max(0));
74 if after > limit {
75 return Err(AppError::PayloadTooLarge(format!(
76 "The upload exceeds {what}: {} of {} would be used. Ask an administrator for more \
77 space or remove old versions.",
78 util::format_bytes(after),
79 util::format_bytes(limit)
80 )));
81 }
82 Ok(())
83}
84
85async fn user_used(conn: &mut PgConnection, user_id: Uuid) -> AppResult<i64> {
86 Ok(sqlx::query_scalar(
87 "SELECT COALESCE(SUM(size_bytes), 0)::BIGINT FROM file_versions WHERE uploaded_by = $1",
88 )
89 .bind(user_id)
90 .fetch_one(&mut *conn)
91 .await?)
92}
93
94async fn vault_used(conn: &mut PgConnection) -> AppResult<i64> {
95 Ok(
96 sqlx::query_scalar("SELECT COALESCE(SUM(size_bytes), 0)::BIGINT FROM file_versions")
97 .fetch_one(&mut *conn)
98 .await?,
99 )
100}
101
102pub async fn user_usage(pool: &PgPool, user_id: Uuid) -> AppResult<Usage> {
104 let mut conn = pool.acquire().await?;
105 let limit: Option<i64> =
106 sqlx::query_scalar("SELECT storage_quota_bytes FROM users WHERE id = $1")
107 .bind(user_id)
108 .fetch_optional(&mut *conn)
109 .await?
110 .ok_or_else(|| AppError::NotFound("User not found.".into()))?;
111 Ok(Usage {
112 limit,
113 used: user_used(&mut conn, user_id).await?,
114 })
115}
116
117pub async fn vault_usage(pool: &PgPool) -> AppResult<Usage> {
119 let mut conn = pool.acquire().await?;
120 let limit: Option<i64> = sqlx::query_scalar("SELECT total_bytes FROM vault_quota")
121 .fetch_optional(&mut *conn)
122 .await?
123 .flatten();
124 Ok(Usage {
125 limit,
126 used: vault_used(&mut conn).await?,
127 })
128}
129
130fn validate_limit(limit: Option<i64>) -> AppResult<()> {
131 if limit.is_some_and(|l| l < 0) {
132 return Err(AppError::BadRequest(
133 "A storage quota cannot be negative.".into(),
134 ));
135 }
136 Ok(())
137}
138
139fn audit_actor(actor: &Actor) -> Option<&Actor> {
142 Some(actor).filter(|a| !a.id.is_nil())
143}
144
145fn describe(limit: Option<i64>) -> String {
146 limit.map_or_else(|| "no limit".to_string(), util::format_bytes)
147}
148
149pub async fn set_user_quota(
153 pool: &PgPool,
154 actor: &Actor,
155 user_id: Uuid,
156 limit: Option<i64>,
157) -> AppResult<()> {
158 actor.require(Role::Admin)?;
159 validate_limit(limit)?;
160 let mut tx = pool.begin().await?;
161 let username: String = sqlx::query_scalar(
162 "UPDATE users SET storage_quota_bytes = $2 WHERE id = $1 RETURNING username",
163 )
164 .bind(user_id)
165 .bind(limit)
166 .fetch_optional(&mut *tx)
167 .await?
168 .ok_or_else(|| AppError::NotFound("User not found.".into()))?;
169 audit::record(
170 &mut tx,
171 audit_actor(actor),
172 "user.quota",
173 "user",
174 Some(user_id),
175 format!("Storage quota of {username} set to {}", describe(limit)),
176 json!({ "storage_quota_bytes": limit }),
177 )
178 .await?;
179 tx.commit().await?;
180 Ok(())
181}
182
183pub async fn set_vault_quota(pool: &PgPool, actor: &Actor, limit: Option<i64>) -> AppResult<()> {
186 actor.require(Role::Admin)?;
187 validate_limit(limit)?;
188 let mut tx = pool.begin().await?;
189 sqlx::query(
190 "INSERT INTO vault_quota (id, total_bytes) VALUES (TRUE, $1)
191 ON CONFLICT (id) DO UPDATE SET total_bytes = EXCLUDED.total_bytes",
192 )
193 .bind(limit)
194 .execute(&mut *tx)
195 .await?;
196 audit::record(
197 &mut tx,
198 audit_actor(actor),
199 "vault.quota",
200 "vault",
201 None,
202 format!("Storage quota of the vault set to {}", describe(limit)),
203 json!({ "total_bytes": limit }),
204 )
205 .await?;
206 tx.commit().await?;
207 Ok(())
208}
209
210pub fn parse_limit_mb(raw: &str) -> Result<Option<i64>, String> {
212 let raw = raw.trim();
213 if raw.eq_ignore_ascii_case("none") || raw.eq_ignore_ascii_case("unlimited") {
214 return Ok(None);
215 }
216 let mb: i64 = raw
217 .parse()
218 .map_err(|_| format!("'{raw}' is neither a number of MiB nor 'none'"))?;
219 if mb < 0 {
220 return Err("a storage quota cannot be negative".into());
221 }
222 mb.checked_mul(1024 * 1024)
223 .map(Some)
224 .ok_or_else(|| format!("{mb} MiB is too large"))
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn limit_is_checked_after_adding_the_pending_bytes() {
233 assert!(check(100, 60, 40, "q").is_ok(), "exactly at the limit");
234 assert!(check(100, 60, 41, "q").is_err());
235 assert!(check(100, 101, 0, "q").is_err(), "already over");
236 assert!(check(100, 100, 0, "q").is_ok());
237 assert!(
238 check(100, 10, -50, "q").is_ok(),
239 "negative pending is ignored"
240 );
241 assert!(check(i64::MAX, i64::MAX, 1, "q").is_ok(), "saturates");
242 match check(1024, 2048, 0, "your storage quota") {
243 Err(AppError::PayloadTooLarge(m)) => assert!(m.contains("your storage quota"), "{m}"),
244 other => panic!("{other:?}"),
245 }
246 }
247
248 #[test]
249 fn cli_limits() {
250 assert_eq!(parse_limit_mb("none"), Ok(None));
251 assert_eq!(parse_limit_mb(" Unlimited "), Ok(None));
252 assert_eq!(parse_limit_mb("0"), Ok(Some(0)));
253 assert_eq!(parse_limit_mb("10"), Ok(Some(10 * 1024 * 1024)));
254 assert!(parse_limit_mb("-1").is_err());
255 assert!(parse_limit_mb("ten").is_err());
256 assert!(parse_limit_mb(&i64::MAX.to_string()).is_err());
257 }
258
259 #[test]
260 fn negative_limits_are_refused() {
261 assert!(validate_limit(Some(-1)).is_err());
262 assert!(validate_limit(Some(0)).is_ok());
263 assert!(validate_limit(None).is_ok());
264 assert_eq!(describe(None), "no limit");
265 }
266}