anvilsign in

collin/anvil

main / crates / anvil-core / src / usage.rs
1//! Disk-usage accounting for the admin dashboard.
2//!
3//! Walks the on-disk stores (bare repositories, CI artifacts, uploaded
4//! attachments) and attributes each repository's bytes to its owner, broken
5//! down by content type. Sizes are actual on-disk bytes, so they include git
6//! packs, artifact tarballs, etc. — not just logical row sizes.
7
8use std::collections::BTreeMap;
9
10use serde::{
11 Deserialize,
12 Serialize,
13};
14
15use crate::{
16 App,
17 error::Result,
18 models::{
19 Repository,
20 User,
21 },
22 storage,
23};
24
25/// One user's disk usage, split by content type (bytes).
26#[derive(Clone, Debug, Default, Deserialize, Serialize)]
27pub struct UserUsage {
28 pub username: String,
29 pub git: u64,
30 pub artifacts: u64,
31 pub attachments: u64,
32}
33
34impl UserUsage {
35 pub fn total(&self) -> u64 {
36 self.git + self.artifacts + self.attachments
37 }
38}
39
40/// Site-wide disk usage: per-user rows (largest first) plus column totals.
41#[derive(Clone, Debug, Default, Deserialize, Serialize)]
42pub struct Usage {
43 pub per_user: Vec<UserUsage>,
44 pub git: u64,
45 pub artifacts: u64,
46 pub attachments: u64,
47}
48
49impl Usage {
50 pub fn total(&self) -> u64 {
51 self.git + self.artifacts + self.attachments
52 }
53}
54
55/// Compute site-wide disk usage by walking every repository's on-disk stores.
56/// O(files on disk) — intended for an on-demand admin page, not a hot path.
57pub async fn compute(app: &App) -> Result<Usage> {
58 let repos_dir = app.config.repositories_dir();
59 let artifacts_dir = app.config.artifacts_dir();
60 let attachments_dir = app.config.attachments_dir();
61
62 let mut conn = app.db.clone();
63 let users = User::all().exec(&mut conn).await?;
64 // Seed a row per user so accounts with no repos still appear (at zero).
65 let mut by_user: BTreeMap<i64, UserUsage> = users
66 .iter()
67 .map(|u| {
68 (
69 u.id,
70 UserUsage {
71 username: u.username.clone(),
72 ..Default::default()
73 },
74 )
75 })
76 .collect();
77
78 let mut conn = app.db.clone();
79 let repos = Repository::all().exec(&mut conn).await?;
80 let mut usage = Usage::default();
81 for repo in repos {
82 // Skip repos whose owner row is gone (shouldn't happen); their bytes
83 // can't be attributed to a user.
84 let Some(entry) = by_user.get_mut(&repo.owner_id) else {
85 continue;
86 };
87 let git = storage::dir_size(&storage::repo_path(&repos_dir, &entry.username, &repo.name));
88 let artifacts = storage::dir_size(&artifacts_dir.join(repo.id.to_string()));
89 let attachments = storage::dir_size(&attachments_dir.join(repo.id.to_string()));
90
91 entry.git += git;
92 entry.artifacts += artifacts;
93 entry.attachments += attachments;
94 usage.git += git;
95 usage.artifacts += artifacts;
96 usage.attachments += attachments;
97 }
98
99 let mut per_user: Vec<UserUsage> = by_user.into_values().collect();
100 per_user.sort_by(|a, b| b.total().cmp(&a.total()).then(a.username.cmp(&b.username)));
101 usage.per_user = per_user;
102 Ok(usage)
103}