Skip to content

Commit af3f6c4

Browse files
chore: add template file scripts/gitlab-storage-report.py [skip ci]
1 parent 45d1021 commit af3f6c4

1 file changed

Lines changed: 99 additions & 0 deletions

File tree

scripts/gitlab-storage-report.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Report storage for all projects listed in config/gitlab-subgroups.yml.
4+
Fetches each project individually (token needs read access per project).
5+
"""
6+
import os, json, urllib.request, urllib.error
7+
8+
token = os.environ["GITLAB_TOKEN"]
9+
api = os.environ.get("GL_API", "https://gitlab.com/api/v4")
10+
11+
# Read subgroups config
12+
import sys
13+
sys.path.insert(0, os.path.dirname(__file__))
14+
15+
config_path = os.path.join(os.path.dirname(__file__), "..", "config", "gitlab-subgroups.yml")
16+
17+
# Parse YAML manually (no PyYAML on runner by default — use simple line parser)
18+
subgroups = {}
19+
current_sg = None
20+
current_path = None
21+
in_repos = False
22+
23+
with open(config_path) as f:
24+
for line in f:
25+
stripped = line.rstrip()
26+
if not stripped or stripped.startswith("#"):
27+
continue
28+
indent = len(line) - len(line.lstrip())
29+
content = stripped.strip()
30+
31+
if indent == 2 and content.endswith(":") and not content.startswith("-"):
32+
current_sg = content[:-1]
33+
subgroups[current_sg] = {"path": None, "repos": []}
34+
in_repos = False
35+
elif indent == 4 and content.startswith("path:"):
36+
if current_sg:
37+
subgroups[current_sg]["path"] = content.split("path:", 1)[1].strip()
38+
elif indent == 4 and content == "repos:":
39+
in_repos = True
40+
elif indent == 6 and content.startswith("- ") and in_repos and current_sg:
41+
subgroups[current_sg]["repos"].append(content[2:].strip())
42+
elif indent == 4 and not content.startswith("-"):
43+
in_repos = False
44+
45+
def gl_get(path):
46+
req = urllib.request.Request(f"{api}{path}",
47+
headers={"PRIVATE-TOKEN": token, "User-Agent": "fork-sync-all"})
48+
try:
49+
with urllib.request.urlopen(req) as r:
50+
return json.load(r)
51+
except urllib.error.HTTPError as e:
52+
return {"_error": e.code}
53+
54+
def fmt(b):
55+
if b >= 1073741824: return f"{b/1073741824:.1f}G"
56+
if b >= 1048576: return f"{b/1048576:.1f}M"
57+
if b >= 1024: return f"{b/1024:.1f}K"
58+
return f"{b}B"
59+
60+
results = []
61+
for sg_name, sg in subgroups.items():
62+
gl_path = sg.get("path", "")
63+
for repo in sg.get("repos", []):
64+
encoded = (gl_path + "/" + repo).replace("/", "%2F")
65+
p = gl_get(f"/projects/{encoded}?statistics=true")
66+
if "_error" in p:
67+
print(f" SKIP {gl_path}/{repo}: HTTP {p['_error']}", flush=True)
68+
continue
69+
s = p.get("statistics", {})
70+
repo_sz = s.get("repository_size", 0)
71+
lfs_sz = s.get("lfs_objects_size", 0)
72+
art_sz = s.get("job_artifacts_size", 0)
73+
total = s.get("storage_size", 0)
74+
full_path = p.get("path_with_namespace", f"{gl_path}/{repo}")
75+
results.append((full_path, repo_sz, lfs_sz, art_sz, total))
76+
print(f" {full_path}: repo={fmt(repo_sz)} lfs={fmt(lfs_sz)} art={fmt(art_sz)} total={fmt(total)}", flush=True)
77+
78+
results.sort(key=lambda x: x[1], reverse=True)
79+
80+
print()
81+
hdr = f"{'Project':<70} {'Repo':>10} {'LFS':>10} {'Artifacts':>10} {'Total':>10}"
82+
print(hdr)
83+
print("=" * len(hdr))
84+
total_repo = total_lfs = total_art = total_all = 0
85+
for name, repo, lfs, art, total in results:
86+
n = name if len(name) <= 68 else name[:65] + "..."
87+
print(f"{n:<70} {fmt(repo):>10} {fmt(lfs):>10} {fmt(art):>10} {fmt(total):>10}")
88+
total_repo += repo; total_lfs += lfs; total_art += art; total_all += total
89+
print("=" * len(hdr))
90+
print(f"{'TOTAL':<70} {fmt(total_repo):>10} {fmt(total_lfs):>10} {fmt(total_art):>10} {fmt(total_all):>10}")
91+
92+
# Flag anything over 1 GiB repository storage
93+
print("\nProjects over 1 GiB repository storage:")
94+
over = [(n, r) for n, r, *_ in results if r >= 1073741824]
95+
if over:
96+
for n, r in over:
97+
print(f" {n}: {fmt(r)}")
98+
else:
99+
print(" None")

0 commit comments

Comments
 (0)