first commit
This commit is contained in:
408
src/interfaces/cli.rs
Normal file
408
src/interfaces/cli.rs
Normal file
@@ -0,0 +1,408 @@
|
||||
use crate::config::{self, NamespaceConfig};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::index::FieldIndex;
|
||||
use crate::retrieve::{RetrievalQuery, SearchEngine};
|
||||
use crate::store::{HeroDbClient, OsirisObject};
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::io::{self, Read};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "osiris")]
|
||||
#[command(about = "OSIRIS - Object Storage, Indexing & Retrieval Intelligent System", long_about = None)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum Commands {
|
||||
/// Initialize OSIRIS configuration
|
||||
Init {
|
||||
/// HeroDB URL
|
||||
#[arg(long, default_value = "redis://localhost:6379")]
|
||||
herodb: String,
|
||||
},
|
||||
|
||||
/// Namespace management
|
||||
Ns {
|
||||
#[command(subcommand)]
|
||||
command: NsCommands,
|
||||
},
|
||||
|
||||
/// Put an object
|
||||
Put {
|
||||
/// Object path (namespace/name)
|
||||
path: String,
|
||||
|
||||
/// File to upload (use '-' for stdin)
|
||||
file: String,
|
||||
|
||||
/// Tags (key=value pairs, comma-separated)
|
||||
#[arg(long)]
|
||||
tags: Option<String>,
|
||||
|
||||
/// MIME type
|
||||
#[arg(long)]
|
||||
mime: Option<String>,
|
||||
|
||||
/// Title
|
||||
#[arg(long)]
|
||||
title: Option<String>,
|
||||
},
|
||||
|
||||
/// Get an object
|
||||
Get {
|
||||
/// Object path (namespace/name or namespace/id)
|
||||
path: String,
|
||||
|
||||
/// Output file (default: stdout)
|
||||
#[arg(long)]
|
||||
output: Option<PathBuf>,
|
||||
|
||||
/// Output raw content only (no metadata)
|
||||
#[arg(long)]
|
||||
raw: bool,
|
||||
},
|
||||
|
||||
/// Delete an object
|
||||
Del {
|
||||
/// Object path (namespace/name or namespace/id)
|
||||
path: String,
|
||||
},
|
||||
|
||||
/// Search/find objects
|
||||
Find {
|
||||
/// Text query (optional)
|
||||
query: Option<String>,
|
||||
|
||||
/// Namespace to search
|
||||
#[arg(long)]
|
||||
ns: String,
|
||||
|
||||
/// Filters (key=value pairs, comma-separated)
|
||||
#[arg(long)]
|
||||
filter: Option<String>,
|
||||
|
||||
/// Maximum number of results
|
||||
#[arg(long, default_value = "10")]
|
||||
topk: usize,
|
||||
|
||||
/// Output as JSON
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
|
||||
/// Show statistics
|
||||
Stats {
|
||||
/// Namespace (optional, shows all if not specified)
|
||||
#[arg(long)]
|
||||
ns: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum NsCommands {
|
||||
/// Create a new namespace
|
||||
Create {
|
||||
/// Namespace name
|
||||
name: String,
|
||||
},
|
||||
|
||||
/// List all namespaces
|
||||
List,
|
||||
|
||||
/// Delete a namespace
|
||||
Delete {
|
||||
/// Namespace name
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
pub async fn run(self) -> Result<()> {
|
||||
match self.command {
|
||||
Commands::Init { herodb } => {
|
||||
let config = config::create_default_config(herodb);
|
||||
config::save_config(&config, None)?;
|
||||
println!("✓ OSIRIS initialized");
|
||||
println!(" Config: {}", config::default_config_path().display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Commands::Ns { ref command } => self.handle_ns_command(command.clone()).await,
|
||||
Commands::Put { ref path, ref file, ref tags, ref mime, ref title } => {
|
||||
self.handle_put(path.clone(), file.clone(), tags.clone(), mime.clone(), title.clone()).await
|
||||
}
|
||||
Commands::Get { ref path, ref output, raw } => {
|
||||
self.handle_get(path.clone(), output.clone(), raw).await
|
||||
}
|
||||
Commands::Del { ref path } => self.handle_del(path.clone()).await,
|
||||
Commands::Find { ref query, ref ns, ref filter, topk, json } => {
|
||||
self.handle_find(query.clone(), ns.clone(), filter.clone(), topk, json).await
|
||||
}
|
||||
Commands::Stats { ref ns } => self.handle_stats(ns.clone()).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_ns_command(&self, command: NsCommands) -> Result<()> {
|
||||
let mut config = config::load_config(None)?;
|
||||
|
||||
match command {
|
||||
NsCommands::Create { name } => {
|
||||
if config.get_namespace(&name).is_some() {
|
||||
return Err(Error::InvalidInput(format!(
|
||||
"Namespace '{}' already exists",
|
||||
name
|
||||
)));
|
||||
}
|
||||
|
||||
let db_id = config.next_db_id();
|
||||
let ns_config = NamespaceConfig { db_id };
|
||||
|
||||
config.set_namespace(name.clone(), ns_config);
|
||||
config::save_config(&config, None)?;
|
||||
|
||||
println!("✓ Created namespace '{}' (DB {})", name, db_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
NsCommands::List => {
|
||||
if config.namespaces.is_empty() {
|
||||
println!("No namespaces configured");
|
||||
} else {
|
||||
println!("Namespaces:");
|
||||
for (name, ns_config) in &config.namespaces {
|
||||
println!(" {} → DB {}", name, ns_config.db_id);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
NsCommands::Delete { name } => {
|
||||
if config.remove_namespace(&name).is_none() {
|
||||
return Err(Error::NotFound(format!("Namespace '{}'", name)));
|
||||
}
|
||||
|
||||
config::save_config(&config, None)?;
|
||||
println!("✓ Deleted namespace '{}'", name);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_put(
|
||||
&self,
|
||||
path: String,
|
||||
file: String,
|
||||
tags: Option<String>,
|
||||
mime: Option<String>,
|
||||
title: Option<String>,
|
||||
) -> Result<()> {
|
||||
let (ns, name) = parse_path(&path)?;
|
||||
let config = config::load_config(None)?;
|
||||
let ns_config = config.get_namespace(&ns)
|
||||
.ok_or_else(|| Error::NotFound(format!("Namespace '{}'", ns)))?;
|
||||
|
||||
// Read content
|
||||
let content = if file == "-" {
|
||||
let mut buffer = String::new();
|
||||
io::stdin().read_to_string(&mut buffer)?;
|
||||
buffer
|
||||
} else {
|
||||
fs::read_to_string(&file)?
|
||||
};
|
||||
|
||||
// Create object
|
||||
let mut obj = OsirisObject::with_id(name.clone(), ns.clone(), Some(content));
|
||||
|
||||
if let Some(title) = title {
|
||||
obj.set_title(Some(title));
|
||||
}
|
||||
|
||||
if let Some(mime) = mime {
|
||||
obj.set_mime(Some(mime));
|
||||
}
|
||||
|
||||
// Parse tags
|
||||
if let Some(tags_str) = tags {
|
||||
let tag_map = parse_tags(&tags_str)?;
|
||||
for (key, value) in tag_map {
|
||||
obj.set_tag(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Store object
|
||||
let client = HeroDbClient::new(&config.herodb.url, ns_config.db_id)?;
|
||||
let index = FieldIndex::new(client.clone());
|
||||
|
||||
client.put_object(&obj).await?;
|
||||
index.index_object(&obj).await?;
|
||||
|
||||
println!("✓ Stored {}/{}", ns, obj.id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_get(&self, path: String, output: Option<PathBuf>, raw: bool) -> Result<()> {
|
||||
let (ns, id) = parse_path(&path)?;
|
||||
let config = config::load_config(None)?;
|
||||
let ns_config = config.get_namespace(&ns)
|
||||
.ok_or_else(|| Error::NotFound(format!("Namespace '{}'", ns)))?;
|
||||
|
||||
let client = HeroDbClient::new(&config.herodb.url, ns_config.db_id)?;
|
||||
let obj = client.get_object(&id).await?;
|
||||
|
||||
if raw {
|
||||
// Output raw content only
|
||||
let content = obj.text.unwrap_or_default();
|
||||
if let Some(output_path) = output {
|
||||
fs::write(output_path, content)?;
|
||||
} else {
|
||||
print!("{}", content);
|
||||
}
|
||||
} else {
|
||||
// Output full object as JSON
|
||||
let json = serde_json::to_string_pretty(&obj)?;
|
||||
if let Some(output_path) = output {
|
||||
fs::write(output_path, json)?;
|
||||
} else {
|
||||
println!("{}", json);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_del(&self, path: String) -> Result<()> {
|
||||
let (ns, id) = parse_path(&path)?;
|
||||
let config = config::load_config(None)?;
|
||||
let ns_config = config.get_namespace(&ns)
|
||||
.ok_or_else(|| Error::NotFound(format!("Namespace '{}'", ns)))?;
|
||||
|
||||
let client = HeroDbClient::new(&config.herodb.url, ns_config.db_id)?;
|
||||
let index = FieldIndex::new(client.clone());
|
||||
|
||||
// Get object first to deindex it
|
||||
let obj = client.get_object(&id).await?;
|
||||
index.deindex_object(&obj).await?;
|
||||
|
||||
let deleted = client.delete_object(&id).await?;
|
||||
|
||||
if deleted {
|
||||
println!("✓ Deleted {}/{}", ns, id);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::NotFound(format!("{}/{}", ns, id)))
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_find(
|
||||
&self,
|
||||
query: Option<String>,
|
||||
ns: String,
|
||||
filter: Option<String>,
|
||||
topk: usize,
|
||||
json: bool,
|
||||
) -> Result<()> {
|
||||
let config = config::load_config(None)?;
|
||||
let ns_config = config.get_namespace(&ns)
|
||||
.ok_or_else(|| Error::NotFound(format!("Namespace '{}'", ns)))?;
|
||||
|
||||
let client = HeroDbClient::new(&config.herodb.url, ns_config.db_id)?;
|
||||
let engine = SearchEngine::new(client.clone());
|
||||
|
||||
// Build query
|
||||
let mut retrieval_query = RetrievalQuery::new(ns.clone()).with_top_k(topk);
|
||||
|
||||
if let Some(text) = query {
|
||||
retrieval_query = retrieval_query.with_text(text);
|
||||
}
|
||||
|
||||
if let Some(filter_str) = filter {
|
||||
let filters = parse_tags(&filter_str)?;
|
||||
for (key, value) in filters {
|
||||
retrieval_query = retrieval_query.with_filter(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Execute search
|
||||
let results = engine.search(&retrieval_query).await?;
|
||||
|
||||
if json {
|
||||
println!("{}", serde_json::to_string_pretty(&results)?);
|
||||
} else {
|
||||
if results.is_empty() {
|
||||
println!("No results found");
|
||||
} else {
|
||||
println!("Found {} result(s):\n", results.len());
|
||||
for (i, result) in results.iter().enumerate() {
|
||||
println!("{}. {} (score: {:.2})", i + 1, result.id, result.score);
|
||||
if let Some(snippet) = &result.snippet {
|
||||
println!(" {}", snippet);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_stats(&self, ns: Option<String>) -> Result<()> {
|
||||
let config = config::load_config(None)?;
|
||||
|
||||
if let Some(ns_name) = ns {
|
||||
let ns_config = config.get_namespace(&ns_name)
|
||||
.ok_or_else(|| Error::NotFound(format!("Namespace '{}'", ns_name)))?;
|
||||
|
||||
let client = HeroDbClient::new(&config.herodb.url, ns_config.db_id)?;
|
||||
let size = client.dbsize().await?;
|
||||
|
||||
println!("Namespace: {}", ns_name);
|
||||
println!(" DB ID: {}", ns_config.db_id);
|
||||
println!(" Keys: {}", size);
|
||||
} else {
|
||||
println!("OSIRIS Statistics\n");
|
||||
println!("Namespaces: {}", config.namespaces.len());
|
||||
for (name, ns_config) in &config.namespaces {
|
||||
let client = HeroDbClient::new(&config.herodb.url, ns_config.db_id)?;
|
||||
let size = client.dbsize().await?;
|
||||
println!(" {} (DB {}) → {} keys", name, ns_config.db_id, size);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a path into namespace and name/id
|
||||
fn parse_path(path: &str) -> Result<(String, String)> {
|
||||
let parts: Vec<&str> = path.splitn(2, '/').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err(Error::InvalidInput(format!(
|
||||
"Invalid path format. Expected 'namespace/name', got '{}'",
|
||||
path
|
||||
)));
|
||||
}
|
||||
Ok((parts[0].to_string(), parts[1].to_string()))
|
||||
}
|
||||
|
||||
/// Parse tags from comma-separated key=value pairs
|
||||
fn parse_tags(tags_str: &str) -> Result<BTreeMap<String, String>> {
|
||||
let mut tags = BTreeMap::new();
|
||||
|
||||
for pair in tags_str.split(',') {
|
||||
let parts: Vec<&str> = pair.trim().splitn(2, '=').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err(Error::InvalidInput(format!(
|
||||
"Invalid tag format. Expected 'key=value', got '{}'",
|
||||
pair
|
||||
)));
|
||||
}
|
||||
tags.insert(parts[0].to_string(), parts[1].to_string());
|
||||
}
|
||||
|
||||
Ok(tags)
|
||||
}
|
||||
3
src/interfaces/mod.rs
Normal file
3
src/interfaces/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod cli;
|
||||
|
||||
pub use cli::Cli;
|
||||
Reference in New Issue
Block a user