Skip to main content

pray_core/
distribution.rs

1use crate::registry_http::{http_get, join_url};
2use crate::{PrayError, PrayResult};
3use serde::{Deserialize, Serialize};
4use std::fs;
5use std::path::Path;
6
7pub const DISTRIBUTION_CONFIG_SPEC: &str = "pray-distribution-config-1";
8const TORRENT_PROTOCOL: &str = "torrent";
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(deny_unknown_fields)]
12pub struct RegistryDistributionSettings {
13    #[serde(default = "distribution_config_spec")]
14    pub spec: String,
15    #[serde(default)]
16    pub protocols: Vec<String>,
17    #[serde(default, skip_serializing_if = "Vec::is_empty")]
18    pub bootstrap_trackers: Vec<String>,
19    #[serde(default, skip_serializing_if = "is_false")]
20    pub enable_dht: bool,
21}
22
23fn is_false(value: &bool) -> bool {
24    !*value
25}
26
27impl Default for RegistryDistributionSettings {
28    fn default() -> Self {
29        Self {
30            spec: distribution_config_spec(),
31            protocols: Vec::new(),
32            bootstrap_trackers: Vec::new(),
33            enable_dht: false,
34        }
35    }
36}
37
38fn distribution_config_spec() -> String {
39    DISTRIBUTION_CONFIG_SPEC.to_string()
40}
41
42impl RegistryDistributionSettings {
43    pub fn allows_torrent(&self) -> bool {
44        self.protocols.iter().any(|name| name == TORRENT_PROTOCOL)
45    }
46
47    fn validate(&self) -> PrayResult<()> {
48        if self.spec != DISTRIBUTION_CONFIG_SPEC {
49            return Err(parse_error(format!(
50                "unsupported distribution config spec: {}",
51                self.spec
52            )));
53        }
54        for name in &self.protocols {
55            if name != TORRENT_PROTOCOL {
56                return Err(parse_error(format!("unsupported protocol: {name}")));
57            }
58        }
59        if self.enable_dht {
60            return Err(parse_error("DHT announce is not implemented".to_string()));
61        }
62        if !self.bootstrap_trackers.is_empty() && !self.allows_torrent() {
63            return Err(parse_error(
64                "bootstrap_trackers requires protocols to include torrent".to_string(),
65            ));
66        }
67        Ok(())
68    }
69}
70
71fn parse_error(message: String) -> PrayError {
72    PrayError::Parse {
73        kind: "distribution config",
74        message,
75    }
76}
77
78pub fn read_registry_distribution_settings(
79    root: &Path,
80) -> PrayResult<RegistryDistributionSettings> {
81    let path = root.join("v1/distribution.json");
82    let Ok(text) = fs::read_to_string(&path) else {
83        return Ok(RegistryDistributionSettings::default());
84    };
85    parse_registry_distribution_settings(&text)
86}
87
88pub fn parse_registry_distribution_settings(
89    text: &str,
90) -> PrayResult<RegistryDistributionSettings> {
91    let settings: RegistryDistributionSettings =
92        serde_json::from_str(text).map_err(|error| parse_error(error.to_string()))?;
93    settings.validate()?;
94    Ok(settings)
95}
96
97pub fn write_registry_distribution_settings(
98    root: &Path,
99    settings: &RegistryDistributionSettings,
100) -> PrayResult<()> {
101    settings.validate()?;
102    let path = root.join("v1/distribution.json");
103    if let Some(parent) = path.parent() {
104        fs::create_dir_all(parent)?;
105    }
106    fs::write(
107        path,
108        serde_json::to_string_pretty(settings)
109            .map_err(|error| PrayError::Manifest(error.to_string()))?,
110    )?;
111    Ok(())
112}
113
114pub fn fetch_registry_distribution_settings(
115    source_url: &str,
116) -> PrayResult<RegistryDistributionSettings> {
117    crate::registry_http_cache::registry_distribution_settings(source_url, || {
118        let url = join_url(source_url, "v1/distribution.json");
119        match http_get(&url) {
120            Ok(bytes) => {
121                let text =
122                    String::from_utf8(bytes).map_err(|error| parse_error(error.to_string()))?;
123                parse_registry_distribution_settings(&text)
124            }
125            Err(PrayError::Resolution(message) | PrayError::Network(message))
126                if message.contains("HTTP 404") =>
127            {
128                Ok(RegistryDistributionSettings::default())
129            }
130            Err(error) => Err(error),
131        }
132    })
133}