diff options
| author | Naz <ndpm13@ch-naseem.com> | 2025-07-30 07:11:24 +0100 |
|---|---|---|
| committer | Naz <ndpm13@ch-naseem.com> | 2025-07-30 07:11:24 +0100 |
| commit | df40ab0fd7d9641297833dc7e0691522c25e2bc9 (patch) | |
| tree | b185168ceedb4f71b255e7e61a365566da41538d | |
| parent | d3c84b04d7a46735dd19a2dae9448e811a609291 (diff) | |
| parent | 11a86042e73bb0eecad61ac6e636dd98563167f5 (diff) | |
Merge pull request '🔧refactor: separate download logic from AppImage struct' (#11) from feat/issue-7 into main
Reviewed-on: https://git.ch-naseem.com/ndpm13/zap-rs/pulls/11
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | src/appimage.rs | 20 | ||||
| -rw-r--r-- | src/args.rs | 2 | ||||
| -rw-r--r-- | src/downloader.rs | 58 | ||||
| -rw-r--r-- | src/index.rs | 42 | ||||
| -rw-r--r-- | src/lib.rs | 29 | ||||
| -rw-r--r-- | src/main.rs | 41 | ||||
| -rw-r--r-- | src/manager.rs | 60 | ||||
| -rw-r--r-- | src/paths.rs | 14 | ||||
| -rw-r--r-- | src/symlink.rs | 42 | ||||
| -rw-r--r-- | src/types.rs | 108 |
11 files changed, 259 insertions, 159 deletions
@@ -12,7 +12,7 @@ cargo install --git https://github.com/ndpm13/zap-rs ```bash # Install from URL -zap-rs install --from https://f.sed.lol/wow.AppImage wow +zap-rs install --from https://f.sed.lol/wow.AppImage wow # Remove zap-rs rm neovim diff --git a/src/appimage.rs b/src/appimage.rs new file mode 100644 index 0000000..5d0f8e5 --- /dev/null +++ b/src/appimage.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Debug, Serialize, Deserialize)] +pub struct AppImage { + pub file_path: PathBuf, + pub executable: String, + pub source: Source, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Source { + pub identifier: String, + pub meta: SourceMetadata, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SourceMetadata { + pub url: String, +} diff --git a/src/args.rs b/src/args.rs index 03f6b51..39354f6 100644 --- a/src/args.rs +++ b/src/args.rs @@ -30,7 +30,7 @@ pub struct InstallArgs { /// Provide a repository slug, or a direct URL to an appimage. #[arg(long)] pub from: String, - + /// Name of the executable #[arg(long)] pub executable: Option<String>, diff --git a/src/downloader.rs b/src/downloader.rs new file mode 100644 index 0000000..2196e25 --- /dev/null +++ b/src/downloader.rs @@ -0,0 +1,58 @@ +use futures_util::StreamExt; +use std::path::PathBuf; +use tokio::{fs, io::AsyncWriteExt}; + +use crate::{appimages_dir, make_progress_bar}; + +#[derive(Debug, Default)] +pub struct Downloader {} + +impl Downloader { + pub fn new() -> Self { + Self {} + } + pub fn prepare_path(&self, url: &str, executable: &str) -> PathBuf { + // Try to extract filename from URL or use default + let filename = match url.split('/').next_back() { + Some(name) => name.to_string(), + None => format!("{executable}.AppImage"), + }; + + appimages_dir().join(filename) + } + pub async fn download_with_progress( + &self, + url: &str, + path: &PathBuf, + ) -> Result<(), Box<dyn std::error::Error>> { + fs::create_dir_all(&appimages_dir()).await?; + + let resp = reqwest::get(&url.to_string()).await?; + let total_size = resp.content_length().unwrap_or(0); + + let bar = make_progress_bar(total_size); + let mut out = tokio::fs::File::create(&path).await?; + + // Stream download with progress updates + let mut stream = resp.bytes_stream(); + while let Some(chunk) = stream.next().await { + let chunk = chunk?; + let len = chunk.len() as u64; + out.write_all(&chunk).await?; + bar.inc(len); + } + + bar.finish_with_message("Download complete!"); + + // Make executable + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&path).await?.permissions(); + perms.set_mode(0o755); + fs::set_permissions(&path, perms).await?; + } + + Ok(()) + } +} diff --git a/src/index.rs b/src/index.rs new file mode 100644 index 0000000..069068a --- /dev/null +++ b/src/index.rs @@ -0,0 +1,42 @@ +use tokio::fs; + +use crate::{AppImage, index_dir}; + +#[derive(Debug, Default)] +pub struct Index {} + +impl Index { + pub fn new() -> Self { + Self {} + } + pub async fn get(&self, appname: &str) -> Result<AppImage, Box<dyn std::error::Error>> { + let index_file_path = index_dir().join(format!("{appname}.json")); + let index_file_content = fs::read_to_string(&index_file_path).await?; + let appimage: AppImage = serde_json::from_str(&index_file_content)?; + + Ok(appimage) + } + pub fn exists(&self, executable: &str) -> bool { + index_dir().join(format!("{}.json", &executable)).exists() + } + pub async fn add( + &self, + appimage: &AppImage, + appname: &str, + ) -> Result<(), Box<dyn std::error::Error>> { + fs::create_dir_all(&index_dir()).await?; + + let index_file = &index_dir().join(format!("{appname}.json")); + + let json = serde_json::to_string_pretty(appimage)?; + fs::write(index_file, json).await?; + + Ok(()) + } + pub async fn remove(&self, appname: &str) -> Result<(), Box<dyn std::error::Error>> { + let index_file_path = index_dir().join(format!("{appname}.json")); + fs::remove_file(index_file_path).await?; + + Ok(()) + } +} @@ -1,22 +1,17 @@ +mod appimage; mod args; +mod downloader; +mod index; +mod manager; +mod paths; +mod symlink; mod tui; -mod types; +pub use crate::appimage::*; pub use crate::args::*; +pub use crate::downloader::*; +pub use crate::index::*; +pub use crate::manager::*; +pub use crate::paths::*; +pub use crate::symlink::*; pub use crate::tui::*; -pub use crate::types::*; - -use std::path::PathBuf; - -pub fn zap_rs_home() -> PathBuf { - let home = std::env::var("HOME").expect("HOME not set"); - PathBuf::from(home).join(".local/share/zap-rs") -} - -pub fn index_dir() -> PathBuf { - zap_rs_home().join("index") -} - -pub fn appimages_dir() -> PathBuf { - zap_rs_home().join("appimages") -} diff --git a/src/main.rs b/src/main.rs index 4c3d5d7..1d9505b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,18 @@ +use std::path::PathBuf; + use clap::Parser; -use tokio::fs; -use zap_rs::{AppImage, Cli, Command, Source, SourceMetadata, appimages_dir, index_dir}; +use zap_rs::{AppImage, Cli, Command, PackageManager, Source, SourceMetadata}; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let args = Cli::parse(); + let pm = PackageManager::new(); match args.command { Command::Install(args) => { - let options = AppImage { - file_path: appimages_dir().join( - args.from - .split('/') - .next_back() - .filter(|s| !s.is_empty()) - .unwrap_or("app.AppImage"), - ), + let mut options = AppImage { + file_path: PathBuf::new(), executable: args.executable.unwrap_or(args.appname.clone()), source: Source { identifier: "raw_url".to_string(), @@ -24,32 +20,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { }, }; - if index_dir() - .join(format!("{}.json", &options.executable)) - .exists() - { - eprintln!("{} is already installed.", &options.executable); - } else { - options.download_from_url().await?; - options.save_to_index(&args.appname).await?; - options.create_symlink().await?; - } + pm.install(&mut options, &args.appname).await?; } Command::Remove(args) => { - let index_file_path = index_dir().join(format!("{}.json", args.appname)); - let index_file_content = fs::read_to_string(&index_file_path).await?; - let appimage: AppImage = serde_json::from_str(&index_file_content)?; - - appimage.remove().await?; + pm.remove(&args.appname).await?; } Command::List => { - let mut appimages = fs::read_dir(index_dir()).await?; - - while let Some(appimage) = appimages.next_entry().await? { - if let Some(name) = appimage.file_name().to_str() { - println!("- {}", name.strip_suffix(".json").unwrap()); - } - } + pm.list().await?; } }; diff --git a/src/manager.rs b/src/manager.rs new file mode 100644 index 0000000..2c75900 --- /dev/null +++ b/src/manager.rs @@ -0,0 +1,60 @@ +use tokio::fs; + +use crate::{AppImage, Downloader, Index, SymlinkManager, index_dir}; + +#[derive(Debug, Default)] +pub struct PackageManager { + pub downloader: Downloader, + pub index: Index, + pub symlink_manager: SymlinkManager, +} + +impl PackageManager { + pub fn new() -> Self { + Self { + downloader: Downloader::new(), + index: Index::new(), + symlink_manager: SymlinkManager::new(), + } + } + pub async fn install( + &self, + appimage: &mut AppImage, + appname: &str, + ) -> Result<(), Box<dyn std::error::Error>> { + if self.index.exists(&appimage.executable) { + return Err(format!("{} is already installed.", &appimage.executable).into()); + } + + appimage.file_path = self + .downloader + .prepare_path(&appimage.source.meta.url, &appimage.executable); + self.downloader + .download_with_progress(&appimage.source.meta.url, &appimage.file_path) + .await?; + + self.index.add(appimage, appname).await?; + self.symlink_manager.create(appimage).await?; + Ok(()) + } + pub async fn remove(&self, appname: &str) -> Result<(), Box<dyn std::error::Error>> { + let appimage = self.index.get(appname).await?; + + fs::remove_file(&appimage.file_path).await?; + self.symlink_manager.remove(&appimage.executable).await?; + self.index.remove(appname).await?; + + Ok(()) + } + pub async fn list(&self) -> Result<(), Box<dyn std::error::Error>> { + let mut appimages = fs::read_dir(index_dir()).await?; + + while let Some(appimage) = appimages.next_entry().await? { + if let Some(name) = appimage.file_name().to_str() { + println!("- {}", name.strip_suffix(".json").unwrap()); + } + } + + Ok(()) + } +} diff --git a/src/paths.rs b/src/paths.rs new file mode 100644 index 0000000..172cae6 --- /dev/null +++ b/src/paths.rs @@ -0,0 +1,14 @@ +use std::path::PathBuf; + +pub fn zap_rs_home() -> PathBuf { + let home = std::env::var("HOME").expect("HOME not set"); + PathBuf::from(home).join(".local/share/zap-rs") +} + +pub fn index_dir() -> PathBuf { + zap_rs_home().join("index") +} + +pub fn appimages_dir() -> PathBuf { + zap_rs_home().join("appimages") +} diff --git a/src/symlink.rs b/src/symlink.rs new file mode 100644 index 0000000..843115c --- /dev/null +++ b/src/symlink.rs @@ -0,0 +1,42 @@ +use std::path::PathBuf; +use tokio::fs; + +use crate::AppImage; + +#[derive(Debug, Default)] +pub struct SymlinkManager {} + +impl SymlinkManager { + pub fn new() -> Self { + Self {} + } + pub async fn remove(&self, executable: &str) -> Result<(), Box<dyn std::error::Error>> { + let home = std::env::var("HOME")?; + let symlink_path = PathBuf::from(home).join(".local/bin").join(executable); + + fs::remove_file(symlink_path).await?; + + Ok(()) + } + pub async fn create(&self, appimage: &AppImage) -> Result<(), Box<dyn std::error::Error>> { + let home = std::env::var("HOME")?; + let local_bin = PathBuf::from(home).join(".local/bin"); + + fs::create_dir_all(&local_bin).await?; + + let symlink_path = local_bin.join(&appimage.executable); + + #[cfg(unix)] + { + use tokio::fs; + + if symlink_path.exists() { + fs::remove_file(&symlink_path).await?; + } + + std::os::unix::fs::symlink(&appimage.file_path, &symlink_path)?; + } + + Ok(()) + } +} diff --git a/src/types.rs b/src/types.rs deleted file mode 100644 index fef9071..0000000 --- a/src/types.rs +++ /dev/null @@ -1,108 +0,0 @@ -use futures_util::StreamExt; -use serde::{Serialize, Deserialize}; -use std::path::PathBuf; -use tokio::{fs, io::AsyncWriteExt}; - -use crate::{appimages_dir, index_dir, make_progress_bar}; - -#[derive(Debug, Serialize, Deserialize)] -pub struct AppImage { - pub file_path: PathBuf, - pub executable: String, - pub source: Source, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Source { - pub identifier: String, - pub meta: SourceMetadata, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct SourceMetadata { - pub url: String, -} - -impl AppImage { - pub async fn save_to_index(&self, appname: &str) -> Result<(), Box<dyn std::error::Error>> { - fs::create_dir_all(&index_dir()).await?; - - let index_file = &index_dir().join(format!("{appname}.json")); - - let json = serde_json::to_string_pretty(self)?; - fs::write(index_file, json).await?; - - Ok(()) - } - pub async fn download_from_url(&self) -> Result<(), Box<dyn std::error::Error>> { - fs::create_dir_all(&appimages_dir()).await?; - - // Try to extract filename from URL or use default - let url = &self.source.meta.url; - let filename = match url.split('/').next_back() { - Some(name) => name.to_string(), - None => format!("{}.AppImage", &self.executable), - }; - let file_path = &appimages_dir().join(filename); - - let resp = reqwest::get(&url.to_string()).await?; - let total_size = resp.content_length().unwrap_or(0); - - let bar = make_progress_bar(total_size); - let mut out = tokio::fs::File::create(&file_path).await?; - - // Stream download with progress updates - let mut stream = resp.bytes_stream(); - while let Some(chunk) = stream.next().await { - let chunk = chunk?; - let len = chunk.len() as u64; - out.write_all(&chunk).await?; - bar.inc(len); - } - - bar.finish_with_message("Download complete!"); - - // Make executable - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let mut perms = fs::metadata(&file_path).await?.permissions(); - perms.set_mode(0o755); - fs::set_permissions(&file_path, perms).await?; - } - - Ok(()) - } - pub async fn create_symlink(&self) -> Result<(), Box<dyn std::error::Error>> { - let home = std::env::var("HOME")?; - let local_bin = PathBuf::from(home).join(".local/bin"); - - fs::create_dir_all(&local_bin).await?; - - let symlink_path = local_bin.join(&self.executable); - - #[cfg(unix)] - { - use tokio::fs; - - if symlink_path.exists() { - fs::remove_file(&symlink_path).await?; - } - - std::os::unix::fs::symlink(&self.file_path, &symlink_path)?; - } - - Ok(()) - } - pub async fn remove(&self) -> Result<(), Box<dyn std::error::Error>> { - let home = std::env::var("HOME")?; - let symlink_path = PathBuf::from(home).join(".local/bin").join(&self.executable); - let index_path = index_dir().join(format!("{}.json", &self.executable)); - - fs::remove_file(&self.file_path).await?; - fs::remove_file(symlink_path).await?; - fs::remove_file(index_path).await?; - - Ok(()) - } -} |
