source.rs 22 KB

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