1use crate::{
2 discover::DiscoveredAdvisory,
3 model::{metadata::ProviderMetadata, store::distribution_base},
4 retrieve::{RetrievalContext, RetrievedAdvisory, RetrievedVisitor},
5 source::Source,
6 validation::{ValidatedAdvisory, ValidatedVisitor, ValidationContext, ValidationError},
7};
8use anyhow::Context;
9use sequoia_openpgp::{Cert, armor::Kind, serialize::SerializeInto};
10use std::{
11 fmt::Debug,
12 io::{ErrorKind, Write},
13 path::{Path, PathBuf},
14 rc::Rc,
15};
16use tokio::fs;
17use walker_common::{
18 retrieve::RetrievalError,
19 store::{Document, StoreError, store_document},
20 utils::openpgp::PublicKey,
21};
22
23pub const DIR_METADATA: &str = "metadata";
24
25#[non_exhaustive]
27pub struct StoreVisitor {
28 pub base: PathBuf,
30
31 pub no_timestamps: bool,
33
34 #[cfg(any(target_os = "linux", target_os = "macos"))]
36 pub no_xattrs: bool,
37}
38
39impl StoreVisitor {
40 pub fn new(base: impl Into<PathBuf>) -> Self {
41 Self {
42 base: base.into(),
43 no_timestamps: false,
44 #[cfg(any(target_os = "linux", target_os = "macos"))]
45 no_xattrs: false,
46 }
47 }
48
49 pub fn no_timestamps(mut self, no_timestamps: bool) -> Self {
50 self.no_timestamps = no_timestamps;
51 self
52 }
53
54 #[cfg(any(target_os = "linux", target_os = "macos"))]
55 pub fn no_xattrs(mut self, no_xattrs: bool) -> Self {
56 self.no_xattrs = no_xattrs;
57 self
58 }
59}
60
61#[derive(Debug, thiserror::Error)]
62#[allow(clippy::large_enum_variant)]
63pub enum StoreRetrievedError<S: Source> {
64 #[error(transparent)]
65 Store(#[from] StoreError),
66 #[error(transparent)]
67 Retrieval(#[from] RetrievalError<DiscoveredAdvisory, S>),
68}
69
70#[derive(Debug, thiserror::Error)]
71pub enum StoreValidatedError<S: Source> {
72 #[error(transparent)]
73 Store(#[from] StoreError),
74 #[error(transparent)]
75 Validation(#[from] ValidationError<S>),
76}
77
78impl<S: Source + Debug> RetrievedVisitor<S> for StoreVisitor {
79 type Error = StoreRetrievedError<S>;
80 type Context = Rc<ProviderMetadata>;
81
82 async fn visit_context(
83 &self,
84 context: &RetrievalContext<'_>,
85 ) -> Result<Self::Context, Self::Error> {
86 self.store_provider_metadata(context.metadata).await?;
87 self.prepare_distributions(context.metadata).await?;
88 self.store_keys(context.keys).await?;
89
90 Ok(Rc::new(context.metadata.clone()))
91 }
92
93 async fn visit_advisory(
94 &self,
95 _context: &Self::Context,
96 result: Result<RetrievedAdvisory, RetrievalError<DiscoveredAdvisory, S>>,
97 ) -> Result<(), Self::Error> {
98 self.store(&result?).await?;
99 Ok(())
100 }
101}
102
103impl<S: Source> ValidatedVisitor<S> for StoreVisitor {
104 type Error = StoreValidatedError<S>;
105 type Context = ();
106
107 async fn visit_context(
108 &self,
109 context: &ValidationContext<'_>,
110 ) -> Result<Self::Context, Self::Error> {
111 self.store_provider_metadata(context.metadata).await?;
112 self.prepare_distributions(context.metadata).await?;
113 self.store_keys(context.retrieval.keys).await?;
114 Ok(())
115 }
116
117 async fn visit_advisory(
118 &self,
119 _context: &Self::Context,
120 result: Result<ValidatedAdvisory, ValidationError<S>>,
121 ) -> Result<(), Self::Error> {
122 self.store(&result?.retrieved).await?;
123 Ok(())
124 }
125}
126
127impl StoreVisitor {
128 async fn prepare_distributions(&self, metadata: &ProviderMetadata) -> Result<(), StoreError> {
129 for dist in &metadata.distributions {
130 if let Some(directory_url) = &dist.directory_url {
131 let base = distribution_base(&self.base, directory_url.as_str());
132 log::debug!("Creating base distribution directory: {}", base.display());
133
134 fs::create_dir_all(&base)
135 .await
136 .with_context(|| {
137 format!(
138 "Unable to create distribution directory: {}",
139 base.display()
140 )
141 })
142 .map_err(StoreError::Io)?;
143 }
144 if let Some(rolie) = &dist.rolie {
145 for feed in &rolie.feeds {
146 let base = distribution_base(&self.base, feed.url.as_str());
147 fs::create_dir_all(&base)
148 .await
149 .with_context(|| {
150 format!(
151 "Unable to create distribution directory: {}",
152 base.display()
153 )
154 })
155 .map_err(StoreError::Io)?;
156 }
157 }
158 }
159
160 Ok(())
161 }
162
163 async fn store_provider_metadata(&self, metadata: &ProviderMetadata) -> Result<(), StoreError> {
164 let metadir = self.base.join(DIR_METADATA);
165
166 fs::create_dir(&metadir)
167 .await
168 .or_else(|err| match err.kind() {
169 ErrorKind::AlreadyExists => Ok(()),
170 _ => Err(err),
171 })
172 .with_context(|| format!("Failed to create metadata directory: {}", metadir.display()))
173 .map_err(StoreError::Io)?;
174
175 let file = metadir.join("provider-metadata.json");
176 let mut out = std::fs::File::create(&file)
177 .with_context(|| {
178 format!(
179 "Unable to open provider metadata file for writing: {}",
180 file.display()
181 )
182 })
183 .map_err(StoreError::Io)?;
184 serde_json::to_writer_pretty(&mut out, metadata)
185 .context("Failed serializing provider metadata")
186 .map_err(StoreError::Io)?;
187 Ok(())
188 }
189
190 async fn store_keys(&self, keys: &[PublicKey]) -> Result<(), StoreError> {
191 let metadata = self.base.join(DIR_METADATA).join("keys");
192 std::fs::create_dir(&metadata)
193 .or_else(|err| match err.kind() {
195 ErrorKind::AlreadyExists => Ok(()),
196 _ => Err(err),
197 })
198 .with_context(|| {
199 format!(
200 "Failed to create metadata directory: {}",
201 metadata.display()
202 )
203 })
204 .map_err(StoreError::Io)?;
205
206 for cert in keys.iter().flat_map(|k| &k.certs) {
207 log::info!("Storing key: {}", cert.fingerprint());
208 self.store_cert(cert, &metadata).await?;
209 }
210
211 Ok(())
212 }
213
214 async fn store_cert(&self, cert: &Cert, path: &Path) -> Result<(), StoreError> {
215 let name = path.join(format!("{}.txt", cert.fingerprint().to_hex()));
216
217 let data = Self::serialize_key(cert).map_err(StoreError::SerializeKey)?;
218
219 fs::write(&name, data)
220 .await
221 .with_context(|| format!("Failed to store key: {}", name.display()))
222 .map_err(StoreError::Io)?;
223 Ok(())
224 }
225
226 fn serialize_key(cert: &Cert) -> Result<Vec<u8>, anyhow::Error> {
227 let mut writer = sequoia_openpgp::armor::Writer::new(Vec::new(), Kind::PublicKey)?;
228 writer.write_all(&cert.to_vec()?)?;
229 Ok(writer.finalize()?)
230 }
231
232 async fn store(&self, advisory: &RetrievedAdvisory) -> Result<(), StoreError> {
233 log::info!(
234 "Storing: {} (modified: {:?})",
235 advisory.url,
236 advisory.metadata.last_modification
237 );
238
239 let relative_url_result = advisory.context.url().make_relative(&advisory.url);
240 let name = match &relative_url_result {
241 Some(name) => name,
242 None => return Err(StoreError::Filename(advisory.url.to_string())),
243 };
244
245 let distribution_base = distribution_base(&self.base, advisory.context.url().as_str());
247
248 let file = distribution_base.join(name);
250
251 store_document(
252 &file,
253 Document {
254 data: &advisory.data,
255 changed: advisory.modified,
256 metadata: &advisory.metadata,
257 sha256: &advisory.sha256,
258 sha512: &advisory.sha512,
259 signature: &advisory.signature,
260 no_timestamps: self.no_timestamps,
261 #[cfg(any(target_os = "linux", target_os = "macos"))]
262 no_xattrs: self.no_xattrs,
263 },
264 )
265 .await?;
266
267 Ok(())
268 }
269}