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