batch_apk_installer 1.1.3

A command-line tool for batch installation of Android Packages (APKs).
use crate::config::{PackageID, Platform};
use crate::error::Error;
use serde::Deserialize;
use std::fs;
use std::path::PathBuf;
use std::process::Command;

pub struct PackageFile {
	path: PathBuf,
	id: PackageID,
}

impl PackageFile {
	fn try_from_path(path: &PathBuf) -> Result<PackageFile, Error> {
		let path_str = path.to_str().ok_or(Error::MalformedPackageFilePath)?;
		let output = Command::new("aapt2")
			.args(["dump", "packagename", path_str])
			.output()?;
		if output.status.success() {
			let output = String::from_utf8(output.stdout)?;
			let id = String::from(output.trim_end());
			let path = PathBuf::from(path);
			let package = PackageFile { path, id };
			Ok(package)
		} else {
			Err(Error::PackageNameNotFound)
		}
	}
}

#[derive(Deserialize)]
pub struct PackageConfig {
	id: PackageID,
	platforms: Vec<Platform>,
	match_file_name: bool,
}

pub struct Package {
	id: PackageID,
	path: String,
	file_name: String,
	platforms: Vec<Platform>,
	match_file_name: bool,
}

impl Package {
	pub fn try_new(
		file: PackageFile, platforms: Vec<Platform>, match_file_name: bool,
	) -> Option<Package> {
		let file_name = file.path.file_name()?.to_str()?.to_string();
		let path = file.path.into_os_string().into_string().ok()?;
		let package = Package {
			id: file.id,
			path,
			file_name,
			platforms,
			match_file_name,
		};
		Some(package)
	}

	pub fn id(&self) -> &PackageID {
		&self.id
	}

	pub fn path(&self) -> &String {
		&self.path
	}

	pub fn platforms(&self) -> &[Platform] {
		&self.platforms
	}

	pub fn file_name(&self) -> &String {
		&self.file_name
	}

	pub fn match_file_name(&self) -> bool {
		self.match_file_name
	}

	fn build_package(file: PackageFile, configs: &[PackageConfig]) -> Option<Package> {
		let config = configs.iter().find(|c| c.id == file.id)?;
		let package = Self::try_new(file, config.platforms.clone(), config.match_file_name)?;
		Some(package)
	}

	pub fn find_all(dir: &PathBuf, configs: &[PackageConfig]) -> Result<Vec<Package>, Error> {
		if let Ok(true) = fs::exists(dir) {
			let files = find_apk_files(dir)?
				.into_iter()
				.filter_map(|f| PackageFile::try_from_path(&f).ok())
				.filter_map(|f| Self::build_package(f, configs))
				.collect();
			Ok(files)
		} else {
			let path = dir.to_string_lossy().to_string();
			Err(Error::NoPackageDirectory(path))
		}
	}
}

fn find_apk_files(dir: &PathBuf) -> Result<Vec<PathBuf>, Error> {
	let entries = fs::read_dir(dir)?
		.filter_map(Result::ok)
		.map(|e| e.path())
		.filter(|path| path.extension().is_some_and(|ext| ext == "apk"))
		.collect();
	Ok(entries)
}