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