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