use std::fs;
use std::path::Path;
use std::path::PathBuf;
use semver::Version;
use crate::CodePushError;
use crate::delta::Delta;
use crate::delta::apply_delta;
use crate::delta::create_delta;
use crate::manifest::BundleManifest;
use crate::manifest::bundle_directory;
use crate::manifest::read_container;
#[derive(Debug, Clone)]
pub struct ReleaseRecord {
pub manifest: BundleManifest,
pub container: Vec<u8>,
pub delta_from_previous: Option<Delta>,
}
#[derive(Debug, Clone)]
pub struct ReleaseStore {
root: PathBuf,
}
impl ReleaseStore {
pub fn open(root: impl Into<PathBuf>) -> Result<Self, CodePushError> {
let root = root.into();
fs::create_dir_all(&root)?;
Ok(Self { root })
}
pub fn root(&self) -> &Path {
&self.root
}
fn release_dir(&self, app: &str, channel: &str, manifest: &BundleManifest) -> PathBuf {
self.root
.join(sanitize(app))
.join(sanitize(channel))
.join(format!("{}+{}", manifest.version, manifest.build))
}
pub fn ingest_directory(
&self,
app: &str,
channel: &str,
version: &Version,
build: u64,
dir: &Path,
mandatory: bool,
) -> Result<ReleaseRecord, CodePushError> {
let staging = self.root.join(".staging");
fs::create_dir_all(&staging)?;
let container_path = staging.join(format!("{}.{}.lkb", sanitize(app), sanitize(channel)));
let mut manifest = bundle_directory(dir, &container_path)?;
manifest.app = app.into();
manifest.channel = channel.into();
manifest.version = version.clone();
manifest.build = build;
manifest.mandatory = mandatory;
let container = fs::read(&container_path)?;
let parsed = read_container(&container)?;
let mut blobs = Vec::with_capacity(parsed.files.len());
for (_, content) in &parsed.files {
let mut encoder =
flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::best());
use std::io::Write;
encoder.write_all(content)?;
blobs.push(encoder.finish()?);
}
let previous = self.latest(app, channel)?;
let delta = previous
.filter(|previous| previous.manifest.bundle_hash() != manifest.bundle_hash())
.map(|previous| create_delta(&previous.container, &container));
let release_dir = self.release_dir(app, channel, &manifest);
fs::create_dir_all(&release_dir)?;
crate::manifest::write_container(&release_dir.join("release.lkb"), &manifest, &blobs)?;
let _ = fs::remove_file(&container_path);
if let Some(delta) = &delta {
let delta_bytes = serde_json::to_vec_pretty(delta)?;
fs::write(
release_dir.join("delta_from_previous.jsondelta"),
delta_bytes,
)?;
}
Ok(ReleaseRecord {
manifest,
container,
delta_from_previous: delta,
})
}
pub fn list(&self, app: &str, channel: &str) -> Result<Vec<ReleaseRecord>, StoreError> {
let channel_dir = self.root.join(sanitize(app)).join(sanitize(channel));
let mut records = Vec::new();
let entries = match fs::read_dir(&channel_dir) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(records),
Err(error) => return Err(error.into()),
};
for entry in entries {
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
let container = fs::read(entry.path().join("release.lkb"))?;
let parsed = read_container(&container)?;
let delta = match fs::read(entry.path().join("delta_from_previous.jsondelta")) {
Ok(bytes) => Some(serde_json::from_slice(&bytes)?),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
Err(error) => return Err(error.into()),
};
records.push(ReleaseRecord {
manifest: parsed.manifest,
container,
delta_from_previous: delta,
});
}
records.sort_by(|left, right| {
(left.manifest.version.clone(), left.manifest.build)
.cmp(&(right.manifest.version.clone(), right.manifest.build))
});
records.reverse();
Ok(records)
}
pub fn latest(&self, app: &str, channel: &str) -> Result<Option<ReleaseRecord>, StoreError> {
Ok(self.list(app, channel)?.into_iter().next())
}
pub fn find(
&self,
app: &str,
channel: &str,
version: &Version,
build: u64,
) -> Result<Option<ReleaseRecord>, StoreError> {
Ok(self
.list(app, channel)?
.into_iter()
.find(|record| record.manifest.version == *version && record.manifest.build == build))
}
pub fn apply_stored_delta(&self, delta: &Delta, base: &[u8]) -> Result<Vec<u8>, CodePushError> {
apply_delta(base, delta)
}
}
fn sanitize(component: &str) -> String {
component
.chars()
.map(|character| {
if character.is_ascii_alphanumeric() || character == '-' || character == '_' {
character
} else {
'_'
}
})
.collect()
}
#[derive(Debug, thiserror::Error)]
pub enum StoreError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("serialization error: {0}")]
Serialize(#[from] serde_json::Error),
#[error("code push error: {0}")]
CodePush(#[from] CodePushError),
}