pray_core/
distribution.rs1use 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}