Skip to main content

appcore_update/
provider.rs

1use crate::filesystem::read_regular_file_bounded;
2use crate::{ArtifactDescriptor, UpdateError, UpdateResult};
3use appcore_contracts::ApplicationId;
4use appcore_provider::{
5    ProviderContext, ProviderError, ProviderFactory, ProviderResult, ProviderRole, SecretProvider,
6};
7use semver::Version;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11/// Query used to select an update candidate.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct UpdateRequest {
14    /// Installed application identity.
15    pub application_id: ApplicationId,
16    /// Current semantic application version.
17    pub current_version: String,
18    /// Selected update channel.
19    pub channel: String,
20}
21
22/// Provider contract for listing and fetching opaque application artifacts.
23pub trait UpdateProvider: Send + Sync {
24    /// Returns the newest eligible artifact, or `None` when no update exists.
25    fn latest(&self, request: &UpdateRequest) -> UpdateResult<Option<ArtifactDescriptor>>;
26
27    /// Fetches complete artifact bytes while respecting `max_bytes`.
28    fn fetch(&self, artifact: &ArtifactDescriptor, max_bytes: usize) -> UpdateResult<Vec<u8>>;
29}
30
31/// Shared update provider interface produced by deployment factories.
32pub type SharedUpdateProvider = Arc<dyn UpdateProvider>;
33
34/// Provider ID for the local-first JSON index and file artifact adapter.
35pub const FILE_UPDATE_PROVIDER_ID: &str = "file-update";
36
37/// Local-first update provider backed by a bounded JSON artifact index.
38#[derive(Debug, Clone)]
39pub struct FileUpdateProvider {
40    index_path: PathBuf,
41}
42
43impl FileUpdateProvider {
44    /// Creates a provider from an installation-owned index path.
45    pub fn new(index_path: impl Into<PathBuf>) -> Self {
46        Self {
47            index_path: index_path.into(),
48        }
49    }
50
51    fn read_index(&self) -> UpdateResult<Vec<ArtifactDescriptor>> {
52        let bytes = read_provider_file(&self.index_path, 1_048_576)?;
53        let artifacts: Vec<ArtifactDescriptor> = serde_json::from_slice(&bytes)
54            .map_err(|error| UpdateError::Provider(error.to_string()))?;
55        for artifact in &artifacts {
56            artifact.validate()?;
57        }
58        Ok(artifacts)
59    }
60}
61
62impl UpdateProvider for FileUpdateProvider {
63    fn latest(&self, request: &UpdateRequest) -> UpdateResult<Option<ArtifactDescriptor>> {
64        let current = Version::parse(&request.current_version).map_err(|error| {
65            UpdateError::Provider(format!("invalid installed application version: {error}"))
66        })?;
67        let mut eligible = self
68            .read_index()?
69            .into_iter()
70            .filter(|artifact| {
71                artifact.application_id() == &request.application_id
72                    && artifact.channel() == request.channel
73            })
74            .filter_map(|artifact| {
75                Version::parse(artifact.application_version())
76                    .ok()
77                    .filter(|version| version > &current)
78                    .map(|version| (version, artifact))
79            })
80            .collect::<Vec<_>>();
81        eligible.sort_by(|left, right| right.0.cmp(&left.0));
82        Ok(eligible.into_iter().next().map(|(_, artifact)| artifact))
83    }
84
85    fn fetch(&self, artifact: &ArtifactDescriptor, max_bytes: usize) -> UpdateResult<Vec<u8>> {
86        let path = artifact
87            .artifact_reference()
88            .strip_prefix("file:")
89            .ok_or_else(|| {
90                UpdateError::Provider("file-update artifact reference must use file:".to_string())
91            })?;
92        match read_regular_file_bounded(Path::new(path), max_bytes) {
93            Ok(bytes) => Ok(bytes),
94            Err(error) if error.kind() == std::io::ErrorKind::InvalidData => {
95                Err(UpdateError::ArtifactTooLarge { max_bytes })
96            }
97            Err(error) => Err(UpdateError::Provider(error.to_string())),
98        }
99    }
100}
101
102fn read_provider_file(path: &Path, max_bytes: usize) -> UpdateResult<Vec<u8>> {
103    read_regular_file_bounded(path, max_bytes)
104        .map_err(|error| UpdateError::Provider(error.to_string()))
105}
106
107/// Factory for the local-first file update provider.
108#[derive(Debug, Clone, Copy, Default)]
109pub struct FileUpdateProviderFactory;
110
111impl ProviderFactory<SharedUpdateProvider> for FileUpdateProviderFactory {
112    fn role(&self) -> ProviderRole {
113        ProviderRole::Update
114    }
115
116    fn provider_id(&self) -> &'static str {
117        FILE_UPDATE_PROVIDER_ID
118    }
119
120    fn create(
121        &self,
122        config: &appcore_contracts::ProviderConfig,
123        _context: &ProviderContext,
124        _secrets: &dyn SecretProvider,
125    ) -> ProviderResult<SharedUpdateProvider> {
126        let endpoint = config.endpoint().ok_or_else(|| {
127            ProviderError::InvalidConfiguration(
128                "file-update provider requires an index endpoint".to_string(),
129            )
130        })?;
131        let path = endpoint.strip_prefix("file:").unwrap_or(endpoint);
132        if path.trim().is_empty() {
133            return Err(ProviderError::InvalidConfiguration(
134                "file-update index path is empty".to_string(),
135            ));
136        }
137        Ok(Arc::new(FileUpdateProvider::new(path)))
138    }
139}