diff --git a/Dockerfile b/Dockerfile index 5d7f50f..520b4fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ RUN cargo build --release FROM archlinux -RUN pacman -Syu --noconfirm openssl-1.1 +RUN pacman -Syu --noconfirm openssl-1.1 tar xz zstd COPY --from=builder /app/target/release/pacco /pacco diff --git a/src/main.rs b/src/main.rs index 2c20f6d..176f2d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -51,23 +51,20 @@ async fn launch() -> _ { ..Default::default() }) .mount_assets() - .mount( - "/", - routes![ - routes::index_page, - routes::pkg_route, - routes::push::upload_pkg, - routes::user::login, - routes::user::login_post, - routes::user::account_page, - routes::ui::pkg_ui, - routes::ui::repo_ui, - routes::user::new_api_key, - routes::user::end_session, - routes::user::change_password, - routes::user::change_password_post - ], - ) + .mount("/", routes![ + routes::index_page, + routes::pkg_route, + routes::push::upload_pkg, + routes::user::login, + routes::user::login_post, + routes::user::account_page, + routes::ui::pkg_ui, + routes::ui::repo_ui, + routes::user::new_api_key, + routes::user::end_session, + routes::user::change_password, + routes::user::change_password_post + ]) .manage(config) .manage(shell) } diff --git a/src/pkg/mirror.rs b/src/pkg/mirror.rs index 1c30e2b..1f2c678 100644 --- a/src/pkg/mirror.rs +++ b/src/pkg/mirror.rs @@ -110,7 +110,7 @@ impl MirrorRepository { } // PKG - let (name, _, _, arch) = Package::extract_pkg_name(pkg_name).unwrap(); + let (name, _, _, arch, _) = Package::extract_pkg_name(pkg_name).unwrap(); self.download_package(pkg_name, &name, arch, mirrorlist) .await; diff --git a/src/pkg/package.rs b/src/pkg/package.rs index 51d4ccc..cf8fb7a 100644 --- a/src/pkg/package.rs +++ b/src/pkg/package.rs @@ -1,7 +1,8 @@ use std::{ fs::File, - io::{BufReader, Cursor, Read}, + io::{BufReader, Cursor, Read, Write}, path::{Path, PathBuf}, + process::{Command, Stdio}, }; use based::get_pg; @@ -23,17 +24,27 @@ pub struct Package { pub rel: i32, /// Version of the package pub version: Option, + /// Compression used + pub compression: Compression, } impl Package { /// Create a new package - pub fn new(repo: &str, arch: Architecture, pkg_name: &str, version: &str, rel: i32) -> Self { + pub fn new( + repo: &str, + arch: Architecture, + pkg_name: &str, + version: &str, + rel: i32, + compression: Compression, + ) -> Self { let pkg = Package { repo: repo.to_string(), arch, name: pkg_name.to_string(), rel: rel, version: Some(version.to_string()), + compression, }; std::fs::create_dir_all(pkg.base_path()).unwrap(); @@ -128,27 +139,38 @@ impl Package { /// ``` /// use pacco::pkg::Package; /// - /// let (name, version, rel, arch) = Package::extract_pkg_name("pkg-ver-rel-x86_64.pkg.tar.zst").unwrap(); + /// let (name, version, rel, arch, compress) = Package::extract_pkg_name("pkg-ver-rel-x86_64.pkg.tar.zst").unwrap(); /// assert_eq!(name, "pkg"); /// assert_eq!(version, "ver"); /// assert_eq!(rel, "rel"); /// assert_eq!(arch, pacco::pkg::arch::Architecture::x86_64); + /// assert_eq!(compress, pacco::pkg::package::Compression::Zstd); /// - /// let (name, version, rel, arch) = Package::extract_pkg_name("my-pkg-ver-rel-x86_64.pkg.tar.zst").unwrap(); + /// let (name, version, rel, arch, compress) = Package::extract_pkg_name("my-pkg-ver-rel-x86_64.pkg.tar.xz").unwrap(); /// assert_eq!(name, "my-pkg"); /// assert_eq!(version, "ver"); /// assert_eq!(rel, "rel"); /// assert_eq!(arch, pacco::pkg::arch::Architecture::x86_64); + /// assert_eq!(compress, pacco::pkg::package::Compression::Xz); /// ``` - pub fn extract_pkg_name(file_name: &str) -> Option<(String, String, String, Architecture)> { + pub fn extract_pkg_name( + file_name: &str, + ) -> Option<(String, String, String, Architecture, Compression)> { // Extract (assuming the filename is "---.pkg.tar.zst") let file_name = file_name.trim_end_matches(".sig").to_string(); let mut splitted = file_name.split('-').collect::>(); let arch = splitted.pop()?; - assert!(arch.ends_with(".pkg.tar.zst"), "{file_name}"); - let arch = arch.trim_end_matches(".pkg.tar.zst"); + + let compression = Compression::from_filename(arch); + assert!(compression.is_some()); + let compression = compression.unwrap(); + + let arch = match compression { + Compression::Zstd => arch.trim_end_matches(".pkg.tar.zst"), + Compression::Xz => arch.trim_end_matches(".pkg.tar.xz"), + }; let relation = splitted.pop()?; let version = splitted.pop()?; @@ -160,29 +182,50 @@ impl Package { version.to_string(), relation.to_string(), Architecture::parse(arch)?, + compression, )) } /// Parse a pkg filename pub fn from_filename(repo: &str, file_name: &str) -> Package { - let (pkg_name, version, rel, arch) = Package::extract_pkg_name(file_name).unwrap(); + let (pkg_name, version, rel, arch, compression) = + Package::extract_pkg_name(file_name).unwrap(); Self { repo: repo.to_string(), arch, name: pkg_name, rel: rel.parse().unwrap(), version: Some(version.to_string()), + compression, } } /// Find a package with latest version pub fn find(repo: &str, arch: Architecture, pkg_name: &str) -> Option { + if let Some(pkg) = Self::find_compressed(repo, arch.clone(), pkg_name, Compression::Zstd) { + return Some(pkg); + } + if let Some(pkg) = Self::find_compressed(repo, arch, pkg_name, Compression::Xz) { + return Some(pkg); + } + + None + } + + /// Find a package with latest version + fn find_compressed( + repo: &str, + arch: Architecture, + pkg_name: &str, + compression: Compression, + ) -> Option { let mut base = Package { repo: repo.to_string(), arch, name: pkg_name.to_string(), rel: 1, version: None, + compression, }; let versions = base.versions(); @@ -397,7 +440,7 @@ impl Package { /// Build a pkg filename from the packages values pub fn file_name(&self) -> String { format!( - "{}-{}-{}-{}.pkg.tar.zst", + "{}-{}-{}-{}.pkg.tar.{}", self.name, if let Some(ver) = &self.version { ver.to_string() @@ -407,9 +450,14 @@ impl Package { }, self.rel, self.arch.to_string(), + self.compression.extension() ) } + pub fn has_pkg_ext(filename: &str) -> bool { + filename.ends_with(".pkg.tar.zst") || filename.ends_with(".pkg.tar.xz") + } + /// Get all versions of the package pub fn versions(&self) -> Vec { let dir_path = self.base_path(); @@ -418,8 +466,8 @@ impl Package { 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 file_name.starts_with(&self.name) && file_name.ends_with(".pkg.tar.zst") { - let (_, version, rel, _) = Package::extract_pkg_name(&file_name).unwrap(); + if file_name.starts_with(&self.name) && Package::has_pkg_ext(&file_name) { + let (_, version, rel, _, _) = Package::extract_pkg_name(&file_name).unwrap(); versions.push(format!("{version}-{rel}")); } } @@ -478,50 +526,63 @@ pub fn repo_add(db_file: &str, pkg_file: &str) { } pub fn read_file_tar(tar: &Path, file_path: &str) -> Option { - let file = File::open(tar).ok()?; - let decoder = Decoder::new(BufReader::new(file)).ok()?; - let mut archive = Archive::new(decoder); + let output = Command::new("tar") + .arg("-xO") // Extract to stdout (-O) + .arg("-f") + .arg(tar) + .arg(file_path) + .output() + .ok()?; - for entry in archive.entries().ok()? { - let mut entry = entry.ok()?; - if entry.path().ok()?.to_str()? == file_path { - let mut file_content = String::new(); - entry.read_to_string(&mut file_content).ok()?; - return Some(file_content); - } + if output.status.success() { + Some(String::from_utf8(output.stdout).ok()?.to_string()) + } else { + None } - - None } pub fn read_file_tar_raw(tar_data: &[u8], file_path: &str) -> Option { - let decoder = Decoder::new(Cursor::new(tar_data)).ok()?; - let mut archive = Archive::new(decoder); + let mut output = Command::new("tar") + .arg("-xO") // Extract to stdout (-O) + .arg("-f") + .arg("-") // Indicate that the file input comes from stdin + .arg(file_path) + .stdin(Stdio::piped()) // Open a pipe to provide the tar data + .spawn() + .ok()?; - for entry in archive.entries().ok()? { - let mut entry = entry.ok()?; - if entry.path().ok()?.to_str()? == file_path { - let mut file_content = String::new(); - entry.read_to_string(&mut file_content).ok()?; - return Some(file_content); - } + if let Some(stdin) = output.stdin.as_mut() { + stdin.write_all(tar_data).ok()?; + stdin.flush().ok()?; } - None + let output = output.wait_with_output().ok()?; + + if output.status.success() { + Some(String::from_utf8(output.stdout).ok()?.to_string()) + } else { + None + } } pub fn list_tar_file(tar: &Path) -> Option> { - let file = File::open(tar).ok()?; - let decoder = Decoder::new(BufReader::new(file)).ok()?; - let mut archive = Archive::new(decoder); - let mut paths = Vec::new(); + let output = Command::new("tar") + .arg("-tf") // List the contents of the tar file + .arg(tar) + .output() + .ok()?; - for entry in archive.entries().ok()? { - let entry = entry.ok()?; - paths.push(entry.path().ok()?.to_string_lossy().into_owned()); + if output.status.success() { + let output_str = String::from_utf8(output.stdout).ok()?; + let paths = output_str + .lines() + .map(|line| line.to_string()) + .collect::>(); + + Some(paths) + } else { + None } - - Some(paths) } #[derive(Debug, Clone, FromRow)] @@ -562,3 +623,28 @@ impl PackageMetaInfo for Package { .execute(get_pg!()).await.unwrap(); } } + +#[derive(Debug, Clone, PartialEq)] +pub enum Compression { + Zstd, + Xz, +} + +impl Compression { + pub fn from_filename(name: &str) -> Option { + if name.ends_with("zst") { + Some(Self::Zstd) + } else if name.ends_with("xz") { + Some(Self::Xz) + } else { + None + } + } + + pub fn extension(&self) -> &str { + match self { + Compression::Zstd => "zst", + Compression::Xz => "xz", + } + } +} diff --git a/src/pkg/repo.rs b/src/pkg/repo.rs index 5afc2eb..7cadf2d 100644 --- a/src/pkg/repo.rs +++ b/src/pkg/repo.rs @@ -140,8 +140,15 @@ impl Repository { pub fn get_pkg(&self, pkg_name: &str) -> Option { // Find package - let (name, version, rel, arch) = Package::extract_pkg_name(pkg_name).unwrap(); - let pkg = Package::new(&self.name, arch, &name, &version, rel.parse().unwrap()); + let (name, version, rel, arch, compress) = Package::extract_pkg_name(pkg_name).unwrap(); + let pkg = Package::new( + &self.name, + arch, + &name, + &version, + rel.parse().unwrap(), + compress, + ); // Return if exists if pkg.exists() { Some(pkg) } else { None } diff --git a/src/routes/mod.rs b/src/routes/mod.rs index a022786..914bbc5 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -9,8 +9,8 @@ use pacco::pkg::package::PackageMetaInfo; use rocket::http::{ContentType, Status}; use rocket::{State, get}; -use pacco::pkg::Repository; use pacco::pkg::arch::Architecture; +use pacco::pkg::{Package, Repository}; use crate::config::Config; @@ -103,14 +103,14 @@ pub async fn pkg_route( .await .unwrap(); - if pkg_name.ends_with("pkg.tar.zst") { + if Package::has_pkg_ext(&pkg_name) { pkg.increase_download_count().await; return DataResponse::new_file( &pkg.pkg_content_path().unwrap(), "application/tar".to_string(), cache_duration, ); - } else if pkg_name.ends_with("pkg.tar.zst.sig") { + } else if pkg_name.ends_with("sig") { return DataResponse::new( pkg.sig_content().unwrap(), "application/pgp-signature".to_string(), @@ -144,13 +144,13 @@ pub async fn pkg_route( let pkg = repo.get_pkg(pkg_name).unwrap(); - if pkg_name.ends_with("pkg.tar.zst") { + if Package::has_pkg_ext(&pkg_name) { return DataResponse::new_file( &pkg.pkg_content_path().unwrap(), "application/tar".to_string(), cache_duration, ); - } else if pkg_name.ends_with("pkg.tar.zst.sig") { + } else if pkg_name.ends_with("sig") { return DataResponse::new( pkg.sig_content().unwrap(), "application/pgp-signature".to_string(), diff --git a/src/routes/push.rs b/src/routes/push.rs index 0881e76..42df102 100644 --- a/src/routes/push.rs +++ b/src/routes/push.rs @@ -77,7 +77,10 @@ pub async fn upload_pkg( let arch = Architecture::parse(&arch).ok_or_else(|| api_error("Invalid architecture"))?; - let pkg = Package::new(repo, arch, pkg_name, &version, rel); + let (_, _, _, _, compression) = Package::extract_pkg_name(upload.pkg.name().unwrap()) + .ok_or_else(|| api_error("Package has weird filename"))?; + + let pkg = Package::new(repo, arch, pkg_name, &version, rel, compression); pkg.save( pkg_file,