diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..158b25a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + push: + branches: + - '**' + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Update local toolchain + run: | + rustup update + rustup component add clippy + rustup install nightly + + - name: Toolchain info + run: | + cargo --version --verbose + rustc --version + cargo clippy --version + + - name: Lint + run: | + cd backend + cargo fmt -- --check + cargo clippy -- -D warnings + + - name: Test + run: | + cd backend + cargo check + cargo test --all + cd src/pacman-repo-utils && cargo test --all \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 5faf474..b82d00a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,12 +25,7 @@ FROM quay.io/podman/stable # Copy the built binary from the previous stage COPY --from=builder /app/target/release/aurcache /usr/local/bin/aurcache - -RUN dnf -y install pacman && dnf clean all COPY entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh /usr/local/bin/aurcache - -# Set the entry point or default command to run your application WORKDIR /app CMD /entrypoint.sh diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 6722e91..f10f373 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -79,9 +79,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.14" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" dependencies = [ "anstyle", "anstyle-parse", @@ -94,33 +94,33 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -237,6 +237,7 @@ dependencies = [ "env_logger", "flate2", "log", + "pacman-repo-utils", "reqwest 0.12.5", "rocket", "rocket_okapi", @@ -442,6 +443,10 @@ name = "cc" version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" +dependencies = [ + "jobserver", + "libc", +] [[package]] name = "cfg-if" @@ -472,9 +477,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.10" +version = "4.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f6b81fb3c84f5563d509c59b5a48d935f689e993afa90fe39047f05adef9142" +checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3" dependencies = [ "clap_builder", "clap_derive", @@ -482,9 +487,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.10" +version = "4.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca6706fd5224857d9ac5eb9355f6683563cc0541c7cd9d014043b57cbec78ac" +checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa" dependencies = [ "anstream", "anstyle", @@ -494,9 +499,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.8" +version = "4.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" +checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -506,15 +511,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "colorchoice" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "const-oid" @@ -1511,9 +1516,9 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.0" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" @@ -1521,6 +1526,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.69" @@ -1633,6 +1647,12 @@ dependencies = [ "digest", ] +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "2.7.4" @@ -1910,6 +1930,20 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "pacman-repo-utils" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.22.1", + "flate2", + "log", + "md5", + "sha2", + "tar", + "zstd", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -3063,9 +3097,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] @@ -3668,9 +3702,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.1" +version = "1.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d040ac2b29ab03b09d4129c2f5bbd012a3ac2f79d38ff506a4bf8dd34b0eac8a" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" dependencies = [ "backtrace", "bytes 1.6.1", @@ -3754,21 +3788,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28" +checksum = "81967dd0dd2c1ab0bc3468bd7caecc32b8a4aa47d0c8c695d8c2b2108168d62c" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.16", + "toml_edit 0.22.17", ] [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "f8fb9f64314842840f1d940ac544da178732128f1c78c21772e876579e0da1db" dependencies = [ "serde", ] @@ -3786,15 +3820,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.16" +version = "0.22.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788" +checksum = "8d9f8729f5aea9562aac1cc0441f5d6de3cff1ee0c5d67293eeca5eb36ee7c16" dependencies = [ "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.15", + "winnow 0.6.16", ] [[package]] @@ -4034,9 +4068,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" @@ -4360,9 +4394,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.15" +version = "0.6.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "557404e450152cd6795bb558bca69e43c585055f4606e3bcae5894fc6dac9ba0" +checksum = "b480ae9340fc261e6be3e95a1ba86d54ae3f9171132a73ce8d4bbaf68339507c" dependencies = [ "memchr", ] @@ -4441,3 +4475,31 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa556e971e7b568dc775c136fc9de8c779b1c2fc3a63defaafadffdbd3181afa" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.12+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index c3e85e8..19f1158 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -25,6 +25,7 @@ shiplift = "0.7.0" bigdecimal = "0.4.5" env_logger = "0.11.5" log = "0.4.22" +pacman-repo-utils = {path = "./src/pacman-repo-utils"} [[bin]] name = "aurcache" diff --git a/backend/src/api/package.rs b/backend/src/api/package.rs index 9159071..d48c931 100644 --- a/backend/src/api/package.rs +++ b/backend/src/api/package.rs @@ -49,10 +49,7 @@ pub async fn package_update_endpoint( #[openapi(tag = "Packages")] #[delete("/package/")] pub async fn package_del(db: &State, id: i32) -> Result<(), String> { - let db = db as &DatabaseConnection; - package_delete(db, id).await.map_err(|e| e.to_string())?; - Ok(()) } diff --git a/backend/src/builder/build.rs b/backend/src/builder/build.rs index 7c1f9fe..f0078a2 100644 --- a/backend/src/builder/build.rs +++ b/backend/src/builder/build.rs @@ -3,7 +3,7 @@ use crate::db::files::ActiveModel; use crate::db::migration::JoinType; use crate::db::prelude::{Builds, Files, PackagesFiles}; use crate::db::{builds, files, packages, packages_files}; -use crate::repo::utils::{repo_add, try_remove_archive_file}; +use crate::repo::utils::try_remove_archive_file; use anyhow::anyhow; use log::info; use rocket::futures::StreamExt; @@ -296,7 +296,11 @@ async fn move_and_add_pkgs( }; package_file.save(&txn).await?; - repo_add(archive_name.clone())?; + pacman_repo_utils::repo_add( + format!("./repo/{}", archive_name).as_str(), + "./repo/repo.db.tar.gz".to_string(), + "./repo/repo.files.tar.gz".to_string(), + )?; info!("Successfully added '{}' to the repo archive", archive_name); } txn.commit().await?; diff --git a/backend/src/pacman-repo-utils/Cargo.toml b/backend/src/pacman-repo-utils/Cargo.toml new file mode 100644 index 0000000..6072708 --- /dev/null +++ b/backend/src/pacman-repo-utils/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "pacman-repo-utils" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +base64 = "0.22.1" +md5 = "0.7.0" +sha2 = "0.10.6" +flate2 = "1.0.30" + + +zstd = "0.13.2" +tar = "0.4" +anyhow = "1.0.86" +log = "0.4.22" \ No newline at end of file diff --git a/backend/src/pacman-repo-utils/src/lib.rs b/backend/src/pacman-repo-utils/src/lib.rs new file mode 100644 index 0000000..43b2ab0 --- /dev/null +++ b/backend/src/pacman-repo-utils/src/lib.rs @@ -0,0 +1,18 @@ +mod pkginfo; +mod repo_add; +mod repo_database; +mod repo_remove; +use crate::repo_add::repo_add_impl; +use crate::repo_remove::repo_remove_impl; + +pub fn repo_add(pkgfile: &str, db_archive: String, files_archive: String) -> anyhow::Result<()> { + repo_add_impl(pkgfile, db_archive, files_archive) +} + +pub fn repo_remove( + filename: String, + db_archive: String, + files_archive: String, +) -> anyhow::Result<()> { + repo_remove_impl(filename, db_archive, files_archive) +} diff --git a/backend/src/pacman-repo-utils/src/pkginfo/mod.rs b/backend/src/pacman-repo-utils/src/pkginfo/mod.rs new file mode 100644 index 0000000..67c567f --- /dev/null +++ b/backend/src/pacman-repo-utils/src/pkginfo/mod.rs @@ -0,0 +1 @@ +pub mod parser; diff --git a/backend/src/pacman-repo-utils/src/pkginfo/parser.rs b/backend/src/pacman-repo-utils/src/pkginfo/parser.rs new file mode 100644 index 0000000..ab1d290 --- /dev/null +++ b/backend/src/pacman-repo-utils/src/pkginfo/parser.rs @@ -0,0 +1,185 @@ +use anyhow::anyhow; +use base64::engine::general_purpose; +use base64::Engine; +use std::io::{BufRead, Read}; +use std::path::Path; +use std::{fs, io}; + +pub struct Pkginfo { + pub groups: Vec, + pub licenses: Vec, + pub replaces: Vec, + pub depends: Vec, + pub conflicts: Vec, + pub provides: Vec, + pub optdepends: Vec, + pub makedepends: Vec, + pub checkdepends: Vec, + pub pkgname: String, + pub pkgbase: String, + pub pkgver: String, + pub pkgdesc: String, + pub size: u32, + pub url: String, + pub arch: String, + pub builddate: String, + pub packager: String, + pub pgpsig: String, +} + +impl Pkginfo { + pub fn new() -> Self { + Self { + groups: vec![], + licenses: vec![], + replaces: vec![], + depends: vec![], + conflicts: vec![], + provides: vec![], + optdepends: vec![], + makedepends: vec![], + checkdepends: vec![], + pkgname: String::new(), + pkgbase: String::new(), + pkgver: String::new(), + pkgdesc: String::new(), + size: 0, + url: String::new(), + arch: String::new(), + builddate: String::new(), + packager: String::new(), + pgpsig: String::new(), + } + } + + pub fn parse(&mut self, file: impl Read) -> anyhow::Result<()> { + let reader = io::BufReader::new(file); + for line in reader.lines() { + let line = line?; + self.parse_line(line); + } + Ok(()) + } + + pub fn parse_line(&mut self, line: String) { + if line.starts_with('#') { + return; + } + let (key, value) = match line.split_once('=') { + None => return, + Some((key, value)) => (key.trim(), value.trim()), + }; + match key { + "group" => self.groups.push(value.to_string()), + "license" => self.licenses.push(value.to_string()), + "replaces" => self.replaces.push(value.to_string()), + "depend" => self.depends.push(value.to_string()), + "conflict" => self.conflicts.push(value.to_string()), + "provides" => self.provides.push(value.to_string()), + "optdepend" => self.optdepends.push(value.to_string()), + "makedepend" => self.makedepends.push(value.to_string()), + "checkdepend" => self.checkdepends.push(value.to_string()), + "pkgname" => self.pkgname = value.to_string(), + "pkgbase" => self.pkgbase = value.to_string(), + "pkgver" => self.pkgver = value.to_string(), + "pkgdesc" => self.pkgdesc = value.to_string(), + "size" => self.size = value.parse().unwrap_or(0), + "url" => self.url = value.to_string(), + "arch" => self.arch = value.to_string(), + "builddate" => self.builddate = value.to_string(), + "packager" => self.packager = value.to_string(), + _ => {} + } + } + + pub fn set_signature(&mut self, pkgfile: &str) -> anyhow::Result<()> { + let sigfile = format!("{}.sig", pkgfile); + if Path::new(&sigfile).exists() { + let sigdata = fs::read(&sigfile)?; + if sigdata.starts_with(b"-----BEGIN PGP SIGNATURE-----") { + eprintln!("Cannot use armored signatures for packages: {}", sigfile); + return Err(anyhow!("Invalid package signature file")); + } + let pgpsigsize = sigdata.len(); + if pgpsigsize > 16384 { + eprintln!("Invalid package signature file '{}'.", sigfile); + return Err(anyhow!("Invalid package signature file")); + } + + self.pgpsig = general_purpose::STANDARD.encode(&sigdata); + } + Ok(()) + } + + pub fn valid(&self) -> bool { + // Ensure $pkgname and $pkgver variables were found + !self.pkgname.is_empty() && !self.pkgver.is_empty() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn name_missing() { + let mut pkginfo = Pkginfo::new(); + assert_eq!(pkginfo.valid(), false); + pkginfo.pkgname = "test".to_string(); + assert_eq!(pkginfo.valid(), false); + pkginfo.pkgver = "1.0".to_string(); + assert_eq!(pkginfo.valid(), true); + } + + #[test] + fn parse() { + let mut pkginfo = Pkginfo::new(); + let data = r#" + pkgname = test + pkgver = 1.0 + pkgdesc = test package + size = 1024 + url = https://example.com + arch = x86_64 + builddate = 2021-01-01 + packager = test + "#; + pkginfo.parse(data.as_bytes()).unwrap(); + assert_eq!(pkginfo.pkgname, "test"); + assert_eq!(pkginfo.pkgver, "1.0"); + assert_eq!(pkginfo.pkgdesc, "test package"); + assert_eq!(pkginfo.size, 1024); + assert_eq!(pkginfo.url, "https://example.com"); + assert_eq!(pkginfo.arch, "x86_64"); + assert_eq!(pkginfo.builddate, "2021-01-01"); + assert_eq!(pkginfo.packager, "test"); + } + + #[test] + fn parse_array() { + let mut pkginfo = Pkginfo::new(); + let data = r#" + group = test + group = secgroup + license = MIT + replaces = test + depend = test + conflict = test + provides = test + optdepend = myoptdep + optdepend = secopdep + makedepend = test + checkdepend = test + "#; + pkginfo.parse(data.as_bytes()).unwrap(); + assert_eq!(pkginfo.groups, vec!["test", "secgroup"]); + assert_eq!(pkginfo.licenses, vec!["MIT"]); + assert_eq!(pkginfo.replaces, vec!["test"]); + assert_eq!(pkginfo.depends, vec!["test"]); + assert_eq!(pkginfo.conflicts, vec!["test"]); + assert_eq!(pkginfo.provides, vec!["test"]); + assert_eq!(pkginfo.optdepends, vec!["myoptdep", "secopdep"]); + assert_eq!(pkginfo.makedepends, vec!["test"]); + assert_eq!(pkginfo.checkdepends, vec!["test"]); + } +} diff --git a/backend/src/pacman-repo-utils/src/repo_add.rs b/backend/src/pacman-repo-utils/src/repo_add.rs new file mode 100644 index 0000000..7620245 --- /dev/null +++ b/backend/src/pacman-repo-utils/src/repo_add.rs @@ -0,0 +1,103 @@ +use anyhow::anyhow; + +use crate::pkginfo::parser::Pkginfo; +use crate::repo_database::db::add_to_db_file; +use crate::repo_database::desc::Desc; +use log::{debug, error}; +use sha2::{Digest, Sha256}; +use std::fs::{self, File}; +use std::io::Read; +use std::path::Path; +use tar::Archive; +use zstd::Decoder; + +pub fn repo_add_impl( + pkgfile: &str, + db_archive: String, + files_archive: String, +) -> anyhow::Result<()> { + let mut files = vec![]; + let mut pkginfo = Pkginfo::new(); + + // Path to the .tar.zst file + let file = File::open(Path::new(pkgfile))?; + let mut archive = Archive::new(Decoder::new(file)?); + + // Iterate over the entries in the tar archive + for entry in archive.entries()? { + let entry = entry?; + + if !entry.path()?.display().to_string().starts_with('.') { + files.push(format!("{}", entry.path()?.display())); + } + + if entry.path()? == Path::new(".PKGINFO") { + debug!("Found .PKGINFO file in '{}'.", pkgfile); + pkginfo.parse(entry)?; + } + } + + if !pkginfo.valid() { + error!("Invalid package file '{}'.", pkgfile); + return Err(anyhow!("Invalid package file")); + } + + // Compute base64'd PGP signature + debug!("Setting signature for '{}'.", pkgfile); + pkginfo.set_signature(pkgfile)?; + + debug!("Calculating compressed size for '{}'.", pkgfile); + let csize = fs::metadata(pkgfile)?.len() as usize; + + debug!("Calculating checksums for '{}'.", pkgfile); + let (md5sum, sha256sum) = calc_checksums(pkgfile)?; + + let filename = Path::new(pkgfile) + .file_name() + .ok_or(anyhow!("invalid path"))? + .to_str() + .ok_or(anyhow!("invalid path"))? + .to_string(); + + let dir_name = format!("{}-{}", pkginfo.pkgname, pkginfo.pkgver); + + debug!("Creating DESC file for db entry"); + let mut desc = Desc::from(pkginfo); + desc.filename = filename.clone(); + desc.md5sum = md5sum.clone(); + desc.csize = csize.to_string(); + desc.sha256sum = sha256sum.clone(); + let desc_str = desc.to_string(); + + debug!("Adding DESC and FILES entries to db archive"); + add_to_db_file( + desc_str.clone(), + dir_name.clone(), + "desc".to_string(), + db_archive, + )?; + + files.sort(); + let files_comb = format!("%FILES%\n{}", files.join("\n")); + add_to_db_file( + desc_str, + dir_name.clone(), + "desc".to_string(), + files_archive.clone(), + )?; + add_to_db_file(files_comb, dir_name, "files".to_string(), files_archive)?; + + Ok(()) +} + +fn calc_checksums(path: &str) -> anyhow::Result<(String, String)> { + let mut file = File::open(path)?; + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer)?; + let md5sum = format!("{:x}", md5::compute(&buffer)); + let mut hasher = Sha256::new(); + hasher.update(&buffer); + let sha256sum = format!("{:x}", hasher.finalize()); + + Ok((md5sum, sha256sum)) +} diff --git a/backend/src/pacman-repo-utils/src/repo_database/db.rs b/backend/src/pacman-repo-utils/src/repo_database/db.rs new file mode 100644 index 0000000..5e3e05a --- /dev/null +++ b/backend/src/pacman-repo-utils/src/repo_database/db.rs @@ -0,0 +1,106 @@ +use std::fs::File; +use std::io::{self, Cursor, Read, Write}; +use std::path::Path; +use tar::{Archive, Builder, Header}; + +use flate2::read::GzDecoder; +use flate2::write::GzEncoder; +use flate2::Compression; + +pub fn remove_from_db_file(db_archive: String, dir_name: String) -> anyhow::Result<()> { + if !Path::new(&db_archive).exists() { + return Ok(()); + } + + let mut new_archive_data = Vec::new(); + { + let mut existing_archive_data = Vec::new(); + { + let mut file = File::open(db_archive.clone())?; + file.read_to_end(&mut existing_archive_data)?; + } + // Decode the existing archive + let mut archive = Archive::new(GzDecoder::new(Cursor::new(existing_archive_data))); + + let enc = GzEncoder::new(&mut new_archive_data, Compression::default()); + let mut tar_builder = Builder::new(enc); + + // Copy existing entries to the new archive + for entry in archive.entries()? { + let mut entry = entry?; + + // skip file and folder we want to delete + if entry.header().path()?.starts_with(dir_name.clone()) { + continue; + } + + tar_builder.append(&entry.header().clone(), &mut entry)?; + } + + tar_builder.finish()?; + } + + let mut new_file = File::create(db_archive)?; + new_file.write_all(&new_archive_data)?; + Ok(()) +} + +pub fn add_to_db_file( + content: String, + dir_name: String, + file_name: String, + db_archive: String, +) -> anyhow::Result<()> { + // Check if the archive exists + let archive_exists = Path::new(&db_archive).exists(); + let mut new_archive_data = Vec::new(); + { + let mut builder = if archive_exists { + let mut existing_archive_data = Vec::new(); + { + let mut file = File::open(db_archive.clone())?; + file.read_to_end(&mut existing_archive_data)?; + } + // Decode the existing archive + let mut archive = Archive::new(GzDecoder::new(Cursor::new(existing_archive_data))); + + let enc = GzEncoder::new(&mut new_archive_data, Compression::default()); + let mut tar_builder = Builder::new(enc); + + // Copy existing entries to the new archive + for entry in archive.entries()? { + let mut entry = entry?; + tar_builder.append(&entry.header().clone(), &mut entry)?; + } + tar_builder + } else { + // Create a new archive + let encoder = GzEncoder::new(&mut new_archive_data, Compression::default()); + + Builder::new(encoder) + }; + + // Add a folder + let mut header = Header::new_gnu(); + header.set_path(dir_name.clone())?; + header.set_entry_type(tar::EntryType::Directory); + header.set_mode(0o755); + header.set_size(0); + header.set_cksum(); + builder.append(&header, io::empty())?; + + // Add a file + let mut header = Header::new_gnu(); + header.set_path(format!("{}/{}", dir_name, file_name))?; + header.set_size(content.len() as u64); + header.set_mode(0o644); + header.set_cksum(); + builder.append(&header, content.as_bytes())?; + + builder.finish()?; + } + let mut new_file = File::create(db_archive)?; + new_file.write_all(&new_archive_data)?; + + Ok(()) +} diff --git a/backend/src/pacman-repo-utils/src/repo_database/desc.rs b/backend/src/pacman-repo-utils/src/repo_database/desc.rs new file mode 100644 index 0000000..913bcff --- /dev/null +++ b/backend/src/pacman-repo-utils/src/repo_database/desc.rs @@ -0,0 +1,267 @@ +use crate::pkginfo::parser::Pkginfo; +use std::fmt::{Display, Formatter}; + +pub struct Desc { + pub filename: String, + pub name: String, + pub base: String, + pub version: String, + pub desc: String, + pub groups: Vec, + pub csize: String, + pub isize: String, + pub md5sum: String, + pub sha256sum: String, + pub pgpsig: String, + pub url: String, + pub licenses: Vec, + pub arch: String, + pub builddate: String, + pub packager: String, + pub replace: Vec, + pub conflicts: Vec, + pub provides: Vec, + pub depends: Vec, + pub optdepends: Vec, + pub makedepends: Vec, + pub checkdepends: Vec, +} + +impl Display for Desc { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let desc_lines = vec![ + self.add_desc_entry("filename", self.filename.clone()), + self.add_desc_entry("NAME", self.name.clone()), + self.add_desc_entry("BASE", self.base.clone()), + self.add_desc_entry("VERSION", self.version.clone()), + self.add_desc_entry("DESC", self.desc.clone()), + self.add_desc_entries("GROUPS", &self.groups), + self.add_desc_entry("CSIZE", self.csize.clone()), + self.add_desc_entry("ISIZE", self.isize.clone()), + self.add_desc_entry("MD5SUM", self.md5sum.clone()), + self.add_desc_entry("SHA256SUM", self.sha256sum.clone()), + self.add_desc_entry("PGPSIG", self.pgpsig.clone()), + self.add_desc_entry("URL", self.url.clone()), + self.add_desc_entries("LICENSE", &self.licenses), + self.add_desc_entry("ARCH", self.arch.clone()), + self.add_desc_entry("BUILDDATE", self.builddate.clone()), + self.add_desc_entry("PACKAGER", self.packager.clone()), + self.add_desc_entries("REPLACES", &self.replace), + self.add_desc_entries("CONFLICTS", &self.conflicts), + self.add_desc_entries("PROVIDES", &self.provides), + self.add_desc_entries("DEPENDS", &self.depends), + self.add_desc_entries("OPTDEPENDS", &self.optdepends), + self.add_desc_entries("MAKEDEPENDS", &self.makedepends), + self.add_desc_entries("CHECKDEPENDS", &self.checkdepends), + ]; + write!(f, "{}", desc_lines.join("")) + } +} +impl Desc { + fn add_desc_entry(&self, header: &str, value: String) -> String { + if value.is_empty() { + return String::new(); + } + format!("%{}%\n{}\n\n", header, value) + } + + fn add_desc_entries(&self, header: &str, values: &[String]) -> String { + if values.is_empty() { + return String::new(); + } + if values.len() == 1 && values.first().unwrap().eq("") { + return String::new(); + } + format!("%{}%\n{}\n\n", header, values.join("\n")) + } +} + +impl From for Desc { + fn from(value: Pkginfo) -> Self { + Desc { + filename: "".to_string(), + name: value.pkgname, + base: value.pkgbase, + version: value.pkgver, + desc: value.pkgdesc, + isize: value.size.to_string(), + md5sum: "".to_string(), + csize: "".to_string(), + url: value.url, + arch: value.arch, + builddate: value.builddate, + packager: value.packager, + pgpsig: value.pgpsig, + groups: value.groups, + licenses: value.licenses, + replace: value.replaces, + conflicts: value.conflicts, + provides: value.provides, + depends: value.depends, + optdepends: value.optdepends, + makedepends: value.makedepends, + checkdepends: value.checkdepends, + sha256sum: "".to_string(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_desc_to_string() { + let desc = Desc { + filename: "myfilename".to_string(), + name: "myname".to_string(), + base: "mybase".to_string(), + version: "vers".to_string(), + desc: "test".to_string(), + groups: vec!["firstgroup".to_string(), "secgroup".to_string()], + csize: "test".to_string(), + isize: "test".to_string(), + md5sum: "test".to_string(), + sha256sum: "test".to_string(), + pgpsig: "test".to_string(), + url: "test".to_string(), + licenses: vec!["test".to_string()], + arch: "test".to_string(), + builddate: "test".to_string(), + packager: "test".to_string(), + replace: vec!["test".to_string()], + conflicts: vec!["test".to_string()], + provides: vec!["test".to_string()], + depends: vec!["test".to_string()], + optdepends: vec!["test".to_string()], + makedepends: vec!["test".to_string()], + checkdepends: vec!["test".to_string()], + }; + + let expected = "\ +%filename% +myfilename + +%NAME% +myname + +%BASE% +mybase + +%VERSION% +vers + +%DESC% +test + +%GROUPS% +firstgroup +secgroup + +%CSIZE% +test + +%ISIZE% +test + +%MD5SUM% +test + +%SHA256SUM% +test + +%PGPSIG% +test + +%URL% +test + +%LICENSE% +test + +%ARCH% +test + +%BUILDDATE% +test + +%PACKAGER% +test + +%REPLACES% +test + +%CONFLICTS% +test + +%PROVIDES% +test + +%DEPENDS% +test + +%OPTDEPENDS% +test + +%MAKEDEPENDS% +test + +%CHECKDEPENDS% +test + +"; + assert_eq!(desc.to_string(), expected); + } + + #[test] + fn test_from_pkginfo() { + let pkginfo = Pkginfo { + pkgname: "myname".to_string(), + pkgbase: "mybase".to_string(), + pkgver: "vers".to_string(), + pkgdesc: "test".to_string(), + groups: vec!["firstgroup".to_string(), "secgroup".to_string()], + size: 1024, + url: "test".to_string(), + arch: "test".to_string(), + builddate: "test".to_string(), + packager: "test".to_string(), + pgpsig: "test".to_string(), + licenses: vec!["test".to_string()], + replaces: vec!["test".to_string()], + conflicts: vec!["test".to_string()], + provides: vec!["test".to_string()], + depends: vec!["test".to_string()], + optdepends: vec!["test".to_string()], + makedepends: vec!["test".to_string()], + checkdepends: vec!["test".to_string()], + }; + + let desc = Desc::from(pkginfo); + + assert_eq!(desc.filename, ""); + assert_eq!(desc.name, "myname"); + assert_eq!(desc.base, "mybase"); + assert_eq!(desc.version, "vers"); + assert_eq!(desc.desc, "test"); + assert_eq!( + desc.groups, + vec!["firstgroup".to_string(), "secgroup".to_string()] + ); + assert_eq!(desc.csize, ""); + assert_eq!(desc.isize, "1024"); + assert_eq!(desc.md5sum, ""); + assert_eq!(desc.sha256sum, ""); + assert_eq!(desc.pgpsig, "test"); + assert_eq!(desc.url, "test"); + assert_eq!(desc.licenses, vec!["test".to_string()]); + assert_eq!(desc.arch, "test"); + assert_eq!(desc.builddate, "test"); + assert_eq!(desc.packager, "test"); + assert_eq!(desc.replace, vec!["test".to_string()]); + assert_eq!(desc.conflicts, vec!["test".to_string()]); + assert_eq!(desc.provides, vec!["test".to_string()]); + assert_eq!(desc.depends, vec!["test".to_string()]); + assert_eq!(desc.optdepends, vec!["test".to_string()]); + } +} diff --git a/backend/src/pacman-repo-utils/src/repo_database/mod.rs b/backend/src/pacman-repo-utils/src/repo_database/mod.rs new file mode 100644 index 0000000..dbc25be --- /dev/null +++ b/backend/src/pacman-repo-utils/src/repo_database/mod.rs @@ -0,0 +1,2 @@ +pub mod db; +pub mod desc; diff --git a/backend/src/pacman-repo-utils/src/repo_remove.rs b/backend/src/pacman-repo-utils/src/repo_remove.rs new file mode 100644 index 0000000..45286c6 --- /dev/null +++ b/backend/src/pacman-repo-utils/src/repo_remove.rs @@ -0,0 +1,19 @@ +use crate::repo_database::db::remove_from_db_file; + +pub fn repo_remove_impl( + filename: String, + db_archive: String, + files_archive: String, +) -> anyhow::Result<()> { + let (dir_name, _) = split_last_occurrence(filename.as_str(), '-'); + remove_from_db_file(db_archive, dir_name.to_string())?; + remove_from_db_file(files_archive, dir_name.to_string())?; + Ok(()) +} + +fn split_last_occurrence(s: &str, delimiter: char) -> (&str, &str) { + match s.rfind(delimiter) { + Some(pos) => (&s[..pos], &s[pos + delimiter.len_utf8()..]), + None => (s, ""), + } +} diff --git a/backend/src/repo/utils.rs b/backend/src/repo/utils.rs index ea9d90e..249a25d 100644 --- a/backend/src/repo/utils.rs +++ b/backend/src/repo/utils.rs @@ -1,54 +1,10 @@ use crate::db::prelude::PackagesFiles; use crate::db::{files, packages_files}; -use anyhow::anyhow; use log::info; use sea_orm::ColumnTrait; use sea_orm::QueryFilter; use sea_orm::{DatabaseTransaction, EntityTrait, ModelTrait}; use std::fs; -use std::process::Command; - -static REPO_NAME: &str = "repo"; - -pub fn repo_add(pkg_file_name: String) -> anyhow::Result<()> { - let db_file = format!("{REPO_NAME}.db.tar.gz"); - - let output = Command::new("repo-add") - .args(&[db_file.clone(), pkg_file_name, "--nocolor".to_string()]) - .current_dir("./repo/") - .output()?; - - if !output.status.success() { - return Err(anyhow!( - "Error exit code when repo-add: {}{}", - String::from_utf8_lossy(output.stdout.as_slice()), - String::from_utf8_lossy(output.stderr.as_slice()) - )); - } - - info!("{db_file} updated successfully"); - Ok(()) -} - -pub fn repo_remove(pkg_file_name: String) -> anyhow::Result<()> { - let db_file = format!("{REPO_NAME}.db.tar.gz"); - - let output = Command::new("repo-remove") - .args(&[db_file.clone(), pkg_file_name, "--nocolor".to_string()]) - .current_dir("./repo/") - .output()?; - - if !output.status.success() { - return Err(anyhow!( - "Error exit code when repo-remove: {}{}", - String::from_utf8_lossy(output.stdout.as_slice()), - String::from_utf8_lossy(output.stderr.as_slice()) - )); - } - - info!("{db_file} updated successfully"); - Ok(()) -} pub async fn try_remove_archive_file( file: files::Model, @@ -61,7 +17,12 @@ pub async fn try_remove_archive_file( if package_files.is_empty() { let filename = file.filename.clone(); file.delete(db).await?; - let _ = repo_remove(filename.clone()); + + pacman_repo_utils::repo_remove( + filename.clone(), + "./repo/repo.db.tar.gz".to_string(), + "./repo/repo.files.tar.gz".to_string(), + )?; fs::remove_file(format!("./repo/{}", filename))?; info!("Removed old file: {}", filename);