#[cfg(feature = "github-update")]
use self_update::cargo_crate_version;
use flate2::read::GzDecoder;
use std::collections::HashSet;
use std::fs;
#[cfg(feature = "github-update")]
use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;
use tar::Archive;
use walkdir::WalkDir;
const LENSFUN_ARCHIVE_URL: &str = "https://codeload.github.com/lensfun/lensfun/tar.gz/master";
const LENSFUN_DB_SUBDIR: &str = "data/db";
#[cfg(feature = "github-update")]
pub(crate) fn run_binary_update(timeout: Duration) -> anyhow::Result<String> {
if !api_host_is_reachable("api.github.com", 443, timeout) {
anyhow::bail!("github host unavailable: api.github.com:443");
}
let status = check_for_update()?;
if let self_update::Status::UpToDate(version) = status {
return Ok(format!("binary already up-to-date (v{version})"));
}
Ok(status.to_string())
}
#[cfg(not(feature = "github-update"))]
pub(crate) fn run_binary_update(_timeout: Duration) -> anyhow::Result<String> {
anyhow::bail!(
"mini-film binary updater disabled in this build (build with --features github-update)"
)
}
#[cfg(feature = "github-update")]
fn check_for_update() -> anyhow::Result<self_update::Status> {
let target = asset_target();
let status = self_update::backends::github::Update::configure()
.repo_owner("alfanick")
.repo_name("mini-film")
.bin_name("mini-film")
.target(&target)
.no_confirm(true)
.show_download_progress(false)
.current_version(cargo_crate_version!())
.build()?
.update()?;
Ok(status)
}
#[cfg(feature = "github-update")]
fn api_host_is_reachable(host: &str, port: u16, timeout: Duration) -> bool {
let Some(target) = (host, port)
.to_socket_addrs()
.ok()
.and_then(|mut addresses| {
addresses.find(|address| matches!(address, SocketAddr::V4(_) | SocketAddr::V6(_)))
})
else {
return false;
};
TcpStream::connect_timeout(&target, timeout).is_ok()
}
#[cfg(feature = "github-update")]
fn asset_target() -> String {
let base = if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
"x86_64-pc-windows-msvc"
} else if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
"x86_64-unknown-linux-gnu"
} else if cfg!(all(target_os = "linux", target_arch = "aarch64")) {
"aarch64-unknown-linux-gnu"
} else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
"aarch64-apple-darwin"
} else {
self_update::get_target()
};
if cfg!(feature = "desktop-app") {
format!("{base}-gui")
} else {
base.to_string()
}
}
pub(crate) fn run_lensfun_update() -> anyhow::Result<LensfunUpdateReport> {
let download_dir = tempfile::tempdir()?;
let archive_path = download_dir.path().join("lensfun.tar.gz");
download_lensfun_archive(&archive_path)?;
let mut archive = Archive::new(GzDecoder::new(fs::File::open(&archive_path)?));
let extracted = download_dir.path().join("source");
archive.unpack(&extracted)?;
let source_db = locate_source_db_dir(&extracted)?;
let target_cache = lensfun_cache_dir();
let copied = copy_directory_contents(&source_db, &target_cache)?;
let synced = mirror_to_default_lensfun_data_root(&target_cache)?;
Ok(LensfunUpdateReport {
cache_dir: target_cache,
synced_dir: synced,
copied_files: copied,
})
}
fn download_lensfun_archive(output: &Path) -> anyhow::Result<()> {
let status = Command::new("curl")
.arg("--fail")
.arg("--location")
.arg("--silent")
.arg("--show-error")
.arg("--max-time")
.arg("120")
.arg("--output")
.arg(output)
.arg(LENSFUN_ARCHIVE_URL)
.status()
.map_err(|error| anyhow::anyhow!("running curl for Lensfun archive: {error}"))?;
if !status.success() {
anyhow::bail!("curl failed downloading Lensfun archive with status {status}");
}
Ok(())
}
fn locate_source_db_dir(extracted_root: &Path) -> anyhow::Result<PathBuf> {
let top = extracted_root
.read_dir()?
.next()
.ok_or_else(|| anyhow::anyhow!("lensfun archive missing extracted root entries"))?
.map_err(|err| anyhow::anyhow!(err))?
.path();
let source = top.join(LENSFUN_DB_SUBDIR);
if !source.is_dir() {
anyhow::bail!("lensfun archive does not contain {LENSFUN_DB_SUBDIR}");
}
Ok(source)
}
fn copy_directory_contents(source: &Path, destination: &Path) -> anyhow::Result<usize> {
if destination.exists() {
fs::remove_dir_all(destination)?;
}
fs::create_dir_all(destination)?;
let mut copied = 0usize;
for entry in WalkDir::new(source).into_iter().filter_map(Result::ok) {
let relative = entry.path().strip_prefix(source)?;
let target = destination.join(relative);
if entry.file_type().is_dir() {
fs::create_dir_all(&target)?;
continue;
}
if let Some(parent) = target.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(entry.path(), &target)?;
copied = copied.saturating_add(1);
}
Ok(copied)
}
pub(crate) fn lensfun_cache_dir() -> PathBuf {
home_dir().join(".cache").join("mini-film").join("lensfun")
}
fn home_dir() -> PathBuf {
std::env::var_os("HOME")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."))
}
fn mirror_to_default_lensfun_data_root(cache: &Path) -> anyhow::Result<PathBuf> {
let data_root = lensfun_default_data_root();
let mut copied_dirs = HashSet::new();
if data_root.exists() {
if data_root == cache {
return Ok(data_root);
}
fs::remove_dir_all(&data_root)?;
}
fs::create_dir_all(data_root.parent().ok_or_else(|| {
anyhow::anyhow!(
"lensfun data root has no parent directory: {}",
data_root.display()
)
})?)?;
for entry in WalkDir::new(cache).into_iter().filter_map(Result::ok) {
let rel = entry.path().strip_prefix(cache)?;
let target = data_root.join(rel);
if entry.file_type().is_dir() {
copied_dirs.insert(target.parent().unwrap_or(data_root.as_path()).to_path_buf());
fs::create_dir_all(&target)?;
continue;
}
if let Some(parent) = target.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(entry.path(), &target)?;
}
if copied_dirs.is_empty() {
fs::create_dir_all(&data_root)?;
}
Ok(data_root)
}
fn lensfun_default_data_root() -> PathBuf {
#[cfg(target_os = "linux")]
{
let base = std::env::var_os("XDG_DATA_HOME")
.map(PathBuf::from)
.or_else(|| {
std::env::var_os("HOME")
.map(PathBuf::from)
.map(|home| home.join(".local/share"))
})
.unwrap_or_else(|| PathBuf::from("."));
base.join("lensfun")
}
#[cfg(target_os = "macos")]
{
home_dir()
.join("Library")
.join("Application Support")
.join("lensfun")
}
#[cfg(target_os = "windows")]
{
let base = std::env::var_os("LOCALAPPDATA")
.or_else(|| std::env::var_os("APPDATA"))
.map(PathBuf::from)
.or_else(|| Some(home_dir().join("AppData").join("Local")))
.unwrap();
base.join("lensfun")
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
home_dir().join(".local").join("share").join("lensfun")
}
}
#[derive(Debug)]
pub(crate) struct LensfunUpdateReport {
pub(crate) cache_dir: PathBuf,
pub(crate) synced_dir: PathBuf,
pub(crate) copied_files: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lensfun_default_data_root_is_deterministic() {
let _ = lensfun_default_data_root();
}
#[test]
fn lensfun_report_fields_are_readable() {
let report = LensfunUpdateReport {
cache_dir: PathBuf::from("cache"),
synced_dir: PathBuf::from("synced"),
copied_files: 7,
};
assert_eq!(report.copied_files, 7);
assert_eq!(report.cache_dir, PathBuf::from("cache"));
assert_eq!(report.synced_dir, PathBuf::from("synced"));
}
}