git_mirror/provider/
gitlab.rs1use log::{debug, error, trace, warn};
9
10use reqwest::StatusCode;
11use reqwest::blocking::Client;
12use reqwest::header::{HeaderMap, HeaderValue};
13
14use crate::provider::{Desc, Mirror, MirrorError, MirrorResult, Provider};
15
16#[derive(Debug)]
17pub struct GitLab {
18 pub url: String,
19 pub group: String,
20 pub use_http: bool,
21 pub private_token: Option<String>,
22 pub recursive: bool,
23}
24
25#[derive(Deserialize, Debug, Clone)]
27struct Project {
28 description: String,
29 web_url: String,
30 ssh_url_to_repo: String,
31 http_url_to_repo: String,
32}
33
34#[derive(Deserialize, Debug, Clone)]
36struct Group {
37 id: u64,
38}
39
40const PER_PAGE: u8 = 100;
42
43impl GitLab {
44 fn get_paged<T: serde::de::DeserializeOwned>(
45 &self,
46 url: &str,
47 client: &Client,
48 headers: &HeaderMap,
49 ) -> Result<Vec<T>, String> {
50 let mut results: Vec<T> = Vec::new();
51
52 for page in 1..u32::MAX {
53 let url = format!("{url}?per_page={PER_PAGE}&page={page}");
54 trace!("URL: {url}");
55
56 let res = client
57 .get(&url)
58 .headers(headers.clone())
59 .send()
60 .map_err(|e| format!("Unable to connect to: {url} ({e})"))?;
61
62 debug!("HTTP Status Received: {}", res.status());
63
64 if res.status() != StatusCode::OK {
65 if res.status() == StatusCode::UNAUTHORIZED {
66 return Err(format!(
67 "API call received unautorized ({}) for: {}. \
68 Please make sure the `GITLAB_PRIVATE_TOKEN` environment \
69 variable is set.",
70 res.status(),
71 url
72 ));
73 } else {
74 return Err(format!(
75 "API call received invalid status ({}) for : {}",
76 res.status(),
77 url
78 ));
79 }
80 }
81
82 let has_next = match res.headers().get("x-next-page") {
83 None => {
84 trace!("No more pages, x-next-page header missing.");
85 false
86 }
87 Some(n) => {
88 if n.is_empty() {
89 trace!("No more pages, x-next-page-header empty.");
90 false
91 } else {
92 trace!("Next page: {n:?}");
93 true
94 }
95 }
96 };
97
98 let results_page: Vec<T> = serde_json::from_reader(res)
99 .map_err(|e| format!("Unable to parse response as JSON ({e})"))?;
100
101 results.extend(results_page);
102
103 if !has_next {
104 break;
105 }
106 }
107 Ok(results)
108 }
109
110 fn get_projects(
111 &self,
112 id: &str,
113 client: &Client,
114 headers: &HeaderMap,
115 ) -> Result<Vec<Project>, String> {
116 let url = format!("{}/api/v4/groups/{}/projects", self.url, id);
117
118 self.get_paged::<Project>(&url, client, headers)
119 }
120
121 fn get_subgroups(
122 &self,
123 id: &str,
124 client: &Client,
125 headers: &HeaderMap,
126 ) -> Result<Vec<String>, String> {
127 let url = format!("{}/api/v4/groups/{}/subgroups", self.url, id);
128
129 let groups = self.get_paged::<Group>(&url, client, headers)?;
130
131 let mut subgroups: Vec<String> = vec![id.to_owned()];
132
133 for group in groups {
134 subgroups.extend(self.get_subgroups(&format!("{}", group.id), client, headers)?);
135 }
136
137 Ok(subgroups)
138 }
139}
140
141impl Provider for GitLab {
142 fn get_label(&self) -> String {
143 format!("{}/{}", self.url, self.group)
144 }
145
146 fn get_mirror_repos(&self) -> Result<Vec<MirrorResult>, String> {
147 let client = Client::new();
148
149 let use_http = self.use_http;
150
151 let mut headers = HeaderMap::new();
152 if let Some(ref token) = self.private_token {
153 match HeaderValue::from_str(token) {
154 Ok(token) => {
155 headers.insert("PRIVATE-TOKEN", token);
156 }
157 Err(err) => {
158 error!("Unable to parse PRIVATE_TOKEN: {err}");
159 }
160 }
161 } else {
162 warn!("PRIVATE_TOKEN not set")
163 }
164
165 let groups = if self.recursive {
166 self.get_subgroups(&self.group, &client, &headers).or_else(
167 |e| -> Result<Vec<String>, String> {
168 warn!("Unable to get subgroups: {e}");
169 Ok(vec![self.group.clone()])
170 },
171 )?
172 } else {
173 vec![self.group.clone()]
174 };
175
176 let mut projects: Vec<Project> = Vec::new();
177
178 for group in groups {
179 projects.extend(self.get_projects(&group, &client, &headers)?);
180 }
181
182 let mut mirrors: Vec<MirrorResult> = Vec::new();
183
184 for p in projects {
185 match serde_yaml::from_str::<Desc>(&p.description) {
186 Ok(desc) => {
187 if desc.skip {
188 mirrors.push(Err(MirrorError::Skip(p.web_url)));
189 continue;
190 }
191 trace!("{0} -> {1}", desc.origin, p.ssh_url_to_repo);
192 let destination = if use_http {
193 p.http_url_to_repo
194 } else {
195 p.ssh_url_to_repo
196 };
197 let m = Mirror {
198 origin: desc.origin,
199 destination,
200 refspec: desc.refspec,
201 lfs: desc.lfs,
202 };
203 mirrors.push(Ok(m));
204 }
205 Err(e) => {
206 mirrors.push(Err(MirrorError::Description(p.web_url, e)));
207 }
208 }
209 }
210
211 Ok(mirrors)
212 }
213}