diff --git a/.gitignore b/.gitignore index 281fbff1..eed0580b 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,9 @@ crates/*/target # file for saving processes from `persist` /persist-dump.json +# miscellaneous ignored folder +/ignored + # alexandrie local testing folders /app-data /crate-index diff --git a/crates/alexandrie-index/src/index/cli.rs b/crates/alexandrie-index/src/index/cli.rs index 4af77fd8..7cb73752 100644 --- a/crates/alexandrie-index/src/index/cli.rs +++ b/crates/alexandrie-index/src/index/cli.rs @@ -5,7 +5,7 @@ use semver::{Version, VersionReq}; use crate::error::Error; use crate::tree::Tree; -use crate::{CrateVersion, Indexer}; +use crate::{ConfigFile, CrateVersion, Indexer}; /// The 'command-line' crate index management strategy type. /// @@ -39,6 +39,10 @@ impl Indexer for CommandLineIndex { self.repo.commit_and_push(msg) } + fn configuration(&self) -> Result { + self.tree.configuration() + } + fn match_record(&self, name: &str, req: VersionReq) -> Result { self.tree.match_record(name, req) } diff --git a/crates/alexandrie-index/src/index/git2.rs b/crates/alexandrie-index/src/index/git2.rs index 89efc1a8..f48cd705 100644 --- a/crates/alexandrie-index/src/index/git2.rs +++ b/crates/alexandrie-index/src/index/git2.rs @@ -5,7 +5,7 @@ use semver::{Version, VersionReq}; use crate::error::Error; use crate::tree::Tree; -use crate::{CrateVersion, Indexer}; +use crate::{ConfigFile, CrateVersion, Indexer}; /// The 'git2' crate index management strategy type. /// @@ -135,6 +135,10 @@ impl Indexer for Git2Index { Ok(()) } + fn configuration(&self) -> Result { + self.tree.configuration() + } + fn match_record(&self, name: &str, req: VersionReq) -> Result { self.tree.match_record(name, req) } diff --git a/crates/alexandrie-index/src/lib.rs b/crates/alexandrie-index/src/lib.rs index 356caca5..3a6d7201 100644 --- a/crates/alexandrie-index/src/lib.rs +++ b/crates/alexandrie-index/src/lib.rs @@ -1,4 +1,5 @@ use semver::{Version, VersionReq}; +use serde::{Deserialize, Serialize}; pub mod config; pub mod error; @@ -7,8 +8,8 @@ mod index; mod models; mod tree; -pub use index::*; -pub use models::{CrateDependency, CrateDependencyKind, CrateVersion}; +pub use crate::index::*; +pub use crate::models::{CrateDependency, CrateDependencyKind, CrateVersion}; use crate::cli::CommandLineIndex; use crate::error::Error; @@ -28,20 +29,34 @@ pub enum Index { Git2(Git2Index), } +/// Represents the index's configuration file. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct ConfigFile { + /// The download link format for crates. + pub dl: String, + /// The base URL where the API for this index's registry can be found. + pub api: String, + /// The URLs to the other registries that crates in this index are allowed to have dependencies on. + pub allowed_registries: Vec, +} + /// The required trait that any crate index management type must implement. pub trait Indexer { /// Gives back the URL of the managed crate index. fn url(&self) -> Result; /// Refreshes the managed crate index (in case another instance made modification to it). fn refresh(&self) -> Result<(), Error>; + /// Commits and pushes changes upstream. + fn commit_and_push(&self, msg: &str) -> Result<(), Error>; + /// Retrieves the index's current configuration file. + fn configuration(&self) -> Result; /// Retrieves all the version records of a crate. fn all_records(&self, name: &str) -> Result, Error>; /// Retrieves the latest version record of a crate. fn latest_record(&self, name: &str) -> Result; /// Retrieves the latest crate version record that matches the given name and version requirement. fn match_record(&self, name: &str, req: VersionReq) -> Result; - /// Commits and pushes changes upstream. - fn commit_and_push(&self, msg: &str) -> Result<(), Error>; /// Adds a new crate record into the index. fn add_record(&self, record: CrateVersion) -> Result<(), Error>; /// Alters an index's crate version record with the passed-in function. @@ -83,6 +98,14 @@ impl Indexer for Index { } } + fn configuration(&self) -> Result { + match self { + Index::CommandLine(idx) => idx.configuration(), + #[cfg(feature = "git2")] + Index::Git2(idx) => idx.configuration(), + } + } + fn all_records(&self, name: &str) -> Result, Error> { match self { Index::CommandLine(idx) => idx.all_records(name), diff --git a/crates/alexandrie-index/src/tree.rs b/crates/alexandrie-index/src/tree.rs index 82ec5f3b..94e459cc 100644 --- a/crates/alexandrie-index/src/tree.rs +++ b/crates/alexandrie-index/src/tree.rs @@ -7,7 +7,7 @@ use semver::{Version, VersionReq}; use crate::error::IndexError; use crate::models::CrateVersion; -use crate::Error; +use crate::{ConfigFile, Error}; #[derive(Debug, Clone, PartialEq)] pub struct Tree { @@ -28,6 +28,13 @@ impl Tree { } } + pub fn configuration(&self) -> Result { + let path = self.path.join("config.json"); + let bytes = fs::read(&path)?; + let config = json::from_slice(&bytes)?; + Ok(config) + } + pub fn match_record(&self, name: &str, req: VersionReq) -> Result { let path = self.compute_record_path(name); let file = fs::File::open(path).map_err(|err| match err.kind() { @@ -96,14 +103,10 @@ impl Tree { }), _ => Error::from(err), })?; - let mut krates: Vec = { - let mut out = Vec::new(); - for line in io::BufReader::new(file).lines() { - let krate = json::from_str(line?.as_str())?; - out.push(krate); - } - out - }; + let mut krates: Vec = io::BufReader::new(file) + .lines() + .map(|line| Ok(json::from_str(line?.as_str())?)) + .collect::>()?; let found = krates .iter_mut() .find(|krate| krate.vers == version) diff --git a/crates/alexandrie/src/api/mod.rs b/crates/alexandrie/src/api/mod.rs index 61496b79..4aead13f 100644 --- a/crates/alexandrie/src/api/mod.rs +++ b/crates/alexandrie/src/api/mod.rs @@ -4,3 +4,5 @@ pub mod account; pub mod categories; /// Crate-related endpoints (eg. "/api/v1/crates/*"). pub mod crates; +/// Sparse index endpoints (eg. "/api/v1/sparse/se/rd/serde"). +pub mod sparse; diff --git a/crates/alexandrie/src/api/sparse/mod.rs b/crates/alexandrie/src/api/sparse/mod.rs new file mode 100644 index 00000000..98bc3ce3 --- /dev/null +++ b/crates/alexandrie/src/api/sparse/mod.rs @@ -0,0 +1,62 @@ +use std::sync::Arc; + +use axum::extract::{Path, State}; +use axum::Json; +use serde::{Deserialize, Serialize}; + +use alexandrie_index::{ConfigFile, Indexer}; + +use crate::config::AppState; +use crate::error::ApiError; + +/// Response body for this route. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PathParams { + /// The crate's name. + pub fst: String, + /// The crate's description. + pub snd: String, + /// The crate's repository link. + #[serde(rename = "crate")] + pub krate: Option, +} + +/// Route to sparsly access index entries for a crate. +pub async fn get( + State(state): State>, + Path(params): Path, +) -> Result { + let (fst, snd, krate) = match params.krate.as_deref() { + Some(krate) => (params.fst.as_str(), Some(params.snd.as_str()), krate), + None => (params.fst.as_str(), None, params.snd.as_str()), + }; + + let maybe_expected = match krate.len() { + 0 => None, + 1 => Some(("1", None)), + 2 => Some(("2", None)), + 3 => Some(("3", Some(&krate[..1]))), + _ => Some((&krate[..2], Some(&krate[2..4]))), + }; + + if maybe_expected.map_or(true, |it| it != (fst, snd)) { + return Err(ApiError::msg("the crate could not be found")); + } + + let records = state.index.all_records(krate)?; + + let mut output = String::default(); + for record in records { + let record = json::to_string(&record)?; + output.push_str(&record); + output.push('\n'); + } + + Ok(output) +} + +/// Route to sparsly access the index's configuration. +pub async fn get_config(State(state): State>) -> Result, ApiError> { + let config = state.index.configuration()?; + Ok(Json(config)) +} diff --git a/crates/alexandrie/src/main.rs b/crates/alexandrie/src/main.rs index 45310609..cc71e66b 100644 --- a/crates/alexandrie/src/main.rs +++ b/crates/alexandrie/src/main.rs @@ -31,6 +31,8 @@ use clap::Parser; use diesel_migrations::MigrationHarness; use tower_http::trace::{self, TraceLayer}; use tracing::Level; +use tracing_subscriber::filter::LevelFilter; +use tracing_subscriber::EnvFilter; #[cfg(feature = "frontend")] use axum::http::StatusCode; @@ -40,8 +42,6 @@ use tower_http::services::ServeDir; use tower_sessions::cookie::SameSite; #[cfg(feature = "frontend")] use tower_sessions::SessionManagerLayer; -use tracing_subscriber::filter::LevelFilter; -use tracing_subscriber::EnvFilter; /// API endpoints definitions. pub mod api; @@ -185,6 +185,9 @@ fn api_routes() -> Router> { "/crates/:name/:version/download", get(api::crates::download::get), ) + .route("/sparse/:fst/:snd/:crate", get(api::sparse::get)) + .route("/sparse/:fst/:snd", get(api::sparse::get)) + .route("/sparse/config.json", get(api::sparse::get_config)) } #[derive(Debug, Parser)]