xref: /DADK/src/executor/source.rs (revision 674005c9789d72f67f795c4397a4b7e462eb25c4)
1 use log::info;
2 use regex::Regex;
3 use reqwest::Url;
4 use serde::{Deserialize, Serialize};
5 use std::os::unix::fs::PermissionsExt;
6 use std::{
7     fs::File,
8     path::PathBuf,
9     process::{Command, Stdio},
10 };
11 use zip::ZipArchive;
12 
13 use crate::utils::{file::FileUtils, stdio::StdioUtils};
14 
15 use super::cache::CacheDir;
16 
17 /// # Git源
18 ///
19 /// 从Git仓库获取源码
20 #[derive(Debug, Clone, Serialize, Deserialize)]
21 pub struct GitSource {
22     /// Git仓库地址
23     url: String,
24     /// 分支(可选,如果为空,则拉取master)branch和revision只能同时指定一个
25     branch: Option<String>,
26     /// 特定的提交的hash值(可选,如果为空,则拉取branch的最新提交)
27     revision: Option<String>,
28 }
29 
30 impl GitSource {
31     pub fn new(url: String, branch: Option<String>, revision: Option<String>) -> Self {
32         Self {
33             url,
34             branch,
35             revision,
36         }
37     }
38     /// # 验证参数合法性
39     ///
40     /// 仅进行形式校验,不会检查Git仓库是否存在,以及分支是否存在、是否有权限访问等
41     pub fn validate(&mut self) -> Result<(), String> {
42         if self.url.is_empty() {
43             return Err("url is empty".to_string());
44         }
45         // branch和revision不能同时为空
46         if self.branch.is_none() && self.revision.is_none() {
47             self.branch = Some("master".to_string());
48         }
49         // branch和revision只能同时指定一个
50         if self.branch.is_some() && self.revision.is_some() {
51             return Err("branch and revision are both specified".to_string());
52         }
53 
54         if self.branch.is_some() {
55             if self.branch.as_ref().unwrap().is_empty() {
56                 return Err("branch is empty".to_string());
57             }
58         }
59         if self.revision.is_some() {
60             if self.revision.as_ref().unwrap().is_empty() {
61                 return Err("revision is empty".to_string());
62             }
63         }
64         return Ok(());
65     }
66 
67     pub fn trim(&mut self) {
68         self.url = self.url.trim().to_string();
69         if let Some(branch) = &mut self.branch {
70             *branch = branch.trim().to_string();
71         }
72 
73         if let Some(revision) = &mut self.revision {
74             *revision = revision.trim().to_string();
75         }
76     }
77 
78     /// # 确保Git仓库已经克隆到指定目录,并且切换到指定分支/Revision
79     ///
80     /// 如果目录不存在,则会自动创建
81     ///
82     /// ## 参数
83     ///
84     /// * `target_dir` - 目标目录
85     ///
86     /// ## 返回
87     ///
88     /// * `Ok(())` - 成功
89     /// * `Err(String)` - 失败,错误信息
90     pub fn prepare(&self, target_dir: &CacheDir) -> Result<(), String> {
91         info!(
92             "Preparing git repo: {}, branch: {:?}, revision: {:?}",
93             self.url, self.branch, self.revision
94         );
95 
96         target_dir.create().map_err(|e| {
97             format!(
98                 "Failed to create target dir: {}, message: {e:?}",
99                 target_dir.path.display()
100             )
101         })?;
102 
103         if target_dir.is_empty().map_err(|e| {
104             format!(
105                 "Failed to check if target dir is empty: {}, message: {e:?}",
106                 target_dir.path.display()
107             )
108         })? {
109             info!("Target dir is empty, cloning repo");
110             self.clone_repo(target_dir)?;
111         }
112 
113         self.checkout(target_dir)?;
114 
115         self.pull(target_dir)?;
116 
117         return Ok(());
118     }
119 
120     fn checkout(&self, target_dir: &CacheDir) -> Result<(), String> {
121         let do_checkout = || -> Result<(), String> {
122             let mut cmd = Command::new("git");
123             cmd.current_dir(&target_dir.path);
124             cmd.arg("checkout");
125 
126             if let Some(branch) = &self.branch {
127                 cmd.arg(branch);
128             }
129             if let Some(revision) = &self.revision {
130                 cmd.arg(revision);
131             }
132 
133             // 强制切换分支,且安静模式
134             cmd.arg("-f").arg("-q");
135 
136             // 创建子进程,执行命令
137             let proc: std::process::Child = cmd
138                 .stderr(Stdio::piped())
139                 .spawn()
140                 .map_err(|e| e.to_string())?;
141             let output = proc.wait_with_output().map_err(|e| e.to_string())?;
142 
143             if !output.status.success() {
144                 return Err(format!(
145                     "Failed to checkout {}, message: {}",
146                     target_dir.path.display(),
147                     String::from_utf8_lossy(&output.stdout)
148                 ));
149             }
150             return Ok(());
151         };
152 
153         if let Err(_) = do_checkout() {
154             // 如果切换分支失败,则尝试重新fetch
155             if self.revision.is_some() {
156                 self.set_fetch_config(target_dir)?;
157                 self.unshallow(target_dir)?
158             };
159 
160             self.fetch_all(target_dir).ok();
161             do_checkout()?;
162         }
163 
164         return Ok(());
165     }
166 
167     pub fn clone_repo(&self, cache_dir: &CacheDir) -> Result<(), String> {
168         let path: &PathBuf = &cache_dir.path;
169         let mut cmd = Command::new("git");
170         cmd.arg("clone").arg(&self.url).arg(".").arg("--recursive");
171 
172         if let Some(branch) = &self.branch {
173             cmd.arg("--branch").arg(branch).arg("--depth").arg("1");
174         }
175 
176         // 对于克隆,如果指定了revision,则直接克隆整个仓库,稍后再切换到指定的revision
177 
178         // 设置工作目录
179         cmd.current_dir(path);
180 
181         // 创建子进程,执行命令
182         let proc: std::process::Child = cmd
183             .stderr(Stdio::piped())
184             .stdout(Stdio::inherit())
185             .spawn()
186             .map_err(|e| e.to_string())?;
187         let output = proc.wait_with_output().map_err(|e| e.to_string())?;
188 
189         if !output.status.success() {
190             return Err(format!(
191                 "clone git repo failed, status: {:?},  stderr: {:?}",
192                 output.status,
193                 StdioUtils::tail_n_str(StdioUtils::stderr_to_lines(&output.stderr), 5)
194             ));
195         }
196         return Ok(());
197     }
198 
199     /// 设置fetch所有分支
200     fn set_fetch_config(&self, target_dir: &CacheDir) -> Result<(), String> {
201         let mut cmd = Command::new("git");
202         cmd.current_dir(&target_dir.path);
203         cmd.arg("config")
204             .arg("remote.origin.fetch")
205             .arg("+refs/heads/*:refs/remotes/origin/*");
206 
207         // 创建子进程,执行命令
208         let proc: std::process::Child = cmd
209             .stderr(Stdio::piped())
210             .spawn()
211             .map_err(|e| e.to_string())?;
212         let output = proc.wait_with_output().map_err(|e| e.to_string())?;
213 
214         if !output.status.success() {
215             return Err(format!(
216                 "Failed to set fetch config {}, message: {}",
217                 target_dir.path.display(),
218                 StdioUtils::tail_n_str(StdioUtils::stderr_to_lines(&output.stderr), 5)
219             ));
220         }
221         return Ok(());
222     }
223     /// # 把浅克隆的仓库变成深克隆
224     fn unshallow(&self, target_dir: &CacheDir) -> Result<(), String> {
225         let mut cmd = Command::new("git");
226         cmd.current_dir(&target_dir.path);
227         cmd.arg("fetch").arg("--unshallow");
228 
229         // 安静模式
230         cmd.arg("-f").arg("-q");
231 
232         // 创建子进程,执行命令
233         let proc: std::process::Child = cmd
234             .stderr(Stdio::piped())
235             .spawn()
236             .map_err(|e| e.to_string())?;
237         let output = proc.wait_with_output().map_err(|e| e.to_string())?;
238 
239         if !output.status.success() {
240             return Err(format!(
241                 "Failed to unshallow {}, message: {}",
242                 target_dir.path.display(),
243                 StdioUtils::tail_n_str(StdioUtils::stderr_to_lines(&output.stderr), 5)
244             ));
245         }
246         return Ok(());
247     }
248 
249     fn fetch_all(&self, target_dir: &CacheDir) -> Result<(), String> {
250         self.set_fetch_config(target_dir)?;
251         let mut cmd = Command::new("git");
252         cmd.current_dir(&target_dir.path);
253         cmd.arg("fetch").arg("--all");
254 
255         // 安静模式
256         cmd.arg("-f").arg("-q");
257 
258         // 创建子进程,执行命令
259         let proc: std::process::Child = cmd
260             .stderr(Stdio::piped())
261             .spawn()
262             .map_err(|e| e.to_string())?;
263         let output = proc.wait_with_output().map_err(|e| e.to_string())?;
264 
265         if !output.status.success() {
266             return Err(format!(
267                 "Failed to fetch all {}, message: {}",
268                 target_dir.path.display(),
269                 StdioUtils::tail_n_str(StdioUtils::stderr_to_lines(&output.stderr), 5)
270             ));
271         }
272 
273         return Ok(());
274     }
275 
276     fn pull(&self, target_dir: &CacheDir) -> Result<(), String> {
277         // 如果没有指定branch,则不执行pull
278         if !self.branch.is_some() {
279             return Ok(());
280         }
281         info!("git pulling: {}", target_dir.path.display());
282 
283         let mut cmd = Command::new("git");
284         cmd.current_dir(&target_dir.path);
285         cmd.arg("pull");
286 
287         // 安静模式
288         cmd.arg("-f").arg("-q");
289 
290         // 创建子进程,执行命令
291         let proc: std::process::Child = cmd
292             .stderr(Stdio::piped())
293             .spawn()
294             .map_err(|e| e.to_string())?;
295         let output = proc.wait_with_output().map_err(|e| e.to_string())?;
296 
297         // 如果pull失败,且指定了branch,则报错
298         if !output.status.success() {
299             return Err(format!(
300                 "Failed to pull {}, message: {}",
301                 target_dir.path.display(),
302                 StdioUtils::tail_n_str(StdioUtils::stderr_to_lines(&output.stderr), 5)
303             ));
304         }
305 
306         return Ok(());
307     }
308 }
309 
310 /// # 本地源
311 #[derive(Debug, Clone, Serialize, Deserialize)]
312 pub struct LocalSource {
313     /// 本地目录/文件的路径
314     path: PathBuf,
315 }
316 
317 impl LocalSource {
318     #[allow(dead_code)]
319     pub fn new(path: PathBuf) -> Self {
320         Self { path }
321     }
322 
323     pub fn validate(&self, expect_file: Option<bool>) -> Result<(), String> {
324         if !self.path.exists() {
325             return Err(format!("path {:?} not exists", self.path));
326         }
327 
328         if let Some(expect_file) = expect_file {
329             if expect_file && !self.path.is_file() {
330                 return Err(format!("path {:?} is not a file", self.path));
331             }
332 
333             if !expect_file && !self.path.is_dir() {
334                 return Err(format!("path {:?} is not a directory", self.path));
335             }
336         }
337 
338         return Ok(());
339     }
340 
341     pub fn trim(&mut self) {}
342 
343     pub fn path(&self) -> &PathBuf {
344         &self.path
345     }
346 }
347 
348 /// # 在线压缩包源
349 #[derive(Debug, Clone, Serialize, Deserialize)]
350 pub struct ArchiveSource {
351     /// 压缩包的URL
352     url: String,
353 }
354 
355 impl ArchiveSource {
356     #[allow(dead_code)]
357     pub fn new(url: String) -> Self {
358         Self { url }
359     }
360     pub fn validate(&self) -> Result<(), String> {
361         if self.url.is_empty() {
362             return Err("url is empty".to_string());
363         }
364 
365         // 判断是一个网址
366         if let Ok(url) = Url::parse(&self.url) {
367             if url.scheme() != "http" && url.scheme() != "https" {
368                 return Err(format!("url {:?} is not a http/https url", self.url));
369             }
370         } else {
371             return Err(format!("url {:?} is not a valid url", self.url));
372         }
373         return Ok(());
374     }
375 
376     pub fn trim(&mut self) {
377         self.url = self.url.trim().to_string();
378     }
379 
380     /// @brief 下载压缩包并把其中的文件提取至target_dir目录下
381     ///
382     ///从URL中下载压缩包到临时文件夹 target_dir/DRAGONOS_ARCHIVE_TEMP
383     ///原地解压,提取文件后删除下载的压缩包。如果 target_dir 非空,就直接使用
384     ///其中内容,不进行重复下载和覆盖
385     ///
386     /// @param target_dir 文件缓存目录
387     ///
388     /// @return 根据结果返回OK或Err
389     pub fn download_unzip(&self, target_dir: &CacheDir) -> Result<(), String> {
390         let url = Url::parse(&self.url).unwrap();
391         let archive_name = url.path_segments().unwrap().last().unwrap();
392         let path = &(target_dir.path.join("DRAGONOS_ARCHIVE_TEMP"));
393         //如果source目录没有临时文件夹,且不为空,说明之前成功执行过一次,那么就直接使用之前的缓存
394         if !path.exists()
395             && !target_dir.is_empty().map_err(|e| {
396                 format!(
397                     "Failed to check if target dir is empty: {}, message: {e:?}",
398                     target_dir.path.display()
399                 )
400             })?
401         {
402             //如果source文件夹非空,就直接使用,不再重复下载压缩文件,这里可以考虑加入交互
403             info!("Source files already exist. Using previous source file cache. You should clean {:?} before re-download the archive ",target_dir);
404             return Ok(());
405         }
406 
407         if path.exists() {
408             std::fs::remove_dir_all(path).map_err(|e| e.to_string())?;
409         }
410         //创建临时目录
411         std::fs::create_dir(path).map_err(|e| e.to_string())?;
412         info!("downloading {:?}", archive_name);
413         FileUtils::download_file(&self.url, path).map_err(|e| e.to_string())?;
414         //下载成功,开始尝试解压
415         info!("download {:?} finished, start unzip", archive_name);
416         let archive_file = ArchiveFile::new(&path.join(archive_name));
417         archive_file.unzip()?;
418         //删除创建的临时文件夹
419         std::fs::remove_dir_all(path).map_err(|e| e.to_string())?;
420         return Ok(());
421     }
422 }
423 
424 pub struct ArchiveFile {
425     archive_path: PathBuf,
426     archive_name: String,
427     archive_type: ArchiveType,
428 }
429 
430 impl ArchiveFile {
431     pub fn new(archive_path: &PathBuf) -> Self {
432         //匹配压缩文件类型
433         let archive_name = archive_path.file_name().unwrap().to_str().unwrap();
434         for (regex, archivetype) in [
435             (Regex::new(r"^(.+)\.tar\.gz$").unwrap(), ArchiveType::TarGz),
436             (Regex::new(r"^(.+)\.zip$").unwrap(), ArchiveType::Zip),
437         ] {
438             if regex.is_match(archive_name) {
439                 return Self {
440                     archive_path: archive_path.parent().unwrap().to_path_buf(),
441                     archive_name: archive_name.to_string(),
442                     archive_type: archivetype,
443                 };
444             }
445         }
446         Self {
447             archive_path: archive_path.parent().unwrap().to_path_buf(),
448             archive_name: archive_name.to_string(),
449             archive_type: ArchiveType::Undefined,
450         }
451     }
452 
453     /// @brief 对self.archive_path路径下名为self.archive_name的压缩文件(tar.gz或zip)进行解压缩
454     ///
455     /// 在此函数中进行路径和文件名有效性的判断,如果有效的话就开始解压缩,根据ArchiveType枚举类型来
456     /// 生成不同的命令来对压缩文件进行解压缩,暂时只支持tar.gz和zip格式,并且都是通过调用bash来解压缩
457     /// 没有引入第三方rust库
458     ///
459     ///
460     /// @return 根据结果返回OK或Err
461 
462     pub fn unzip(&self) -> Result<(), String> {
463         let path = &self.archive_path;
464         if !path.is_dir() {
465             return Err(format!("Archive directory {:?} is wrong", path));
466         }
467         if !path.join(&self.archive_name).is_file() {
468             return Err(format!(
469                 " {:?} is not a file",
470                 path.join(&self.archive_name)
471             ));
472         }
473         //根据压缩文件的类型生成cmd指令
474         match &self.archive_type {
475             ArchiveType::TarGz => {
476                 let mut cmd = Command::new("tar -xzf");
477                 cmd.arg(&self.archive_name);
478                 let proc: std::process::Child = cmd
479                     .stderr(Stdio::piped())
480                     .stdout(Stdio::inherit())
481                     .spawn()
482                     .map_err(|e| e.to_string())?;
483                 let output = proc.wait_with_output().map_err(|e| e.to_string())?;
484                 if !output.status.success() {
485                     return Err(format!(
486                         "unzip failed, status: {:?},  stderr: {:?}",
487                         output.status,
488                         StdioUtils::tail_n_str(StdioUtils::stderr_to_lines(&output.stderr), 5)
489                     ));
490                 }
491             }
492 
493             ArchiveType::Zip => {
494                 let file = File::open(&self.archive_path.join(&self.archive_name))
495                     .map_err(|e| e.to_string())?;
496                 let mut archive = ZipArchive::new(file).map_err(|e| e.to_string())?;
497                 for i in 0..archive.len() {
498                     let mut file = archive.by_index(i).map_err(|e| e.to_string())?;
499                     let outpath = match file.enclosed_name() {
500                         Some(path) => self.archive_path.join(path),
501                         None => continue,
502                     };
503                     if (*file.name()).ends_with('/') {
504                         std::fs::create_dir_all(&outpath).map_err(|e| e.to_string())?;
505                     } else {
506                         if let Some(p) = outpath.parent() {
507                             if !p.exists() {
508                                 std::fs::create_dir_all(&p).map_err(|e| e.to_string())?;
509                             }
510                         }
511                         let mut outfile = File::create(&outpath).map_err(|e| e.to_string())?;
512                         std::io::copy(&mut file, &mut outfile).map_err(|e| e.to_string())?;
513                     }
514                     //设置解压后权限,在Linux中Unzip会丢失权限
515                     #[cfg(unix)]
516                     {
517                         if let Some(mode) = file.unix_mode() {
518                             std::fs::set_permissions(
519                                 &outpath,
520                                 std::fs::Permissions::from_mode(mode),
521                             )
522                             .map_err(|e| e.to_string())?;
523                         }
524                     }
525                 }
526             }
527             _ => {
528                 return Err("unsupported archive type".to_string());
529             }
530         }
531         //删除下载的压缩包
532         info!("unzip successfully, removing archive ");
533         std::fs::remove_file(path.join(&self.archive_name)).map_err(|e| e.to_string())?;
534         //从解压的文件夹中提取出文件并删除下载的压缩包等价于指令"cd *;mv ./* ../../"
535         for entry in path.read_dir().map_err(|e| e.to_string())? {
536             let entry = entry.map_err(|e| e.to_string())?;
537             let path = entry.path();
538             FileUtils::move_files(&path, &self.archive_path.parent().unwrap())
539                 .map_err(|e| e.to_string())?;
540             //删除空的单独文件夹
541             std::fs::remove_dir_all(&path).map_err(|e| e.to_string())?;
542         }
543         return Ok(());
544     }
545 }
546 
547 pub enum ArchiveType {
548     TarGz,
549     Zip,
550     Undefined,
551 }
552