Skip to main content

appcore_update/
provider.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: provider.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/07/22 15:41:18 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/08/02 13:24:05 by dnettoRaw
8//      ###########      S: 1.0.1-rc.8
9// =============================================================================
10
11use 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/// Query used to select an update candidate.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct UpdateRequest {
24    /// Installed application identity.
25    pub application_id: ApplicationId,
26    /// Current semantic application version.
27    pub current_version: String,
28    /// Selected update channel.
29    pub channel: String,
30}
31
32/// Provider contract for listing and fetching opaque application artifacts.
33pub trait UpdateProvider: Send + Sync {
34    /// Returns the newest eligible artifact, or `None` when no update exists.
35    fn latest(&self, request: &UpdateRequest) -> UpdateResult<Option<ArtifactDescriptor>>;
36
37    /// Fetches complete artifact bytes while respecting `max_bytes`.
38    fn fetch(&self, artifact: &ArtifactDescriptor, max_bytes: usize) -> UpdateResult<Vec<u8>>;
39}
40
41/// Shared update provider interface produced by deployment factories.
42pub type SharedUpdateProvider = Arc<dyn UpdateProvider>;
43
44/// Provider ID for the local-first JSON index and file artifact adapter.
45pub const FILE_UPDATE_PROVIDER_ID: &str = "file-update";
46
47/// Local-first update provider backed by a bounded JSON artifact index.
48#[derive(Debug, Clone)]
49pub struct FileUpdateProvider {
50    index_path: PathBuf,
51}
52
53impl FileUpdateProvider {
54    /// Creates a provider from an installation-owned index path.
55    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 > &current)
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/// Factory for the local-first file update provider.
118#[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}