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}
499
500impl 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 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 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 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 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 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 info!("download {:?} finished, start unzip", archive_name);
564 let archive_file = ArchiveFile::new(&path.join(archive_name));
565 archive_file.unzip()?;
566 std::fs::remove_dir_all(path).map_err(|e| e.to_string())?;
568 return Ok(());
569 }
570}
571
572pub struct ArchiveFile {
573 archive_path: PathBuf,
574 archive_name: String,
575 archive_type: ArchiveType,
576}
577
578impl ArchiveFile {
579 pub fn new(archive_path: &PathBuf) -> Self {
580 info!("archive_path: {:?}", archive_path);
581 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 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 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 #[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 info!("unzip successfully, removing archive ");
684 std::fs::remove_file(path.join(&self.archive_name)).map_err(|e| e.to_string())?;
685 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 std::fs::remove_dir_all(&path).map_err(|e| e.to_string())?;
693 }
694 return Ok(());
695 }
696}
697
698pub enum ArchiveType {
699 TarGz,
700 TarXz,
701 Zip,
702 Undefined,
703}