Ugh
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use crate::compression_engine::CompressionType;
|
||||
/// Common utilities shared across different modes in the Keep application.
|
||||
///
|
||||
/// This module provides helper functions for formatting, configuration parsing,
|
||||
@@ -13,11 +14,10 @@
|
||||
/// let format = OutputFormat::from_str("json")?;
|
||||
/// ```
|
||||
use crate::config;
|
||||
use crate::compression_engine::CompressionType;
|
||||
use crate::meta_plugin::MetaPluginType;
|
||||
use clap::Command;
|
||||
use clap::error::ErrorKind;
|
||||
use comfy_table::{Table, ContentArrangement};
|
||||
use comfy_table::{ContentArrangement, Table};
|
||||
use log::debug;
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
@@ -116,7 +116,7 @@ pub fn format_size(size: u64, human_readable: bool) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, strum::EnumIter, strum::Display, strum::EnumString)]
|
||||
#[derive(Debug, Eq, PartialEq, Clone, strum::EnumIter, strum::Display)]
|
||||
#[strum(ascii_case_insensitive)]
|
||||
/// Enum representing column types for table display.
|
||||
///
|
||||
@@ -151,34 +151,20 @@ pub enum ColumnType {
|
||||
Meta,
|
||||
}
|
||||
|
||||
impl ColumnType {
|
||||
/// Parses a string to a ColumnType, handling "meta:<name>" pattern.
|
||||
///
|
||||
/// Supports direct enum variants or "meta:<name>" for metadata columns.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `s` - Input string to parse, e.g., "size" or "meta:hostname".
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(ColumnType)` - Parsed type on success.
|
||||
/// * `Err(anyhow::Error)` - If the string doesn't match any variant.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use keep::modes::common::ColumnType;
|
||||
/// let meta = ColumnType::from_str("meta:hostname").unwrap();
|
||||
/// assert_eq!(meta, ColumnType::Meta);
|
||||
/// ```
|
||||
pub fn from_str(s: &str) -> anyhow::Result<Self> {
|
||||
impl std::str::FromStr for ColumnType {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> anyhow::Result<Self> {
|
||||
let lower_s = s.to_lowercase();
|
||||
if s.starts_with("meta:") {
|
||||
// Handle meta:<name> pattern - this is still a Meta column type
|
||||
Ok(ColumnType::Meta)
|
||||
} else {
|
||||
// Handle regular column types
|
||||
Ok(Self::try_from(s)?)
|
||||
for variant in ColumnType::iter() {
|
||||
if variant.to_string().to_lowercase() == lower_s {
|
||||
return Ok(variant);
|
||||
}
|
||||
}
|
||||
Err(anyhow::anyhow!("Invalid column type: {}", s))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,30 +185,34 @@ impl ColumnType {
|
||||
/// # Panics
|
||||
///
|
||||
/// Exits via Clap error if unknown plugin type specified.
|
||||
pub fn settings_meta_plugin_types(cmd: &mut Command, settings: &config::Settings) -> Vec<MetaPluginType> {
|
||||
pub fn settings_meta_plugin_types(
|
||||
cmd: &mut Command,
|
||||
settings: &config::Settings,
|
||||
) -> Vec<MetaPluginType> {
|
||||
let mut meta_plugin_types = Vec::new();
|
||||
|
||||
|
||||
// Handle comma-separated values in each meta_plugins argument
|
||||
for meta_plugin_names_str in &settings.meta_plugins_names() {
|
||||
let meta_plugin_names: Vec<&str> = meta_plugin_names_str.split(',').collect();
|
||||
|
||||
|
||||
for name in meta_plugin_names {
|
||||
let trimmed_name = name.trim();
|
||||
if trimmed_name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Try to find the MetaPluginType by meta name
|
||||
let mut found = false;
|
||||
for meta_plugin_type in MetaPluginType::iter() {
|
||||
let meta_plugin = crate::meta_plugin::get_meta_plugin(meta_plugin_type.clone(), None, None);
|
||||
let meta_plugin =
|
||||
crate::meta_plugin::get_meta_plugin(meta_plugin_type.clone(), None, None);
|
||||
if meta_plugin.meta_type().to_string() == trimmed_name {
|
||||
meta_plugin_types.push(meta_plugin_type);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if !found {
|
||||
cmd.error(
|
||||
ErrorKind::InvalidValue,
|
||||
@@ -252,7 +242,10 @@ pub fn settings_meta_plugin_types(cmd: &mut Command, settings: &config::Settings
|
||||
/// # Panics
|
||||
///
|
||||
/// Exits via Clap error if invalid compression specified.
|
||||
pub fn settings_compression_type(cmd: &mut Command, settings: &config::Settings) -> CompressionType {
|
||||
pub fn settings_compression_type(
|
||||
cmd: &mut Command,
|
||||
settings: &config::Settings,
|
||||
) -> CompressionType {
|
||||
let compression_name = settings
|
||||
.compression()
|
||||
.unwrap_or(CompressionType::LZ4.to_string());
|
||||
@@ -261,7 +254,10 @@ pub fn settings_compression_type(cmd: &mut Command, settings: &config::Settings)
|
||||
if compression_type_opt.is_err() {
|
||||
cmd.error(
|
||||
ErrorKind::InvalidValue,
|
||||
format!("Invalid compression algorithm '{}'. Supported algorithms: lz4, gzip, xz, zstd", compression_name),
|
||||
format!(
|
||||
"Invalid compression algorithm '{}'. Supported algorithms: lz4, gzip, xz, zstd",
|
||||
compression_name
|
||||
),
|
||||
)
|
||||
.exit();
|
||||
}
|
||||
@@ -288,7 +284,8 @@ pub fn settings_compression_type(cmd: &mut Command, settings: &config::Settings)
|
||||
/// assert_eq!(format, OutputFormat::Json); // If settings.output_format = Some("json")
|
||||
/// ```
|
||||
pub fn settings_output_format(settings: &config::Settings) -> OutputFormat {
|
||||
settings.output_format
|
||||
settings
|
||||
.output_format
|
||||
.as_ref()
|
||||
.and_then(|s| OutputFormat::from_str(s).ok())
|
||||
.unwrap_or(OutputFormat::Table)
|
||||
@@ -340,7 +337,7 @@ pub fn trim_lines_end(s: &str) -> String {
|
||||
pub fn create_table(use_styling: bool) -> Table {
|
||||
let mut table = Table::new();
|
||||
table.set_content_arrangement(ContentArrangement::Dynamic);
|
||||
|
||||
|
||||
if use_styling {
|
||||
if std::io::stdout().is_terminal() {
|
||||
table
|
||||
@@ -352,7 +349,7 @@ pub fn create_table(use_styling: bool) -> Table {
|
||||
} else {
|
||||
table.load_preset(comfy_table::presets::NOTHING);
|
||||
}
|
||||
|
||||
|
||||
if !std::io::stdout().is_terminal() {
|
||||
table.force_no_tty();
|
||||
}
|
||||
@@ -379,14 +376,20 @@ pub fn create_table(use_styling: bool) -> Table {
|
||||
/// ```
|
||||
pub fn create_table_with_config(table_config: &crate::config::TableConfig) -> Table {
|
||||
let mut table = Table::new();
|
||||
|
||||
|
||||
// Set content arrangement
|
||||
match table_config.content_arrangement {
|
||||
crate::config::ContentArrangement::Dynamic => table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic),
|
||||
crate::config::ContentArrangement::DynamicFullWidth => table.set_content_arrangement(comfy_table::ContentArrangement::DynamicFullWidth),
|
||||
crate::config::ContentArrangement::Disabled => table.set_content_arrangement(comfy_table::ContentArrangement::Disabled),
|
||||
crate::config::ContentArrangement::Dynamic => {
|
||||
table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic)
|
||||
}
|
||||
crate::config::ContentArrangement::DynamicFullWidth => {
|
||||
table.set_content_arrangement(comfy_table::ContentArrangement::DynamicFullWidth)
|
||||
}
|
||||
crate::config::ContentArrangement::Disabled => {
|
||||
table.set_content_arrangement(comfy_table::ContentArrangement::Disabled)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Set style preset
|
||||
match &table_config.style {
|
||||
crate::config::TableStyle::Ascii => {
|
||||
@@ -414,7 +417,7 @@ pub fn create_table_with_config(table_config: &crate::config::TableConfig) -> Ta
|
||||
// Add more presets as needed
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Apply modifiers
|
||||
for modifier in &table_config.modifiers {
|
||||
match modifier.as_str() {
|
||||
@@ -427,16 +430,15 @@ pub fn create_table_with_config(table_config: &crate::config::TableConfig) -> Ta
|
||||
_ => {} // Ignore unknown modifiers
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Set truncation indicator if specified
|
||||
if !table_config.truncation_indicator.is_empty() {
|
||||
table.set_truncation_indicator(&table_config.truncation_indicator);
|
||||
}
|
||||
|
||||
|
||||
if !std::io::stdout().is_terminal() {
|
||||
table.force_no_tty();
|
||||
}
|
||||
|
||||
|
||||
table
|
||||
}
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@ pub fn mode_delete(
|
||||
_cmd: &mut Command,
|
||||
_settings: &config::Settings,
|
||||
_config: &config::Settings,
|
||||
ids: &mut Vec<i64>,
|
||||
_tags: &mut Vec<String>,
|
||||
ids: &mut [i64],
|
||||
_tags: &mut [String],
|
||||
conn: &mut Connection,
|
||||
data_path: PathBuf,
|
||||
) -> Result<()> {
|
||||
@@ -65,7 +65,10 @@ pub fn mode_delete(
|
||||
CoreError::ItemNotFound(_) => {
|
||||
warn!("Unable to find item {item_id} in database");
|
||||
}
|
||||
_ => return Err(anyhow::Error::from(e).context(format!("Failed to delete item {}", item_id))),
|
||||
_ => {
|
||||
return Err(anyhow::Error::from(e)
|
||||
.context(format!("Failed to delete item {}", item_id)));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
use crate::config;
|
||||
use crate::services::item_service::ItemService;
|
||||
/// Diff mode implementation.
|
||||
///
|
||||
/// This module provides functionality for comparing two items and displaying their
|
||||
/// differences using external diff tools.
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Command;
|
||||
use crate::config;
|
||||
use crate::services::item_service::ItemService;
|
||||
use log::debug;
|
||||
|
||||
fn validate_diff_args(_cmd: &mut Command, ids: &Vec<i64>, tags: &Vec<String>) -> anyhow::Result<()> {
|
||||
fn validate_diff_args(
|
||||
_cmd: &mut Command,
|
||||
ids: &Vec<i64>,
|
||||
tags: &Vec<String>,
|
||||
) -> anyhow::Result<()> {
|
||||
if !tags.is_empty() {
|
||||
return Err(anyhow::anyhow!("Tags are not supported with --diff. Please provide exactly two IDs."));
|
||||
return Err(anyhow::anyhow!(
|
||||
"Tags are not supported with --diff. Please provide exactly two IDs."
|
||||
));
|
||||
}
|
||||
if ids.len() != 2 {
|
||||
return Err(anyhow::anyhow!("You must supply exactly two IDs when using --diff."));
|
||||
return Err(anyhow::anyhow!(
|
||||
"You must supply exactly two IDs when using --diff."
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -34,9 +42,12 @@ fn validate_diff_args(_cmd: &mut Command, ids: &Vec<i64>, tags: &Vec<String>) ->
|
||||
/// * `Result<(ItemWithMeta, ItemWithMeta)>` - Tuple of items with metadata or error.
|
||||
fn fetch_and_validate_items(
|
||||
conn: &mut rusqlite::Connection,
|
||||
ids: &Vec<i64>,
|
||||
ids: &[i64],
|
||||
item_service: &ItemService,
|
||||
) -> Result<(crate::services::types::ItemWithMeta, crate::services::types::ItemWithMeta)> {
|
||||
) -> Result<(
|
||||
crate::services::types::ItemWithMeta,
|
||||
crate::services::types::ItemWithMeta,
|
||||
)> {
|
||||
// Fetch items using the service, which handles validation
|
||||
let item_a = item_service
|
||||
.get_item(conn, ids[0])
|
||||
@@ -69,12 +80,15 @@ fn setup_diff_paths_and_compression(
|
||||
item_service: &ItemService,
|
||||
item_a: &crate::services::types::ItemWithMeta,
|
||||
item_b: &crate::services::types::ItemWithMeta,
|
||||
) -> Result<(
|
||||
std::path::PathBuf,
|
||||
std::path::PathBuf,
|
||||
)> {
|
||||
let item_a_id = item_a.item.id.ok_or_else(|| anyhow::anyhow!("Item A missing ID"))?;
|
||||
let item_b_id = item_b.item.id.ok_or_else(|| anyhow::anyhow!("Item B missing ID"))?;
|
||||
) -> Result<(std::path::PathBuf, std::path::PathBuf)> {
|
||||
let item_a_id = item_a
|
||||
.item
|
||||
.id
|
||||
.ok_or_else(|| anyhow::anyhow!("Item A missing ID"))?;
|
||||
let item_b_id = item_b
|
||||
.item
|
||||
.id
|
||||
.ok_or_else(|| anyhow::anyhow!("Item B missing ID"))?;
|
||||
|
||||
// Use the service's data path to construct proper file paths
|
||||
let data_path = item_service.get_data_path();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::meta_plugin::MetaPlugin;
|
||||
use anyhow::Result;
|
||||
use clap::Command;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml;
|
||||
use crate::meta_plugin::MetaPlugin;
|
||||
|
||||
/// Mode for generating a default configuration file.
|
||||
///
|
||||
@@ -71,131 +71,131 @@ struct MetaPluginConfig {
|
||||
outputs: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Generates and prints a default commented YAML configuration template.
|
||||
///
|
||||
/// Creates instances of available meta plugins to populate default options and outputs,
|
||||
/// then serializes the config to YAML with all lines commented for easy editing.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `_cmd` - Unused Clap command reference.
|
||||
/// * `_settings` - Unused settings reference.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `Ok(())` on success.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// mode_generate_config(&mut cmd, &settings)?;
|
||||
/// ```
|
||||
pub fn mode_generate_config(_cmd: &mut Command, _settings: &crate::config::Settings) -> Result<()> {
|
||||
// Create instances of each meta plugin to get their default options and outputs
|
||||
let cwd_plugin = crate::meta_plugin::cwd::CwdMetaPlugin::new(None, None);
|
||||
let digest_plugin = crate::meta_plugin::digest::DigestMetaPlugin::new(None, None);
|
||||
let hostname_plugin = crate::meta_plugin::hostname::HostnameMetaPlugin::new(None, None);
|
||||
#[cfg(feature = "magic")]
|
||||
let magic_file_plugin = crate::meta_plugin::magic_file::MagicFileMetaPlugin::new(None, None);
|
||||
let env_plugin = crate::meta_plugin::env::EnvMetaPlugin::new(None, None);
|
||||
|
||||
// Create a default configuration
|
||||
let default_config = DefaultConfig {
|
||||
dir: Some("~/.local/share/keep".to_string()),
|
||||
list_format: vec![
|
||||
ColumnConfig {
|
||||
name: "id".to_string(),
|
||||
label: Some("Item".to_string()),
|
||||
align: ColumnAlignment::Right,
|
||||
max_len: None,
|
||||
},
|
||||
ColumnConfig {
|
||||
name: "time".to_string(),
|
||||
label: Some("Time".to_string()),
|
||||
align: ColumnAlignment::Right,
|
||||
max_len: None,
|
||||
},
|
||||
ColumnConfig {
|
||||
name: "size".to_string(),
|
||||
label: Some("Size".to_string()),
|
||||
align: ColumnAlignment::Right,
|
||||
max_len: None,
|
||||
},
|
||||
ColumnConfig {
|
||||
name: "tags".to_string(),
|
||||
label: Some("Tags".to_string()),
|
||||
align: ColumnAlignment::Left,
|
||||
max_len: Some("40".to_string()),
|
||||
},
|
||||
ColumnConfig {
|
||||
name: "meta:hostname_full".to_string(),
|
||||
label: Some("Hostname".to_string()),
|
||||
align: ColumnAlignment::Left,
|
||||
max_len: Some("28".to_string()),
|
||||
},
|
||||
],
|
||||
human_readable: false,
|
||||
output_format: Some("table".to_string()),
|
||||
quiet: false,
|
||||
force: false,
|
||||
server: Some(ServerConfig {
|
||||
address: Some("127.0.0.1".to_string()),
|
||||
port: Some(8080),
|
||||
password_file: None,
|
||||
password: None,
|
||||
password_hash: None,
|
||||
}),
|
||||
compression_plugin: None,
|
||||
meta_plugins: Some(vec![
|
||||
MetaPluginConfig {
|
||||
name: "cwd".to_string(),
|
||||
options: cwd_plugin.options().clone(),
|
||||
outputs: convert_outputs_to_string_map(cwd_plugin.outputs()),
|
||||
},
|
||||
MetaPluginConfig {
|
||||
name: "digest".to_string(),
|
||||
options: digest_plugin.options().clone(),
|
||||
outputs: convert_outputs_to_string_map(digest_plugin.outputs()),
|
||||
},
|
||||
MetaPluginConfig {
|
||||
name: "hostname".to_string(),
|
||||
options: hostname_plugin.options().clone(),
|
||||
outputs: convert_outputs_to_string_map(hostname_plugin.outputs()),
|
||||
},
|
||||
#[cfg(feature = "magic")]
|
||||
MetaPluginConfig {
|
||||
name: "magic_file".to_string(),
|
||||
options: magic_file_plugin.options().clone(),
|
||||
outputs: convert_outputs_to_string_map(magic_file_plugin.outputs()),
|
||||
},
|
||||
MetaPluginConfig {
|
||||
name: "env".to_string(),
|
||||
options: env_plugin.options().clone(),
|
||||
outputs: convert_outputs_to_string_map(env_plugin.outputs()),
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
// Serialize to YAML and comment out all lines
|
||||
let yaml = serde_yaml::to_string(&default_config)?;
|
||||
|
||||
// Comment out every line
|
||||
let commented_yaml = yaml
|
||||
.lines()
|
||||
.map(|line| {
|
||||
if line.trim().is_empty() {
|
||||
line.to_string()
|
||||
} else {
|
||||
format!("# {}", line)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
|
||||
println!("{}", commented_yaml);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
/// Generates and prints a default commented YAML configuration template.
|
||||
///
|
||||
/// Creates instances of available meta plugins to populate default options and outputs,
|
||||
/// then serializes the config to YAML with all lines commented for easy editing.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `_cmd` - Unused Clap command reference.
|
||||
/// * `_settings` - Unused settings reference.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `Ok(())` on success.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// mode_generate_config(&mut cmd, &settings)?;
|
||||
/// ```
|
||||
pub fn mode_generate_config(_cmd: &mut Command, _settings: &crate::config::Settings) -> Result<()> {
|
||||
// Create instances of each meta plugin to get their default options and outputs
|
||||
let cwd_plugin = crate::meta_plugin::cwd::CwdMetaPlugin::new(None, None);
|
||||
let digest_plugin = crate::meta_plugin::digest::DigestMetaPlugin::new(None, None);
|
||||
let hostname_plugin = crate::meta_plugin::hostname::HostnameMetaPlugin::new(None, None);
|
||||
#[cfg(feature = "magic")]
|
||||
let magic_file_plugin = crate::meta_plugin::magic_file::MagicFileMetaPlugin::new(None, None);
|
||||
let env_plugin = crate::meta_plugin::env::EnvMetaPlugin::new(None, None);
|
||||
|
||||
// Create a default configuration
|
||||
let default_config = DefaultConfig {
|
||||
dir: Some("~/.local/share/keep".to_string()),
|
||||
list_format: vec![
|
||||
ColumnConfig {
|
||||
name: "id".to_string(),
|
||||
label: Some("Item".to_string()),
|
||||
align: ColumnAlignment::Right,
|
||||
max_len: None,
|
||||
},
|
||||
ColumnConfig {
|
||||
name: "time".to_string(),
|
||||
label: Some("Time".to_string()),
|
||||
align: ColumnAlignment::Right,
|
||||
max_len: None,
|
||||
},
|
||||
ColumnConfig {
|
||||
name: "size".to_string(),
|
||||
label: Some("Size".to_string()),
|
||||
align: ColumnAlignment::Right,
|
||||
max_len: None,
|
||||
},
|
||||
ColumnConfig {
|
||||
name: "tags".to_string(),
|
||||
label: Some("Tags".to_string()),
|
||||
align: ColumnAlignment::Left,
|
||||
max_len: Some("40".to_string()),
|
||||
},
|
||||
ColumnConfig {
|
||||
name: "meta:hostname_full".to_string(),
|
||||
label: Some("Hostname".to_string()),
|
||||
align: ColumnAlignment::Left,
|
||||
max_len: Some("28".to_string()),
|
||||
},
|
||||
],
|
||||
human_readable: false,
|
||||
output_format: Some("table".to_string()),
|
||||
quiet: false,
|
||||
force: false,
|
||||
server: Some(ServerConfig {
|
||||
address: Some("127.0.0.1".to_string()),
|
||||
port: Some(8080),
|
||||
password_file: None,
|
||||
password: None,
|
||||
password_hash: None,
|
||||
}),
|
||||
compression_plugin: None,
|
||||
meta_plugins: Some(vec![
|
||||
MetaPluginConfig {
|
||||
name: "cwd".to_string(),
|
||||
options: cwd_plugin.options().clone(),
|
||||
outputs: convert_outputs_to_string_map(cwd_plugin.outputs()),
|
||||
},
|
||||
MetaPluginConfig {
|
||||
name: "digest".to_string(),
|
||||
options: digest_plugin.options().clone(),
|
||||
outputs: convert_outputs_to_string_map(digest_plugin.outputs()),
|
||||
},
|
||||
MetaPluginConfig {
|
||||
name: "hostname".to_string(),
|
||||
options: hostname_plugin.options().clone(),
|
||||
outputs: convert_outputs_to_string_map(hostname_plugin.outputs()),
|
||||
},
|
||||
#[cfg(feature = "magic")]
|
||||
MetaPluginConfig {
|
||||
name: "magic_file".to_string(),
|
||||
options: magic_file_plugin.options().clone(),
|
||||
outputs: convert_outputs_to_string_map(magic_file_plugin.outputs()),
|
||||
},
|
||||
MetaPluginConfig {
|
||||
name: "env".to_string(),
|
||||
options: env_plugin.options().clone(),
|
||||
outputs: convert_outputs_to_string_map(env_plugin.outputs()),
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
// Serialize to YAML and comment out all lines
|
||||
let yaml = serde_yaml::to_string(&default_config)?;
|
||||
|
||||
// Comment out every line
|
||||
let commented_yaml = yaml
|
||||
.lines()
|
||||
.map(|line| {
|
||||
if line.trim().is_empty() {
|
||||
line.to_string()
|
||||
} else {
|
||||
format!("# {}", line)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
|
||||
println!("{}", commented_yaml);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper function to convert outputs from serde_yaml::Value to String.
|
||||
///
|
||||
@@ -223,7 +223,10 @@ fn convert_outputs_to_string_map(
|
||||
}
|
||||
_ => {
|
||||
// Convert other values to their YAML string representation
|
||||
result.insert(key.clone(), serde_yaml::to_string(value).unwrap_or_default());
|
||||
result.insert(
|
||||
key.clone(),
|
||||
serde_yaml::to_string(value).unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{Result, anyhow};
|
||||
use std::io::Write;
|
||||
|
||||
use crate::common::is_binary::is_binary;
|
||||
use crate::common::PIPESIZE;
|
||||
use crate::common::is_binary::is_binary;
|
||||
use crate::config;
|
||||
use crate::filter_plugin::FilterChain;
|
||||
use crate::services::item_service::ItemService;
|
||||
use clap::Command;
|
||||
use is_terminal::IsTerminal;
|
||||
use std::path::PathBuf;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Handles the get mode: retrieves and streams item content to stdout, applying filters if specified.
|
||||
///
|
||||
@@ -29,25 +29,34 @@ use std::io::Read;
|
||||
pub fn mode_get(
|
||||
cmd: &mut Command,
|
||||
settings: &config::Settings,
|
||||
ids: &mut Vec<i64>,
|
||||
tags: &mut Vec<String>,
|
||||
ids: &mut [i64],
|
||||
tags: &mut [String],
|
||||
conn: &mut rusqlite::Connection,
|
||||
data_path: PathBuf,
|
||||
filter_chain: Option<FilterChain>,
|
||||
) -> Result<()> {
|
||||
if !ids.is_empty() && !tags.is_empty() {
|
||||
cmd.error(clap::error::ErrorKind::InvalidValue, "Both ID and tags given, you must supply either IDs or tags when using --get").exit();
|
||||
cmd.error(
|
||||
clap::error::ErrorKind::InvalidValue,
|
||||
"Both ID and tags given, you must supply either IDs or tags when using --get",
|
||||
)
|
||||
.exit();
|
||||
} else if ids.len() > 1 {
|
||||
cmd.error(clap::error::ErrorKind::InvalidValue, "More than one ID given, you must supply exactly one ID when using --get").exit();
|
||||
cmd.error(
|
||||
clap::error::ErrorKind::InvalidValue,
|
||||
"More than one ID given, you must supply exactly one ID when using --get",
|
||||
)
|
||||
.exit();
|
||||
}
|
||||
// If both are empty, find_item will find the last item
|
||||
|
||||
let item_service = ItemService::new(data_path.clone());
|
||||
let item_with_meta = item_service.find_item(conn, ids, tags, &std::collections::HashMap::new())
|
||||
let item_with_meta = item_service
|
||||
.find_item(conn, ids, tags, &std::collections::HashMap::new())
|
||||
.map_err(|e| anyhow!("Unable to find matching item in database: {}", e))?;
|
||||
|
||||
|
||||
let item_id = item_with_meta.item.id.unwrap();
|
||||
|
||||
|
||||
// Determine if we should detect binary data
|
||||
let mut detect_binary = !settings.force && std::io::stdout().is_terminal();
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use crate::config;
|
||||
use crate::modes::common::{OutputFormat, format_size};
|
||||
use crate::services::types::ItemWithMeta;
|
||||
use crate::modes::common::{format_size, OutputFormat};
|
||||
use anyhow::{anyhow, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use anyhow::{Result, anyhow};
|
||||
use clap::Command;
|
||||
use clap::error::ErrorKind;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::services::item_service::ItemService;
|
||||
use chrono::prelude::*;
|
||||
use comfy_table::{Cell, Attribute};
|
||||
use comfy_table::{Attribute, Cell};
|
||||
|
||||
/// Displays detailed information about an item or the last item if no ID/tags specified.
|
||||
///
|
||||
@@ -42,16 +42,24 @@ use comfy_table::{Cell, Attribute};
|
||||
pub fn mode_info(
|
||||
cmd: &mut Command,
|
||||
settings: &config::Settings,
|
||||
ids: &mut Vec<i64>,
|
||||
tags: &mut Vec<String>,
|
||||
ids: &mut [i64],
|
||||
tags: &mut [String],
|
||||
conn: &mut rusqlite::Connection,
|
||||
data_path: PathBuf,
|
||||
) -> Result<()> {
|
||||
// For --info, we can use either IDs or tags, but not both
|
||||
if !ids.is_empty() && !tags.is_empty() {
|
||||
cmd.error(ErrorKind::InvalidValue, "Both ID and tags given, you must supply either IDs or tags when using --info").exit();
|
||||
cmd.error(
|
||||
ErrorKind::InvalidValue,
|
||||
"Both ID and tags given, you must supply either IDs or tags when using --info",
|
||||
)
|
||||
.exit();
|
||||
} else if ids.len() > 1 {
|
||||
cmd.error(ErrorKind::InvalidValue, "More than one ID given, you must supply exactly one ID when using --info").exit();
|
||||
cmd.error(
|
||||
ErrorKind::InvalidValue,
|
||||
"More than one ID given, you must supply exactly one ID when using --info",
|
||||
)
|
||||
.exit();
|
||||
}
|
||||
// If both are empty, find_item will find the last item
|
||||
|
||||
@@ -139,7 +147,7 @@ fn show_item(
|
||||
// Add all the rows
|
||||
table.add_row(vec![
|
||||
Cell::new("ID").add_attribute(Attribute::Bold),
|
||||
Cell::new(&item_id.to_string()),
|
||||
Cell::new(item_id.to_string()),
|
||||
]);
|
||||
|
||||
let timestamp_str = item.ts.with_timezone(&Local).format("%F %T %Z").to_string();
|
||||
@@ -150,7 +158,10 @@ fn show_item(
|
||||
|
||||
let mut item_path_buf = data_path.clone();
|
||||
item_path_buf.push(item.id.unwrap().to_string());
|
||||
let path_str = item_path_buf.to_str().expect("Unable to get item path").to_string();
|
||||
let path_str = item_path_buf
|
||||
.to_str()
|
||||
.expect("Unable to get item path")
|
||||
.to_string();
|
||||
table.add_row(vec![
|
||||
Cell::new("Path").add_attribute(Attribute::Bold),
|
||||
Cell::new(&path_str),
|
||||
@@ -194,7 +205,10 @@ fn show_item(
|
||||
]);
|
||||
}
|
||||
|
||||
println!("{}", crate::modes::common::trim_lines_end(&table.trim_fmt()));
|
||||
println!(
|
||||
"{}",
|
||||
crate::modes::common::trim_lines_end(&table.trim_fmt())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
/// formatting, filtering by tags, and support for different output formats
|
||||
/// including table, JSON, and YAML.
|
||||
use crate::config;
|
||||
use crate::modes::common::ColumnType;
|
||||
use crate::modes::common::{OutputFormat, format_size};
|
||||
use crate::services::item_service::ItemService;
|
||||
use crate::services::types::ItemWithMeta;
|
||||
use crate::modes::common::ColumnType;
|
||||
use crate::modes::common::{format_size, OutputFormat};
|
||||
use anyhow::{Result};
|
||||
use comfy_table::{Cell, Row, Color, Attribute};
|
||||
use anyhow::Result;
|
||||
use comfy_table::CellAlignment;
|
||||
use comfy_table::{Attribute, Cell, Color, Row};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use serde_yaml;
|
||||
@@ -80,7 +80,7 @@ struct ListItem {
|
||||
fn apply_color(mut cell: Cell, color: &crate::config::TableColor, is_foreground: bool) -> Cell {
|
||||
use crate::config::TableColor::*;
|
||||
use comfy_table::Color;
|
||||
|
||||
|
||||
let comfy_color = match color {
|
||||
Black => Color::Black,
|
||||
Red => Color::Red,
|
||||
@@ -97,15 +97,19 @@ fn apply_color(mut cell: Cell, color: &crate::config::TableColor, is_foreground:
|
||||
DarkBlue => Color::DarkBlue,
|
||||
DarkMagenta => Color::DarkMagenta,
|
||||
DarkCyan => Color::DarkCyan,
|
||||
Rgb(r, g, b) => Color::Rgb { r: *r, g: *g, b: *b },
|
||||
Rgb(r, g, b) => Color::Rgb {
|
||||
r: *r,
|
||||
g: *g,
|
||||
b: *b,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
if is_foreground {
|
||||
cell = cell.fg(comfy_color);
|
||||
} else {
|
||||
cell = cell.bg(comfy_color);
|
||||
}
|
||||
|
||||
|
||||
cell
|
||||
}
|
||||
|
||||
@@ -125,7 +129,7 @@ fn apply_color(mut cell: Cell, color: &crate::config::TableColor, is_foreground:
|
||||
fn apply_attribute(mut cell: Cell, attribute: &crate::config::TableAttribute) -> Cell {
|
||||
use crate::config::TableAttribute::*;
|
||||
use comfy_table::Attribute;
|
||||
|
||||
|
||||
match attribute {
|
||||
Bold => cell = cell.add_attribute(Attribute::Bold),
|
||||
Dim => cell = cell.add_attribute(Attribute::Dim),
|
||||
@@ -137,7 +141,7 @@ fn apply_attribute(mut cell: Cell, attribute: &crate::config::TableAttribute) ->
|
||||
Hidden => cell = cell.add_attribute(Attribute::Hidden),
|
||||
CrossedOut => cell = cell.add_attribute(Attribute::CrossedOut),
|
||||
}
|
||||
|
||||
|
||||
cell
|
||||
}
|
||||
|
||||
@@ -161,8 +165,8 @@ fn apply_attribute(mut cell: Cell, attribute: &crate::config::TableAttribute) ->
|
||||
pub fn mode_list(
|
||||
cmd: &mut clap::Command,
|
||||
settings: &config::Settings,
|
||||
ids: &mut Vec<i64>,
|
||||
tags: &Vec<String>,
|
||||
ids: &mut [i64],
|
||||
tags: &[String],
|
||||
conn: &mut rusqlite::Connection,
|
||||
data_path: std::path::PathBuf,
|
||||
) -> Result<()> {
|
||||
@@ -203,7 +207,9 @@ pub fn mode_list(
|
||||
let mut table_row = Row::new();
|
||||
|
||||
for column in &settings.list_format {
|
||||
let column_type = ColumnType::from_str(&column.name)
|
||||
let column_type = column
|
||||
.name
|
||||
.parse::<ColumnType>()
|
||||
.unwrap_or_else(|_| panic!("Unknown column {:?}", column.name));
|
||||
|
||||
let mut meta_name: Option<&str> = None;
|
||||
@@ -217,7 +223,8 @@ pub fn mode_list(
|
||||
|
||||
let cell_content = match column_type {
|
||||
ColumnType::Id => item.id.unwrap_or(0).to_string(),
|
||||
ColumnType::Time => item.ts
|
||||
ColumnType::Time => item
|
||||
.ts
|
||||
.with_timezone(&chrono::Local)
|
||||
.format("%F %T")
|
||||
.to_string(),
|
||||
@@ -243,9 +250,10 @@ pub fn mode_list(
|
||||
None => "".to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
// Truncate content to max 3 lines
|
||||
let mut cell_lines: Vec<String> = cell_content.split('\n').map(|s| s.to_string()).collect();
|
||||
let mut cell_lines: Vec<String> =
|
||||
cell_content.split('\n').map(|s| s.to_string()).collect();
|
||||
if cell_lines.len() > 3 {
|
||||
cell_lines.truncate(3);
|
||||
// Add ellipsis to the last line if we truncated
|
||||
@@ -257,47 +265,53 @@ pub fn mode_list(
|
||||
}
|
||||
}
|
||||
let truncated_content = cell_lines.join("\n");
|
||||
|
||||
|
||||
let mut cell = Cell::new(truncated_content);
|
||||
|
||||
|
||||
// Apply column-specific styling
|
||||
if let Some(fg_color) = &column.fg_color {
|
||||
cell = apply_color(cell, fg_color, true);
|
||||
}
|
||||
|
||||
|
||||
if let Some(bg_color) = &column.bg_color {
|
||||
cell = apply_color(cell, bg_color, false);
|
||||
}
|
||||
|
||||
|
||||
for attribute in &column.attributes {
|
||||
cell = apply_attribute(cell, attribute);
|
||||
}
|
||||
|
||||
|
||||
// Apply padding if specified
|
||||
if let Some((_left_padding, _right_padding)) = column.padding {
|
||||
// Note: comfy-table doesn't directly support padding, so we'd need to handle this
|
||||
// by adding spaces to the content, or use a different approach
|
||||
}
|
||||
|
||||
|
||||
// Apply styling for specific cases
|
||||
match column_type {
|
||||
ColumnType::Size => {
|
||||
if item.size.is_none() {
|
||||
if item_path.metadata().is_ok() {
|
||||
cell = cell.fg(comfy_table::Color::Yellow).add_attribute(Attribute::Bold);
|
||||
cell = cell
|
||||
.fg(comfy_table::Color::Yellow)
|
||||
.add_attribute(Attribute::Bold);
|
||||
} else {
|
||||
cell = cell.fg(comfy_table::Color::Red).add_attribute(Attribute::Bold);
|
||||
cell = cell
|
||||
.fg(comfy_table::Color::Red)
|
||||
.add_attribute(Attribute::Bold);
|
||||
}
|
||||
}
|
||||
}
|
||||
ColumnType::FileSize => {
|
||||
if item_path.metadata().is_err() {
|
||||
cell = cell.fg(comfy_table::Color::Red).add_attribute(Attribute::Bold);
|
||||
cell = cell
|
||||
.fg(comfy_table::Color::Red)
|
||||
.add_attribute(Attribute::Bold);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
||||
// Apply alignment
|
||||
cell = match column.align {
|
||||
crate::config::ColumnAlignment::Right => cell.set_alignment(CellAlignment::Right),
|
||||
@@ -309,7 +323,10 @@ pub fn mode_list(
|
||||
table.add_row(table_row);
|
||||
}
|
||||
|
||||
println!("{}", crate::modes::common::trim_lines_end(&table.trim_fmt()));
|
||||
println!(
|
||||
"{}",
|
||||
crate::modes::common::trim_lines_end(&table.trim_fmt())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::services::item_service::ItemService;
|
||||
/// # Panics
|
||||
///
|
||||
/// Exits the program via Clap error if IDs are provided.
|
||||
fn validate_save_args(cmd: &mut Command, ids: &Vec<i64>) {
|
||||
fn validate_save_args(cmd: &mut Command, ids: &[i64]) {
|
||||
if !ids.is_empty() {
|
||||
cmd.error(
|
||||
clap::error::ErrorKind::InvalidValue,
|
||||
@@ -111,7 +111,7 @@ impl<R: Read, W: Write> Read for TeeReader<R, W> {
|
||||
pub fn mode_save(
|
||||
cmd: &mut Command,
|
||||
settings: &config::Settings,
|
||||
ids: &mut Vec<i64>,
|
||||
ids: &mut [i64],
|
||||
tags: &mut Vec<String>,
|
||||
conn: &mut rusqlite::Connection,
|
||||
data_path: std::path::PathBuf,
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
use crate::modes::server::common::{
|
||||
ApiResponse, AppState, ItemContentQuery, ItemInfo, ItemInfoListResponse, ItemInfoResponse,
|
||||
ItemQuery, ListItemsQuery, MetadataResponse, TagsQuery,
|
||||
};
|
||||
use crate::services::async_item_service::AsyncItemService;
|
||||
use crate::services::error::CoreError;
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
http::{StatusCode, header},
|
||||
@@ -5,9 +11,6 @@ use axum::{
|
||||
};
|
||||
use log::{debug, warn};
|
||||
use std::collections::HashMap;
|
||||
use crate::services::async_item_service::AsyncItemService;
|
||||
use crate::services::error::CoreError;
|
||||
use crate::modes::server::common::{AppState, ApiResponse, ItemInfo, TagsQuery, ListItemsQuery, ItemInfoListResponse, ItemInfoResponse, MetadataResponse, ItemQuery, ItemContentQuery};
|
||||
|
||||
// Helper functions to replace the missing binary_detection module
|
||||
async fn check_binary_content_allowed(
|
||||
@@ -35,13 +38,17 @@ async fn is_content_binary(
|
||||
Ok(text_val == "false")
|
||||
} else {
|
||||
// If text metadata isn't set, we need to check the content using streaming approach
|
||||
match item_service.get_item_content_info_streaming(
|
||||
item_id,
|
||||
None
|
||||
).await {
|
||||
match item_service
|
||||
.get_item_content_info_streaming(item_id, None)
|
||||
.await
|
||||
{
|
||||
Ok((_, _, is_binary)) => Ok(is_binary),
|
||||
Err(e) => {
|
||||
log::warn!("Failed to get content info for binary check for item {}: {}", item_id, e);
|
||||
log::warn!(
|
||||
"Failed to get content info for binary check for item {}: {}",
|
||||
item_id,
|
||||
e
|
||||
);
|
||||
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
@@ -63,7 +70,7 @@ impl ResponseBuilder {
|
||||
log::warn!("Failed to serialize response: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
|
||||
Response::builder()
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.header(header::CONTENT_LENGTH, json.len().to_string())
|
||||
@@ -73,7 +80,7 @@ impl ResponseBuilder {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
pub fn binary(content: &[u8], mime_type: &str) -> Result<Response, StatusCode> {
|
||||
Response::builder()
|
||||
.header(header::CONTENT_TYPE, mime_type)
|
||||
@@ -86,7 +93,6 @@ impl ResponseBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Helper function to get mime type from metadata
|
||||
fn get_mime_type(metadata: &HashMap<String, String>) -> String {
|
||||
metadata
|
||||
@@ -104,7 +110,7 @@ fn apply_offset_length(content: &[u8], offset: u64, length: u64) -> &[u8] {
|
||||
} else {
|
||||
content_len
|
||||
};
|
||||
|
||||
|
||||
if start < content_len {
|
||||
&content[start as usize..end as usize]
|
||||
} else {
|
||||
@@ -126,11 +132,11 @@ fn handle_item_error(error: CoreError) -> StatusCode {
|
||||
/// Helper function to create AsyncItemService from AppState
|
||||
fn create_item_service(state: &AppState) -> AsyncItemService {
|
||||
AsyncItemService::new(
|
||||
state.data_dir.clone(),
|
||||
state.db.clone(),
|
||||
state.data_dir.clone(),
|
||||
state.db.clone(),
|
||||
state.item_service.clone(),
|
||||
state.cmd.clone(),
|
||||
state.settings.clone()
|
||||
state.settings.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -185,13 +191,18 @@ pub async fn handle_list_items(
|
||||
// Apply pagination
|
||||
let start = params.start.unwrap_or(0) as usize;
|
||||
let count = params.count.unwrap_or(100) as usize;
|
||||
let items_with_meta: Vec<_> = items_with_meta.into_iter().skip(start).take(count).collect();
|
||||
let items_with_meta: Vec<_> = items_with_meta
|
||||
.into_iter()
|
||||
.skip(start)
|
||||
.take(count)
|
||||
.collect();
|
||||
|
||||
let item_infos: Vec<ItemInfo> = items_with_meta
|
||||
.into_iter()
|
||||
.map(|item_with_meta| {
|
||||
let item_id = item_with_meta.item.id.unwrap_or(0);
|
||||
let item_tags: Vec<String> = item_with_meta.tags.iter().map(|t| t.name.clone()).collect();
|
||||
let item_tags: Vec<String> =
|
||||
item_with_meta.tags.iter().map(|t| t.name.clone()).collect();
|
||||
let item_meta = item_with_meta.meta_as_map();
|
||||
|
||||
ItemInfo {
|
||||
@@ -239,7 +250,7 @@ async fn handle_as_meta_response_with_metadata(
|
||||
) -> Result<Response, StatusCode> {
|
||||
// Check if content is binary
|
||||
let is_binary = is_content_binary(item_service, item_id, metadata).await?;
|
||||
|
||||
|
||||
// Get the content if it's not binary
|
||||
if is_binary {
|
||||
// Return JSON with content as None and error message
|
||||
@@ -248,7 +259,7 @@ async fn handle_as_meta_response_with_metadata(
|
||||
"content": serde_json::Value::Null,
|
||||
"error": "Content is binary"
|
||||
});
|
||||
|
||||
|
||||
Response::builder()
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.status(StatusCode::UNPROCESSABLE_ENTITY)
|
||||
@@ -256,10 +267,7 @@ async fn handle_as_meta_response_with_metadata(
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
|
||||
} else {
|
||||
// Get the content as text
|
||||
match item_service.get_item_content_info(
|
||||
item_id,
|
||||
None
|
||||
).await {
|
||||
match item_service.get_item_content_info(item_id, None).await {
|
||||
Ok((content, _, _)) => {
|
||||
// Apply offset and length
|
||||
let content_len = content.len() as u64;
|
||||
@@ -269,13 +277,13 @@ async fn handle_as_meta_response_with_metadata(
|
||||
} else {
|
||||
content_len
|
||||
};
|
||||
|
||||
|
||||
let response_content = if start < content_len {
|
||||
&content[start as usize..end as usize]
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
|
||||
|
||||
// Convert to UTF-8 string
|
||||
let content_str = match String::from_utf8(response_content.to_vec()) {
|
||||
Ok(s) => s,
|
||||
@@ -286,7 +294,7 @@ async fn handle_as_meta_response_with_metadata(
|
||||
"content": serde_json::Value::Null,
|
||||
"error": "Content is not valid UTF-8"
|
||||
});
|
||||
|
||||
|
||||
let response = Response::builder()
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.status(StatusCode::UNPROCESSABLE_ENTITY)
|
||||
@@ -295,14 +303,14 @@ async fn handle_as_meta_response_with_metadata(
|
||||
return Ok(response);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Return JSON with metadata and content
|
||||
let response_body = serde_json::json!({
|
||||
"metadata": metadata,
|
||||
"content": content_str,
|
||||
"error": serde_json::Value::Null
|
||||
});
|
||||
|
||||
|
||||
Response::builder()
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.body(axum::body::Body::from(response_body.to_string()))
|
||||
@@ -316,7 +324,6 @@ async fn handle_as_meta_response_with_metadata(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/item/",
|
||||
@@ -342,21 +349,19 @@ async fn handle_as_meta_response_with_metadata(
|
||||
pub async fn handle_post_item(
|
||||
State(_state): State<AppState>,
|
||||
) -> Result<Json<ApiResponse<ItemInfo>>, StatusCode> {
|
||||
|
||||
// This is a simplified implementation
|
||||
// In a real implementation, you'd need to properly parse multipart/form-data
|
||||
// or JSON payload with the item data
|
||||
|
||||
|
||||
let response = ApiResponse::<ItemInfo> {
|
||||
success: false,
|
||||
data: None,
|
||||
error: Some("POST /api/item/ not yet implemented".to_string()),
|
||||
};
|
||||
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/item/latest/content",
|
||||
@@ -397,9 +402,7 @@ pub async fn handle_get_item_latest_content(
|
||||
let item_service = create_item_service(&state);
|
||||
|
||||
// First find the item to get its ID and metadata
|
||||
let item_with_meta = item_service
|
||||
.find_item(vec![], tags, HashMap::new())
|
||||
.await;
|
||||
let item_with_meta = item_service.find_item(vec![], tags, HashMap::new()).await;
|
||||
|
||||
match item_with_meta {
|
||||
Ok(item) => {
|
||||
@@ -408,9 +411,26 @@ pub async fn handle_get_item_latest_content(
|
||||
// Handle as_meta parameter
|
||||
if params.as_meta {
|
||||
// Force stream=false and allow_binary=false for as_meta=true
|
||||
handle_as_meta_response_with_metadata(&item_service, item_id, &metadata, params.offset, params.length).await
|
||||
handle_as_meta_response_with_metadata(
|
||||
&item_service,
|
||||
item_id,
|
||||
&metadata,
|
||||
params.offset,
|
||||
params.length,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
stream_item_content_response_with_metadata(&item_service, item_id, &metadata, params.allow_binary, params.offset, params.length, params.stream, None).await
|
||||
stream_item_content_response_with_metadata(
|
||||
&item_service,
|
||||
item_id,
|
||||
&metadata,
|
||||
params.allow_binary,
|
||||
params.offset,
|
||||
params.length,
|
||||
params.stream,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
Err(CoreError::ItemNotFoundGeneric) => Err(StatusCode::NOT_FOUND),
|
||||
@@ -421,7 +441,6 @@ pub async fn handle_get_item_latest_content(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/item/{item_id}/content",
|
||||
@@ -459,8 +478,10 @@ pub async fn handle_get_item_content(
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
debug!("ITEM_API: Getting content for item {} with stream={}, allow_binary={}, offset={}, length={}",
|
||||
item_id, params.stream, params.allow_binary, params.offset, params.length);
|
||||
debug!(
|
||||
"ITEM_API: Getting content for item {} with stream={}, allow_binary={}, offset={}, length={}",
|
||||
item_id, params.stream, params.allow_binary, params.offset, params.length
|
||||
);
|
||||
|
||||
let filter = build_filter_string(¶ms);
|
||||
|
||||
@@ -468,15 +489,31 @@ pub async fn handle_get_item_content(
|
||||
// Handle as_meta parameter
|
||||
if params.as_meta {
|
||||
// Force stream=false and allow_binary=false for as_meta=true
|
||||
let result = handle_as_meta_response(&item_service, item_id, params.offset, params.length).await;
|
||||
let result =
|
||||
handle_as_meta_response(&item_service, item_id, params.offset, params.length).await;
|
||||
if let Ok(response) = &result {
|
||||
debug!("ITEM_API: Response content-length: {:?}", response.headers().get("content-length"));
|
||||
debug!(
|
||||
"ITEM_API: Response content-length: {:?}",
|
||||
response.headers().get("content-length")
|
||||
);
|
||||
}
|
||||
result
|
||||
} else {
|
||||
let result = stream_item_content_response(&item_service, item_id, params.allow_binary, params.offset, params.length, params.stream, filter).await;
|
||||
let result = stream_item_content_response(
|
||||
&item_service,
|
||||
item_id,
|
||||
params.allow_binary,
|
||||
params.offset,
|
||||
params.length,
|
||||
params.stream,
|
||||
filter,
|
||||
)
|
||||
.await;
|
||||
if let Ok(response) = &result {
|
||||
debug!("ITEM_API: Response content-length: {:?}", response.headers().get("content-length"));
|
||||
debug!(
|
||||
"ITEM_API: Response content-length: {:?}",
|
||||
response.headers().get("content-length")
|
||||
);
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -499,7 +536,17 @@ async fn stream_item_content_response(
|
||||
})?;
|
||||
|
||||
let metadata = item_with_meta.meta_as_map();
|
||||
stream_item_content_response_with_metadata(item_service, item_id, &metadata, allow_binary, offset, length, stream, filter).await
|
||||
stream_item_content_response_with_metadata(
|
||||
item_service,
|
||||
item_id,
|
||||
&metadata,
|
||||
allow_binary,
|
||||
offset,
|
||||
length,
|
||||
stream,
|
||||
filter,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn stream_item_content_response_with_metadata(
|
||||
@@ -512,22 +559,23 @@ async fn stream_item_content_response_with_metadata(
|
||||
stream: bool,
|
||||
filter: Option<String>,
|
||||
) -> Result<Response, StatusCode> {
|
||||
debug!("STREAM_ITEM_CONTENT_RESPONSE_WITH_METADATA: stream={}", stream);
|
||||
debug!(
|
||||
"STREAM_ITEM_CONTENT_RESPONSE_WITH_METADATA: stream={}",
|
||||
stream
|
||||
);
|
||||
let mime_type = get_mime_type(metadata);
|
||||
|
||||
|
||||
// Check if content is binary when allow_binary is false
|
||||
check_binary_content_allowed(item_service, item_id, metadata, allow_binary).await?;
|
||||
|
||||
|
||||
if stream {
|
||||
debug!("STREAMING: Using streaming approach");
|
||||
match item_service.stream_item_content_by_id_with_metadata(
|
||||
item_id,
|
||||
metadata,
|
||||
true,
|
||||
offset,
|
||||
length,
|
||||
filter
|
||||
).await {
|
||||
match item_service
|
||||
.stream_item_content_by_id_with_metadata(
|
||||
item_id, metadata, true, offset, length, filter,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((stream, _)) => {
|
||||
let body = axum::body::Body::from_stream(stream);
|
||||
let response = Response::builder()
|
||||
@@ -543,16 +591,16 @@ async fn stream_item_content_response_with_metadata(
|
||||
}
|
||||
} else {
|
||||
debug!("NON-STREAMING: Building full response in memory");
|
||||
match item_service.get_item_content_info(
|
||||
item_id,
|
||||
filter
|
||||
).await {
|
||||
match item_service.get_item_content_info(item_id, filter).await {
|
||||
Ok((content, _, _)) => {
|
||||
let response_content = apply_offset_length(&content, offset, length);
|
||||
|
||||
debug!("NON-STREAMING: Content length: {}, response length: {}",
|
||||
content.len(), response_content.len());
|
||||
|
||||
|
||||
debug!(
|
||||
"NON-STREAMING: Content length: {}, response length: {}",
|
||||
content.len(),
|
||||
response_content.len()
|
||||
);
|
||||
|
||||
ResponseBuilder::binary(response_content, &mime_type)
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -563,8 +611,6 @@ async fn stream_item_content_response_with_metadata(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/item/latest/meta",
|
||||
@@ -655,4 +701,3 @@ pub async fn handle_get_item_meta(
|
||||
Err(e) => Err(handle_item_error(e)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
response::sse::{Event, KeepAlive, Sse},
|
||||
http::StatusCode,
|
||||
response::sse::{Event, KeepAlive, Sse},
|
||||
};
|
||||
use futures::stream::{self, Stream};
|
||||
use log::{debug, info};
|
||||
@@ -31,15 +31,15 @@ pub async fn handle_mcp_sse(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, StatusCode> {
|
||||
debug!("MCP: Starting SSE endpoint");
|
||||
|
||||
|
||||
let _mcp_server = KeepMcpServer::new(state);
|
||||
|
||||
|
||||
// Create a simple message channel for SSE communication
|
||||
let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<String>();
|
||||
|
||||
|
||||
// Send initial connection message
|
||||
let _ = tx.send("data: {\"type\":\"connection\",\"status\":\"connected\"}\n\n".to_string());
|
||||
|
||||
|
||||
// For now, create a simple stream that sends periodic keep-alive messages
|
||||
// In a full implementation, this would integrate with the rmcp transport layer
|
||||
let stream = stream::unfold((rx, tx), |(mut rx, tx)| async move {
|
||||
@@ -61,9 +61,9 @@ pub async fn handle_mcp_sse(
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
info!("MCP: SSE endpoint established");
|
||||
|
||||
|
||||
Ok(Sse::new(stream).keep_alive(
|
||||
KeepAlive::new()
|
||||
.interval(Duration::from_secs(30))
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
#[cfg(feature = "swagger")]
|
||||
pub mod item;
|
||||
pub mod status;
|
||||
#[cfg(feature = "mcp")]
|
||||
pub mod mcp;
|
||||
pub mod status;
|
||||
|
||||
use axum::{
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use axum::{Router, routing::get};
|
||||
|
||||
use crate::modes::server::common::AppState;
|
||||
use utoipa::OpenApi;
|
||||
@@ -62,26 +59,36 @@ pub fn add_routes(router: Router<AppState>) -> Router<AppState> {
|
||||
let router = router
|
||||
// Status endpoints
|
||||
.route("/api/status", get(status::handle_status))
|
||||
|
||||
// Item endpoints
|
||||
.route("/api/item/", get(item::handle_list_items).post(item::handle_post_item))
|
||||
.route("/api/item/latest/meta", get(item::handle_get_item_latest_meta))
|
||||
.route("/api/item/latest/content", get(item::handle_get_item_latest_content))
|
||||
.route(
|
||||
"/api/item/",
|
||||
get(item::handle_list_items).post(item::handle_post_item),
|
||||
)
|
||||
.route(
|
||||
"/api/item/latest/meta",
|
||||
get(item::handle_get_item_latest_meta),
|
||||
)
|
||||
.route(
|
||||
"/api/item/latest/content",
|
||||
get(item::handle_get_item_latest_content),
|
||||
)
|
||||
.route("/api/item/{item_id}/meta", get(item::handle_get_item_meta))
|
||||
.route("/api/item/{item_id}/content", get(item::handle_get_item_content));
|
||||
|
||||
.route(
|
||||
"/api/item/{item_id}/content",
|
||||
get(item::handle_get_item_content),
|
||||
);
|
||||
|
||||
#[cfg(feature = "mcp")]
|
||||
{
|
||||
router = router.route("/mcp/sse", get(mcp::handle_mcp_sse));
|
||||
}
|
||||
|
||||
|
||||
router
|
||||
}
|
||||
|
||||
#[cfg(feature = "swagger")]
|
||||
pub fn add_docs_routes(router: Router<AppState>) -> Router<AppState> {
|
||||
router
|
||||
.merge(SwaggerUi::new("/swagger").url("/openapi.json", ApiDoc::openapi()))
|
||||
router.merge(SwaggerUi::new("/swagger").url("/openapi.json", ApiDoc::openapi()))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "swagger"))]
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
response::Json,
|
||||
};
|
||||
use axum::{extract::State, http::StatusCode, response::Json};
|
||||
|
||||
use crate::modes::server::common::{AppState, StatusInfoResponse};
|
||||
|
||||
@@ -52,10 +48,15 @@ use crate::modes::server::common::{AppState, StatusInfoResponse};
|
||||
pub async fn handle_status(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<StatusInfoResponse>, StatusCode> {
|
||||
|
||||
// Get database path
|
||||
let db_path = state.db.lock().await.path().unwrap_or("unknown").to_string();
|
||||
|
||||
let db_path = state
|
||||
.db
|
||||
.lock()
|
||||
.await
|
||||
.path()
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
// Use the status service to generate status info showing configured plugins
|
||||
let status_service = crate::services::status_service::StatusService::new();
|
||||
let mut cmd = state.cmd.lock().await;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::services::item_service::ItemService;
|
||||
/// Common utilities and types for the server module.
|
||||
///
|
||||
/// This module provides shared structures, functions, and middleware used across
|
||||
@@ -13,7 +14,7 @@
|
||||
/// ```
|
||||
use anyhow::Result;
|
||||
use axum::{
|
||||
extract::{Request, ConnectInfo},
|
||||
extract::{ConnectInfo, Request},
|
||||
http::{HeaderMap, StatusCode},
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
@@ -28,7 +29,6 @@ use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use tokio::sync::Mutex;
|
||||
use utoipa::ToSchema;
|
||||
use crate::services::item_service::ItemService;
|
||||
|
||||
/// Server configuration structure.
|
||||
///
|
||||
@@ -133,7 +133,9 @@ pub struct AppState {
|
||||
/// };
|
||||
/// ```
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[schema(description = "Standard API response wrapper containing success status, data payload, and error information")]
|
||||
#[schema(
|
||||
description = "Standard API response wrapper containing success status, data payload, and error information"
|
||||
)]
|
||||
pub struct ApiResponse<T> {
|
||||
/// Success indicator.
|
||||
///
|
||||
@@ -584,18 +586,22 @@ fn default_as_meta() -> bool {
|
||||
/// # Errors
|
||||
///
|
||||
/// None; returns false on failure.
|
||||
fn check_bearer_auth(auth_str: &str, expected_password: &str, expected_hash: &Option<String>) -> bool {
|
||||
fn check_bearer_auth(
|
||||
auth_str: &str,
|
||||
expected_password: &str,
|
||||
expected_hash: &Option<String>,
|
||||
) -> bool {
|
||||
if !auth_str.starts_with("Bearer ") {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
let provided_password = &auth_str[7..];
|
||||
|
||||
|
||||
// If we have a password hash, verify against it
|
||||
if let Some(hash) = expected_hash {
|
||||
return pwhash::unix::verify(provided_password, hash);
|
||||
}
|
||||
|
||||
|
||||
// Otherwise, do direct comparison
|
||||
provided_password == expected_password
|
||||
}
|
||||
@@ -619,22 +625,26 @@ fn check_bearer_auth(auth_str: &str, expected_password: &str, expected_hash: &Op
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns false on decode or validation failure.
|
||||
fn check_basic_auth(auth_str: &str, expected_password: &str, expected_hash: &Option<String>) -> bool {
|
||||
fn check_basic_auth(
|
||||
auth_str: &str,
|
||||
expected_password: &str,
|
||||
expected_hash: &Option<String>,
|
||||
) -> bool {
|
||||
if !auth_str.starts_with("Basic ") {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
let encoded = &auth_str[6..];
|
||||
if let Ok(decoded_bytes) = base64::engine::general_purpose::STANDARD.decode(encoded) {
|
||||
if let Ok(decoded_str) = String::from_utf8(decoded_bytes) {
|
||||
if let Some(colon_pos) = decoded_str.find(':') {
|
||||
let provided_password = &decoded_str[colon_pos + 1..];
|
||||
|
||||
|
||||
// If we have a password hash, verify against it
|
||||
if let Some(hash) = expected_hash {
|
||||
return pwhash::unix::verify(provided_password, hash);
|
||||
}
|
||||
|
||||
|
||||
// Otherwise, do direct comparison
|
||||
let expected_credentials = format!("keep:{}", expected_password);
|
||||
return decoded_str == expected_credentials;
|
||||
@@ -667,16 +677,20 @@ fn check_basic_auth(auth_str: &str, expected_password: &str, expected_hash: &Opt
|
||||
/// // Proceed
|
||||
/// }
|
||||
/// ```
|
||||
pub fn check_auth(headers: &HeaderMap, password: &Option<String>, password_hash: &Option<String>) -> bool {
|
||||
pub fn check_auth(
|
||||
headers: &HeaderMap,
|
||||
password: &Option<String>,
|
||||
password_hash: &Option<String>,
|
||||
) -> bool {
|
||||
// If neither password nor hash is set, no authentication required
|
||||
if password.is_none() && password_hash.is_none() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
if let Some(auth_header) = headers.get("authorization") {
|
||||
if let Ok(auth_str) = auth_header.to_str() {
|
||||
return check_bearer_auth(auth_str, password.as_deref().unwrap_or(""), password_hash) ||
|
||||
check_basic_auth(auth_str, password.as_deref().unwrap_or(""), password_hash);
|
||||
return check_bearer_auth(auth_str, password.as_deref().unwrap_or(""), password_hash)
|
||||
|| check_basic_auth(auth_str, password.as_deref().unwrap_or(""), password_hash);
|
||||
}
|
||||
}
|
||||
false
|
||||
@@ -707,28 +721,38 @@ pub async fn logging_middleware(
|
||||
) -> Response {
|
||||
let method = request.method().clone();
|
||||
let uri = request.uri().clone();
|
||||
|
||||
|
||||
// Log the Accept header - extract before moving the request
|
||||
let accept_header = request.headers()
|
||||
let accept_header = request
|
||||
.headers()
|
||||
.get("accept")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("-")
|
||||
.to_string();
|
||||
|
||||
|
||||
let start = Instant::now();
|
||||
let response = next.run(request).await;
|
||||
let duration = start.elapsed();
|
||||
|
||||
|
||||
// Try to get response body size from content-length header, or default to 0
|
||||
let response_content_length = response.headers()
|
||||
let response_content_length = response
|
||||
.headers()
|
||||
.get("content-length")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
info!("{} {} {} {} {} bytes - {:?} - Accept: {}",
|
||||
addr, method, uri, response.status(), response_content_length, duration, accept_header);
|
||||
|
||||
|
||||
info!(
|
||||
"{} {} {} {} {} bytes - {:?} - Accept: {}",
|
||||
addr,
|
||||
method,
|
||||
uri,
|
||||
response.status(),
|
||||
response_content_length,
|
||||
duration,
|
||||
accept_header
|
||||
);
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
@@ -756,14 +780,21 @@ pub async fn logging_middleware(
|
||||
pub fn create_auth_middleware(
|
||||
password: Option<String>,
|
||||
password_hash: Option<String>,
|
||||
) -> impl Fn(ConnectInfo<SocketAddr>, Request, Next) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Response, StatusCode>> + Send>> + Clone + Send {
|
||||
) -> impl Fn(
|
||||
ConnectInfo<SocketAddr>,
|
||||
Request,
|
||||
Next,
|
||||
)
|
||||
-> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Response, StatusCode>> + Send>>
|
||||
+ Clone
|
||||
+ Send {
|
||||
move |ConnectInfo(addr): ConnectInfo<SocketAddr>, request: Request, next: Next| {
|
||||
let password = password.clone();
|
||||
let password_hash = password_hash.clone();
|
||||
Box::pin(async move {
|
||||
let headers = request.headers().clone();
|
||||
let uri = request.uri().clone();
|
||||
|
||||
|
||||
if !check_auth(&headers, &password, &password_hash) {
|
||||
warn!("Unauthorized request to {} from {}", uri, addr);
|
||||
// Add WWW-Authenticate header to trigger basic auth in browsers
|
||||
@@ -771,11 +802,13 @@ pub fn create_auth_middleware(
|
||||
*response.status_mut() = StatusCode::UNAUTHORIZED;
|
||||
response.headers_mut().insert(
|
||||
"www-authenticate",
|
||||
"Basic realm=\"Keep Server\", charset=\"UTF-8\"".parse().unwrap(),
|
||||
"Basic realm=\"Keep Server\", charset=\"UTF-8\""
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
|
||||
let response = next.run(request).await;
|
||||
Ok(response)
|
||||
})
|
||||
|
||||
@@ -7,17 +7,12 @@ pub use server::KeepMcpServer;
|
||||
///
|
||||
/// Provides handlers for JSON-RPC style requests to interact with Keep's storage
|
||||
/// via the API.
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
Json,
|
||||
};
|
||||
use axum::{Json, extract::State, http::StatusCode, response::IntoResponse};
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::modes::server::common::AppState;
|
||||
use crate::modes::server::common::ApiResponse;
|
||||
use crate::modes::server::common::AppState;
|
||||
|
||||
/// Request structure for MCP JSON-RPC calls.
|
||||
///
|
||||
@@ -31,57 +26,58 @@ pub struct McpRequest {
|
||||
pub params: Option<Value>,
|
||||
}
|
||||
|
||||
/// Handles an MCP request via the Axum framework.
|
||||
///
|
||||
/// Parses the JSON request, delegates to `KeepMcpServer`, and returns an API response.
|
||||
/// Attempts to parse the result as JSON; falls back to string if invalid.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `State(state)` - The application state.
|
||||
/// * `Json(request)` - The deserialized MCP request.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// An `IntoResponse` with status code and JSON API response.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns 400 Bad Request on handler errors.
|
||||
pub async fn handle_mcp_request(
|
||||
State(state): State<AppState>,
|
||||
Json(request): Json<McpRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let mcp_server = KeepMcpServer::new(state);
|
||||
|
||||
match mcp_server.handle_request(&request.method, request.params).await {
|
||||
Ok(result) => {
|
||||
match serde_json::from_str(&result) {
|
||||
Ok(parsed_result) => {
|
||||
let response = ApiResponse {
|
||||
success: true,
|
||||
data: Some(parsed_result),
|
||||
error: None,
|
||||
};
|
||||
(StatusCode::OK, Json(response))
|
||||
}
|
||||
Err(_) => {
|
||||
let response = ApiResponse {
|
||||
success: true,
|
||||
data: Some(serde_json::Value::String(result)),
|
||||
error: None,
|
||||
};
|
||||
(StatusCode::OK, Json(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
/// Handles an MCP request via the Axum framework.
|
||||
///
|
||||
/// Parses the JSON request, delegates to `KeepMcpServer`, and returns an API response.
|
||||
/// Attempts to parse the result as JSON; falls back to string if invalid.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `State(state)` - The application state.
|
||||
/// * `Json(request)` - The deserialized MCP request.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// An `IntoResponse` with status code and JSON API response.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns 400 Bad Request on handler errors.
|
||||
pub async fn handle_mcp_request(
|
||||
State(state): State<AppState>,
|
||||
Json(request): Json<McpRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let mcp_server = KeepMcpServer::new(state);
|
||||
|
||||
match mcp_server
|
||||
.handle_request(&request.method, request.params)
|
||||
.await
|
||||
{
|
||||
Ok(result) => match serde_json::from_str(&result) {
|
||||
Ok(parsed_result) => {
|
||||
let response = ApiResponse {
|
||||
success: false,
|
||||
data: None,
|
||||
error: Some(e.to_string()),
|
||||
success: true,
|
||||
data: Some(parsed_result),
|
||||
error: None,
|
||||
};
|
||||
(StatusCode::BAD_REQUEST, Json(response))
|
||||
(StatusCode::OK, Json(response))
|
||||
}
|
||||
Err(_) => {
|
||||
let response = ApiResponse {
|
||||
success: true,
|
||||
data: Some(serde_json::Value::String(result)),
|
||||
error: None,
|
||||
};
|
||||
(StatusCode::OK, Json(response))
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
let response = ApiResponse {
|
||||
success: false,
|
||||
data: None,
|
||||
error: Some(e.to_string()),
|
||||
};
|
||||
(StatusCode::BAD_REQUEST, Json(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use log::debug;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::modes::server::common::AppState;
|
||||
use super::tools::{KeepTools, ToolError};
|
||||
use crate::modes::server::common::AppState;
|
||||
|
||||
/// Server handler for MCP (Model Context Protocol) requests.
|
||||
///
|
||||
@@ -36,34 +36,41 @@ impl KeepMcpServer {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
/// Handles an MCP request by routing to the appropriate tool.
|
||||
///
|
||||
/// Supports methods like "save_item", "get_item", "list_items". Logs the request and delegates to KeepTools.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `method` - The MCP method name (string).
|
||||
/// * `params` - Optional JSON parameters as serde_json::Value.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `Ok(String)` with JSON-serialized response on success, or `Err(ToolError)` on failure.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// * ToolError::UnknownTool if method unsupported.
|
||||
/// * Propagates tool-specific errors (e.g., invalid args, DB failures).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// let result = server.handle_request("save_item", Some(params)).await?;
|
||||
/// ```
|
||||
pub async fn handle_request(&self, method: &str, params: Option<Value>) -> Result<String, ToolError> {
|
||||
debug!("MCP: Handling request '{}' with params: {:?}", method, params);
|
||||
|
||||
/// Handles an MCP request by routing to the appropriate tool.
|
||||
///
|
||||
/// Supports methods like "save_item", "get_item", "list_items". Logs the request and delegates to KeepTools.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `method` - The MCP method name (string).
|
||||
/// * `params` - Optional JSON parameters as serde_json::Value.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `Ok(String)` with JSON-serialized response on success, or `Err(ToolError)` on failure.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// * ToolError::UnknownTool if method unsupported.
|
||||
/// * Propagates tool-specific errors (e.g., invalid args, DB failures).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// let result = server.handle_request("save_item", Some(params)).await?;
|
||||
/// ```
|
||||
pub async fn handle_request(
|
||||
&self,
|
||||
method: &str,
|
||||
params: Option<Value>,
|
||||
) -> Result<String, ToolError> {
|
||||
debug!(
|
||||
"MCP: Handling request '{}' with params: {:?}",
|
||||
method, params
|
||||
);
|
||||
|
||||
let tools = KeepTools::new(self.state.clone());
|
||||
|
||||
|
||||
match method {
|
||||
"save_item" => tools.save_item(params).await,
|
||||
"get_item" => tools.get_item(params).await,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use log::debug;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use log::{debug};
|
||||
|
||||
use crate::modes::server::common::AppState;
|
||||
use crate::services::async_item_service::AsyncItemService;
|
||||
@@ -35,7 +35,8 @@ impl KeepTools {
|
||||
}
|
||||
|
||||
pub async fn save_item(&self, args: Option<Value>) -> Result<String, ToolError> {
|
||||
let args = args.ok_or_else(|| ToolError::InvalidArguments("Missing arguments".to_string()))?;
|
||||
let args =
|
||||
args.ok_or_else(|| ToolError::InvalidArguments("Missing arguments".to_string()))?;
|
||||
|
||||
let content = args
|
||||
.get("content")
|
||||
@@ -70,11 +71,11 @@ impl KeepTools {
|
||||
);
|
||||
|
||||
let service = AsyncItemService::new(
|
||||
self.state.data_dir.clone(),
|
||||
self.state.db.clone(),
|
||||
self.state.data_dir.clone(),
|
||||
self.state.db.clone(),
|
||||
self.state.item_service.clone(),
|
||||
self.state.cmd.clone(),
|
||||
self.state.settings.clone()
|
||||
self.state.settings.clone(),
|
||||
);
|
||||
let item_with_meta = service
|
||||
.save_item_from_mcp(content.as_bytes().to_vec(), tags, metadata)
|
||||
@@ -90,28 +91,39 @@ impl KeepTools {
|
||||
}
|
||||
|
||||
pub async fn get_item(&self, args: Option<Value>) -> Result<String, ToolError> {
|
||||
let args = args.ok_or_else(|| ToolError::InvalidArguments("Missing arguments".to_string()))?;
|
||||
|
||||
let item_id = args.get("id")
|
||||
.and_then(|v| v.as_i64())
|
||||
.ok_or_else(|| ToolError::InvalidArguments("Missing or invalid 'id' field".to_string()))?;
|
||||
let args =
|
||||
args.ok_or_else(|| ToolError::InvalidArguments("Missing arguments".to_string()))?;
|
||||
|
||||
let item_id = args.get("id").and_then(|v| v.as_i64()).ok_or_else(|| {
|
||||
ToolError::InvalidArguments("Missing or invalid 'id' field".to_string())
|
||||
})?;
|
||||
|
||||
let service = AsyncItemService::new(
|
||||
self.state.data_dir.clone(),
|
||||
self.state.db.clone(),
|
||||
self.state.data_dir.clone(),
|
||||
self.state.db.clone(),
|
||||
self.state.item_service.clone(),
|
||||
self.state.cmd.clone(),
|
||||
self.state.settings.clone()
|
||||
self.state.settings.clone(),
|
||||
);
|
||||
|
||||
let item_with_content = match service.get_item_content(item_id).await {
|
||||
Ok(iwc) => iwc,
|
||||
Err(CoreError::ItemNotFound(_)) => return Err(ToolError::InvalidArguments(format!("Item {} not found", item_id))),
|
||||
Err(CoreError::ItemNotFound(_)) => {
|
||||
return Err(ToolError::InvalidArguments(format!(
|
||||
"Item {} not found",
|
||||
item_id
|
||||
)));
|
||||
}
|
||||
Err(e) => return Err(ToolError::Other(anyhow::Error::from(e))),
|
||||
};
|
||||
|
||||
let content = String::from_utf8_lossy(&item_with_content.content).to_string();
|
||||
let tags: Vec<String> = item_with_content.item_with_meta.tags.iter().map(|t| t.name.clone()).collect();
|
||||
let tags: Vec<String> = item_with_content
|
||||
.item_with_meta
|
||||
.tags
|
||||
.iter()
|
||||
.map(|t| t.name.clone())
|
||||
.collect();
|
||||
let metadata = item_with_content.item_with_meta.meta_as_map();
|
||||
let item = item_with_content.item_with_meta.item;
|
||||
|
||||
@@ -124,7 +136,7 @@ impl KeepTools {
|
||||
"tags": tags,
|
||||
"metadata": metadata,
|
||||
});
|
||||
|
||||
|
||||
Ok(serde_json::to_string_pretty(&response)?)
|
||||
}
|
||||
|
||||
@@ -133,28 +145,45 @@ impl KeepTools {
|
||||
.as_ref()
|
||||
.and_then(|v| v.get("tags"))
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let service = AsyncItemService::new(
|
||||
self.state.data_dir.clone(),
|
||||
self.state.db.clone(),
|
||||
self.state.data_dir.clone(),
|
||||
self.state.db.clone(),
|
||||
self.state.item_service.clone(),
|
||||
self.state.cmd.clone(),
|
||||
self.state.settings.clone()
|
||||
self.state.settings.clone(),
|
||||
);
|
||||
|
||||
let item_with_meta = match service.find_item(vec![], tags, HashMap::new()).await {
|
||||
Ok(iwm) => iwm,
|
||||
Err(CoreError::ItemNotFoundGeneric) => return Err(ToolError::InvalidArguments("No items found".to_string())),
|
||||
Err(CoreError::ItemNotFoundGeneric) => {
|
||||
return Err(ToolError::InvalidArguments("No items found".to_string()));
|
||||
}
|
||||
Err(e) => return Err(ToolError::Other(anyhow::Error::from(e))),
|
||||
};
|
||||
|
||||
let item_id = item_with_meta.item.id.ok_or_else(|| anyhow!("Item missing ID after find"))?;
|
||||
let item_with_content = service.get_item_content(item_id).await.map_err(|e| ToolError::Other(anyhow::Error::from(e)))?;
|
||||
|
||||
|
||||
let item_id = item_with_meta
|
||||
.item
|
||||
.id
|
||||
.ok_or_else(|| anyhow!("Item missing ID after find"))?;
|
||||
let item_with_content = service
|
||||
.get_item_content(item_id)
|
||||
.await
|
||||
.map_err(|e| ToolError::Other(anyhow::Error::from(e)))?;
|
||||
|
||||
let content = String::from_utf8_lossy(&item_with_content.content).to_string();
|
||||
let tags: Vec<String> = item_with_content.item_with_meta.tags.iter().map(|t| t.name.clone()).collect();
|
||||
let tags: Vec<String> = item_with_content
|
||||
.item_with_meta
|
||||
.tags
|
||||
.iter()
|
||||
.map(|t| t.name.clone())
|
||||
.collect();
|
||||
let metadata = item_with_content.item_with_meta.meta_as_map();
|
||||
let item = item_with_content.item_with_meta.item;
|
||||
|
||||
@@ -167,7 +196,7 @@ impl KeepTools {
|
||||
"tags": tags,
|
||||
"metadata": metadata,
|
||||
});
|
||||
|
||||
|
||||
Ok(serde_json::to_string_pretty(&response)?)
|
||||
}
|
||||
|
||||
@@ -176,40 +205,52 @@ impl KeepTools {
|
||||
let tags: Vec<String> = args_ref
|
||||
.and_then(|v| v.get("tags"))
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
|
||||
let limit = args_ref
|
||||
.and_then(|v| v.get("limit"))
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(10) as usize;
|
||||
|
||||
|
||||
let offset = args_ref
|
||||
.and_then(|v| v.get("offset"))
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0) as usize;
|
||||
|
||||
let service = AsyncItemService::new(
|
||||
self.state.data_dir.clone(),
|
||||
self.state.db.clone(),
|
||||
self.state.data_dir.clone(),
|
||||
self.state.db.clone(),
|
||||
self.state.item_service.clone(),
|
||||
self.state.cmd.clone(),
|
||||
self.state.settings.clone()
|
||||
self.state.settings.clone(),
|
||||
);
|
||||
let mut items_with_meta = service.list_items(tags, HashMap::new()).await.map_err(|e| ToolError::Other(anyhow::Error::from(e)))?;
|
||||
let mut items_with_meta = service
|
||||
.list_items(tags, HashMap::new())
|
||||
.await
|
||||
.map_err(|e| ToolError::Other(anyhow::Error::from(e)))?;
|
||||
|
||||
// Sort by timestamp (newest first) and apply pagination
|
||||
items_with_meta.sort_by(|a, b| b.item.ts.cmp(&a.item.ts));
|
||||
let items_with_meta: Vec<_> = items_with_meta.into_iter().skip(offset).take(limit).collect();
|
||||
|
||||
let items_with_meta: Vec<_> = items_with_meta
|
||||
.into_iter()
|
||||
.skip(offset)
|
||||
.take(limit)
|
||||
.collect();
|
||||
|
||||
let items_info: Vec<_> = items_with_meta
|
||||
.into_iter()
|
||||
.map(|item_with_meta| {
|
||||
let item_tags: Vec<String> = item_with_meta.tags.iter().map(|t| t.name.clone()).collect();
|
||||
let item_tags: Vec<String> =
|
||||
item_with_meta.tags.iter().map(|t| t.name.clone()).collect();
|
||||
let item_meta = item_with_meta.meta_as_map();
|
||||
let item = item_with_meta.item;
|
||||
let item_id = item.id.unwrap_or(0);
|
||||
|
||||
|
||||
serde_json::json!({
|
||||
"id": item_id,
|
||||
"timestamp": item.ts.to_rfc3339(),
|
||||
@@ -220,14 +261,14 @@ impl KeepTools {
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
let response = serde_json::json!({
|
||||
"items": items_info,
|
||||
"count": items_info.len(),
|
||||
"offset": offset,
|
||||
"limit": limit
|
||||
});
|
||||
|
||||
|
||||
Ok(serde_json::to_string_pretty(&response)?)
|
||||
}
|
||||
|
||||
@@ -236,38 +277,48 @@ impl KeepTools {
|
||||
.as_ref()
|
||||
.and_then(|v| v.get("tags"))
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
|
||||
let metadata: HashMap<String, String> = args
|
||||
.as_ref()
|
||||
.and_then(|v| v.get("metadata"))
|
||||
.and_then(|v| v.as_object())
|
||||
.map(|obj| obj.iter().filter_map(|(k, v)| {
|
||||
v.as_str().map(|s| (k.clone(), s.to_string()))
|
||||
}).collect())
|
||||
.map(|obj| {
|
||||
obj.iter()
|
||||
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let service = AsyncItemService::new(
|
||||
self.state.data_dir.clone(),
|
||||
self.state.db.clone(),
|
||||
self.state.data_dir.clone(),
|
||||
self.state.db.clone(),
|
||||
self.state.item_service.clone(),
|
||||
self.state.cmd.clone(),
|
||||
self.state.settings.clone()
|
||||
self.state.settings.clone(),
|
||||
);
|
||||
let mut items_with_meta = service.list_items(tags.clone(), metadata.clone()).await.map_err(|e| ToolError::Other(anyhow::Error::from(e)))?;
|
||||
let mut items_with_meta = service
|
||||
.list_items(tags.clone(), metadata.clone())
|
||||
.await
|
||||
.map_err(|e| ToolError::Other(anyhow::Error::from(e)))?;
|
||||
|
||||
// Sort by timestamp (newest first)
|
||||
items_with_meta.sort_by(|a, b| b.item.ts.cmp(&a.item.ts));
|
||||
|
||||
|
||||
let items_info: Vec<_> = items_with_meta
|
||||
.into_iter()
|
||||
.map(|item_with_meta| {
|
||||
let item_tags: Vec<String> = item_with_meta.tags.iter().map(|t| t.name.clone()).collect();
|
||||
let item_tags: Vec<String> =
|
||||
item_with_meta.tags.iter().map(|t| t.name.clone()).collect();
|
||||
let item_meta = item_with_meta.meta_as_map();
|
||||
let item = item_with_meta.item;
|
||||
let item_id = item.id.unwrap_or(0);
|
||||
|
||||
|
||||
serde_json::json!({
|
||||
"id": item_id,
|
||||
"timestamp": item.ts.to_rfc3339(),
|
||||
@@ -278,7 +329,7 @@ impl KeepTools {
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
let response = serde_json::json!({
|
||||
"items": items_info,
|
||||
"count": items_info.len(),
|
||||
@@ -287,7 +338,7 @@ impl KeepTools {
|
||||
"metadata": metadata
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Ok(serde_json::to_string_pretty(&response)?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
use crate::config;
|
||||
use crate::services::item_service::ItemService;
|
||||
use anyhow::Result;
|
||||
use axum::{
|
||||
Router,
|
||||
routing::post,
|
||||
};
|
||||
use axum::{Router, routing::post};
|
||||
use clap::Command;
|
||||
use log::{debug, info};
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use crate::config;
|
||||
use crate::services::item_service::ItemService;
|
||||
|
||||
pub mod common;
|
||||
mod api;
|
||||
mod pages;
|
||||
pub mod common;
|
||||
#[cfg(feature = "mcp")]
|
||||
mod mcp;
|
||||
mod pages;
|
||||
|
||||
pub use common::{AppState, logging_middleware, create_auth_middleware};
|
||||
pub use common::{AppState, create_auth_middleware, logging_middleware};
|
||||
|
||||
pub fn mode_server(
|
||||
cmd: &mut Command,
|
||||
@@ -33,11 +30,14 @@ pub fn mode_server(
|
||||
let server_address = if let Some(addr) = &settings.server_address() {
|
||||
addr.clone()
|
||||
} else if let Some(server_config) = &settings.server {
|
||||
server_config.address.clone().unwrap_or_else(|| "127.0.0.1".to_string())
|
||||
server_config
|
||||
.address
|
||||
.clone()
|
||||
.unwrap_or_else(|| "127.0.0.1".to_string())
|
||||
} else {
|
||||
"127.0.0.1".to_string()
|
||||
};
|
||||
|
||||
|
||||
// Get server port from args or config with default
|
||||
let server_port = if let Some(port) = settings.server_port() {
|
||||
port
|
||||
@@ -46,24 +46,31 @@ pub fn mode_server(
|
||||
} else {
|
||||
21080
|
||||
};
|
||||
|
||||
|
||||
let server_config = common::ServerConfig {
|
||||
address: server_address,
|
||||
port: Some(server_port),
|
||||
password: settings.server_password(),
|
||||
password_hash: settings.server_password_hash(),
|
||||
};
|
||||
|
||||
|
||||
// Create ItemService once
|
||||
let item_service = ItemService::new(data_path.clone());
|
||||
|
||||
|
||||
// We need to move the connection into the async runtime
|
||||
let rt = tokio::runtime::Runtime::new()?;
|
||||
// Take ownership of the connection and move it into the async runtime
|
||||
let owned_conn = std::mem::replace(conn, rusqlite::Connection::open_in_memory()?);
|
||||
let cmd = cmd.clone();
|
||||
let settings = settings.clone();
|
||||
rt.block_on(run_server(server_config, owned_conn, data_path, item_service, cmd, settings))
|
||||
rt.block_on(run_server(
|
||||
server_config,
|
||||
owned_conn,
|
||||
data_path,
|
||||
item_service,
|
||||
cmd,
|
||||
settings,
|
||||
))
|
||||
}
|
||||
|
||||
async fn run_server(
|
||||
@@ -80,12 +87,12 @@ async fn run_server(
|
||||
} else {
|
||||
format!("{}:21080", config.address)
|
||||
};
|
||||
|
||||
|
||||
debug!("SERVER: Starting REST HTTP server on {}", bind_address);
|
||||
|
||||
|
||||
// Use the existing database connection
|
||||
let db_conn = Arc::new(Mutex::new(conn));
|
||||
|
||||
|
||||
let state = AppState {
|
||||
db: db_conn,
|
||||
data_dir: data_dir.clone(),
|
||||
@@ -93,24 +100,25 @@ async fn run_server(
|
||||
cmd: Arc::new(Mutex::new(Command::new("keep"))),
|
||||
settings: Arc::new(settings.clone()),
|
||||
};
|
||||
|
||||
|
||||
#[cfg(feature = "mcp")]
|
||||
let mcp_router = Router::new()
|
||||
.route("/mcp", post(mcp::handle_mcp_request))
|
||||
.with_state(state.clone());
|
||||
|
||||
|
||||
let mut protected_router = Router::new()
|
||||
.merge(api::add_routes(Router::new()))
|
||||
.merge(pages::add_routes(Router::new()));
|
||||
|
||||
|
||||
#[cfg(feature = "mcp")]
|
||||
{
|
||||
protected_router = protected_router.merge(mcp_router);
|
||||
}
|
||||
|
||||
let protected_router = protected_router
|
||||
.layer(axum::middleware::from_fn(create_auth_middleware(config.password.clone(), config.password_hash.clone())));
|
||||
|
||||
|
||||
let protected_router = protected_router.layer(axum::middleware::from_fn(
|
||||
create_auth_middleware(config.password.clone(), config.password_hash.clone()),
|
||||
));
|
||||
|
||||
// Create the app with documentation routes open and others protected
|
||||
let app = Router::new()
|
||||
// Add documentation routes without authentication
|
||||
@@ -124,18 +132,19 @@ async fn run_server(
|
||||
.layer(
|
||||
ServiceBuilder::new()
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.layer(CorsLayer::permissive())
|
||||
.layer(CorsLayer::permissive()),
|
||||
);
|
||||
|
||||
|
||||
let addr: SocketAddr = bind_address.parse()?;
|
||||
|
||||
|
||||
info!("SERVER: HTTP server listening on {}", addr);
|
||||
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
axum::serve(
|
||||
listener,
|
||||
app.into_make_service_with_connect_info::<SocketAddr>()
|
||||
).await?;
|
||||
|
||||
listener,
|
||||
app.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::config::ColumnConfig;
|
||||
use crate::db;
|
||||
use crate::modes::server::AppState;
|
||||
use anyhow::Result;
|
||||
@@ -8,7 +9,6 @@ use axum::{
|
||||
use log::debug;
|
||||
use rusqlite::Connection;
|
||||
use serde::Deserialize;
|
||||
use crate::config::ColumnConfig;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -72,8 +72,7 @@ fn default_count() -> usize {
|
||||
/// let app = pages::add_routes(axum::Router::new());
|
||||
/// ```
|
||||
pub fn add_routes(app: axum::Router<AppState>) -> axum::Router<AppState> {
|
||||
app
|
||||
.route("/", axum::routing::get(list_items))
|
||||
app.route("/", axum::routing::get(list_items))
|
||||
.route("/item/{item_id}", axum::routing::get(show_item))
|
||||
.route("/style.css", axum::routing::get(style_css))
|
||||
}
|
||||
@@ -84,9 +83,9 @@ async fn list_items(
|
||||
) -> Result<Response, Html<String>> {
|
||||
let conn = state.db.lock().await;
|
||||
let settings = &state.settings;
|
||||
|
||||
|
||||
let result = build_item_list(&conn, ¶ms, &settings.list_format);
|
||||
|
||||
|
||||
match result {
|
||||
Ok(html) => {
|
||||
// Build response with explicit Content-Length
|
||||
@@ -96,23 +95,28 @@ async fn list_items(
|
||||
.body(axum::body::Body::from(html))
|
||||
.map_err(|_| Html("<html><body>Internal Server Error</body></html>".to_string()))?;
|
||||
Ok(response)
|
||||
},
|
||||
}
|
||||
Err(e) => Err(Html(format!("<html><body>Error: {}</body></html>", e))),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_item_list(conn: &Connection, params: &ListQueryParams, columns: &[ColumnConfig]) -> Result<String> {
|
||||
let tags: Vec<String> = params.tags
|
||||
fn build_item_list(
|
||||
conn: &Connection,
|
||||
params: &ListQueryParams,
|
||||
columns: &[ColumnConfig],
|
||||
) -> Result<String> {
|
||||
let tags: Vec<String> = params
|
||||
.tags
|
||||
.as_ref()
|
||||
.map(|t| t.split(',').map(|s| s.trim().to_string()).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
|
||||
let items = if tags.is_empty() {
|
||||
db::query_all_items(conn)?
|
||||
} else {
|
||||
db::query_tagged_items(conn, &tags)?
|
||||
};
|
||||
|
||||
|
||||
// Sort items
|
||||
let mut sorted_items = items;
|
||||
if params.sort == "newest" {
|
||||
@@ -120,7 +124,7 @@ fn build_item_list(conn: &Connection, params: &ListQueryParams, columns: &[Colum
|
||||
} else {
|
||||
sorted_items.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
}
|
||||
|
||||
|
||||
// Apply pagination
|
||||
let start = params.start;
|
||||
let end = std::cmp::min(start + params.count, sorted_items.len());
|
||||
@@ -129,29 +133,29 @@ fn build_item_list(conn: &Connection, params: &ListQueryParams, columns: &[Colum
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
|
||||
// Get tags and meta for all items in the page
|
||||
let item_ids: Vec<i64> = page_items.iter().filter_map(|item| item.id).collect();
|
||||
let tags_map = db::get_tags_for_items(conn, &item_ids)?;
|
||||
let meta_map = db::get_meta_for_items(conn, &item_ids)?;
|
||||
|
||||
|
||||
// Debug: print number of tags per item
|
||||
for item_id in &item_ids {
|
||||
if let Some(tags) = tags_map.get(item_id) {
|
||||
debug!("Item {} has {} tags: {:?}", item_id, tags.len(), tags);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let mut html = String::new();
|
||||
html.push_str("<html><head><title>Keep - Items</title>");
|
||||
html.push_str("<link rel=\"stylesheet\" href=\"/style.css\">");
|
||||
html.push_str("</head><body>");
|
||||
html.push_str("<h1>Items</h1>");
|
||||
html.push_str("<p><a href=\"/swagger\">API Documentation</a></p>");
|
||||
|
||||
|
||||
// Add recent tags section using the items we already have
|
||||
html.push_str("<h2>Recent Tags</h2>");
|
||||
|
||||
|
||||
// Collect all tags from all items, keeping track of their timestamps
|
||||
let mut all_tags_with_time: Vec<(String, chrono::DateTime<chrono::Utc>)> = Vec::new();
|
||||
for item in &sorted_items {
|
||||
@@ -163,10 +167,10 @@ fn build_item_list(conn: &Connection, params: &ListQueryParams, columns: &[Colum
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Sort by timestamp descending (most recent first)
|
||||
all_tags_with_time.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
|
||||
|
||||
// Get unique tags in order of most recent appearance
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut recent_tags = Vec::new();
|
||||
@@ -179,20 +183,23 @@ fn build_item_list(conn: &Connection, params: &ListQueryParams, columns: &[Colum
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if recent_tags.is_empty() {
|
||||
html.push_str("<p>No tags found</p>");
|
||||
} else {
|
||||
html.push_str("<p>");
|
||||
for tag in recent_tags {
|
||||
html.push_str(&format!("<a href=\"/?tags={}\" style=\"margin-right: 8px;\">{}</a>", tag, tag));
|
||||
html.push_str(&format!(
|
||||
"<a href=\"/?tags={}\" style=\"margin-right: 8px;\">{}</a>",
|
||||
tag, tag
|
||||
));
|
||||
}
|
||||
html.push_str("</p>");
|
||||
}
|
||||
|
||||
|
||||
// Start table
|
||||
html.push_str("<table>");
|
||||
|
||||
|
||||
// Table headers
|
||||
html.push_str("<tr>");
|
||||
for column in columns {
|
||||
@@ -200,19 +207,21 @@ fn build_item_list(conn: &Connection, params: &ListQueryParams, columns: &[Colum
|
||||
}
|
||||
html.push_str("<th>Actions</th>");
|
||||
html.push_str("</tr>");
|
||||
|
||||
|
||||
// Table rows
|
||||
for item in page_items {
|
||||
let item_id = item.id.unwrap_or(0);
|
||||
let tags = tags_map.get(&item_id).cloned().unwrap_or_default();
|
||||
let meta: HashMap<String, String> = meta_map.get(&item_id)
|
||||
let meta: HashMap<String, String> = meta_map
|
||||
.get(&item_id)
|
||||
.map(|metas| {
|
||||
metas.iter()
|
||||
metas
|
||||
.iter()
|
||||
.map(|(name, value)| (name.clone(), value.clone()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
|
||||
html.push_str("<tr>");
|
||||
for column in columns {
|
||||
let value = match column.name.as_str() {
|
||||
@@ -220,16 +229,17 @@ fn build_item_list(conn: &Connection, params: &ListQueryParams, columns: &[Colum
|
||||
let id_value = item.id.map(|id| id.to_string()).unwrap_or_default();
|
||||
// Make the ID a link to the item details page
|
||||
format!("<a href=\"/item/{}\">{}</a>", item_id, id_value)
|
||||
},
|
||||
}
|
||||
"time" => item.ts.format("%Y-%m-%d %H:%M:%S").to_string(),
|
||||
"size" => item.size.map(|s| s.to_string()).unwrap_or_default(),
|
||||
"tags" => {
|
||||
// Make sure we're using all tags for the item
|
||||
let tag_links: Vec<String> = tags.iter()
|
||||
let tag_links: Vec<String> = tags
|
||||
.iter()
|
||||
.map(|t| format!("<a href=\"/?tags={}\">{}</a>", t.name, t.name))
|
||||
.collect();
|
||||
tag_links.join(", ")
|
||||
},
|
||||
}
|
||||
_ => {
|
||||
if column.name.starts_with("meta:") {
|
||||
let meta_key = &column.name[5..];
|
||||
@@ -239,7 +249,7 @@ fn build_item_list(conn: &Connection, params: &ListQueryParams, columns: &[Colum
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Apply max_len if specified, but skip for tags column to avoid truncating HTML
|
||||
let display_value = if column.name == "tags" {
|
||||
value
|
||||
@@ -257,36 +267,41 @@ fn build_item_list(conn: &Connection, params: &ListQueryParams, columns: &[Colum
|
||||
} else {
|
||||
value
|
||||
};
|
||||
|
||||
|
||||
// Apply alignment
|
||||
let align_style = match column.align {
|
||||
crate::config::ColumnAlignment::Left => "text-align: left;",
|
||||
crate::config::ColumnAlignment::Right => "text-align: right;",
|
||||
crate::config::ColumnAlignment::Center => "text-align: center;",
|
||||
};
|
||||
|
||||
html.push_str(&format!("<td style=\"{}\">{}</td>", align_style, display_value));
|
||||
|
||||
html.push_str(&format!(
|
||||
"<td style=\"{}\">{}</td>",
|
||||
align_style, display_value
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
// Actions column
|
||||
html.push_str(&format!(
|
||||
"<td><a href=\"/item/{}\">View</a> | <a href=\"/api/item/{}/content\">Download</a></td>",
|
||||
item_id, item_id
|
||||
));
|
||||
|
||||
|
||||
html.push_str("</tr>");
|
||||
}
|
||||
|
||||
|
||||
html.push_str("</table>");
|
||||
|
||||
|
||||
// Add pagination info
|
||||
html.push_str(&format!("<p>Showing {} to {} of {} items</p>",
|
||||
start + 1,
|
||||
std::cmp::min(end, sorted_items.len()),
|
||||
sorted_items.len()));
|
||||
|
||||
html.push_str(&format!(
|
||||
"<p>Showing {} to {} of {} items</p>",
|
||||
start + 1,
|
||||
std::cmp::min(end, sorted_items.len()),
|
||||
sorted_items.len()
|
||||
));
|
||||
|
||||
html.push_str("</body></html>");
|
||||
|
||||
|
||||
Ok(html)
|
||||
}
|
||||
|
||||
@@ -344,9 +359,9 @@ async fn show_item(
|
||||
Path(id): Path<i64>,
|
||||
) -> Result<Response, Html<String>> {
|
||||
let conn = state.db.lock().await;
|
||||
|
||||
|
||||
let result = build_item_details(&conn, id);
|
||||
|
||||
|
||||
match result {
|
||||
Ok(html) => {
|
||||
// Build response with explicit Content-Length
|
||||
@@ -356,7 +371,7 @@ async fn show_item(
|
||||
.body(axum::body::Body::from(html))
|
||||
.map_err(|_| Html("<html><body>Internal Server Error</body></html>".to_string()))?;
|
||||
Ok(response)
|
||||
},
|
||||
}
|
||||
Err(e) => Err(Html(format!("<html><body>Error: {}</body></html>", e))),
|
||||
}
|
||||
}
|
||||
@@ -366,51 +381,70 @@ fn build_item_details(conn: &Connection, id: i64) -> Result<String> {
|
||||
Some(item) => item,
|
||||
None => return Err(anyhow::anyhow!("Item not found")),
|
||||
};
|
||||
|
||||
|
||||
let tags = db::get_item_tags(conn, &item)?;
|
||||
let metas = db::get_item_meta(conn, &item)?;
|
||||
|
||||
|
||||
let mut html = String::new();
|
||||
html.push_str(&format!("<html><head><title>Keep - Item #{}</title>", id));
|
||||
html.push_str("<link rel=\"stylesheet\" href=\"/style.css\">");
|
||||
html.push_str("</head><body>");
|
||||
html.push_str(&format!("<h1>Item #{}</h1>", id));
|
||||
|
||||
|
||||
// Single table for all details
|
||||
html.push_str("<table>");
|
||||
html.push_str(&format!("<tr><th>ID</th><td>{}</td></tr>", item.id.unwrap_or(0)));
|
||||
html.push_str(&format!("<tr><th>Timestamp</th><td>{}</td></tr>", item.ts.format("%Y-%m-%d %H:%M:%S")));
|
||||
html.push_str(&format!("<tr><th>Size</th><td>{}</td></tr>", item.size.unwrap_or(0)));
|
||||
html.push_str(&format!("<tr><th>Compression</th><td>{}</td></tr>", item.compression));
|
||||
|
||||
html.push_str(&format!(
|
||||
"<tr><th>ID</th><td>{}</td></tr>",
|
||||
item.id.unwrap_or(0)
|
||||
));
|
||||
html.push_str(&format!(
|
||||
"<tr><th>Timestamp</th><td>{}</td></tr>",
|
||||
item.ts.format("%Y-%m-%d %H:%M:%S")
|
||||
));
|
||||
html.push_str(&format!(
|
||||
"<tr><th>Size</th><td>{}</td></tr>",
|
||||
item.size.unwrap_or(0)
|
||||
));
|
||||
html.push_str(&format!(
|
||||
"<tr><th>Compression</th><td>{}</td></tr>",
|
||||
item.compression
|
||||
));
|
||||
|
||||
// Tags row
|
||||
html.push_str("<tr><th>Tags</th><td>");
|
||||
if tags.is_empty() {
|
||||
html.push_str("No tags");
|
||||
} else {
|
||||
let tag_links: Vec<String> = tags.iter()
|
||||
let tag_links: Vec<String> = tags
|
||||
.iter()
|
||||
.map(|t| format!("<a href=\"/?tags={}\">{}</a>", t.name, t.name))
|
||||
.collect();
|
||||
html.push_str(&tag_links.join(", "));
|
||||
}
|
||||
html.push_str("</td></tr>");
|
||||
|
||||
|
||||
// Metadata rows
|
||||
if metas.is_empty() {
|
||||
html.push_str("<tr><th>Metadata</th><td>No metadata</td></tr>");
|
||||
} else {
|
||||
for meta in metas {
|
||||
html.push_str(&format!("<tr><th>{}</th><td>{}</td></tr>", meta.name, meta.value));
|
||||
html.push_str(&format!(
|
||||
"<tr><th>{}</th><td>{}</td></tr>",
|
||||
meta.name, meta.value
|
||||
));
|
||||
}
|
||||
}
|
||||
html.push_str("</table>");
|
||||
|
||||
|
||||
// Links
|
||||
html.push_str("<h2>Actions</h2>");
|
||||
html.push_str(&format!("<p><a href=\"/api/item/{}/content\">Download Content</a></p>", id));
|
||||
html.push_str(&format!(
|
||||
"<p><a href=\"/api/item/{}/content\">Download Content</a></p>",
|
||||
id
|
||||
));
|
||||
html.push_str("<p><a href=\"/\">Back to list</a></p>");
|
||||
|
||||
|
||||
html.push_str("</body></html>");
|
||||
|
||||
|
||||
Ok(html)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use clap::*;
|
||||
use log::debug;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use log::debug;
|
||||
|
||||
use crate::modes::common::OutputFormat;
|
||||
use crate::config;
|
||||
use crate::common::status::StatusInfo;
|
||||
use crate::config;
|
||||
use crate::modes::common::OutputFormat;
|
||||
use comfy_table::{Attribute, Cell, Table};
|
||||
use serde_json;
|
||||
use serde_yaml;
|
||||
use comfy_table::{Table, Cell, Attribute};
|
||||
|
||||
use crate::common::status::PathInfo;
|
||||
use crate::meta_plugin::MetaPluginType;
|
||||
@@ -28,7 +28,6 @@ fn build_path_table(path_info: &PathInfo) -> Table {
|
||||
path_table
|
||||
}
|
||||
|
||||
|
||||
fn build_config_table(settings: &config::Settings) -> Table {
|
||||
let mut config_table = crate::modes::common::create_table(true);
|
||||
|
||||
@@ -49,7 +48,7 @@ fn build_config_table(settings: &config::Settings) -> Table {
|
||||
if let Some(compression) = settings.compression() {
|
||||
config_table.add_row(vec!["Compression", &compression]);
|
||||
}
|
||||
|
||||
|
||||
config_table
|
||||
}
|
||||
|
||||
@@ -70,61 +69,55 @@ fn build_meta_plugins_configured_table(status_info: &StatusInfo) -> Option<Table
|
||||
Cell::new("Options").add_attribute(Attribute::Bold),
|
||||
Cell::new("Outputs").add_attribute(Attribute::Bold),
|
||||
]);
|
||||
|
||||
|
||||
for plugin_config in sorted_meta_plugins {
|
||||
// Create the plugin to get its default options
|
||||
let meta_plugin_type = match MetaPluginType::from_str(&plugin_config.name) {
|
||||
Ok(plugin_type) => plugin_type,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
|
||||
// First, create a default plugin to get its default options
|
||||
let default_plugin = get_meta_plugin(
|
||||
meta_plugin_type.clone(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let default_plugin = get_meta_plugin(meta_plugin_type.clone(), None, None);
|
||||
|
||||
// Start with the default options
|
||||
let mut effective_options = default_plugin.options().clone();
|
||||
|
||||
|
||||
// Merge with the configured options
|
||||
for (key, value) in &plugin_config.options {
|
||||
effective_options.insert(key.clone(), value.clone());
|
||||
}
|
||||
|
||||
|
||||
// Convert outputs from HashMap<String, String> to HashMap<String, serde_yaml::Value>
|
||||
let outputs_converted: std::collections::HashMap<String, serde_yaml::Value> = plugin_config.outputs
|
||||
let outputs_converted: std::collections::HashMap<String, serde_yaml::Value> = plugin_config
|
||||
.outputs
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), serde_yaml::Value::String(v.clone())))
|
||||
.collect();
|
||||
|
||||
|
||||
// Create the actual plugin with merged options - the constructor will handle setting up outputs
|
||||
let actual_plugin = get_meta_plugin(
|
||||
meta_plugin_type.clone(),
|
||||
Some(effective_options.clone()),
|
||||
Some(outputs_converted),
|
||||
);
|
||||
|
||||
|
||||
// Get the default plugin to see its default options
|
||||
let default_plugin = get_meta_plugin(
|
||||
meta_plugin_type.clone(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let default_plugin = get_meta_plugin(meta_plugin_type.clone(), None, None);
|
||||
|
||||
// Start with the default options
|
||||
let mut all_options = default_plugin.options().clone();
|
||||
// Merge with the configured options
|
||||
for (key, value) in &effective_options {
|
||||
all_options.insert(key.clone(), value.clone());
|
||||
}
|
||||
|
||||
|
||||
// Sort options by key and convert to a YAML string
|
||||
let mut sorted_options: Vec<_> = all_options.iter().collect();
|
||||
sorted_options.sort_by(|a, b| a.0.cmp(b.0));
|
||||
let sorted_options_map: std::collections::BTreeMap<_, _> = sorted_options.into_iter().collect();
|
||||
|
||||
let sorted_options_map: std::collections::BTreeMap<_, _> =
|
||||
sorted_options.into_iter().collect();
|
||||
|
||||
let options_str = if sorted_options_map.is_empty() {
|
||||
"{}".to_string()
|
||||
} else {
|
||||
@@ -133,7 +126,7 @@ fn build_meta_plugins_configured_table(status_info: &StatusInfo) -> Option<Table
|
||||
.trim()
|
||||
.to_string()
|
||||
};
|
||||
|
||||
|
||||
// Show only non-null outputs from the plugin
|
||||
// Collect and sort outputs by their string representation
|
||||
let mut enabled_output_pairs = Vec::new();
|
||||
@@ -142,7 +135,7 @@ fn build_meta_plugins_configured_table(status_info: &StatusInfo) -> Option<Table
|
||||
if value.is_null() {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Convert serde_yaml::Value to a string representation
|
||||
let value_str = match value {
|
||||
serde_yaml::Value::String(s) => s.clone(),
|
||||
@@ -167,31 +160,27 @@ fn build_meta_plugins_configured_table(status_info: &StatusInfo) -> Option<Table
|
||||
enabled_output_pairs.push((key.clone(), format!("{}->{}", key, value_str)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Sort outputs by their display value (second element of the tuple)
|
||||
enabled_output_pairs.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
|
||||
|
||||
// Join each output on a new line
|
||||
let outputs_str = if enabled_output_pairs.is_empty() {
|
||||
"{}".to_string()
|
||||
} else {
|
||||
enabled_output_pairs.into_iter()
|
||||
enabled_output_pairs
|
||||
.into_iter()
|
||||
.map(|(_, display)| display)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
};
|
||||
|
||||
table.add_row(vec![
|
||||
plugin_config.name.clone(),
|
||||
options_str,
|
||||
outputs_str,
|
||||
]);
|
||||
|
||||
table.add_row(vec![plugin_config.name.clone(), options_str, outputs_str]);
|
||||
}
|
||||
|
||||
|
||||
Some(table)
|
||||
}
|
||||
|
||||
|
||||
pub fn mode_status(
|
||||
cmd: &mut Command,
|
||||
settings: &config::Settings,
|
||||
@@ -199,7 +188,7 @@ pub fn mode_status(
|
||||
db_path: PathBuf,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
debug!("STATUS: Starting mode_status function");
|
||||
|
||||
|
||||
let status_service = crate::services::status_service::StatusService::new();
|
||||
let output_format = crate::modes::common::settings_output_format(settings);
|
||||
debug!("STATUS: About to generate status info");
|
||||
@@ -210,18 +199,27 @@ pub fn mode_status(
|
||||
OutputFormat::Table => {
|
||||
println!("CONFIG:");
|
||||
let config_table = build_config_table(settings);
|
||||
println!("{}", crate::modes::common::trim_lines_end(&config_table.trim_fmt()));
|
||||
println!(
|
||||
"{}",
|
||||
crate::modes::common::trim_lines_end(&config_table.trim_fmt())
|
||||
);
|
||||
println!();
|
||||
|
||||
|
||||
println!("PATHS:");
|
||||
let path_table = build_path_table(&status_info.paths);
|
||||
println!("{}", crate::modes::common::trim_lines_end(&path_table.trim_fmt()));
|
||||
println!(
|
||||
"{}",
|
||||
crate::modes::common::trim_lines_end(&path_table.trim_fmt())
|
||||
);
|
||||
println!();
|
||||
|
||||
// Always try to print META PLUGINS CONFIGURED section using status_info
|
||||
if let Some(meta_plugins_table) = build_meta_plugins_configured_table(&status_info) {
|
||||
println!("META PLUGINS CONFIGURED:");
|
||||
println!("{}", crate::modes::common::trim_lines_end(&meta_plugins_table.trim_fmt()));
|
||||
println!(
|
||||
"{}",
|
||||
crate::modes::common::trim_lines_end(&meta_plugins_table.trim_fmt())
|
||||
);
|
||||
println!();
|
||||
} else {
|
||||
println!("META PLUGINS CONFIGURED:");
|
||||
@@ -229,12 +227,12 @@ pub fn mode_status(
|
||||
println!();
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
OutputFormat::Json => {
|
||||
// Create a subset for status info that includes everything
|
||||
println!("{}", serde_json::to_string_pretty(&status_info)?);
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
OutputFormat::Yaml => {
|
||||
println!("{}", serde_yaml::to_string(&status_info)?);
|
||||
Ok(())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use clap::*;
|
||||
use log::debug;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use log::debug;
|
||||
|
||||
/// Helper function to convert serde_json::Value to serde_yaml::Value.
|
||||
///
|
||||
@@ -49,17 +49,18 @@ fn convert_json_to_yaml_value(value: &serde_json::Value) -> serde_yaml::Value {
|
||||
}
|
||||
}
|
||||
|
||||
use crate::modes::common::OutputFormat;
|
||||
use crate::config;
|
||||
use crate::modes::common::OutputFormat;
|
||||
use comfy_table::{Attribute, Cell, Table};
|
||||
use serde_json;
|
||||
use serde_yaml;
|
||||
use comfy_table::{Table, Cell, Attribute};
|
||||
|
||||
use crate::common::status::{CompressionInfo, MetaPluginInfo};
|
||||
use crate::meta_plugin::{MetaPluginType, get_meta_plugin};
|
||||
use crate::common::status::{MetaPluginInfo, CompressionInfo};
|
||||
|
||||
|
||||
fn build_meta_plugin_table(meta_plugin_info: &std::collections::HashMap<String, MetaPluginInfo>) -> Table {
|
||||
fn build_meta_plugin_table(
|
||||
meta_plugin_info: &std::collections::HashMap<String, MetaPluginInfo>,
|
||||
) -> Table {
|
||||
// Builds a formatted table displaying meta plugin information.
|
||||
//
|
||||
// Sorts plugins by name and displays options as YAML and outputs as a list.
|
||||
@@ -91,11 +92,7 @@ fn build_meta_plugin_table(meta_plugin_info: &std::collections::HashMap<String,
|
||||
};
|
||||
|
||||
// Create a default plugin to get its default options
|
||||
let default_plugin = get_meta_plugin(
|
||||
meta_plugin_type.clone(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let default_plugin = get_meta_plugin(meta_plugin_type.clone(), None, None);
|
||||
|
||||
// Get and sort options
|
||||
let mut options: Vec<_> = default_plugin.options().iter().collect();
|
||||
@@ -121,11 +118,7 @@ fn build_meta_plugin_table(meta_plugin_info: &std::collections::HashMap<String,
|
||||
output_keys.join("\n")
|
||||
};
|
||||
|
||||
meta_plugin_table.add_row(vec![
|
||||
info.meta_name.clone(),
|
||||
options_str,
|
||||
outputs_display,
|
||||
]);
|
||||
meta_plugin_table.add_row(vec![info.meta_name.clone(), options_str, outputs_display]);
|
||||
}
|
||||
|
||||
meta_plugin_table
|
||||
@@ -172,7 +165,7 @@ fn build_compression_table(compression_info: &Vec<CompressionInfo>) -> Table {
|
||||
compression_table
|
||||
}
|
||||
|
||||
fn build_filter_plugin_table(filter_plugins: &Vec<crate::common::status::FilterPluginInfo>) -> Table {
|
||||
fn build_filter_plugin_table(filter_plugins: &[crate::common::status::FilterPluginInfo]) -> Table {
|
||||
// Builds a formatted table displaying filter plugin information.
|
||||
//
|
||||
// Sorts plugins by name and formats options as YAML sequence.
|
||||
@@ -244,10 +237,7 @@ fn build_filter_plugin_table(filter_plugins: &Vec<crate::common::status::FilterP
|
||||
serde_yaml::Value::Mapping(yaml_mapping)
|
||||
}
|
||||
};
|
||||
opt_map.insert(
|
||||
serde_yaml::Value::String("default".to_string()),
|
||||
yaml_value,
|
||||
);
|
||||
opt_map.insert(serde_yaml::Value::String("default".to_string()), yaml_value);
|
||||
} else {
|
||||
opt_map.insert(
|
||||
serde_yaml::Value::String("default".to_string()),
|
||||
@@ -275,11 +265,7 @@ fn build_filter_plugin_table(filter_plugins: &Vec<crate::common::status::FilterP
|
||||
|
||||
// If no filter plugins are available, add a row indicating that
|
||||
if filter_plugins.is_empty() {
|
||||
filter_plugin_table.add_row(vec![
|
||||
"No filter plugins available",
|
||||
"{}",
|
||||
"",
|
||||
]);
|
||||
filter_plugin_table.add_row(vec!["No filter plugins available", "{}", ""]);
|
||||
}
|
||||
|
||||
filter_plugin_table
|
||||
@@ -306,7 +292,7 @@ pub fn mode_status_plugins(
|
||||
//
|
||||
// `Ok(())` on success, or anyhow::Error.
|
||||
debug!("STATUS_PLUGINS: Starting mode_status_plugins function");
|
||||
|
||||
|
||||
let status_service = crate::services::status_service::StatusService::new();
|
||||
let output_format = crate::modes::common::settings_output_format(settings);
|
||||
debug!("STATUS_PLUGINS: About to generate status info");
|
||||
@@ -317,20 +303,29 @@ pub fn mode_status_plugins(
|
||||
OutputFormat::Table => {
|
||||
println!("META PLUGINS:");
|
||||
let meta_table = build_meta_plugin_table(&status_info.meta_plugins);
|
||||
println!("{}", crate::modes::common::trim_lines_end(&meta_table.trim_fmt()));
|
||||
println!(
|
||||
"{}",
|
||||
crate::modes::common::trim_lines_end(&meta_table.trim_fmt())
|
||||
);
|
||||
println!();
|
||||
|
||||
|
||||
println!("COMPRESSION PLUGINS:");
|
||||
let compression_table = build_compression_table(&status_info.compression);
|
||||
println!("{}", crate::modes::common::trim_lines_end(&compression_table.trim_fmt()));
|
||||
println!(
|
||||
"{}",
|
||||
crate::modes::common::trim_lines_end(&compression_table.trim_fmt())
|
||||
);
|
||||
println!();
|
||||
|
||||
|
||||
println!("FILTER PLUGINS:");
|
||||
let filter_table = build_filter_plugin_table(&status_info.filter_plugins);
|
||||
println!("{}", crate::modes::common::trim_lines_end(&filter_table.trim_fmt()));
|
||||
println!(
|
||||
"{}",
|
||||
crate::modes::common::trim_lines_end(&filter_table.trim_fmt())
|
||||
);
|
||||
println!();
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
OutputFormat::Json => {
|
||||
// Create a subset for plugins only using status_info
|
||||
let plugins_info = serde_json::json!({
|
||||
@@ -340,18 +335,18 @@ pub fn mode_status_plugins(
|
||||
});
|
||||
println!("{}", serde_json::to_string_pretty(&plugins_info)?);
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
OutputFormat::Yaml => {
|
||||
// Create a proper structure for plugins info using status_info
|
||||
use serde_yaml::Mapping;
|
||||
let mut plugins_mapping = Mapping::new();
|
||||
|
||||
|
||||
// Add available plugins
|
||||
plugins_mapping.insert(
|
||||
serde_yaml::Value::String("meta_plugins_available".to_string()),
|
||||
serde_yaml::to_value(&status_info.meta_plugins)?,
|
||||
);
|
||||
|
||||
|
||||
// Add configured plugins if they exist
|
||||
if let Some(configured_plugins) = &status_info.configured_meta_plugins {
|
||||
plugins_mapping.insert(
|
||||
@@ -359,13 +354,13 @@ pub fn mode_status_plugins(
|
||||
serde_yaml::to_value(configured_plugins)?,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Add filter plugins
|
||||
plugins_mapping.insert(
|
||||
serde_yaml::Value::String("filter_plugins".to_string()),
|
||||
serde_yaml::to_value(&status_info.filter_plugins)?,
|
||||
);
|
||||
|
||||
|
||||
println!("{}", serde_yaml::to_string(&plugins_mapping)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user