| 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 | |
| 8 | use std::collections::BTreeMap; |
| 9 | |
| 10 | use serde::{ |
| 11 | Deserialize, |
| 12 | Serialize, |
| 13 | }; |
| 14 | |
| 15 | use 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)] |
| 27 | pub struct UserUsage { |
| 28 | pub username: String, |
| 29 | pub git: u64, |
| 30 | pub artifacts: u64, |
| 31 | pub attachments: u64, |
| 32 | } |
| 33 | |
| 34 | impl 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)] |
| 42 | pub struct Usage { |
| 43 | pub per_user: Vec<UserUsage>, |
| 44 | pub git: u64, |
| 45 | pub artifacts: u64, |
| 46 | pub attachments: u64, |
| 47 | } |
| 48 | |
| 49 | impl 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. |
| 57 | pub 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 | } |