use crate::filesystem::read_regular_file_bounded;
use crate::{ArtifactDescriptor, UpdateError, UpdateResult};
use appcore_contracts::ApplicationId;
use appcore_provider::{
ProviderContext, ProviderError, ProviderFactory, ProviderResult, ProviderRole, SecretProvider,
};
use semver::Version;
use std::path::{Path, PathBuf};
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpdateRequest {
pub application_id: ApplicationId,
pub current_version: String,
pub channel: String,
}
pub trait UpdateProvider: Send + Sync {
fn latest(&self, request: &UpdateRequest) -> UpdateResult<Option<ArtifactDescriptor>>;
fn fetch(&self, artifact: &ArtifactDescriptor, max_bytes: usize) -> UpdateResult<Vec<u8>>;
}
pub type SharedUpdateProvider = Arc<dyn UpdateProvider>;
pub const FILE_UPDATE_PROVIDER_ID: &str = "file-update";
#[derive(Debug, Clone)]
pub struct FileUpdateProvider {
index_path: PathBuf,
}
impl FileUpdateProvider {
pub fn new(index_path: impl Into<PathBuf>) -> Self {
Self {
index_path: index_path.into(),
}
}
fn read_index(&self) -> UpdateResult<Vec<ArtifactDescriptor>> {
let bytes = read_provider_file(&self.index_path, 1_048_576)?;
let artifacts: Vec<ArtifactDescriptor> = serde_json::from_slice(&bytes)
.map_err(|error| UpdateError::Provider(error.to_string()))?;
for artifact in &artifacts {
artifact.validate()?;
}
Ok(artifacts)
}
}
impl UpdateProvider for FileUpdateProvider {
fn latest(&self, request: &UpdateRequest) -> UpdateResult<Option<ArtifactDescriptor>> {
let current = Version::parse(&request.current_version).map_err(|error| {
UpdateError::Provider(format!("invalid installed application version: {error}"))
})?;
let mut eligible = self
.read_index()?
.into_iter()
.filter(|artifact| {
artifact.application_id() == &request.application_id
&& artifact.channel() == request.channel
})
.filter_map(|artifact| {
Version::parse(artifact.application_version())
.ok()
.filter(|version| version > ¤t)
.map(|version| (version, artifact))
})
.collect::<Vec<_>>();
eligible.sort_by(|left, right| right.0.cmp(&left.0));
Ok(eligible.into_iter().next().map(|(_, artifact)| artifact))
}
fn fetch(&self, artifact: &ArtifactDescriptor, max_bytes: usize) -> UpdateResult<Vec<u8>> {
let path = artifact
.artifact_reference()
.strip_prefix("file:")
.ok_or_else(|| {
UpdateError::Provider("file-update artifact reference must use file:".to_string())
})?;
match read_regular_file_bounded(Path::new(path), max_bytes) {
Ok(bytes) => Ok(bytes),
Err(error) if error.kind() == std::io::ErrorKind::InvalidData => {
Err(UpdateError::ArtifactTooLarge { max_bytes })
}
Err(error) => Err(UpdateError::Provider(error.to_string())),
}
}
}
fn read_provider_file(path: &Path, max_bytes: usize) -> UpdateResult<Vec<u8>> {
read_regular_file_bounded(path, max_bytes)
.map_err(|error| UpdateError::Provider(error.to_string()))
}
#[derive(Debug, Clone, Copy, Default)]
pub struct FileUpdateProviderFactory;
impl ProviderFactory<SharedUpdateProvider> for FileUpdateProviderFactory {
fn role(&self) -> ProviderRole {
ProviderRole::Update
}
fn provider_id(&self) -> &'static str {
FILE_UPDATE_PROVIDER_ID
}
fn create(
&self,
config: &appcore_contracts::ProviderConfig,
_context: &ProviderContext,
_secrets: &dyn SecretProvider,
) -> ProviderResult<SharedUpdateProvider> {
let endpoint = config.endpoint().ok_or_else(|| {
ProviderError::InvalidConfiguration(
"file-update provider requires an index endpoint".to_string(),
)
})?;
let path = endpoint.strip_prefix("file:").unwrap_or(endpoint);
if path.trim().is_empty() {
return Err(ProviderError::InvalidConfiguration(
"file-update index path is empty".to_string(),
));
}
Ok(Arc::new(FileUpdateProvider::new(path)))
}
}