anvilsign in

collin/anvil

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