appcore_update/
provider.rs1use crate::filesystem::read_regular_file_bounded;
12use crate::{ArtifactDescriptor, UpdateError, UpdateResult};
13use appcore_contracts::ApplicationId;
14use appcore_provider::{
15 ProviderContext, ProviderError, ProviderFactory, ProviderResult, ProviderRole, SecretProvider,
16};
17use semver::Version;
18use std::path::{Path, PathBuf};
19use std::sync::Arc;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct UpdateRequest {
24 pub application_id: ApplicationId,
26 pub current_version: String,
28 pub channel: String,
30}
31
32pub trait UpdateProvider: Send + Sync {
34 fn latest(&self, request: &UpdateRequest) -> UpdateResult<Option<ArtifactDescriptor>>;
36
37 fn fetch(&self, artifact: &ArtifactDescriptor, max_bytes: usize) -> UpdateResult<Vec<u8>>;
39}
40
41pub type SharedUpdateProvider = Arc<dyn UpdateProvider>;
43
44pub const FILE_UPDATE_PROVIDER_ID: &str = "file-update";
46
47#[derive(Debug, Clone)]
49pub struct FileUpdateProvider {
50 index_path: PathBuf,
51}
52
53impl FileUpdateProvider {
54 pub fn new(index_path: impl Into<PathBuf>) -> Self {
56 Self {
57 index_path: index_path.into(),
58 }
59 }
60
61 fn read_index(&self) -> UpdateResult<Vec<ArtifactDescriptor>> {
62 let bytes = read_provider_file(&self.index_path, 1_048_576)?;
63 let artifacts: Vec<ArtifactDescriptor> = serde_json::from_slice(&bytes)
64 .map_err(|error| UpdateError::Provider(error.to_string()))?;
65 for artifact in &artifacts {
66 artifact.validate()?;
67 }
68 Ok(artifacts)
69 }
70}
71
72impl UpdateProvider for FileUpdateProvider {
73 fn latest(&self, request: &UpdateRequest) -> UpdateResult<Option<ArtifactDescriptor>> {
74 let current = Version::parse(&request.current_version).map_err(|error| {
75 UpdateError::Provider(format!("invalid installed application version: {error}"))
76 })?;
77 let mut eligible = self
78 .read_index()?
79 .into_iter()
80 .filter(|artifact| {
81 artifact.application_id() == &request.application_id
82 && artifact.channel() == request.channel
83 })
84 .filter_map(|artifact| {
85 Version::parse(artifact.application_version())
86 .ok()
87 .filter(|version| version > ¤t)
88 .map(|version| (version, artifact))
89 })
90 .collect::<Vec<_>>();
91 eligible.sort_by(|left, right| right.0.cmp(&left.0));
92 Ok(eligible.into_iter().next().map(|(_, artifact)| artifact))
93 }
94
95 fn fetch(&self, artifact: &ArtifactDescriptor, max_bytes: usize) -> UpdateResult<Vec<u8>> {
96 let path = artifact
97 .artifact_reference()
98 .strip_prefix("file:")
99 .ok_or_else(|| {
100 UpdateError::Provider("file-update artifact reference must use file:".to_string())
101 })?;
102 match read_regular_file_bounded(Path::new(path), max_bytes) {
103 Ok(bytes) => Ok(bytes),
104 Err(error) if error.kind() == std::io::ErrorKind::InvalidData => {
105 Err(UpdateError::ArtifactTooLarge { max_bytes })
106 }
107 Err(error) => Err(UpdateError::Provider(error.to_string())),
108 }
109 }
110}
111
112fn read_provider_file(path: &Path, max_bytes: usize) -> UpdateResult<Vec<u8>> {
113 read_regular_file_bounded(path, max_bytes)
114 .map_err(|error| UpdateError::Provider(error.to_string()))
115}
116
117#[derive(Debug, Clone, Copy, Default)]
119pub struct FileUpdateProviderFactory;
120
121impl ProviderFactory<SharedUpdateProvider> for FileUpdateProviderFactory {
122 fn role(&self) -> ProviderRole {
123 ProviderRole::Update
124 }
125
126 fn provider_id(&self) -> &'static str {
127 FILE_UPDATE_PROVIDER_ID
128 }
129
130 fn create(
131 &self,
132 config: &appcore_contracts::ProviderConfig,
133 _context: &ProviderContext,
134 _secrets: &dyn SecretProvider,
135 ) -> ProviderResult<SharedUpdateProvider> {
136 let endpoint = config.endpoint().ok_or_else(|| {
137 ProviderError::InvalidConfiguration(
138 "file-update provider requires an index endpoint".to_string(),
139 )
140 })?;
141 let path = endpoint.strip_prefix("file:").unwrap_or(endpoint);
142 if path.trim().is_empty() {
143 return Err(ProviderError::InvalidConfiguration(
144 "file-update index path is empty".to_string(),
145 ));
146 }
147 Ok(Arc::new(FileUpdateProvider::new(path)))
148 }
149}