[WIP] Minapk rework
This commit is contained in:
@@ -16,6 +16,7 @@ pedantic = { level = "warn", priority = -1 }
|
||||
nursery = { level = "warn", priority = -1 }
|
||||
unwrap_used = "deny"
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
|
||||
@@ -3,3 +3,5 @@ name = "minapk"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
124
crates/minapk/src/index.rs
Normal file
124
crates/minapk/src/index.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use std::{
|
||||
fs, io,
|
||||
path::{Path, PathBuf},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
usize,
|
||||
};
|
||||
|
||||
use crate::shell::{sh_out, sh_status};
|
||||
use crate::{CACHE_DIR, Cfg};
|
||||
|
||||
const INDEX_TTL_SECS: u64 = 86_400;
|
||||
|
||||
#[must_use]
|
||||
pub fn cache_index_path(cfg: &Cfg) -> PathBuf {
|
||||
PathBuf::from(CACHE_DIR).join(format!(
|
||||
"APKINDEX-{}-{}-{}.txt",
|
||||
cfg.branch, cfg.repo, cfg.arch
|
||||
))
|
||||
}
|
||||
#[must_use]
|
||||
pub fn repo_base(cfg: &Cfg) -> String {
|
||||
format!(
|
||||
"{}/alpine/{}/{}/{}/",
|
||||
cfg.mirror.trim_end_matches('/'),
|
||||
cfg.branch,
|
||||
cfg.repo,
|
||||
cfg.arch
|
||||
)
|
||||
}
|
||||
#[must_use]
|
||||
pub fn index_stale(cfg: &Cfg) -> bool {
|
||||
let p = cache_index_path(cfg);
|
||||
fs::metadata(&p)
|
||||
.and_then(|m| m.modified())
|
||||
.and_then(|t| {
|
||||
t.elapsed()
|
||||
.map_err(|_| io::Error::from(io::ErrorKind::Other))
|
||||
})
|
||||
.map_or(true, |age| age.as_secs() > INDEX_TTL_SECS)
|
||||
}
|
||||
pub fn index_refresh(cfg: &Cfg) -> Result<(), String> {
|
||||
if !index_stale(cfg) {
|
||||
return Ok(());
|
||||
}
|
||||
let url = format!("{}APKINDEX.tar.gz", repo_base(cfg));
|
||||
println!("Downloading index...");
|
||||
let txt = sh_out(r#"curl -fsSL "$1" | tar -xzO APKINDEX"#, &[&url])?;
|
||||
if txt.is_empty() {
|
||||
return Err("APKINDEX empty".into());
|
||||
}
|
||||
fs::create_dir_all(CACHE_DIR).map_err(|e| format!("mkdir cache: {e}"))?;
|
||||
fs::write(cache_index_path(cfg), txt).map_err(|e| format!("write index: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn index_ensure(cfg: &Cfg) -> Result<(), String> {
|
||||
let p = cache_index_path(cfg);
|
||||
if p.exists() {
|
||||
println!("Index already cached");
|
||||
return Ok(());
|
||||
}
|
||||
index_refresh(cfg)
|
||||
}
|
||||
|
||||
pub fn fetch_package(cfg: &Cfg, pkg_name: &str, version: &str) -> Result<PathBuf, String> {
|
||||
let url = format!("{}{}", repo_base(cfg), pkg_name);
|
||||
let dest = tmp_apk(pkg_name, version);
|
||||
let dest_str = dest.to_str().expect("Should be a valid dest string");
|
||||
println!("Downloading {url} to {dest_str}...");
|
||||
sh_status(r#"curl -fsSL "$1" -o "$2""#, &[&url, dest_str])?;
|
||||
Ok(dest)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Return: (`file_name`, `version`, `download_size`, `install_size`)
|
||||
pub fn resolve_package(cfg: &Cfg, wanted_pkg: &str) -> Option<(String, String, usize, usize)> {
|
||||
let index = fs::read_to_string(cache_index_path(cfg)).ok()?;
|
||||
let mut name = "";
|
||||
let mut version = "";
|
||||
let mut download_size = "";
|
||||
let mut install_size = "";
|
||||
|
||||
for line in index.lines().map(str::trim_end) {
|
||||
if line.is_empty() {
|
||||
if name == wanted_pkg {
|
||||
return Some((
|
||||
format!("{name}-{version}.apk"),
|
||||
version.to_string(),
|
||||
download_size.parse().ok()?,
|
||||
install_size.parse().ok()?,
|
||||
));
|
||||
}
|
||||
name = "";
|
||||
version = "";
|
||||
download_size = "";
|
||||
install_size = "";
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(x) = line.strip_prefix("P:") {
|
||||
name = x.trim();
|
||||
} else if let Some(x) = line.strip_prefix("V:") {
|
||||
version = x.trim();
|
||||
} else if let Some(x) = line.strip_prefix("S:") {
|
||||
download_size = x.trim();
|
||||
} else if let Some(x) = line.strip_prefix("I:") {
|
||||
install_size = x.trim();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn tmp_apk(name: &str, ver: &str) -> PathBuf {
|
||||
let ts = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
Path::new("/tmp").join(format!(
|
||||
"minapk-{}-{}-{}.apk",
|
||||
name,
|
||||
ver.replace('/', "_"),
|
||||
ts
|
||||
))
|
||||
}
|
||||
31
crates/minapk/src/lib.rs
Normal file
31
crates/minapk/src/lib.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub const MINAPK_DATA: &str = "/var/lib/minapk";
|
||||
pub const CACHE_DIR: &str = "/var/cache/minapk";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Cfg {
|
||||
pub root: PathBuf,
|
||||
pub name: Option<String>,
|
||||
pub arch: String,
|
||||
pub branch: String,
|
||||
pub repo: String,
|
||||
pub mirror: String,
|
||||
}
|
||||
|
||||
impl Default for Cfg {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
root: PathBuf::from("/"),
|
||||
name: None,
|
||||
arch: "riscv64".into(),
|
||||
branch: "edge".into(),
|
||||
repo: "main".into(),
|
||||
mirror: "https://dl-cdn.alpinelinux.org".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod index;
|
||||
pub mod manifest;
|
||||
pub mod shell;
|
||||
@@ -5,39 +5,16 @@
|
||||
// Defaults: --arch riscv64 --branch edge --repo main --mirror https://dl-cdn.alpinelinux.org
|
||||
// Notes: Network only for APKINDEX when stale or on package download.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, BufRead, BufReader, BufWriter, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
const MANIFEST_PATH: &str = "var/lib/minapk/manifest";
|
||||
const CACHE_DIR: &str = "/var/cache/minapk";
|
||||
const INDEX_TTL_SECS: u64 = 86_400;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Cfg {
|
||||
root: PathBuf,
|
||||
name: Option<String>,
|
||||
arch: String,
|
||||
branch: String,
|
||||
repo: String,
|
||||
mirror: String,
|
||||
}
|
||||
impl Default for Cfg {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
root: PathBuf::from("/"),
|
||||
name: None,
|
||||
arch: "riscv64".into(),
|
||||
branch: "edge".into(),
|
||||
repo: "main".into(),
|
||||
mirror: "https://dl-cdn.alpinelinux.org".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
use minapk::{
|
||||
Cfg,
|
||||
index::{fetch_package, index_ensure, index_refresh, resolve_package},
|
||||
manifest::{manifest_load, manifest_save},
|
||||
shell::{extract_filtered, space_left_on},
|
||||
};
|
||||
use std::{
|
||||
env, fs, io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
fn main() {
|
||||
if let Err(e) = run() {
|
||||
@@ -55,30 +32,33 @@ fn run() -> Result<(), String> {
|
||||
|
||||
match cmd.as_str() {
|
||||
"add" => {
|
||||
let t = pos.last().ok_or("usage: add <.apk|name>")?.clone();
|
||||
let p = Path::new(&t);
|
||||
if p.exists() || t.ends_with(".apk") {
|
||||
let to_install = pos.last().ok_or("usage: add <.apk|name>")?.clone();
|
||||
let p = Path::new(&to_install);
|
||||
if p.exists()
|
||||
|| std::path::Path::new(&to_install)
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("apk"))
|
||||
{
|
||||
add(&root, p, cfg.name)
|
||||
} else {
|
||||
// by name
|
||||
let name = t;
|
||||
let m = manifest_load(&root)?;
|
||||
// treat it as pkg name
|
||||
let name = to_install;
|
||||
let m = manifest_load()?;
|
||||
if m.contains_key(&name) {
|
||||
println!("installed {name}");
|
||||
println!("{name} is already installed");
|
||||
return Ok(());
|
||||
}
|
||||
index_ensure(&cfg)?;
|
||||
let apk = resolve_and_fetch(&cfg, &name)?;
|
||||
let res = add(&root, &apk, Some(name));
|
||||
let _ = fs::remove_file(&apk);
|
||||
res
|
||||
check_and_install(&cfg, &name, &root)?;
|
||||
println!("Installed {name}");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
"remove" => {
|
||||
let name = pos.first().ok_or("usage: remove <name>")?;
|
||||
remove(&root, name)
|
||||
}
|
||||
"list" => list(&root),
|
||||
"list" => list(),
|
||||
"update" => {
|
||||
let what = pos.first().ok_or("usage: update <index|name>")?;
|
||||
if *what == "index" {
|
||||
@@ -87,22 +67,55 @@ fn run() -> Result<(), String> {
|
||||
return Ok(());
|
||||
}
|
||||
index_refresh(&cfg)?;
|
||||
let m = manifest_load(&root)?;
|
||||
let m = manifest_load()?;
|
||||
if m.contains_key(what) {
|
||||
drop(m);
|
||||
remove(&root, what)?;
|
||||
}
|
||||
let apk = resolve_and_fetch(&cfg, what)?;
|
||||
let res = add(&root, &apk, Some(what.clone()));
|
||||
let _ = fs::remove_file(&apk);
|
||||
println!("updated");
|
||||
res
|
||||
|
||||
check_and_install(&cfg, what, &root)?;
|
||||
println!("Updated {what}");
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(usage()),
|
||||
}
|
||||
}
|
||||
|
||||
// ----- CLI -----
|
||||
fn check_and_install(cfg: &Cfg, name: &str, root: &PathBuf) -> Result<(), String> {
|
||||
let Some((file_name, version, download_size, install_size)) = resolve_package(cfg, name) else {
|
||||
return Err(format!("Package {name} not found in index"));
|
||||
};
|
||||
|
||||
println!("You are going to install {name} version {version}");
|
||||
|
||||
let availabe_space = space_left_on(root)?;
|
||||
if download_size > availabe_space {
|
||||
return Err(format!(
|
||||
"Cannot download apk archive not enough space available on {}:\n available:{availabe_space}b < required:{download_size}b",
|
||||
root.to_string_lossy()
|
||||
));
|
||||
}
|
||||
println!(
|
||||
"Unfilterd install size is {install_size}b.\n Are you sure you want to continue? [y/N]:"
|
||||
);
|
||||
let mut input = String::new();
|
||||
io::stdin()
|
||||
.read_line(&mut input)
|
||||
.map_err(|_| "Could not read response")?;
|
||||
let lower_ans = input.to_lowercase();
|
||||
let trimed_ans = lower_ans.trim();
|
||||
match trimed_ans {
|
||||
"y" | "yes" | "o" => {}
|
||||
_ => {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let apk = fetch_package(cfg, name, &version)?;
|
||||
let res = add(root, &apk, Some(name.to_string()));
|
||||
let _ = fs::remove_file(&apk);
|
||||
res
|
||||
}
|
||||
|
||||
fn parse_args(argv: &[String]) -> Result<(Cfg, String, Vec<String>), String> {
|
||||
let mut cfg = Cfg::default();
|
||||
@@ -142,11 +155,13 @@ fn parse_args(argv: &[String]) -> Result<(Cfg, String, Vec<String>), String> {
|
||||
let pos = argv.iter().skip(i + 1).cloned().collect();
|
||||
Ok((cfg, cmd, pos))
|
||||
}
|
||||
|
||||
fn req<'a>(argv: &'a [String], i: usize, f: &str) -> Result<&'a str, String> {
|
||||
argv.get(i)
|
||||
.map(|s| s.as_str())
|
||||
.map(std::string::String::as_str)
|
||||
.ok_or_else(|| format!("{f} requires a value"))
|
||||
}
|
||||
|
||||
fn usage() -> String {
|
||||
"usage:
|
||||
minapk [--root DIR] [--name NAME] [--arch ARCH] [--branch BRANCH] [--repo REPO] [--mirror URL] add <.apk|name>
|
||||
@@ -155,205 +170,6 @@ fn usage() -> String {
|
||||
minapk [--root DIR] [--arch ARCH] [--branch BRANCH] [--repo REPO] [--mirror URL] update <index|name>".into()
|
||||
}
|
||||
|
||||
// ----- Manifest -----
|
||||
|
||||
fn manifest_path(root: &Path) -> Result<PathBuf, String> {
|
||||
let p = root.join(MANIFEST_PATH);
|
||||
if let Some(d) = p.parent() {
|
||||
fs::create_dir_all(d).map_err(|e| format!("mkdir {}: {e}", d.display()))?;
|
||||
}
|
||||
Ok(p)
|
||||
}
|
||||
fn manifest_load(root: &Path) -> Result<BTreeMap<String, Vec<String>>, String> {
|
||||
let p = manifest_path(root)?;
|
||||
let f = match File::open(&p) {
|
||||
Ok(f) => f,
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(BTreeMap::new()),
|
||||
Err(e) => return Err(format!("read {}: {e}", p.display())),
|
||||
};
|
||||
let mut out: BTreeMap<String, Vec<String>> = BTreeMap::new();
|
||||
let mut cur: Option<String> = None;
|
||||
let mut files: Vec<String> = Vec::new();
|
||||
for line in BufReader::new(f).lines() {
|
||||
let line = line.map_err(|e| format!("manifest read: {e}"))?;
|
||||
if line.trim().is_empty() {
|
||||
if let Some(n) = cur.take() {
|
||||
out.insert(n, std::mem::take(&mut files));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if cur.is_none() {
|
||||
cur = Some(line.trim().into());
|
||||
} else {
|
||||
files.push(line.trim().into());
|
||||
}
|
||||
}
|
||||
if let Some(n) = cur {
|
||||
out.insert(n, files);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
fn manifest_save(root: &Path, m: &BTreeMap<String, Vec<String>>) -> Result<(), String> {
|
||||
let p = manifest_path(root)?;
|
||||
let mut w =
|
||||
BufWriter::new(File::create(&p).map_err(|e| format!("write {}: {e}", p.display()))?);
|
||||
for (n, files) in m {
|
||||
writeln!(w, "{n}").map_err(|e| format!("manifest write: {e}"))?;
|
||||
for f in files {
|
||||
writeln!(w, "{f}").map_err(|e| format!("manifest write: {e}"))?;
|
||||
}
|
||||
writeln!(w).map_err(|e| format!("manifest write: {e}"))?;
|
||||
}
|
||||
w.flush().map_err(|e| format!("manifest flush: {e}"))
|
||||
}
|
||||
|
||||
// ----- Index cache + resolver -----
|
||||
|
||||
fn cache_index_path(cfg: &Cfg) -> PathBuf {
|
||||
PathBuf::from(CACHE_DIR).join(format!(
|
||||
"APKINDEX-{}-{}-{}.txt",
|
||||
cfg.branch, cfg.repo, cfg.arch
|
||||
))
|
||||
}
|
||||
fn repo_base(cfg: &Cfg) -> String {
|
||||
format!(
|
||||
"{}/alpine/{}/{}/{}/",
|
||||
cfg.mirror.trim_end_matches('/'),
|
||||
cfg.branch,
|
||||
cfg.repo,
|
||||
cfg.arch
|
||||
)
|
||||
}
|
||||
fn index_stale(cfg: &Cfg) -> bool {
|
||||
let p = cache_index_path(cfg);
|
||||
match fs::metadata(&p).and_then(|m| m.modified()).and_then(|t| {
|
||||
t.elapsed()
|
||||
.map_err(|_| io::Error::from(io::ErrorKind::Other))
|
||||
}) {
|
||||
Ok(age) => age.as_secs() > INDEX_TTL_SECS,
|
||||
Err(_) => true,
|
||||
}
|
||||
}
|
||||
fn index_refresh(cfg: &Cfg) -> Result<(), String> {
|
||||
if !index_stale(cfg) {
|
||||
return Ok(());
|
||||
}
|
||||
let url = format!("{}APKINDEX.tar.gz", repo_base(cfg));
|
||||
println!("Downloading index...");
|
||||
let txt = sh_out(r#"curl -fsSL "$1" | tar -xzO APKINDEX"#, &[&url])?;
|
||||
if txt.is_empty() {
|
||||
return Err("APKINDEX empty".into());
|
||||
}
|
||||
fs::create_dir_all(CACHE_DIR).map_err(|e| format!("mkdir cache: {e}"))?;
|
||||
fs::write(cache_index_path(cfg), txt).map_err(|e| format!("write index: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
fn index_ensure(cfg: &Cfg) -> Result<(), String> {
|
||||
let p = cache_index_path(cfg);
|
||||
if p.exists() {
|
||||
println!("Index already cached");
|
||||
return Ok(());
|
||||
}
|
||||
index_refresh(cfg)
|
||||
}
|
||||
|
||||
fn resolve_and_fetch(cfg: &Cfg, name: &str) -> Result<PathBuf, String> {
|
||||
let idx = fs::read_to_string(cache_index_path(cfg)).map_err(|e| format!("read index: {e}"))?;
|
||||
let mut best: Option<(String, String, String)> = None; // (filename, version, arch)
|
||||
for r in parse_apkindex(&idx, name) {
|
||||
if r.2 == cfg.arch || r.2 == "noarch" {
|
||||
best = Some(match best {
|
||||
None => r,
|
||||
Some(b) => {
|
||||
if r.1 > b.1 {
|
||||
r
|
||||
} else {
|
||||
b
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
let (fname, ver, _) = best.ok_or_else(|| {
|
||||
format!(
|
||||
"{name} not found in {}/{}/{}",
|
||||
cfg.branch, cfg.repo, cfg.arch
|
||||
)
|
||||
})?;
|
||||
let url = format!("{}{}", repo_base(cfg), fname);
|
||||
let dest = tmp_apk(name, &ver);
|
||||
let dest_str = dest.to_str().unwrap();
|
||||
println!("Downloading {url} to {dest_str}...");
|
||||
sh_status(r#"curl -fsSL "$1" -o "$2""#, &[&url, dest_str])?;
|
||||
Ok(dest)
|
||||
}
|
||||
|
||||
fn parse_apkindex(s: &str, want: &str) -> Vec<(String, String, String)> {
|
||||
// returns (filename, version, arch)
|
||||
let mut out = Vec::new();
|
||||
let mut p: Option<&str> = None;
|
||||
let mut v: Option<&str> = None;
|
||||
let mut a: Option<&str> = None;
|
||||
let mut f: Option<&str> = None;
|
||||
for line in s.lines().map(|l| l.trim_end()) {
|
||||
if line.is_empty() {
|
||||
if matches!(p, Some(n) if n==want) {
|
||||
out.push((
|
||||
f.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| format!("{}-{}.apk", want, v.unwrap_or_default())),
|
||||
v.unwrap_or_default().to_string(),
|
||||
a.unwrap_or_default().to_string(),
|
||||
));
|
||||
}
|
||||
p = None;
|
||||
v = None;
|
||||
a = None;
|
||||
f = None;
|
||||
continue;
|
||||
}
|
||||
if let Some(x) = line.strip_prefix("P:") {
|
||||
p = Some(x.trim());
|
||||
continue;
|
||||
}
|
||||
if let Some(x) = line.strip_prefix("V:") {
|
||||
v = Some(x.trim());
|
||||
continue;
|
||||
}
|
||||
if let Some(x) = line.strip_prefix("A:") {
|
||||
a = Some(x.trim());
|
||||
continue;
|
||||
}
|
||||
if let Some(x) = line.strip_prefix("F:") {
|
||||
f = Some(x.trim());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if matches!(p, Some(n) if n==want) {
|
||||
out.push((
|
||||
f.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| format!("{}-{}.apk", want, v.unwrap_or_default())),
|
||||
v.unwrap_or_default().to_string(),
|
||||
a.unwrap_or_default().to_string(),
|
||||
));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn tmp_apk(name: &str, ver: &str) -> PathBuf {
|
||||
let ts = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
Path::new("/tmp").join(format!(
|
||||
"minapk-{}-{}-{}.apk",
|
||||
name,
|
||||
ver.replace('/', "_"),
|
||||
ts
|
||||
))
|
||||
}
|
||||
|
||||
// ----- Install/remove/list -----
|
||||
|
||||
fn add(root: &Path, apk: &Path, name_override: Option<String>) -> Result<(), String> {
|
||||
if !apk.exists() {
|
||||
return Err(format!("{} missing", apk.display()));
|
||||
@@ -361,7 +177,7 @@ fn add(root: &Path, apk: &Path, name_override: Option<String>) -> Result<(), Str
|
||||
let name = name_override
|
||||
.or_else(|| apk.file_stem().map(|s| s.to_string_lossy().to_string()))
|
||||
.ok_or("unable to derive package name; use --name")?;
|
||||
let mut m = manifest_load(root)?;
|
||||
let mut m = manifest_load()?;
|
||||
if m.contains_key(&name) {
|
||||
println!("installed {name}");
|
||||
return Ok(());
|
||||
@@ -379,13 +195,13 @@ fn add(root: &Path, apk: &Path, name_override: Option<String>) -> Result<(), Str
|
||||
],
|
||||
)?;
|
||||
m.insert(name.clone(), files);
|
||||
manifest_save(root, &m)?;
|
||||
manifest_save(&m)?;
|
||||
println!("installed {name}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove(root: &Path, name: &str) -> Result<(), String> {
|
||||
let mut m = manifest_load(root)?;
|
||||
let mut m = manifest_load()?;
|
||||
let Some(entries) = m.remove(name) else {
|
||||
return Err(format!("package {name} is not installed"));
|
||||
};
|
||||
@@ -416,13 +232,13 @@ fn remove(root: &Path, name: &str) -> Result<(), String> {
|
||||
for d in prune.into_iter().rev() {
|
||||
let _ = fs::remove_dir(&d);
|
||||
}
|
||||
manifest_save(root, &m)?;
|
||||
manifest_save(&m)?;
|
||||
println!("removed {name}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn list(root: &Path) -> Result<(), String> {
|
||||
let m = manifest_load(root)?;
|
||||
fn list() -> Result<(), String> {
|
||||
let m = manifest_load()?;
|
||||
if m.is_empty() {
|
||||
println!("no packages installed");
|
||||
} else {
|
||||
@@ -432,114 +248,3 @@ fn list(root: &Path) -> Result<(), String> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ----- tar + shell -----
|
||||
|
||||
fn extract_filtered(root: &Path, apk: &Path, excludes: &[&str]) -> Result<Vec<String>, String> {
|
||||
let root = root
|
||||
.to_str()
|
||||
.ok_or_else(|| format!("non UTF-8 root: {}", root.display()))?;
|
||||
let apk = apk
|
||||
.to_str()
|
||||
.ok_or_else(|| format!("non UTF-8 path: {}", apk.display()))?;
|
||||
|
||||
// 1) list filtered
|
||||
let mut list_args = vec![
|
||||
"-tf",
|
||||
apk,
|
||||
"--exclude=.PKGINFO",
|
||||
"--exclude=.SIGN*",
|
||||
];
|
||||
for pat in excludes {
|
||||
list_args.push("--exclude");
|
||||
list_args.push(pat);
|
||||
}
|
||||
let out = Command::new("tar")
|
||||
.args(&list_args)
|
||||
.output()
|
||||
.map_err(|e| format!("tar -tf: {e}"))?;
|
||||
if !out.status.success() {
|
||||
return Err("tar list failed".into());
|
||||
}
|
||||
|
||||
let files: Vec<String> = String::from_utf8_lossy(&out.stdout)
|
||||
.lines()
|
||||
.filter_map(|l| {
|
||||
let s = sanitize(l);
|
||||
if s.is_empty() || s.ends_with('/') {
|
||||
None
|
||||
} else {
|
||||
Some(s)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 2) extract filtered
|
||||
let mut ex_args = vec![
|
||||
"-xzC",
|
||||
root,
|
||||
"--exclude=.PKGINFO",
|
||||
"--exclude=.SIGN*",
|
||||
"-f",
|
||||
apk,
|
||||
];
|
||||
for pat in excludes {
|
||||
ex_args.push("--exclude");
|
||||
ex_args.push(pat);
|
||||
}
|
||||
let st = Command::new("tar")
|
||||
.args(&ex_args)
|
||||
.status()
|
||||
.map_err(|e| format!("tar -xz: {e}"))?;
|
||||
if !st.success() {
|
||||
return Err("tar extraction failed".into());
|
||||
}
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
fn sanitize(e: &str) -> String {
|
||||
let t = e.trim().trim_start_matches("./");
|
||||
if t.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
if t.starts_with('/') {
|
||||
return t.trim_start_matches('/').to_string();
|
||||
}
|
||||
if t.contains("../") {
|
||||
return String::new();
|
||||
}
|
||||
t.to_string()
|
||||
}
|
||||
|
||||
// shell helpers
|
||||
|
||||
fn sh_out(script: &str, args: &[&str]) -> Result<Vec<u8>, String> {
|
||||
let mut c = Command::new("sh");
|
||||
c.arg("-c").arg(script).arg("minapk");
|
||||
for a in args {
|
||||
c.arg(a);
|
||||
}
|
||||
let out = c.output().map_err(|e| format!("spawn: {e}"))?;
|
||||
if !out.status.success() {
|
||||
let err = String::from_utf8_lossy(&out.stderr).trim().to_string();
|
||||
return Err(if err.is_empty() {
|
||||
"command failed".into()
|
||||
} else {
|
||||
err
|
||||
});
|
||||
}
|
||||
Ok(out.stdout)
|
||||
}
|
||||
fn sh_status(script: &str, args: &[&str]) -> Result<(), String> {
|
||||
let mut c = Command::new("sh");
|
||||
c.arg("-c").arg(script).arg("minapk");
|
||||
for a in args {
|
||||
c.arg(a);
|
||||
}
|
||||
let st = c.status().map_err(|e| format!("spawn: {e}"))?;
|
||||
if !st.success() {
|
||||
return Err("command failed".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
57
crates/minapk/src/manifest.rs
Normal file
57
crates/minapk/src/manifest.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, BufRead, BufReader, BufWriter, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::MINAPK_DATA;
|
||||
|
||||
pub fn manifest_path() -> Result<PathBuf, String> {
|
||||
let p = PathBuf::from(MINAPK_DATA).join("manifset");
|
||||
if let Some(d) = p.parent() {
|
||||
fs::create_dir_all(d).map_err(|e| format!("mkdir {}: {e}", d.display()))?;
|
||||
}
|
||||
Ok(p)
|
||||
}
|
||||
pub fn manifest_load() -> Result<BTreeMap<String, Vec<String>>, String> {
|
||||
let p = manifest_path()?;
|
||||
let f = match File::open(&p) {
|
||||
Ok(f) => f,
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(BTreeMap::new()),
|
||||
Err(e) => return Err(format!("read {}: {e}", p.display())),
|
||||
};
|
||||
let mut out: BTreeMap<String, Vec<String>> = BTreeMap::new();
|
||||
let mut cur: Option<String> = None;
|
||||
let mut files: Vec<String> = Vec::new();
|
||||
for line in BufReader::new(f).lines() {
|
||||
let line = line.map_err(|e| format!("manifest read: {e}"))?;
|
||||
if line.trim().is_empty() {
|
||||
if let Some(n) = cur.take() {
|
||||
out.insert(n, std::mem::take(&mut files));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if cur.is_none() {
|
||||
cur = Some(line.trim().into());
|
||||
} else {
|
||||
files.push(line.trim().into());
|
||||
}
|
||||
}
|
||||
if let Some(n) = cur {
|
||||
out.insert(n, files);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn manifest_save(m: &BTreeMap<String, Vec<String>>) -> Result<(), String> {
|
||||
let p = manifest_path()?;
|
||||
let mut w =
|
||||
BufWriter::new(File::create(&p).map_err(|e| format!("write {}: {e}", p.display()))?);
|
||||
for (n, files) in m {
|
||||
writeln!(w, "{n}").map_err(|e| format!("manifest write: {e}"))?;
|
||||
for f in files {
|
||||
writeln!(w, "{f}").map_err(|e| format!("manifest write: {e}"))?;
|
||||
}
|
||||
writeln!(w).map_err(|e| format!("manifest write: {e}"))?;
|
||||
}
|
||||
w.flush().map_err(|e| format!("manifest flush: {e}"))
|
||||
}
|
||||
115
crates/minapk/src/shell.rs
Normal file
115
crates/minapk/src/shell.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
pub fn space_left_on(location: &PathBuf) -> Result<usize, String> {
|
||||
let a = sh_out("df $1 | awk 'NR==2 {print $3*1024}'", &[&location.to_string_lossy()])?;
|
||||
Ok(String::from_utf8_lossy(&a)
|
||||
.trim()
|
||||
.parse()
|
||||
.map_err(|_| "Could get available space")?)
|
||||
}
|
||||
|
||||
pub fn extract_filtered(root: &Path, apk: &Path, excludes: &[&str]) -> Result<Vec<String>, String> {
|
||||
let root = root
|
||||
.to_str()
|
||||
.ok_or_else(|| format!("non UTF-8 root: {}", root.display()))?;
|
||||
let apk = apk
|
||||
.to_str()
|
||||
.ok_or_else(|| format!("non UTF-8 path: {}", apk.display()))?;
|
||||
|
||||
// 1) list filtered
|
||||
let mut list_args = vec!["-tf", apk, "--exclude=.PKGINFO", "--exclude=.SIGN*"];
|
||||
for pat in excludes {
|
||||
list_args.push("--exclude");
|
||||
list_args.push(pat);
|
||||
}
|
||||
let out = Command::new("tar")
|
||||
.args(&list_args)
|
||||
.output()
|
||||
.map_err(|e| format!("tar -tf: {e}"))?;
|
||||
if !out.status.success() {
|
||||
return Err("tar list failed".into());
|
||||
}
|
||||
|
||||
let files: Vec<String> = String::from_utf8_lossy(&out.stdout)
|
||||
.lines()
|
||||
.filter_map(|l| {
|
||||
let s = sanitize(l);
|
||||
if s.is_empty() || s.ends_with('/') {
|
||||
None
|
||||
} else {
|
||||
Some(s)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 2) extract filtered
|
||||
let mut ex_args = vec![
|
||||
"-xzC",
|
||||
root,
|
||||
"--exclude=.PKGINFO",
|
||||
"--exclude=.SIGN*",
|
||||
"-f",
|
||||
apk,
|
||||
];
|
||||
for pat in excludes {
|
||||
ex_args.push("--exclude");
|
||||
ex_args.push(pat);
|
||||
}
|
||||
let st = Command::new("tar")
|
||||
.args(&ex_args)
|
||||
.status()
|
||||
.map_err(|e| format!("tar -xz: {e}"))?;
|
||||
if !st.success() {
|
||||
return Err("tar extraction failed".into());
|
||||
}
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn sanitize(e: &str) -> String {
|
||||
let t = e.trim().trim_start_matches("./");
|
||||
if t.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
if t.starts_with('/') {
|
||||
return t.trim_start_matches('/').to_string();
|
||||
}
|
||||
if t.contains("../") {
|
||||
return String::new();
|
||||
}
|
||||
t.to_string()
|
||||
}
|
||||
|
||||
pub fn sh_out(script: &str, args: &[&str]) -> Result<Vec<u8>, String> {
|
||||
let mut c = Command::new("sh");
|
||||
c.arg("-c").arg(script).arg("minapk");
|
||||
for a in args {
|
||||
c.arg(a);
|
||||
}
|
||||
let out = c.output().map_err(|e| format!("spawn: {e}"))?;
|
||||
if !out.status.success() {
|
||||
let err = String::from_utf8_lossy(&out.stderr).trim().to_string();
|
||||
return Err(if err.is_empty() {
|
||||
"command failed".into()
|
||||
} else {
|
||||
err
|
||||
});
|
||||
}
|
||||
Ok(out.stdout)
|
||||
}
|
||||
|
||||
pub fn sh_status(script: &str, args: &[&str]) -> Result<(), String> {
|
||||
let mut c = Command::new("sh");
|
||||
c.arg("-c").arg(script).arg("minapk");
|
||||
for a in args {
|
||||
c.arg(a);
|
||||
}
|
||||
let st = c.status().map_err(|e| format!("spawn: {e}"))?;
|
||||
if !st.success() {
|
||||
return Err("command failed".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
5
scripts/curl-tls.sh
Normal file
5
scripts/curl-tls.sh
Normal file
@@ -0,0 +1,5 @@
|
||||
#! /bin/sh
|
||||
import.lua
|
||||
mv cacert.pem /etc/ssl/certs
|
||||
ln -s /etc/ssl/certs/cacert.pem /etc/ssl/cert.pem
|
||||
echo tls-max = 1.2 > .curlrc
|
||||
2
scripts/setup-eth-dev.sh
Normal file
2
scripts/setup-eth-dev.sh
Normal file
@@ -0,0 +1,2 @@
|
||||
#! /bin/sh
|
||||
ip link set eth1 up && ip addr add 192.168.2.1/30 dev eth1 && ip route add default via 192.168.2.2 && echo nameserver 1.1.1.1 >/etc/resolv.conf
|
||||
Reference in New Issue
Block a user