Skip to content

Commit

Permalink
Merge pull request #823 from winpax/refactor-app-commands
Browse files Browse the repository at this point in the history
refactor app commands
  • Loading branch information
jewlexx authored Jun 10, 2024
2 parents 34d249e + 2fec61b commit 7961090
Show file tree
Hide file tree
Showing 41 changed files with 553 additions and 497 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added sprinkles contributors to credits
- Enable `contexts` feature by default
- Logs are now moved to the new logging directory if any are found in the old location
- `app` command for managing apps

### Changed

Expand All @@ -26,6 +27,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Use Rust nightly toolchain
- Logs now go into `LocalAppData\sfsu\logs` instead of `<sfsu install folder>\logs`
- Run debug build on push and only run release build on release
- Internal: Do not make `wrappers` module public
- Moved `purge` command into `app` subcommand
- Internal: allow dead code in `Signature` impl (functions reserved for future use)
- Moved all app related commands into `app` subcommand, and added aliases in root command
- Internal: move command docs to structs for modularity

### Removed

Expand Down
35 changes: 10 additions & 25 deletions src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod app;
pub mod bucket;
pub mod cache;
pub mod cat;
Expand All @@ -15,7 +16,6 @@ pub mod info;
pub mod list;
#[cfg(not(feature = "v2"))]
pub mod outdated;
pub mod purge;
pub mod search;
pub mod status;
pub mod update;
Expand Down Expand Up @@ -107,56 +107,41 @@ impl<T: Command> CommandRunner for T {}

#[derive(Debug, Clone, Subcommand, Hooks, Runnable)]
pub enum Commands {
/// Search for a package
Search(search::Args),
/// List all installed packages
#[cfg(not(feature = "v2"))]
List(list::Args),
#[no_hook]
/// Generate hooks for the given shell
Hook(hook::Args),
#[cfg(not(feature = "v2"))]
/// Find buckets that do not have any installed packages
UnusedBuckets(bucket::unused::Args),
/// Manages buckets
Bucket(bucket::Args),
#[cfg(not(feature = "v2"))]
/// Describe a package
Describe(describe::Args),
/// Display information about a package
#[cfg(not(feature = "v2"))]
Info(info::Args),
#[cfg(not(feature = "v2"))]
/// List outdated buckets and/or packages
Outdated(outdated::Args),
/// List the dependencies of a given package, in the order that they will be installed
Depends(depends::Args),
#[cfg(feature = "download")]
/// Download the specified app.
#[cfg(all(feature = "download", not(feature = "v2")))]
Download(download::Args),
/// Show status and check for new app versions
Status(status::Args),
#[cfg_attr(not(feature = "v2"), no_hook)]
/// Update Scoop and Scoop buckets
Update(update::Args),
/// Opens the app homepage
#[cfg(not(feature = "v2"))]
Home(home::Args),
/// Show content of specified manifest
#[cfg(not(feature = "v2"))]
Cat(cat::Args),
/// Exports installed apps, buckets (and optionally configs) in JSON format
Export(export::Args),
/// Check for common issues
Checkup(checkup::Args),
#[cfg(feature = "download")]
/// Show or clear the download cache
Cache(cache::Args),
/// Scan a file with `VirusTotal`
Virustotal(virustotal::Args),
#[hook_name = "virustotal"]
#[clap(alias = "virustotal")]
Scan(virustotal::Args),
#[no_hook]
/// Show credits
Credits(credits::Args),
/// Purge package's persist folder
Purge(purge::Args),
App(app::Args),
#[no_hook]
#[cfg(debug_assertions)]
/// Debugging commands
Debug(debug::Args),
}
39 changes: 39 additions & 0 deletions src/commands/app.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
pub mod cat;
#[cfg(feature = "download")]
pub mod download;
pub mod home;
pub mod info;
pub mod list;
pub mod purge;

use clap::{Parser, Subcommand};

use sfsu_macros::Runnable;
use sprinkles::{config, contexts::ScoopContext};

use super::{Command, CommandRunner};

#[derive(Debug, Clone, Subcommand, Runnable)]
pub enum Commands {
Cat(cat::Args),
#[cfg(feature = "download")]
Download(download::Args),
Home(home::Args),
Info(info::Args),
List(list::Args),
Purge(purge::Args),
}

#[derive(Debug, Clone, Parser)]
/// Commands for managing apps
pub struct Args {
#[command(subcommand)]
command: Commands,
}

impl Command for Args {
#[inline]
async fn runner(self, ctx: &impl ScoopContext<config::Scoop>) -> Result<(), anyhow::Error> {
self.command.run(ctx).await
}
}
47 changes: 47 additions & 0 deletions src/commands/app/cat.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use std::{fs::File, io::Read, sync::atomic::Ordering};

use clap::Parser;
use sprinkles::{config, contexts::ScoopContext, packages::reference::package};

use crate::{abandon, COLOR_ENABLED};

#[derive(Debug, Clone, Parser)]
/// Show content of specified manifest
pub struct Args {
#[clap(help = "The manifest to display")]
package: package::Reference,
}

impl super::Command for Args {
async fn runner(self, ctx: &impl ScoopContext<config::Scoop>) -> Result<(), anyhow::Error> {
let manifests = self.package.list_manifest_paths(ctx);

if manifests.is_empty() {
abandon!("No manifests found for {}", self.package);
}

let manifest = &manifests[0];

let manifest_content = {
let mut buf = vec![];

let mut file = File::open(manifest)?;
file.read_to_end(&mut buf)?;

buf
};

if COLOR_ENABLED.load(Ordering::Relaxed) {
use bat::PrettyPrinter;

PrettyPrinter::new()
.input_from_bytes(&manifest_content)
.language("json")
.print()?;
} else {
print!("{}", String::from_utf8_lossy(&manifest_content));
}

Ok(())
}
}
118 changes: 118 additions & 0 deletions src/commands/app/download.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
use clap::Parser;

use sprinkles::{
cache::{Downloader, Handle},
config,
contexts::ScoopContext,
packages::reference::package,
progress::indicatif::MultiProgress,
requests::AsyncClient,
Architecture,
};

use crate::{abandon, output::colours::eprintln_yellow};

#[derive(Debug, Clone, Parser)]
/// Download the specified app.
pub struct Args {
#[clap(short, long, help = "Use the specified architecture, if the app supports it", default_value_t = Architecture::ARCH)]
arch: Architecture,

#[clap(short = 'H', long, help = "Disable hash validation")]
no_hash_check: bool,

#[clap(help = "The packages to download")]
packages: Vec<package::Reference>,

#[clap(from_global)]
json: bool,
}

impl super::Command for Args {
const BETA: bool = true;

async fn runner(self, ctx: &impl ScoopContext<config::Scoop>) -> Result<(), anyhow::Error> {
if self.packages.is_empty() {
abandon!("No packages provided")
}

if self.no_hash_check {
eprintln_yellow!(
"Hash check has been disabled! This may allow modified files to be downloaded"
);
}

let mp = MultiProgress::new();

eprint!("Attempting to generate manifest(s)");
let downloaders: Vec<Downloader> =
futures::future::try_join_all(self.packages.into_iter().map(|package| {
let mp = mp.clone();
async move {
let manifest = match package.manifest(ctx).await {
Ok(manifest) => manifest,
Err(e) => abandon!("\rFailed to generate manifest: {e}"),
};

let dl = Handle::open_manifest(ctx.cache_path(), &manifest, self.arch)?;

let downloaders = dl.into_iter().map(|dl| {
let mp = mp.clone();
async move {
match Downloader::new::<AsyncClient>(dl, Some(&mp)).await {
Ok(dl) => anyhow::Ok(dl),
Err(e) => match e {
sprinkles::cache::Error::ErrorCode(status) => {
abandon!("Found {status} error while downloading")
}
_ => Err(e.into()),
},
}
}
});
let downloaders = futures::future::try_join_all(downloaders).await?;

anyhow::Ok(downloaders)
}
}))
.await?
.into_iter()
.flatten()
.collect();
eprintln!("\r📜 Generated manifest for any and all mismatched versions");

let threads = downloaders
.into_iter()
.map(|dl| tokio::spawn(async move { dl.download().await }));

let results = futures::future::try_join_all(threads).await?;

for result in results {
let result = result?;

if !self.no_hash_check {
eprint!("🔓 Checking {} hash...", result.file_name.url);

let actual_hash = result.actual_hash.no_prefix();

if result.actual_hash == result.computed_hash {
eprintln!("\r🔒 Hash matched: {actual_hash}");
} else {
eprintln!();
abandon!(
"🔓 Hash mismatch: expected {actual_hash}, found {}",
result.computed_hash.no_prefix()
);
}
// } else {
// eprintln!();
// warn!("🔓 No hash provided, skipping hash check");
// }
}

eprintln!("✅ Downloaded {}", result.file_name.url);
}

Ok(())
}
}
28 changes: 28 additions & 0 deletions src/commands/app/home.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use clap::Parser;
use sprinkles::{config, contexts::ScoopContext, packages::reference::package};

use crate::abandon;

#[derive(Debug, Clone, Parser)]
/// Opens the app homepage
pub struct Args {
#[clap(help = "The package to open the homepage for")]
package: package::Reference,
}

impl super::Command for Args {
async fn runner(self, ctx: &impl ScoopContext<config::Scoop>) -> Result<(), anyhow::Error> {
let manifest = self
.package
.first(ctx)
.ok_or(anyhow::anyhow!("Package not found"))?;

let Some(homepage) = manifest.homepage else {
abandon!("No homepage found for package");
};

open::that_detached(homepage)?;

Ok(())
}
}
Loading

0 comments on commit 7961090

Please sign in to comment.