1use flams_utils::prelude::HSet;
2use ftml_uris::ArchiveId;
3use gitlab::api::AsyncQuery;
4use tracing::{instrument, Instrument};
5
6pub mod auth;
7
8static GITLAB: std::sync::LazyLock<GLInstance> = std::sync::LazyLock::new(GLInstance::default);
9
10#[derive(Debug)]
11struct ProjectWithId {
12 pub project: flams_backend_types::git::Project,
13 #[allow(clippy::option_option)]
14 pub id: Option<Option<ArchiveId>>,
15}
16impl std::borrow::Borrow<u64> for ProjectWithId {
17 #[inline]
18 fn borrow(&self) -> &u64 {
19 &self.project.id
20 }
21}
22impl std::hash::Hash for ProjectWithId {
23 #[inline]
24 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
25 self.project.id.hash(state);
26 }
27}
28impl PartialEq for ProjectWithId {
29 #[inline]
30 fn eq(&self, other: &Self) -> bool {
31 self.project.id == other.project.id
32 }
33}
34impl Eq for ProjectWithId {}
35
36#[derive(Debug)]
37struct GitLabI {
38 inner: gitlab::AsyncGitlab,
39 url: url::Url,
40 id: Option<Box<str>>,
41 secret: Option<Box<str>>,
42 projects: parking_lot::Mutex<HSet<ProjectWithId>>,
43}
44
45#[derive(Debug, Clone)]
46pub struct GitLab(std::sync::Arc<GitLabI>);
47
48#[derive(Debug, Clone, Default)]
49enum MaybeGitlab {
50 #[default]
51 None,
52 Loading,
53 Loaded(GitLab),
54 Failed,
55}
56#[derive(Clone, Debug, Default)]
57pub struct GLInstance {
58 inner: std::sync::Arc<parking_lot::RwLock<MaybeGitlab>>,
59}
60
61impl GLInstance {
62 #[inline]
63 #[must_use]
64 pub fn global() -> &'static Self {
65 &GITLAB
66 }
67 pub fn load(self, cfg: GitlabConfig) {
68 *self.inner.write() = MaybeGitlab::Loading;
69 let span = tracing::info_span!(target:"git","loading gitlab");
70 tokio::spawn(
71 async move {
72 match GitLab::new(cfg).in_current_span().await {
73 Ok(gl) => {
74 *self.inner.write() = MaybeGitlab::Loaded(gl.clone());
75 let Ok(ps) = gl.get_projects().in_current_span().await else {
76 tracing::error!("Failed to load projects");
77 return;
78 };
79 tracing::info!("Loaded {} projects", ps.len());
80 let span2 = tracing::info_span!("loading archive IDs");
81 let mut js = tokio::task::JoinSet::new();
82 for p in ps {
83 if let Some(d) = p.default_branch {
84 let gl = gl.clone();
85 let span = span2.clone();
86 let f = async move {
87 gl.get_archive_id(p.id, &d).instrument(span).await
88 };
89 let _ = js.spawn(f);
90 }
91 }
92 let _ = js.join_all().in_current_span().await;
93 }
94 Err(e) => {
95 tracing::error!("Failed to load gitlab: {e}");
96 *self.inner.write() = MaybeGitlab::Failed;
97 }
98 }
99 }
100 .instrument(span),
101 );
102 }
103
104 pub async fn get(&self) -> Option<GitLab> {
105 loop {
106 match &*self.inner.read() {
107 MaybeGitlab::None | MaybeGitlab::Failed => return None,
108 MaybeGitlab::Loading => (),
109 MaybeGitlab::Loaded(gl) => return Some(gl.clone()),
110 }
111 tokio::time::sleep(std::time::Duration::from_secs_f32(0.1)).await;
112 }
113 }
114
115 #[inline]
116 #[must_use]
117 pub fn exists(&self) -> bool {
118 !matches!(&*self.inner.read(), MaybeGitlab::None)
119 }
120
121 #[inline]
122 #[must_use]
123 pub fn has_loaded(&self) -> bool {
124 matches!(&*self.inner.read(), MaybeGitlab::Loaded(_))
125 }
126}
127
128#[derive(Debug, Clone)]
129pub struct GitlabConfig {
130 url: url::Url,
131 token: Option<String>,
132 app_id: Option<String>,
133 app_secret: Option<String>,
134}
135impl GitlabConfig {
136 #[inline]
137 #[must_use]
138 pub const fn new(
139 url: url::Url,
140 token: Option<String>,
141 app_id: Option<String>,
142 app_secret: Option<String>,
143 ) -> Self {
144 Self {
145 url,
146 token,
147 app_id,
148 app_secret,
149 }
150 }
151
152 }
162
163impl GitLab {
164 pub async fn new(cfg: GitlabConfig) -> Result<Self, gitlab::GitlabError> {
166 let GitlabConfig {
167 url,
168 token,
169 app_id,
170 app_secret,
171 } = cfg;
172 let Some(url_str) = url.host_str() else {
174 return Err(gitlab::GitlabError::UrlParse {
175 source: url::ParseError::EmptyHost,
176 });
177 };
178 let mut builder = token.map_or_else(
179 || gitlab::GitlabBuilder::new_unauthenticated(url_str),
180 |token| gitlab::GitlabBuilder::new(url_str, token),
181 );
182 if matches!(url.scheme(), "http") {
183 builder.insecure();
184 }
185 Ok(Self(std::sync::Arc::new(GitLabI {
186 inner: builder.build_async().in_current_span().await?,
187 url,
188 id: app_id.map(Into::into),
189 secret: app_secret.map(Into::into),
190 projects: parking_lot::Mutex::new(HSet::default()),
191 })))
192 }
193
194 #[must_use]
195 pub fn new_background(cfg: GitlabConfig) -> GLInstance {
196 let r = GLInstance {
197 inner: std::sync::Arc::new(parking_lot::RwLock::new(MaybeGitlab::Loading)),
198 };
199 r.clone().load(cfg);
200 r
201 }
202
203 #[instrument(
205 level = "debug",
206 target = "git",
207 name = "getting all gitlab projects",
208 skip_all
209 )]
210 pub async fn get_projects(&self) -> Result<Vec<flams_backend_types::git::Project>, Err> {
211 use gitlab::api::AsyncQuery;
212 let q = gitlab::api::projects::Projects::builder()
213 .simple(true)
214 .build()
215 .unwrap_or_else(|_| unreachable!());
216 let v: Vec<flams_backend_types::git::Project> =
217 gitlab::api::paged(q, gitlab::api::Pagination::All)
218 .query_async(&self.0.inner)
219 .await
220 .map_err(|e| {
221 tracing::error!("Failed to load projects: {e}");
222 e
223 })?;
224 let mut prs = self.0.projects.lock();
225 for p in &v {
226 if !prs.contains(&p.id) {
227 prs.insert(ProjectWithId {
228 project: p.clone(),
229 id: None,
230 });
231 }
232 }
233 drop(prs);
234 Ok(v)
235 }
238
239 #[instrument(
241 level = "debug",
242 target = "git",
243 name = "getting archive id",
244 skip(self)
245 )]
246 pub async fn get_archive_id(&self, id: u64, branch: &str) -> Result<Option<ArchiveId>, Err> {
247 {
248 let vs = self.0.projects.lock();
249 if let Some(ProjectWithId { id: Some(id), .. }) = vs.get(&id) {
250 return Ok(id.clone());
251 }
252 }
253
254 macro_rules! ret {
255 ($v:expr) => {{
256 tracing::info!("Found {:?}", $v);
257 let mut lock = self.0.projects.lock();
258 if let Some(mut v) = lock.take(&id) {
259 v.id = Some($v.clone());
260 lock.insert(v);
261 }
262 drop(lock);
263 return Ok($v);
264 }};
265 }
266 let r = gitlab::api::projects::repository::TreeBuilder::default()
267 .project(id)
268 .ref_(branch)
269 .recursive(false)
270 .build()
271 .unwrap_or_else(|_| unreachable!());
272 let r: Vec<flams_backend_types::git::TreeEntry> = r.query_async(&self.0.inner).await?;
273 let Some(p) = r.into_iter().find_map(|e| {
274 if e.path.eq_ignore_ascii_case("meta-inf")
275 && matches!(e.tp, flams_backend_types::git::DirOrFile::Dir)
276 {
277 Some(e.path)
278 } else {
279 None
280 }
281 }) else {
282 ret!(None::<ArchiveId>)
283 };
284
285 let r = gitlab::api::projects::repository::TreeBuilder::default()
286 .project(id)
287 .ref_(branch)
288 .path(p)
289 .recursive(false)
290 .build()
291 .unwrap_or_else(|_| unreachable!());
292 let r: Vec<flams_backend_types::git::TreeEntry> = r.query_async(&self.0.inner).await?;
293 let Some(p) = r.into_iter().find_map(|e| {
294 if e.name.eq_ignore_ascii_case("manifest.mf")
295 && matches!(e.tp, flams_backend_types::git::DirOrFile::File)
296 {
297 Some(e.path)
298 } else {
299 None
300 }
301 }) else {
302 ret!(None::<ArchiveId>)
303 };
304
305 let blob = gitlab::api::projects::repository::files::FileRaw::builder()
306 .project(id)
307 .file_path(p)
308 .ref_(branch)
309 .build()
310 .unwrap_or_else(|_| unreachable!());
311 let r = gitlab::api::raw(blob).query_async(&self.0.inner).await?;
312 let r = std::str::from_utf8(&r)?;
313 let r = r.split('\n').find_map(|line| {
314 let line = line.trim();
315 line.strip_prefix("id:")
316 .and_then(|rest| ArchiveId::new(rest.trim()).ok())
317 });
318 ret!(r)
319 }
320}
321
322#[derive(Debug)]
323pub enum Err {
324 Api(gitlab::api::ApiError<gitlab::RestError>),
325 Str(std::str::Utf8Error),
326 Gitlab(gitlab::GitlabError),
327 Other(String),
328}
329impl From<gitlab::api::ApiError<gitlab::RestError>> for Err {
330 #[inline]
331 fn from(e: gitlab::api::ApiError<gitlab::RestError>) -> Self {
332 Self::Api(e)
333 }
334}
335impl From<std::str::Utf8Error> for Err {
336 #[inline]
337 fn from(e: std::str::Utf8Error) -> Self {
338 Self::Str(e)
339 }
340}
341impl From<gitlab::GitlabError> for Err {
342 #[inline]
343 fn from(e: gitlab::GitlabError) -> Self {
344 Self::Gitlab(e)
345 }
346}
347impl std::fmt::Display for Err {
348 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
349 match self {
350 Self::Api(e) => e.fmt(f),
351 Self::Str(e) => e.fmt(f),
352 Self::Gitlab(e) => e.fmt(f),
353 Self::Other(s) => s.fmt(f),
354 }
355 }
356}