Skip to content

Commit

Permalink
Initial implementation of tool storage
Browse files Browse the repository at this point in the history
  • Loading branch information
filiptibell committed Mar 24, 2024
1 parent a11837a commit 27641f7
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 5 deletions.
7 changes: 7 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"
async-signal = "0.2"
async-once-cell = "0.5"
clap = { version = "4.5", features = ["derive"] }
const_format = "0.2"
dashmap = "5.5"
Expand Down
18 changes: 15 additions & 3 deletions lib/storage/home.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

use super::{InstallCache, StorageError, StorageResult, TrustCache};
use super::{InstallCache, StorageError, StorageResult, ToolStorage, TrustCache};

/**
Aftman's home directory - this is where Aftman stores its
Expand All @@ -19,6 +19,7 @@ pub struct Home {
saved: Arc<AtomicBool>,
trust_cache: TrustCache,
install_cache: InstallCache,
tool_storage: ToolStorage,
}

impl Home {
Expand All @@ -29,14 +30,18 @@ impl Home {
let path: Arc<Path> = path.into().into();
let saved = Arc::new(AtomicBool::new(false));

let (trust_cache, install_cache) =
tokio::try_join!(TrustCache::load(&path), InstallCache::load(&path))?;
let (trust_cache, install_cache, tool_storage) = tokio::try_join!(
TrustCache::load(&path),
InstallCache::load(&path),
ToolStorage::load(&path)
)?;

Ok(Self {
path,
saved,
trust_cache,
install_cache,
tool_storage,
})
}

Expand Down Expand Up @@ -76,6 +81,13 @@ impl Home {
&self.install_cache
}

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

/**
Saves the contents of this `Home` to disk.
*/
Expand Down
2 changes: 2 additions & 0 deletions lib/storage/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
mod home;
mod install_cache;
mod result;
mod tool_storage;
mod trust_cache;
mod util;

pub use home::Home;
pub use install_cache::InstallCache;
pub use result::{StorageError, StorageResult};
pub use tool_storage::ToolStorage;
pub use trust_cache::TrustCache;
2 changes: 2 additions & 0 deletions lib/storage/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ pub enum StorageError {
HomeNotFound,
#[error("file not found: {0}")]
FileNotFound(PathBuf),
#[error("task join error: {0}")]
TaskJoinError(#[from] tokio::task::JoinError),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON error: {0}")]
Expand Down
173 changes: 173 additions & 0 deletions lib/storage/tool_storage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
use std::{
env::{consts::EXE_SUFFIX, current_exe},
path::{Path, PathBuf},
sync::Arc,
};

use tokio::{
fs::{create_dir_all, read, read_dir, write},
sync::Mutex as AsyncMutex,
task::{spawn_blocking, JoinSet},
};

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

use super::StorageResult;

/**
Storage for tool binaries and aliases.
Can be cheaply cloned while still
referring to the same underlying data.
*/
#[derive(Debug, Clone)]
pub struct ToolStorage {
pub(super) tools_dir: Arc<Path>,
pub(super) aliases_dir: Arc<Path>,
current_exe_path: Arc<Path>,
current_exe_contents: Arc<AsyncMutex<Option<Vec<u8>>>>,
}

impl ToolStorage {
fn tool_paths(&self, spec: &ToolSpec) -> (PathBuf, PathBuf) {
let tool_dir = self
.tools_dir
.join(spec.author())
.join(spec.name())
.join(spec.version().to_string());
let tool_file = tool_dir.join(format!("{}{EXE_SUFFIX}", spec.name()));
(tool_dir, tool_file)
}

fn aftman_path(&self) -> PathBuf {
self.aliases_dir.join(format!("aftman{EXE_SUFFIX}"))
}

async fn aftman_contents(&self) -> StorageResult<Vec<u8>> {
let mut guard = self.current_exe_contents.lock().await;
if let Some(contents) = &*guard {
return Ok(contents.clone());
}
let contents = read(&self.current_exe_path).await?;
*guard = Some(contents.clone());
Ok(contents)
}

/**
Returns the path to the binary for the given tool.
Note that this does not check if the binary actually exists.
*/
pub fn tool_path(&self, spec: &ToolSpec) -> PathBuf {
self.tool_paths(spec).1
}

/**
Replaces the binary contents for the given tool.
*/
pub async fn replace_tool_contents(
&self,
spec: &ToolSpec,
contents: impl AsRef<[u8]>,
) -> StorageResult<()> {
let (dir_path, file_path) = self.tool_paths(spec);
create_dir_all(dir_path).await?;
write(file_path, contents).await?;
Ok(())
}

/**
Replaces the contents of the stored aftman binary.
If `contents` is `None`, the current executable will
be used, otherwise the given contents will be used.
This would also update the cached contents of
the current executable stored in this struct.
*/
pub async fn replace_aftman_contents(&self, contents: Option<Vec<u8>>) -> StorageResult<()> {
let contents = match contents {
Some(contents) => {
self.current_exe_contents
.lock()
.await
.replace(contents.clone());
contents
}
None => self.aftman_contents().await?,
};
write(self.aftman_path(), &contents).await?;
Ok(())
}

/**
Creates a link for the given tool alias.
Note that if the link already exists, it will be overwritten.
*/
pub async fn create_tool_link(&self, alias: &ToolAlias) -> StorageResult<()> {
let path = self.aliases_dir.join(alias.name());
let contents = self.aftman_contents().await?;
write(&path, &contents).await?;
Ok(())
}

/**
Recreates all known links for tool aliases in the binary directory.
This includes the link for Aftman itself - and if the link for Aftman does
not exist, `true` will be returned to indicate that the link was created.
*/
pub async fn recreate_all_links(&self) -> StorageResult<bool> {
let contents = self.aftman_contents().await?;

let mut link_paths = Vec::new();
let mut link_reader = read_dir(&self.aliases_dir).await?;
while let Some(entry) = link_reader.next_entry().await? {
link_paths.push(entry.path());
}

let aftman_path = self.aftman_path();
let aftman_existed = if link_paths.contains(&aftman_path) {
true
} else {
link_paths.push(aftman_path);
false
};

let mut futures = JoinSet::new();
for link_path in link_paths {
futures.spawn(write(link_path, contents.clone()));
}
while let Some(result) = futures.join_next().await {
result??;
}

Ok(!aftman_existed)
}

pub(crate) async fn load(home_path: impl AsRef<Path>) -> StorageResult<Self> {
let home_path = home_path.as_ref();

let tools_dir = home_path.join("tool-storage").into();
let aliases_dir = home_path.join("bin").into();

let (_, _, current_exe_res) = tokio::try_join!(
create_dir_all(&tools_dir),
create_dir_all(&aliases_dir),
// NOTE: A call to current_exe is blocking on some
// platforms, so we spawn it in a blocking task here.
async { Ok(spawn_blocking(current_exe).await?) },
)?;

let current_exe_path = current_exe_res?.into();
let current_exe_contents = Arc::new(AsyncMutex::new(None));

Ok(Self {
current_exe_path,
current_exe_contents,
tools_dir,
aliases_dir,
})
}
}
3 changes: 3 additions & 0 deletions lib/tool/alias.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ impl FromStr for ToolAlias {
if s.chars().any(char::is_whitespace) {
return Err(ToolAliasParseError::ContainsWhitespace);
}
if s.eq_ignore_ascii_case("aftman") {
return Err(ToolAliasParseError::Invalid);
}
Ok(Self {
name: s.to_string(),
})
Expand Down
3 changes: 1 addition & 2 deletions src/cli/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use anyhow::{Context, Result};
use clap::Parser;

use aftman::{sources::GitHubSource, storage::Home, tool::ToolAlias};
use semver::Version;

use crate::util::ToolIdOrSpec;

Expand All @@ -23,7 +22,7 @@ pub struct AddSubcommand {
}

impl AddSubcommand {
pub async fn run(&self, home: &Home) -> Result<()> {
pub async fn run(&self, _home: &Home) -> Result<()> {
let source = GitHubSource::new()?;

// If we only got an id without a specified version, we
Expand Down

0 comments on commit 27641f7

Please sign in to comment.