1use log::info;
2use regex::Regex;
3use reqwest::Url;
4use serde::{Deserialize, Serialize};
5use std::os::unix::fs::PermissionsExt;
6use std::{
7 fs::File,
8 path::PathBuf,
9 process::{Command, Stdio},
10};
11use zip::ZipArchive;
12
13use crate::utils::{file::FileUtils, stdio::StdioUtils};
14
15use super::cache::CacheDir;
16
17use anyhow::{Error, Result};
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
23pub struct GitSource {
24 url: String,
26 branch: Option<String>,
28 revision: Option<String>,
30}
31
32impl 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 pub fn validate(&mut self) -> Result<()> {
44 if self.url.is_empty() {
45 return Err(Error::msg("url is empty"));
46 }
47 if self.branch.is_none() && self.revision.is_none() {
49 self.branch = Some("master".to_string());
50 }
51 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 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 cmd.current_dir(path);
129
130 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 cmd.current_dir(path);
161
162 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 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 cmd.arg("-f").arg("-q");
205
206 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 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 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 cmd.current_dir(path);
269
270 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 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 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 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 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 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 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 cmd.arg("-f").arg("-q");
399
400 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 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 cmd.arg("-f").arg("-q");
431
432 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 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
454pub struct LocalSource {
455 path: PathBuf,
457}
458
459impl 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
495pub struct ArchiveSource {
496 url: String,
498 #[serde(default)]
501 rootdir: Option<String>,
502}
503
504impl ArchiveSource {
505 #[allow(dead_code)]
506 pub fn new(url: String, rootdir: Option<String>) -> Self {
507 Self { url, rootdir }
508 }
509
510 pub fn validate(&self) -> Result<()> {
511 if self.url.is_empty() {
512 return Err(Error::msg("url is empty"));
513 }
514
515 if let Ok(url) = Url::parse(&self.url) {
517 if url.scheme() != "http" && url.scheme() != "https" {
518 return Err(Error::msg(format!(
519 "url {:?} is not a http/https url",
520 self.url
521 )));
522 }
523 } else {
524 return Err(Error::msg(format!("url {:?} is not a valid url", self.url)));
525 }
526
527 if self.rootdir.is_some() && self.rootdir.as_ref().unwrap().starts_with('/') {
528 return Err(Error::msg(format!(
529 "archive rootdir {:?} starts with '/'",
530 self.rootdir
531 )));
532 }
533 return Ok(());
534 }
535
536 pub fn trim(&mut self) {
537 self.url = self.url.trim().to_string();
538 }
539
540 pub fn download_unzip(&self, target_dir: &CacheDir) -> Result<(), String> {
550 let url = Url::parse(&self.url).unwrap();
551 let archive_name = url.path_segments().unwrap().last().unwrap();
552 let path = &(target_dir.path.join("DRAGONOS_ARCHIVE_TEMP"));
553 if !path.exists()
555 && !target_dir.is_empty().map_err(|e| {
556 format!(
557 "Failed to check if target dir is empty: {}, message: {e:?}",
558 target_dir.path.display()
559 )
560 })?
561 {
562 info!("Source files already exist. Using previous source file cache. You should clean {:?} before re-download the archive ", target_dir.path);
564 return Ok(());
565 }
566
567 if path.exists() {
568 std::fs::remove_dir_all(path).map_err(|e| e.to_string())?;
569 }
570 std::fs::create_dir(path).map_err(|e| e.to_string())?;
572 info!("downloading {:?}, url: {:?}", archive_name, self.url);
573 FileUtils::download_file(&self.url, path).map_err(|e| e.to_string())?;
574 info!("download {:?} finished, start unzip", archive_name);
576 let archive_file = ArchiveFile::new(&path.join(archive_name));
577 archive_file.unzip(self.rootdir.as_ref())?;
578 return Ok(());
581 }
582}
583
584pub struct ArchiveFile {
585 archive_path: PathBuf,
587 archive_name: String,
589 archive_type: ArchiveType,
590}
591
592impl ArchiveFile {
593 pub fn new(archive_path: &PathBuf) -> Self {
594 info!("archive_path: {:?}", archive_path);
595 let archive_name = archive_path.file_name().unwrap().to_str().unwrap();
597 for (regex, archivetype) in [
598 (Regex::new(r"^(.+)\.tar\.gz$").unwrap(), ArchiveType::TarGz),
599 (Regex::new(r"^(.+)\.tar\.xz$").unwrap(), ArchiveType::TarXz),
600 (Regex::new(r"^(.+)\.zip$").unwrap(), ArchiveType::Zip),
601 ] {
602 if regex.is_match(archive_name) {
603 return Self {
604 archive_path: archive_path.parent().unwrap().to_path_buf(),
605 archive_name: archive_name.to_string(),
606 archive_type: archivetype,
607 };
608 }
609 }
610 Self {
611 archive_path: archive_path.parent().unwrap().to_path_buf(),
612 archive_name: archive_name.to_string(),
613 archive_type: ArchiveType::Undefined,
614 }
615 }
616
617 fn do_unzip_tar_file(&self, in_archive_rootdir: Option<&String>) -> Result<(), String> {
618 let mut cmd = Command::new("tar");
619 cmd.arg("-xf").arg(&self.archive_name);
620
621 if let Some(in_archive_rootdir) = in_archive_rootdir {
623 let mut components = 0;
624 in_archive_rootdir.split('/').for_each(|x| {
625 if x != "" {
626 components += 1;
627 }
628 });
629
630 cmd.arg(format!("--strip-components={}", components));
631 cmd.arg(&in_archive_rootdir);
632 }
633
634 cmd.current_dir(&self.archive_path);
635
636 log::debug!("unzip tar file: {:?}", cmd);
637
638 let proc: std::process::Child = cmd
639 .stderr(Stdio::piped())
640 .stdout(Stdio::inherit())
641 .spawn()
642 .map_err(|e| e.to_string())?;
643 let output = proc.wait_with_output().map_err(|e| e.to_string())?;
644 if !output.status.success() {
645 return Err(format!(
646 "unzip tar file failed, status: {:?}, stderr: {:?}",
647 output.status,
648 StdioUtils::tail_n_str(StdioUtils::stderr_to_lines(&output.stderr), 5)
649 ));
650 }
651 Ok(())
652 }
653
654 fn do_unzip_zip_file(&self, in_archive_rootdir: Option<&String>) -> Result<(), String> {
655 let file =
656 File::open(&self.archive_path.join(&self.archive_name)).map_err(|e| e.to_string())?;
657 let mut archive = ZipArchive::new(file).map_err(|e| e.to_string())?;
658 for i in 0..archive.len() {
659 let mut file = archive.by_index(i).map_err(|e| e.to_string())?;
660 let file_name = file.name();
661
662 let outpath = if let Some(rootdir) = in_archive_rootdir {
664 if !file_name.starts_with(rootdir) {
665 continue;
666 }
667 let relative_path = file_name.strip_prefix(rootdir).unwrap();
669 let relative_path = relative_path.trim_start_matches("/");
670 self.archive_path.join(relative_path)
671 } else {
672 match file.enclosed_name() {
673 Some(path) => self.archive_path.join(path),
674 None => continue,
675 }
676 };
677 if (*file.name()).ends_with('/') {
678 std::fs::create_dir_all(&outpath).map_err(|e| e.to_string())?;
679 } else {
680 if let Some(p) = outpath.parent() {
681 if !p.exists() {
682 std::fs::create_dir_all(&p).map_err(|e| e.to_string())?;
683 }
684 }
685 let mut outfile = File::create(&outpath).map_err(|e| e.to_string())?;
686 std::io::copy(&mut file, &mut outfile).map_err(|e| e.to_string())?;
687 }
688 #[cfg(unix)]
690 {
691 if let Some(mode) = file.unix_mode() {
692 std::fs::set_permissions(&outpath, std::fs::Permissions::from_mode(mode))
693 .map_err(|e| e.to_string())?;
694 }
695 }
696 }
697 Ok(())
698 }
699
700 pub fn unzip(&self, in_archive_rootdir: Option<&String>) -> Result<(), String> {
709 let path = &self.archive_path;
710 if !path.is_dir() {
711 return Err(format!("Archive directory {:?} is wrong", path));
712 }
713 if !path.join(&self.archive_name).is_file() {
714 return Err(format!(
715 " {:?} is not a file",
716 path.join(&self.archive_name)
717 ));
718 }
719 match &self.archive_type {
721 ArchiveType::TarGz | ArchiveType::TarXz => {
722 self.do_unzip_tar_file(in_archive_rootdir)?;
723 }
724
725 ArchiveType::Zip => {
726 self.do_unzip_zip_file(in_archive_rootdir)?;
727 }
728 _ => {
729 return Err("unsupported archive type".to_string());
730 }
731 }
732 info!("unzip successfully, removing archive ");
734 std::fs::remove_file(path.join(&self.archive_name)).map_err(|e| e.to_string())?;
735 std::process::Command::new("sh")
745 .arg("-c")
746 .arg(format!(
747 "mv {p}/* {parent} && rm -rf {p}",
748 p = &self.archive_path.to_string_lossy(),
749 parent = self.archive_path.parent().unwrap().to_string_lossy()
750 ))
751 .output()
752 .map_err(|e| e.to_string())?;
753 return Ok(());
754 }
755}
756
757pub enum ArchiveType {
758 TarGz,
759 TarXz,
760 Zip,
761 Undefined,
762}