debian_packaging/repository/
filesystem.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5/*! Filesystem based Debian repositories. */
6
7use {
8    crate::{
9        error::{DebianError, Result},
10        io::{Compression, ContentDigest, DataResolver, DigestingReader},
11        repository::{
12            release::ReleaseFile, ReleaseReader, RepositoryPathVerification,
13            RepositoryPathVerificationState, RepositoryRootReader, RepositoryWrite,
14            RepositoryWriter,
15        },
16    },
17    async_trait::async_trait,
18    futures::{io::BufReader, AsyncRead, AsyncReadExt},
19    std::{
20        borrow::Cow,
21        path::{Path, PathBuf},
22        pin::Pin,
23    },
24    url::Url,
25};
26
27/// A readable interface to a Debian repository backed by a filesystem.
28#[derive(Clone, Debug)]
29pub struct FilesystemRepositoryReader {
30    root_dir: PathBuf,
31}
32
33impl FilesystemRepositoryReader {
34    /// Construct a new instance, bound to the root directory specified.
35    ///
36    /// No validation of the passed path is performed.
37    pub fn new(path: impl AsRef<Path>) -> Self {
38        Self {
39            root_dir: path.as_ref().to_path_buf(),
40        }
41    }
42}
43
44#[async_trait]
45impl DataResolver for FilesystemRepositoryReader {
46    async fn get_path(&self, path: &str) -> Result<Pin<Box<dyn AsyncRead + Send>>> {
47        let path = self.root_dir.join(path);
48
49        let f = std::fs::File::open(&path)
50            .map_err(|e| DebianError::RepositoryIoPath(format!("{}", path.display()), e))?;
51
52        Ok(Box::pin(futures::io::AllowStdIo::new(f)))
53    }
54}
55
56#[async_trait]
57impl RepositoryRootReader for FilesystemRepositoryReader {
58    fn url(&self) -> Result<Url> {
59        Url::from_file_path(&self.root_dir)
60            .map_err(|_| DebianError::Other("error converting filesystem path to URL".to_string()))
61    }
62
63    async fn release_reader_with_distribution_path(
64        &self,
65        path: &str,
66    ) -> Result<Box<dyn ReleaseReader>> {
67        let distribution_path = path.trim_matches('/').to_string();
68        let inrelease_path = format!("{}/InRelease", distribution_path);
69        let release_path = format!("{}/Release", distribution_path);
70        let distribution_dir = self.root_dir.join(&distribution_path);
71
72        let release = self
73            .fetch_inrelease_or_release(&inrelease_path, &release_path)
74            .await?;
75
76        let fetch_compression = Compression::default_preferred_order()
77            .next()
78            .expect("iterator should not be empty");
79
80        Ok(Box::new(FilesystemReleaseClient {
81            distribution_dir,
82            relative_path: distribution_path,
83            release,
84            fetch_compression,
85        }))
86    }
87}
88
89pub struct FilesystemReleaseClient {
90    distribution_dir: PathBuf,
91    relative_path: String,
92    release: ReleaseFile<'static>,
93    fetch_compression: Compression,
94}
95
96#[async_trait]
97impl DataResolver for FilesystemReleaseClient {
98    async fn get_path(&self, path: &str) -> Result<Pin<Box<dyn AsyncRead + Send>>> {
99        let path = self.distribution_dir.join(path);
100
101        let f = std::fs::File::open(&path)
102            .map_err(|e| DebianError::RepositoryIoPath(format!("{}", path.display()), e))?;
103
104        Ok(Box::pin(BufReader::new(futures::io::AllowStdIo::new(f))))
105    }
106}
107
108#[async_trait]
109impl ReleaseReader for FilesystemReleaseClient {
110    fn url(&self) -> Result<Url> {
111        Url::from_file_path(&self.distribution_dir)
112            .map_err(|_| DebianError::Other("error converting filesystem path to URL".to_string()))
113    }
114
115    fn root_relative_path(&self) -> &str {
116        &self.relative_path
117    }
118
119    fn release_file(&self) -> &ReleaseFile<'static> {
120        &self.release
121    }
122
123    fn preferred_compression(&self) -> Compression {
124        self.fetch_compression
125    }
126
127    fn set_preferred_compression(&mut self, compression: Compression) {
128        self.fetch_compression = compression;
129    }
130}
131
132/// A writable Debian repository backed by a filesystem.
133pub struct FilesystemRepositoryWriter {
134    root_dir: PathBuf,
135}
136
137impl FilesystemRepositoryWriter {
138    /// Construct a new instance, bound to the root directory specified.
139    ///
140    /// No validation of the passed path is performed. The directory does not need to exist.
141    pub fn new(path: impl AsRef<Path>) -> Self {
142        Self {
143            root_dir: path.as_ref().to_path_buf(),
144        }
145    }
146}
147
148#[async_trait]
149impl RepositoryWriter for FilesystemRepositoryWriter {
150    async fn verify_path<'path>(
151        &self,
152        path: &'path str,
153        expected_content: Option<(u64, ContentDigest)>,
154    ) -> Result<RepositoryPathVerification<'path>> {
155        let dest_path = self.root_dir.join(path);
156
157        let metadata = match async_std::fs::metadata(&dest_path).await {
158            Ok(res) => res,
159            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
160                return Ok(RepositoryPathVerification {
161                    path,
162                    state: RepositoryPathVerificationState::Missing,
163                });
164            }
165            Err(e) => return Err(DebianError::RepositoryIoPath(path.to_string(), e)),
166        };
167
168        if metadata.is_file() {
169            if let Some((expected_size, expected_digest)) = expected_content {
170                if metadata.len() != expected_size {
171                    Ok(RepositoryPathVerification {
172                        path,
173                        state: RepositoryPathVerificationState::ExistsIntegrityMismatch,
174                    })
175                } else {
176                    let f = async_std::fs::File::open(&dest_path)
177                        .await
178                        .map_err(|e| DebianError::RepositoryIoPath(path.to_string(), e))?;
179
180                    let mut remaining = expected_size;
181                    let mut reader = DigestingReader::new(f);
182                    let mut buf = [0u8; 16384];
183
184                    loop {
185                        let size = reader
186                            .read(&mut buf[..])
187                            .await
188                            .map_err(|e| DebianError::RepositoryIoPath(path.to_string(), e))?
189                            as u64;
190
191                        if size >= remaining || size == 0 {
192                            break;
193                        }
194
195                        remaining -= size;
196                    }
197
198                    let digest = reader.finish().1;
199
200                    Ok(RepositoryPathVerification {
201                        path,
202                        state: if digest.matches_digest(&expected_digest) {
203                            RepositoryPathVerificationState::ExistsIntegrityVerified
204                        } else {
205                            RepositoryPathVerificationState::ExistsIntegrityMismatch
206                        },
207                    })
208                }
209            } else {
210                Ok(RepositoryPathVerification {
211                    path,
212                    state: RepositoryPathVerificationState::ExistsNoIntegrityCheck,
213                })
214            }
215        } else {
216            Ok(RepositoryPathVerification {
217                path,
218                state: RepositoryPathVerificationState::Missing,
219            })
220        }
221    }
222
223    async fn write_path<'path, 'reader>(
224        &self,
225        path: Cow<'path, str>,
226        reader: Pin<Box<dyn AsyncRead + Send + 'reader>>,
227    ) -> Result<RepositoryWrite<'path>> {
228        let dest_path = self.root_dir.join(path.as_ref());
229
230        if let Some(parent) = dest_path.parent() {
231            std::fs::create_dir_all(parent)
232                .map_err(|e| DebianError::RepositoryIoPath(format!("{}", parent.display()), e))?;
233        }
234
235        let fh = std::fs::File::create(&dest_path)
236            .map_err(|e| DebianError::RepositoryIoPath(format!("{}", dest_path.display()), e))?;
237
238        let mut writer = futures::io::AllowStdIo::new(fh);
239
240        let bytes_written = futures::io::copy(reader, &mut writer)
241            .await
242            .map_err(|e| DebianError::RepositoryIoPath(format!("{}", dest_path.display()), e))?;
243
244        Ok(RepositoryWrite {
245            path,
246            bytes_written,
247        })
248    }
249}