abbaye/builders/
archive.rs1use std::{
2 fs::File,
3 path::{Path, PathBuf},
4};
5
6use flate2::{Compression, write::GzEncoder};
7use globset::{Glob, GlobSet, GlobSetBuilder};
8use ignore::WalkBuilder;
9use miette::{IntoDiagnostic, Result};
10use serde::Deserialize;
11
12use crate::builders::{ArtifactPath, Builder};
13
14fn default_ignore_patterns() -> Vec<String> {
15 vec![".git".to_owned(), "*.local".to_owned()]
16}
17
18#[derive(Debug, Clone, Deserialize)]
20pub struct ArchiveBuilderConfig {
21 pub source_dir: Option<PathBuf>,
23
24 pub output: Option<PathBuf>,
27
28 pub prefix: Option<String>,
33
34 #[serde(default = "default_ignore_patterns")]
40 pub ignore_patterns: Vec<String>,
41}
42
43impl Default for ArchiveBuilderConfig {
44 fn default() -> Self {
45 Self {
46 source_dir: None,
47 output: None,
48 prefix: None,
49 ignore_patterns: default_ignore_patterns(),
50 }
51 }
52}
53
54pub struct ArchiveBuilder;
57
58impl Builder for ArchiveBuilder {
59 type ConfigType = ArchiveBuilderConfig;
60
61 async fn build(&self, config: Self::ConfigType) -> Result<Vec<ArtifactPath>> {
62 let source_dir = config
63 .source_dir
64 .unwrap_or_else(|| PathBuf::from("."))
65 .canonicalize()
66 .into_diagnostic()?;
67
68 let output = config
69 .output
70 .unwrap_or_else(|| PathBuf::from("../source.tar.gz"));
71
72 let prefix = config.prefix.unwrap_or_else(|| {
73 source_dir
74 .file_name()
75 .map(|n| n.to_string_lossy().into_owned())
76 .unwrap_or_else(|| "source".to_owned())
77 });
78
79 let ignore_set = build_ignore_set(&config.ignore_patterns)?;
80
81 let archive_path = tokio::task::spawn_blocking(move || {
82 create_archive(&source_dir, &output, &prefix, &ignore_set)
83 })
84 .await
85 .into_diagnostic()??;
86
87 let name = archive_path
88 .file_name()
89 .map(|n| n.to_string_lossy().into_owned())
90 .unwrap_or_default();
91
92 let hash = Some(super::hash_file(&archive_path).await?);
93
94 Ok(vec![ArtifactPath {
95 path: archive_path,
96 name,
97 hash,
98 }])
99 }
100}
101
102fn build_ignore_set(patterns: &[String]) -> Result<GlobSet> {
104 let mut builder = GlobSetBuilder::new();
105 for pattern in patterns {
106 builder.add(Glob::new(pattern).into_diagnostic()?);
107 }
108 builder.build().into_diagnostic()
109}
110
111fn create_archive(
116 source_dir: &Path,
117 output: &Path,
118 prefix: &str,
119 ignore_set: &GlobSet,
120) -> Result<PathBuf> {
121 let file = File::create(output).into_diagnostic()?;
122 let output_canonical = output.canonicalize().into_diagnostic()?;
125 let encoder = GzEncoder::new(file, Compression::default());
126 let mut archive = tar::Builder::new(encoder);
127
128 for result in WalkBuilder::new(source_dir)
129 .hidden(false) .build()
131 {
132 let entry = result.into_diagnostic()?;
133 let path = entry.path();
134
135 let relative = path.strip_prefix(source_dir).into_diagnostic()?;
136
137 if relative.components().any(|c| c.as_os_str() == ".git") {
139 continue;
140 }
141
142 if path == output_canonical {
144 continue;
145 }
146
147 if relative
149 .components()
150 .any(|c| ignore_set.is_match(Path::new(c.as_os_str())))
151 {
152 continue;
153 }
154
155 if !path.is_file() {
156 continue;
157 }
158
159 let entry_path = Path::new(prefix).join(relative);
160
161 archive
162 .append_path_with_name(path, &entry_path)
163 .into_diagnostic()?;
164 }
165
166 archive
168 .into_inner()
169 .into_diagnostic()?
170 .finish()
171 .into_diagnostic()?;
172
173 Ok(output.to_path_buf())
174}