From 6eb3678c1cb79b66364a8eccb25504c6cc61bafe Mon Sep 17 00:00:00 2001
From: JMARyA <jmarya@hydrar.de>
Date: Sun, 29 Dec 2024 09:09:54 +0100
Subject: [PATCH] refactor

---
 .woodpecker/build.yml          |   1 -
 src/pkg/arch.rs                |  26 ++++
 src/pkg/db.rs                  |  13 ++
 src/pkg/mod.rs                 |   9 ++
 src/{pkg.rs => pkg/package.rs} | 213 +++++++++++++--------------------
 src/pkg/repo.rs                | 111 +++++++++++++++++
 src/routes/mod.rs              |  13 +-
 7 files changed, 252 insertions(+), 134 deletions(-)
 create mode 100644 src/pkg/arch.rs
 create mode 100644 src/pkg/db.rs
 create mode 100644 src/pkg/mod.rs
 rename src/{pkg.rs => pkg/package.rs} (50%)
 create mode 100644 src/pkg/repo.rs

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")