Add update and check commands with UTC package timestamps

This commit is contained in:
2026-03-10 13:50:11 -05:00
parent 242d9a7384
commit 233bd1f1fe
12 changed files with 1823 additions and 28 deletions
+153 -12
View File
@@ -1,5 +1,6 @@
//! Repository management and SQLite database generation
use crate::metadata_time;
use anyhow::{Context, Result};
use rusqlite::{Connection, params};
use sha2::{Digest, Sha256, Sha512};
@@ -73,6 +74,7 @@ struct IndexedPackage {
name: String,
version: String,
revision: u32,
completed_at: Option<i64>,
description: Option<String>,
homepage: Option<String>,
license: Option<String>,
@@ -107,6 +109,7 @@ pub struct BinaryRepoPackageRecord {
pub name: String,
pub version: String,
pub revision: u32,
pub completed_at: Option<i64>,
pub filename: String,
pub size: u64,
pub sha256: String,
@@ -279,6 +282,7 @@ impl RepoManager {
name TEXT NOT NULL,
version TEXT NOT NULL,
revision INTEGER NOT NULL,
completed_at INTEGER,
description TEXT,
homepage TEXT,
license TEXT,
@@ -335,6 +339,7 @@ impl RepoManager {
let mut name = String::new();
let mut version = String::new();
let mut revision = 1;
let mut completed_at = path_modified_unix_timestamp(pkg_path)?;
let mut description = None;
let mut homepage = None;
let mut license = None;
@@ -372,6 +377,8 @@ impl RepoManager {
.get("revision")
.and_then(|v| v.as_integer())
.unwrap_or(1) as u32;
completed_at =
metadata_time::parse_completed_at_value(&metadata).or(completed_at);
description = metadata
.get("description")
.and_then(|v| v.as_str())
@@ -444,6 +451,7 @@ impl RepoManager {
name,
version,
revision,
completed_at,
description,
homepage,
license,
@@ -463,6 +471,7 @@ impl RepoManager {
name,
version,
revision,
completed_at,
description,
homepage,
license,
@@ -478,12 +487,13 @@ impl RepoManager {
// Insert into database
conn.execute(
"INSERT INTO packages (name, version, revision, description, homepage, license, filename, size, sha256, sha512)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
"INSERT INTO packages (name, version, revision, completed_at, description, homepage, license, filename, size, sha256, sha512)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
params![
name,
version,
revision as i64,
completed_at,
description,
homepage,
license,
@@ -1503,12 +1513,18 @@ fn find_cached_binary_repo_packages(
let conn = Connection::open(db_path)
.with_context(|| format!("Failed to open binary repo DB {}", db_path.display()))?;
let mut stmt = conn.prepare(
let completed_at_expr = if repo_packages_have_completed_at(&conn)? {
"p.completed_at"
} else {
"NULL"
};
let sql = format!(
"SELECT
p.id,
p.name,
p.version,
p.revision,
{completed_at_expr},
p.filename,
p.size,
p.sha256,
@@ -1525,8 +1541,9 @@ fn find_cached_binary_repo_packages(
)
ORDER BY
CASE WHEN lower(p.name) = lower(?1) THEN 0 ELSE 1 END,
p.name ASC",
)?;
p.name ASC"
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt.query_map(params![query], |row| {
let package_id = row.get::<_, i64>(0)?;
@@ -1537,13 +1554,14 @@ fn find_cached_binary_repo_packages(
name: row.get(1)?,
version: row.get(2)?,
revision: row.get::<_, i64>(3)? as u32,
filename: row.get(4)?,
size: row.get::<_, i64>(5)? as u64,
sha256: row.get(6)?,
sha512: row.get(7)?,
description: row.get(8)?,
homepage: row.get(9)?,
license: row.get(10)?,
completed_at: row.get(4)?,
filename: row.get(5)?,
size: row.get::<_, i64>(6)? as u64,
sha256: row.get(7)?,
sha512: row.get(8)?,
description: row.get(9)?,
homepage: row.get(10)?,
license: row.get(11)?,
provides: Vec::new(),
runtime_dependencies: Vec::new(),
optional_dependencies: Vec::new(),
@@ -1562,6 +1580,93 @@ fn find_cached_binary_repo_packages(
Ok(out)
}
fn list_cached_binary_repo_packages(
repo_name: &str,
db_path: &Path,
) -> Result<Vec<BinaryRepoPackageRecord>> {
let conn = Connection::open(db_path)
.with_context(|| format!("Failed to open binary repo DB {}", db_path.display()))?;
let completed_at_expr = if repo_packages_have_completed_at(&conn)? {
"p.completed_at"
} else {
"NULL"
};
let sql = format!(
"SELECT
p.id,
p.name,
p.version,
p.revision,
{completed_at_expr},
p.filename,
p.size,
p.sha256,
p.sha512,
p.description,
p.homepage,
p.license
FROM packages p
ORDER BY p.name ASC, p.version ASC, p.revision ASC"
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt.query_map([], |row| {
let package_id = row.get::<_, i64>(0)?;
Ok((
package_id,
BinaryRepoPackageRecord {
repo_name: repo_name.to_string(),
name: row.get(1)?,
version: row.get(2)?,
revision: row.get::<_, i64>(3)? as u32,
completed_at: row.get(4)?,
filename: row.get(5)?,
size: row.get::<_, i64>(6)? as u64,
sha256: row.get(7)?,
sha512: row.get(8)?,
description: row.get(9)?,
homepage: row.get(10)?,
license: row.get(11)?,
provides: Vec::new(),
runtime_dependencies: Vec::new(),
optional_dependencies: Vec::new(),
},
))
})?;
let mut out = Vec::new();
for row in rows {
let (package_id, mut rec) = row?;
rec.provides = query_package_provides(&conn, package_id)?;
rec.runtime_dependencies = query_package_runtime_deps(&conn, package_id)?;
rec.optional_dependencies = query_package_optional_deps(&conn, package_id)?;
out.push(rec);
}
Ok(out)
}
fn repo_packages_have_completed_at(conn: &Connection) -> Result<bool> {
conn.query_row(
"SELECT COUNT(*) FROM pragma_table_info('packages') WHERE name = 'completed_at'",
[],
|row| {
let count: i64 = row.get(0)?;
Ok(count > 0)
},
)
.context("Failed to inspect binary repo DB schema")
}
fn path_modified_unix_timestamp(path: &Path) -> Result<Option<i64>> {
let metadata = fs::metadata(path)
.with_context(|| format!("Failed to read metadata for {}", path.display()))?;
let modified = metadata
.modified()
.with_context(|| format!("Failed to read modification time for {}", path.display()))?;
Ok(Some(metadata_time::system_time_to_unix(modified)?))
}
/// Resolve an exact package name/provide match from a binary repo after verifying
/// and caching its signed `repo.db.zst`.
pub fn find_binary_repo_package(
@@ -1595,6 +1700,17 @@ pub fn find_binary_repo_packages(
find_cached_binary_repo_packages(repo_name, &db_path, query)
}
/// List all binary packages from a cached, verified repository database.
pub fn list_binary_repo_packages(
repo_name: &str,
repo: &crate::config::BinaryRepo,
rootfs: &Path,
package_cache_dir: &Path,
) -> Result<Vec<BinaryRepoPackageRecord>> {
let db_path = fetch_binary_repo_db(repo_name, repo, rootfs, package_cache_dir)?;
list_cached_binary_repo_packages(repo_name, &db_path)
}
fn verify_hex_digest(path: &Path, algorithm: &str, expected_hex: &str) -> Result<bool> {
let expected = expected_hex.trim().to_ascii_lowercase();
if expected.is_empty() {
@@ -2109,6 +2225,18 @@ mod tests {
)
.unwrap();
assert!(has_sha512);
let has_completed_at: bool = conn
.query_row(
"SELECT COUNT(*) FROM pragma_table_info('packages') WHERE name = 'completed_at'",
[],
|r| {
let n: i64 = r.get(0)?;
Ok(n > 0)
},
)
.unwrap();
assert!(has_completed_at);
}
#[test]
@@ -2129,6 +2257,7 @@ revision = 1
description = "test description"
homepage = "https://example.com"
license = "MIT"
completed_at = "2026-03-10T12:34:56Z"
provides = ["test-feature"]
[dependencies]
@@ -2144,6 +2273,11 @@ optional = []
let encoder = tar.into_inner().unwrap();
encoder.finish().unwrap();
filetime::set_file_mtime(
&pkg_path,
filetime::FileTime::from_unix_time(1_700_000_000, 0),
)
.unwrap();
let mut conn = Connection::open_in_memory().unwrap();
let manager = RepoManager::new(repo_dir.to_path_buf());
@@ -2190,6 +2324,11 @@ optional = []
assert_eq!(sha256.len(), 64);
assert_eq!(sha512.len(), 128);
let completed_at: Option<i64> = conn
.query_row("SELECT completed_at FROM packages", [], |r| r.get(0))
.unwrap();
assert_eq!(completed_at, Some(1_773_146_096));
let provides_count: i64 = conn
.query_row("SELECT count(*) FROM provides", [], |r| r.get(0))
.unwrap();
@@ -2323,6 +2462,7 @@ license = ["MIT", "Apache-2.0"]
name: "pkg".into(),
version: "1.0".into(),
revision: 1,
completed_at: None,
filename: "pkg.depot.pkg.tar.zst".into(),
size: 7,
sha256,
@@ -2357,6 +2497,7 @@ license = ["MIT", "Apache-2.0"]
name: "pkg".into(),
version: "1.0".into(),
revision: 1,
completed_at: None,
filename: filename.to_string(),
size: payload.len() as u64,
sha256,