diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml index 3ea4b09..7b4c3bc 100644 --- a/.woodpecker/build.yml +++ b/.woodpecker/build.yml @@ -13,4 +13,3 @@ steps: username: jmarya password: from_secret: registry_token - diff --git a/src/pkg/arch.rs b/src/pkg/arch.rs new file mode 100644 index 0000000..2be03a1 --- /dev/null +++ b/src/pkg/arch.rs @@ -0,0 +1,26 @@ +#![allow(non_camel_case_types)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Architecture { + x86_64, + aarch64, + any, +} + +impl Architecture { + pub fn parse(val: &str) -> Option<Architecture> { + match val { + "x86_64" => Some(Architecture::x86_64), + "aarch64" => Some(Architecture::aarch64), + "any" => Some(Architecture::any), + _ => None, + } + } + + pub fn to_string(&self) -> String { + match self { + Architecture::x86_64 => "x86_64".to_string(), + Architecture::aarch64 => "aarch64".to_string(), + Architecture::any => "any".to_string(), + } + } +} diff --git a/src/pkg/db.rs b/src/pkg/db.rs new file mode 100644 index 0000000..178d01c --- /dev/null +++ b/src/pkg/db.rs @@ -0,0 +1,13 @@ +// TODO : Implement repo.db.tar.gz parsing + +pub struct RepoDB { + file: String, +} + +impl RepoDB { + pub fn new(file: &str) -> Self { + Self { + file: file.to_string(), + } + } +} diff --git a/src/pkg/mod.rs b/src/pkg/mod.rs new file mode 100644 index 0000000..db303be --- /dev/null +++ b/src/pkg/mod.rs @@ -0,0 +1,9 @@ +// TODO : Read DB Info +// TODO : Read PKG Info + Content + +pub mod repo; +pub use repo::Repository; +pub mod package; +pub use package::Package; +pub mod arch; +pub mod db; diff --git a/src/pkg.rs b/src/pkg/package.rs similarity index 50% rename from src/pkg.rs rename to src/pkg/package.rs index 1af5e49..8f65a93 100644 --- a/src/pkg.rs +++ b/src/pkg/package.rs @@ -1,131 +1,69 @@ use std::path::{Path, PathBuf}; -pub struct Repository { - pub name: String, -} - -impl Repository { - pub fn list() -> Vec<String> { - let mut repos = vec![]; - - for entry in std::fs::read_dir("./data").unwrap() { - let path = entry.unwrap().path(); - let file_name = path.file_name().unwrap().to_str().unwrap().to_string(); - repos.push(file_name); - } - - repos - } - - pub fn create(name: &str) -> Repository { - let path = PathBuf::from("./data").join(name); - std::fs::create_dir_all(path).unwrap(); - Repository::new(name).unwrap() - } -} - -impl Repository { - pub fn new(name: &str) -> Option<Self> { - if PathBuf::from("./data").join(name).exists() { - Some(Repository { - name: name.to_string(), - }) - } else { - None - } - } - - pub fn arch(&self) -> Vec<String> { - let dir_path = PathBuf::from("./data").join(&self.name); - let mut arch = vec![]; - - if let Ok(entries) = std::fs::read_dir(dir_path) { - for entry in entries.filter_map(Result::ok) { - let file_name = entry.file_name().into_string().unwrap_or_default(); - arch.push(file_name); - } - } - - arch - } - - pub fn base_path(&self, arch: &str) -> PathBuf { - PathBuf::from("./data").join(&self.name).join(arch) - } - - pub fn db_content(&self, arch: &str) -> Option<Vec<u8>> { - std::fs::read( - self.base_path(arch) - .join(format!("{}.db.tar.gz", self.name)), - ) - .ok() - } - - pub fn sig_content(&self, arch: &str) -> Option<Vec<u8>> { - std::fs::read( - self.base_path(arch) - .join(format!("{}.db.tar.gz.sig", self.name)), - ) - .ok() - } - - pub fn extract_pkg_name(name: &str) -> (String, String, String, String) { - // "{}-{}-{}-{}.pkg.tar.zst" - let splitted: Vec<&str> = name.split('-').collect(); - let name = splitted.get(0).unwrap(); - let version = splitted.get(1).unwrap(); - let rel = splitted.get(2).unwrap(); - let arch = splitted.get(3).unwrap().trim_end_matches(".pkg.tar.zst"); - - ( - name.to_string(), - version.to_string(), - rel.to_string(), - arch.to_string(), - ) - } - - pub fn get_pkg(&self, arch: &str, pkg_name: &str) -> Option<Package> { - let pkg_name = if pkg_name.ends_with(".sig") { - pkg_name.trim_end_matches(".sig").to_string() - } else { - pkg_name.to_string() - }; - - let (name, version, _, _) = Repository::extract_pkg_name(&pkg_name); - - let pkg = Package::new(&self.name, &arch, &name, &version); - - if pkg.exists() { - return Some(pkg); - } else { - None - } - } -} +use super::{Repository, arch::Architecture}; +/// General Package #[derive(Debug, Clone)] pub struct Package { + /// Repository of the package repo: String, - arch: String, + /// `Architecture` of the package + arch: Architecture, + /// Name of the package name: String, + /// Version of the package version: Option<String>, } impl Package { - pub fn new(repo: &str, arch: &str, pkg_name: &str, version: &str) -> Self { + /// Create a new package + pub fn new(repo: &str, arch: Architecture, pkg_name: &str, version: &str) -> Self { Package { repo: repo.to_string(), - arch: arch.to_string(), + arch: arch, name: pkg_name.to_string(), version: Some(version.to_string()), } } + /// Extract values from a package filename + pub fn extract_pkg_name(file_name: &str) -> Option<(String, String, String, Architecture)> { + // Extract (assuming the filename is "<pkg_name>-<version>-<relation>-<arch>.pkg.tar.zst") + let mut splitted = file_name.split('-').collect::<Vec<&str>>(); + + let arch = splitted.pop()?; + assert!(arch.ends_with(".pkg.tar.zst")); + let arch = arch.trim_end_matches(".pkg.tar.zst"); + + let relation = splitted.pop()?; + let version = splitted.pop()?; + + let pkg_name = splitted.join(" "); + + return Some(( + pkg_name, + version.to_string(), + relation.to_string(), + Architecture::parse(arch)?, + )); + } + + /// Parse a pkg filename + pub fn from_filename(repo: &str, file_name: &str) -> Package { + let (pkg_name, version, _, arch) = Package::extract_pkg_name(file_name).unwrap(); + Self { + repo: repo.to_string(), + arch, + name: pkg_name, + version: Some(version.to_string()), + } + } + + /// Find a package with latest version pub fn find(repo: &str, arch: &str, pkg_name: &str) -> Self { let mut base = Package { repo: repo.to_string(), - arch: arch.to_string(), + arch: Architecture::parse(arch).unwrap(), name: pkg_name.to_string(), version: None, }; @@ -138,6 +76,7 @@ impl Package { base } + /// Save a new package to repository pub fn save(&self, pkg: Vec<u8>, sig: Option<Vec<u8>>) { let pkg_file = self.base_path().join(&self.file_name()); let sig_file = self.base_path().join(format!("{}.sig", self.file_name())); @@ -149,64 +88,69 @@ impl Package { let db_file = PathBuf::from("./data") .join(&self.repo) - .join(&self.arch) + .join(&self.arch.to_string()) .join(format!("{}.db.tar.gz", self.repo)); - run_command(vec![ - "repo-add", - db_file.to_str().unwrap(), - pkg_file.to_str().unwrap(), - ]); + repo_add(db_file.to_str().unwrap(), pkg_file.to_str().unwrap()); - if &self.arch == "any" { + // Add to any arch repo db if `arch=any` + if self.arch == Architecture::any { let archs = Repository::new(&self.repo).unwrap().arch(); for arch in archs { - if arch == "any" { + if arch == Architecture::any { continue; } let db_file = PathBuf::from("./data") .join(&self.repo) - .join(&arch) + .join(&arch.to_string()) .join(format!("{}.db.tar.gz", self.repo)); - run_command(vec![ - "repo-add", - db_file.to_str().unwrap(), - pkg_file.to_str().unwrap(), - ]); + repo_add(db_file.to_str().unwrap(), pkg_file.to_str().unwrap()); } } } fn base_path(&self) -> PathBuf { - Path::new("./data").join(&self.repo).join(&self.arch) + // <repo>/<arch>/<pkg>/ + let p = Path::new("./data") + .join(&self.repo) + .join(&self.arch.to_string()) + .join(&self.name); + std::fs::create_dir_all(&p).unwrap(); + p } + /// Switch the `Architecture` of the package pub fn switch_arch(&self, arch: &str) -> Self { let mut new = self.clone(); - new.arch = arch.to_string(); + new.arch = Architecture::parse(arch).unwrap(); new } - pub fn exists(&self) -> bool { - let pkg_file = self.base_path().join(self.file_name()); - pkg_file.exists() - } - + /// Switch to a specific `version` of the package pub fn get_version(&self, version: &str) -> Self { let mut new_pkg = self.clone(); new_pkg.version = Some(version.to_string()); new_pkg } + /// Check if the specific package exists + pub fn exists(&self) -> bool { + let pkg_file = self.base_path().join(self.file_name()); + pkg_file.exists() + } + + /// Checks if the package has a signature pub fn is_signed(&self) -> bool { let signed_file = self.base_path().join(format!("{}.sig", self.file_name())); signed_file.exists() } + /// Build a pkg filename from the packages values pub fn file_name(&self) -> String { + // TODO : pkgrel support format!( "{}-{}-{}-{}.pkg.tar.zst", self.name, @@ -217,10 +161,11 @@ impl Package { versions.first().unwrap().clone() }, 1, - self.arch, + self.arch.to_string(), ) } + /// Get all versions of the package pub fn versions(&self) -> Vec<String> { let dir_path = self.base_path(); let mut versions = vec![]; @@ -229,8 +174,7 @@ impl Package { for entry in entries.filter_map(Result::ok) { let file_name = entry.file_name().into_string().unwrap_or_default(); if file_name.starts_with(&self.name) && file_name.ends_with(".pkg.tar.zst") { - // Extract version (assuming the filename is "<pkg_name>-<version>-<relation>-<arch>.pkg.tar.zst") - if let Some(version) = file_name.split('-').nth(1) { + if let Some(version) = Package::from_filename(&self.repo, &file_name).version { versions.push(version.to_string()); } } @@ -242,6 +186,7 @@ impl Package { versions } + /// Get the content of the `.pkg.tar.zst` pub fn pkg_content(&self) -> Option<Vec<u8>> { if self.exists() { return std::fs::read(self.base_path().join(self.file_name())).ok(); @@ -250,6 +195,7 @@ impl Package { None } + /// Get the content of the `.pkg.tar.zst.sig` pub fn sig_content(&self) -> Option<Vec<u8>> { if self.exists() { return std::fs::read(self.base_path().join(format!("{}.sig", &self.file_name()))).ok(); @@ -267,3 +213,8 @@ pub fn run_command(cmd: Vec<&str>) { .wait() .unwrap(); } + +/// Add a package file to a repo db +pub fn repo_add(db_file: &str, pkg_file: &str) { + run_command(vec!["repo-add", db_file, pkg_file]); +} diff --git a/src/pkg/repo.rs b/src/pkg/repo.rs new file mode 100644 index 0000000..633f71c --- /dev/null +++ b/src/pkg/repo.rs @@ -0,0 +1,111 @@ +use std::path::PathBuf; + +use super::{Package, arch::Architecture}; + +/// Package Repository +pub struct Repository { + pub name: String, +} + +impl Repository { + /// Get a list of all package repositories + pub fn list() -> Vec<String> { + let mut repos = vec![]; + + for entry in std::fs::read_dir("./data").unwrap() { + let path = entry.unwrap().path(); + let file_name = path.file_name().unwrap().to_str().unwrap().to_string(); + repos.push(file_name); + } + + repos + } + + /// Create a new package repository with architectures from `arch` + pub fn create(name: &str, arch: Vec<Architecture>) -> Repository { + let path = PathBuf::from("./data").join(name); + std::fs::create_dir_all(&path).unwrap(); + + for arch in arch { + let np = path.join(arch.to_string()); + std::fs::create_dir_all(np).unwrap(); + } + + Repository::new(name).unwrap() + } +} + +impl Repository { + /// Get a repository if it exists + pub fn new(name: &str) -> Option<Self> { + if PathBuf::from("./data").join(name).exists() { + Some(Repository { + name: name.to_string(), + }) + } else { + None + } + } + + /// Get a list of architectures for this repository + pub fn arch(&self) -> Vec<Architecture> { + let dir_path = PathBuf::from("./data").join(&self.name); + let mut arch = vec![]; + + if let Ok(entries) = std::fs::read_dir(dir_path) { + for entry in entries.filter_map(Result::ok) { + let file_name = entry.file_name().into_string().unwrap_or_default(); + if let Some(repo_arch) = Architecture::parse(&file_name) { + arch.push(repo_arch); + } + } + } + + arch + } + + /// Get the base path for the repository with `arch`. + pub fn base_path(&self, arch: Architecture) -> PathBuf { + PathBuf::from("./data") + .join(&self.name) + .join(&arch.to_string()) + } + + /// Get the `.db.tar.gz` content for the repository of `arch` + pub fn db_content(&self, arch: Architecture) -> Option<Vec<u8>> { + std::fs::read( + self.base_path(arch) + .join(format!("{}.db.tar.gz", self.name)), + ) + .ok() + } + + /// Get the `.db.tar.gz.sig` content for the repository of `arch` + pub fn sig_content(&self, arch: Architecture) -> Option<Vec<u8>> { + std::fs::read( + self.base_path(arch) + .join(format!("{}.db.tar.gz.sig", self.name)), + ) + .ok() + } + + pub fn get_pkg(&self, arch: Architecture, pkg_name: &str) -> Option<Package> { + // Normalize name + let pkg_name = if pkg_name.ends_with(".sig") { + pkg_name.trim_end_matches(".sig").to_string() + } else { + pkg_name.to_string() + }; + + // Find package + let (name, version, _, _) = Package::extract_pkg_name(&pkg_name).unwrap(); + let pkg = Package::new(&self.name, arch, &name, &version); + + // Return if exists + if pkg.exists() { + return Some(pkg); + } else { + None + } + } +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 4dae008..c202be5 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -5,6 +5,7 @@ use rocket::tokio::io::AsyncReadExt; use rocket::{FromForm, get, post}; use serde_json::json; +use crate::pkg::arch::Architecture; use crate::pkg::{Package, Repository}; // /pkg/<repo>/<arch>/<pkg_name> @@ -39,11 +40,17 @@ pub async fn upload_pkg( upload: Form<PkgUpload<'_>>, user: based::auth::APIUser, ) -> FallibleApiResponse { + // TODO : Permission System if !user.0.is_admin() { return Err(api_error("Forbidden")); } - let pkg = Package::new(repo, &upload.arch, &upload.name, &upload.version); + let pkg = Package::new( + repo, + Architecture::parse(&upload.arch).ok_or_else(|| api_error("Invalid architecture"))?, + &upload.name, + &upload.version, + ); pkg.save( tmp_file_to_vec(&upload.pkg).await, @@ -54,11 +61,13 @@ pub async fn upload_pkg( }, ); - Ok(json!({"ok": 1})) + Ok(json!({"ok": format!("Added '{}' to '{}'", pkg.file_name(), repo)})) } #[get("/pkg/<repo>/<arch>/<pkg_name>")] pub async fn pkg_route(repo: &str, arch: &str, pkg_name: &str, ctx: RequestContext) -> RawResponse { + let arch = Architecture::parse(arch).unwrap(); + if let Some(repo) = Repository::new(repo) { if pkg_name.ends_with("db.tar.gz") || pkg_name.ends_with("db")