Skip to content

Commit

Permalink
Implement list subcommand and installed tools storage, various other …
Browse files Browse the repository at this point in the history
…improvements
  • Loading branch information
filiptibell committed Mar 22, 2024
1 parent 1d6e53b commit 2681e2b
Show file tree
Hide file tree
Showing 10 changed files with 272 additions and 19 deletions.
27 changes: 27 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ default-run = "aftman"
[dependencies]
anyhow = "1.0"
clap = { version = "4.5", features = ["derive"] }
const_format = "0.2"
dashmap = "5.5"
dialoguer = "0.11"
dirs = "5.0"
Expand Down
43 changes: 31 additions & 12 deletions lib/storage/home.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

use super::{StorageError, StorageResult, TrustStorage};
use super::{InstalledStorage, StorageError, StorageResult, TrustStorage};

/**
Aftman's home directory.
This is where Aftman stores its configuration, tools, and other data.
Aftman's home directory - this is where Aftman stores its
configuration, tools, and other data. Can be cheaply cloned
while still referring to the same underlying data.
By default, this is `$HOME/.aftman`, but can be overridden
by setting the `AFTMAN_ROOT` environment variable.
Expand All @@ -18,6 +18,7 @@ pub struct Home {
path: Arc<Path>,
saved: Arc<AtomicBool>,
trust: TrustStorage,
installed: InstalledStorage,
}

impl Home {
Expand All @@ -29,8 +30,14 @@ impl Home {
let saved = Arc::new(AtomicBool::new(false));

let trust = TrustStorage::load(&path).await?;
let installed = InstalledStorage::load(&path).await?;

Ok(Self { path, saved, trust })
Ok(Self {
path,
saved,
trust,
installed,
})
}

/**
Expand Down Expand Up @@ -62,25 +69,37 @@ impl Home {
&self.trust
}

/**
Returns a reference to the `InstalledStorage` for this `Home`.
*/
pub fn installed(&self) -> &InstalledStorage {
&self.installed
}

/**
Saves the contents of this `Home` to disk.
*/
pub async fn save(&self) -> StorageResult<()> {
self.trust.save(&self.path).await?;
self.installed.save(&self.path).await?;
self.saved.store(true, Ordering::SeqCst);
Ok(())
}
}

// Implement Drop with an error message if the Home was dropped
// without being saved - this should never happen since a Home
// should always be loaded once on startup and saved on shutdown
// in the CLI, but this detail may be missed during refactoring.
// In the future, if AsyncDrop ever becomes a thing, we can just
// force the save to happen in the Drop implementation instead.
/*
Implement Drop with an error message if the Home was dropped
without being saved - this should never happen since a Home
should always be loaded once on startup and saved on shutdown
in the CLI, but this detail may be missed during refactoring.
In the future, if AsyncDrop ever becomes a thing, we can just
force the save to happen in the Drop implementation instead.
*/
impl Drop for Home {
fn drop(&mut self) {
if !self.saved.load(Ordering::SeqCst) {
let is_last = Arc::strong_count(&self.path) <= 1;
if is_last && !self.saved.load(Ordering::SeqCst) {
tracing::error!(
"Aftman home was dropped without being saved!\
\nChanges to trust, tools, and more may have been lost."
Expand Down
143 changes: 143 additions & 0 deletions lib/storage/installed.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#![allow(clippy::should_implement_trait)]
#![allow(clippy::inherent_to_string)]

use std::{collections::BTreeSet, convert::Infallible, str::FromStr, sync::Arc};

use dashmap::DashSet;
use semver::Version;

use crate::tool::{ToolId, ToolSpec};

/**
Storage for installed tool specifications.
Can be cheaply cloned while still
referring to the same underlying data.
*/
#[derive(Debug, Default, Clone)]
pub struct InstalledStorage {
tools: Arc<DashSet<ToolSpec>>,
}

impl InstalledStorage {
/**
Create a new, **empty** `InstalledStorage`.
*/
pub fn new() -> Self {
Self::default()
}

/**
Parse the contents of a string into a `InstalledStorage`.
Note that this is not fallible - any invalid
lines or tool specifications will simply be ignored.
This means that, worst case, if the installed storage file is corrupted,
the user will simply have to re-install the tools they want to use.
*/
pub fn from_str(s: impl AsRef<str>) -> Self {
let tools = s
.as_ref()
.lines()
.filter_map(|line| line.parse::<ToolSpec>().ok())
.collect::<DashSet<_>>();
Self {
tools: Arc::new(tools),
}
}

/**
Add a tool to this `InstalledStorage`.
Returns `true` if the tool was added and not already trusted.
*/
pub fn add_spec(&self, tool: ToolSpec) -> bool {
self.tools.insert(tool)
}

/**
Remove a tool from this `InstalledStorage`.
Returns `true` if the tool was previously trusted and has now been removed.
*/
pub fn remove_spec(&self, tool: &ToolSpec) -> bool {
self.tools.remove(tool).is_some()
}

/**
Check if a tool is cached in this `InstalledStorage`.
*/
pub fn is_installed(&self, tool: &ToolSpec) -> bool {
self.tools.contains(tool)
}

/**
Get a sorted copy of the installed tools in this `InstalledStorage`.
*/
pub fn all_specs(&self) -> Vec<ToolSpec> {
let mut sorted_tools = self.tools.iter().map(|id| id.clone()).collect::<Vec<_>>();
sorted_tools.sort();
sorted_tools
}

/**
Get a sorted list of all unique tool identifiers in this `InstalledStorage`.
*/
pub fn all_ids(&self) -> Vec<ToolId> {
let sorted_set = self
.all_specs()
.into_iter()
.map(ToolId::from)
.collect::<BTreeSet<_>>();
sorted_set.into_iter().collect()
}

/**
Get a sorted list of all unique versions for a
given tool identifier in this `InstalledStorage`.
*/
pub fn all_versions_for_id(&self, id: &ToolId) -> Vec<Version> {
let sorted_set = self
.all_specs()
.into_iter()
.filter_map(|spec| {
if ToolId::from(spec.clone()) == *id {
Some(spec.version().clone())
} else {
None
}
})
.collect::<BTreeSet<_>>();
sorted_set.into_iter().collect()
}

/**
Render the contents of this `InstalledStorage` to a string.
This will be a sorted list of all tool specifications, separated by newlines.
*/
pub fn to_string(&self) -> String {
let mut contents = self
.all_specs()
.into_iter()
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join("\n");
contents.push('\n');
contents
}
}

impl FromStr for InstalledStorage {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(InstalledStorage::from_str(s))
}
}

impl ToString for InstalledStorage {
fn to_string(&self) -> String {
self.to_string()
}
}
22 changes: 20 additions & 2 deletions lib/storage/load_and_save.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use std::path::Path;
use std::path::{Path, MAIN_SEPARATOR};

use const_format::concatcp;
use tokio::fs::{read_to_string, write};

use super::{StorageResult, TrustStorage};
use super::{InstalledStorage, StorageResult, TrustStorage};

const FILE_PATH_TRUST: &str = "trusted.txt";
const FILE_PATH_INSTALLED: &str = concatcp!("tool-storage", MAIN_SEPARATOR, "installed.txt");

impl TrustStorage {
pub(super) async fn load(home_path: impl AsRef<Path>) -> StorageResult<Self> {
Expand All @@ -21,3 +23,19 @@ impl TrustStorage {
Ok(write(path, self.to_string()).await?)
}
}

impl InstalledStorage {
pub(super) async fn load(home_path: impl AsRef<Path>) -> StorageResult<Self> {
let path = home_path.as_ref().join(FILE_PATH_INSTALLED);
match read_to_string(&path).await {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(InstalledStorage::new()),
Err(e) => Err(e.into()),
Ok(s) => Ok(InstalledStorage::from_str(s)),
}
}

pub(super) async fn save(&self, home_path: impl AsRef<Path>) -> StorageResult<()> {
let path = home_path.as_ref().join(FILE_PATH_INSTALLED);
Ok(write(path, self.to_string()).await?)
}
}
2 changes: 2 additions & 0 deletions lib/storage/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
mod home;
mod installed;
mod load_and_save;
mod result;
mod trust;

pub use home::Home;
pub use installed::InstalledStorage;
pub use result::{StorageError, StorageResult};
pub use trust::TrustStorage;
3 changes: 3 additions & 0 deletions lib/storage/trust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ use crate::tool::ToolId;

/**
Storage for trusted tool identifiers.
Can be cheaply cloned while still
referring to the same underlying data.
*/
#[derive(Debug, Default, Clone)]
pub struct TrustStorage {
Expand Down
13 changes: 12 additions & 1 deletion lib/tool/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ pub enum ToolSpecParseError {
This is an extension of [`ToolId`] used to uniquely identify
a *specific version requirement* of a given tool.
*/
#[derive(Debug, Clone, PartialEq, Eq, Hash, DeserializeFromStr, SerializeDisplay)]
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, DeserializeFromStr, SerializeDisplay,
)]
pub struct ToolSpec {
pub(super) author: String,
pub(super) name: String,
Expand Down Expand Up @@ -96,6 +98,15 @@ impl From<(ToolId, Version)> for ToolSpec {
}
}

impl From<ToolSpec> for ToolId {
fn from(spec: ToolSpec) -> Self {
ToolId {
author: spec.author,
name: spec.name,
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading

0 comments on commit 2681e2b

Please sign in to comment.