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}