apt_swarm/
config.rs

1use crate::args::Args;
2use crate::errors::*;
3use crate::signed::Signed;
4use bstr::BString;
5use bytes::Bytes;
6use sequoia_openpgp::armor;
7use serde::{Deserialize, Serialize};
8use std::borrow::Cow;
9use std::io::prelude::*;
10use std::path::{Path, PathBuf};
11use tokio::fs;
12
13#[derive(Debug, PartialEq, Default)]
14pub struct Config {
15    pub data: ConfigData,
16    pub config_path: Option<PathBuf>,
17    pub data_path: Option<PathBuf>,
18}
19
20impl Config {
21    pub async fn load_with_args(args: &Args) -> Result<Self> {
22        let (config_path, data) = if let Some(path) = &args.config {
23            if path.to_str() == Some("#") {
24                debug!("Config loading has been explicitly disabled, using default config");
25                (None, ConfigData::default())
26            } else {
27                let data = ConfigData::load_config_from(path)
28                    .await
29                    .with_context(|| anyhow!("Failed to load configuration from {:?}", path))?;
30                (Some(path.to_owned()), data)
31            }
32        } else if let Some((path, buf)) = Self::find_config().await {
33            debug!("Using configuration from {:?}", path);
34            let data = ConfigData::load_config_from_str(&buf)?;
35            (Some(path), data)
36        } else {
37            (None, ConfigData::default())
38        };
39
40        Ok(Config {
41            data,
42            config_path,
43            data_path: args.data_path.clone(),
44        })
45    }
46
47    async fn find_config() -> Option<(PathBuf, String)> {
48        for path in [
49            Self::default_config_path(),
50            Ok("/etc/apt-swarm.conf".into()),
51        ]
52        .into_iter()
53        .flatten()
54        {
55            match fs::read_to_string(&path).await {
56                Ok(buf) => return Some((path, buf)),
57                Err(err) => {
58                    debug!("Attempt to read config from {path:?} failed: {err:#}");
59                }
60            }
61        }
62
63        None
64    }
65
66    pub fn apt_swarm_path(&self) -> Result<Cow<PathBuf>> {
67        let path = if let Some(path) = &self.data_path {
68            Cow::Borrowed(path)
69        } else {
70            let data_dir = dirs::data_dir().context("Failed to detect data directory")?;
71            let path = data_dir.join("apt-swarm");
72            Cow::Owned(path)
73        };
74
75        Ok(path)
76    }
77
78    pub fn database_path(&self) -> Result<PathBuf> {
79        let data_dir = self.apt_swarm_path()?;
80        let path = data_dir.join("storage");
81        Ok(path)
82    }
83
84    pub fn database_migrate_path(&self) -> Result<PathBuf> {
85        let data_dir = self.apt_swarm_path()?;
86        let path = data_dir.join("storage~");
87        Ok(path)
88    }
89
90    pub fn database_delete_path(&self) -> Result<PathBuf> {
91        let data_dir = self.apt_swarm_path()?;
92        let path = data_dir.join("storage=");
93        Ok(path)
94    }
95
96    pub fn db_socket_path(&self) -> Result<PathBuf> {
97        let data_dir = self.apt_swarm_path()?;
98        let path = data_dir.join("db.sock");
99        Ok(path)
100    }
101
102    fn default_config_path() -> Result<PathBuf> {
103        let config_dir = dirs::config_dir().context("Failed to detect config directory")?;
104        let path = config_dir.join("apt-swarm.conf");
105        Ok(path)
106    }
107
108    pub fn peerdb_path(&self) -> Result<PathBuf> {
109        let data_dir = self.apt_swarm_path()?;
110        let path = data_dir.join("peerdb.json");
111        Ok(path)
112    }
113
114    pub fn peerdb_new_path(&self) -> Result<PathBuf> {
115        let data_dir = self.apt_swarm_path()?;
116        let path = data_dir.join("peerdb.json-");
117        Ok(path)
118    }
119}
120
121#[derive(Debug, PartialEq, Default, Serialize, Deserialize)]
122pub struct ConfigData {
123    #[serde(rename = "repository", default)]
124    pub repositories: Vec<Repository>,
125}
126
127impl ConfigData {
128    pub fn load_config_from_str(buf: &str) -> Result<Self> {
129        let config = toml::from_str(buf)?;
130        Ok(config)
131    }
132
133    pub async fn load_config_from(path: &Path) -> Result<Self> {
134        let buf = fs::read_to_string(&path).await?;
135        Self::load_config_from_str(&buf)
136    }
137}
138
139#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
140pub struct Repository {
141    #[serde(default)]
142    pub urls: Vec<UrlSource>,
143    pub keyring: String,
144}
145
146#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
147#[serde(untagged)]
148pub enum UrlSource {
149    Url(String),
150    Detached { content: String, sig: String },
151}
152
153impl UrlSource {
154    pub async fn fetch(&self, client: &reqwest::Client) -> Result<Signed> {
155        match self {
156            UrlSource::Url(url) => {
157                let body = Self::fetch_data(client, url).await?;
158
159                let (signed, _remaining) = Signed::from_bytes(&body)
160                    .context("Failed to parse http response as release")?;
161
162                Ok(signed)
163            }
164            UrlSource::Detached { content, sig } => {
165                let content = Self::fetch_data(client, content).await?;
166                if !content.ends_with(b"\n") {
167                    bail!("Detached signatures are currently only supported if the signed data ends with a newline");
168                }
169                let sig = Self::fetch_data(client, sig).await?;
170
171                let mut reader = armor::Reader::from_bytes(
172                    &sig,
173                    armor::ReaderMode::Tolerant(Some(armor::Kind::Signature)),
174                );
175
176                let mut signature = Vec::new();
177                reader.read_to_end(&mut signature)?;
178
179                Ok(Signed {
180                    content: BString::new(content.into()),
181                    signature,
182                })
183            }
184        }
185    }
186
187    async fn fetch_data(client: &reqwest::Client, url: &str) -> Result<Bytes> {
188        info!("Fetching url {:?}...", url);
189        let r = client
190            .get(url)
191            .send()
192            .await
193            .context("Failed to send request")?
194            .error_for_status()
195            .context("Received http error")?;
196        let body = r
197            .bytes()
198            .await
199            .context("Failed to download http response")?;
200        Ok(body)
201    }
202}