appcore_update/
provider.rs1use 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#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct UpdateRequest {
14 pub application_id: ApplicationId,
16 pub current_version: String,
18 pub channel: String,
20}
21
22pub trait UpdateProvider: Send + Sync {
24 fn latest(&self, request: &UpdateRequest) -> UpdateResult<Option<ArtifactDescriptor>>;
26
27 fn fetch(&self, artifact: &ArtifactDescriptor, max_bytes: usize) -> UpdateResult<Vec<u8>>;
29}
30
31pub type SharedUpdateProvider = Arc<dyn UpdateProvider>;
33
34pub const FILE_UPDATE_PROVIDER_ID: &str = "file-update";
36
37#[derive(Debug, Clone)]
39pub struct FileUpdateProvider {
40 index_path: PathBuf,
41}
42
43impl FileUpdateProvider {
44 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 > ¤t)
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#[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}