This commit is contained in:
Andrew Phillips
2026-02-19 13:57:39 -04:00
parent a72395fe83
commit fdeb5f7951
82 changed files with 2756 additions and 2018 deletions

View File

@@ -2,25 +2,53 @@
**IMPORTANT:** Prefer to use the `write_file` tool if the edit is for the majority of a file, or if you are correcting previous problems made edits from other tools.
## Commands
## Tools
**IMPORTANT:** Always export `TERM=dumb`.
**IMPORTANT**: Be very careful when quoting text in tool calls to add the right amount of escaping.
### `write_file`
When editing files use the `write_file` tool to output the complete version of the corrected file.
**IMPORTANT**: You must provide the whole file to `write_file`, even the unchanged parts.
## Build/Test Commands
**IMPORTANT**: Do not run application, start the web server, or the trunk server.
**IMPORTANT:** The cargo command cannot be ran in parallel.
### Build
- Build: `TERM=dumb cargo build`
- Build release: `TERM=dumb cargo build --release`
```bash
# Check project
TERM=dumb cargo check
### Test
- Run all tests: `TERM=dumb cargo test`
- Run specific test: `TERM=dumb cargo test TEST_NAME`
- Run tests with output: `TERM=dumb cargo test -- --nocapture`
# Build project
TERM=dumb cargo build
### Lint/Format Commands
- Check formatting: `TERM=dumb cargo fmt --check`
- Format code: `TERM=dumb cargo fmt`
- Lint: `TERM=dumb cargo clippy`
- Lint with errors: `TERM=dumb cargo clippy -- -D warnings`
# DO NOT RUN RUN APPLICATION (native)
# TERM=dumb cargo run
# Run all tests
TERM=dumb cargo test
# Run specific test (by name substring)
TERM=dumb cargo test test_function_name
# Run specific test with verbose output
TERM=dumb cargo test test_function_name -- --nocapture
# Check formatting
TERM=dumb cargo fmt --check
# Apply formatting
TERM=dumb cargo fmt
# Lint with clippy
TERM=dumb cargo clippy -- -D warnings
# Build for release
TERM=dumb cargo build --release
```
Prefix commands with `TERM=dumb` for consistent output.
## Code Style Guidelines
@@ -28,29 +56,9 @@
- Group imports in order: standard library, external crates, local modules
- Use explicit imports over glob imports (`use std::fs::File;` not `use std::fs::*;`)
### Formatting
- Use rustfmt (configured via rustfmt.toml if exists)
- Max line length: 100 characters
- Indent with 4 spaces
### Types
- Prefer explicit types in public API
- Use `&str` for string literals, `String` for owned strings
- Use `Option<T>` for optional values, `Result<T, E>` for error handling
### Naming Conventions
- Use snake_case for variables and functions
- Use PascalCase for types and traits
- Use UPPER_SNAKE_CASE for constants and statics
### Error Handling
- Use `anyhow::Result` for most error handling
- Use `anyhow::Context` to add context to errors
- Avoid `unwrap()` in production code
### Documentation
- Document all public APIs with rustdoc
- Use examples in documentation when helpful
- Use examples in documentation only when helpful
## Procedures

View File

@@ -14,3 +14,4 @@ set mydir [ file normalize $mydir_base ]
module-whatis Keep
prepend-path PATH $mydir/bin
setenv KEEP_BASH_PROFILE ${mydir}/profile.bash

View File

@@ -41,7 +41,6 @@ pub struct ModeArgs {
#[arg(help("List items, filtering on tags or metadata if given"))]
pub list: bool,
#[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["save", "get", "diff", "list", "info", "status"]))]
#[arg(help("Delete items either by ID or by matching tags"))]
#[arg(requires = "ids_or_tags")]
@@ -85,7 +84,12 @@ pub struct ItemArgs {
#[arg(help("Compression algorithm to use when saving items"))]
pub compression: Option<String>,
#[arg(help_heading("Item Options"), short('M'), long, env("KEEP_META_PLUGINS"))]
#[arg(
help_heading("Item Options"),
short('M'),
long,
env("KEEP_META_PLUGINS")
)]
#[arg(help("Meta plugins to use when saving items"))]
pub meta_plugins: Vec<String>,
@@ -94,7 +98,6 @@ pub struct ItemArgs {
pub filters: Option<String>,
}
/// Struct for general options, including verbosity, paths, and output settings.
#[derive(Parser, Debug, Default, Clone)]
pub struct OptionsArgs {
@@ -138,7 +141,10 @@ pub struct OptionsArgs {
#[arg(help("Password hash for server authentication (requires --server)"))]
pub server_password_hash: Option<String>,
#[arg(long, help("Force output even when binary data would be sent to a TTY"))]
#[arg(
long,
help("Force output even when binary data would be sent to a TTY")
)]
pub force: bool,
}
@@ -183,4 +189,3 @@ impl Args {
Ok(())
}
}

View File

@@ -1,4 +1,3 @@
/// Detect if data is binary or text
/// Returns true if data is likely binary, false if likely text
pub fn is_binary(data: &[u8]) -> bool {
@@ -12,11 +11,11 @@ pub fn is_binary(data: &[u8]) -> bool {
}
// Check for UTF-16 BOM (text)
if data.len() >= 2 {
if (data[0] == 0xFF && data[1] == 0xFE) || (data[0] == 0xFE && data[1] == 0xFF) {
if data.len() >= 2
&& ((data[0] == 0xFF && data[1] == 0xFE) || (data[0] == 0xFE && data[1] == 0xFF))
{
return false; // UTF-16 with BOM is text
}
}
// Check for UTF-8 BOM (text)
if data.len() >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF {
@@ -59,7 +58,6 @@ fn has_binary_signature(data: &[u8]) -> bool {
(&[0x4D, 0x4D, 0x00, 0x2A], 4), // TIFF (big endian)
(&[0x52, 0x49, 0x46, 0x46], 4), // WebP (RIFF container)
(&[0x00, 0x00, 0x00, 0x0C, 0x6A, 0x50, 0x20, 0x20], 8), // JPEG 2000
// Audio/Video formats
(&[0x49, 0x44, 0x33], 3), // MP3 with ID3v2
(&[0xFF, 0xFB], 2), // MP3
@@ -70,7 +68,6 @@ fn has_binary_signature(data: &[u8]) -> bool {
(&[0x52, 0x49, 0x46, 0x46], 4), // WAV/AVI (RIFF)
(&[0x46, 0x4C, 0x56], 3), // FLV
(&[0x1A, 0x45, 0xDF, 0xA3], 4), // MKV/WebM
// Archive formats
(&[0x50, 0x4B, 0x03, 0x04], 4), // ZIP
(&[0x50, 0x4B, 0x05, 0x06], 4), // ZIP (empty)
@@ -85,13 +82,11 @@ fn has_binary_signature(data: &[u8]) -> bool {
(&[0x1F, 0x9D], 2), // LZW compressed
(&[0x1F, 0xA0], 2), // LZH compressed
(&[0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C], 6), // 7-Zip
// Document formats
(&[0x25, 0x50, 0x44, 0x46], 4), // PDF
(&[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1], 8), // MS Office (OLE)
(&[0x50, 0x4B, 0x03, 0x04], 4), // Office Open XML (also ZIP)
(&[0x7B, 0x5C, 0x72, 0x74, 0x66], 5), // RTF
// Executables and object files
(&[0x7F, 0x45, 0x4C, 0x46], 4), // ELF
(&[0x4D, 0x5A], 2), // Windows PE/DOS
@@ -102,26 +97,34 @@ fn has_binary_signature(data: &[u8]) -> bool {
(&[0xCF, 0xFA, 0xED, 0xFE], 4), // Mach-O 64-bit (big endian)
(&[0xCA, 0xFE, 0xBA, 0xBE], 4), // Java class file
(&[0xDE, 0xC0, 0x17, 0x0B], 4), // Dalvik executable
// Database formats
(&[0x53, 0x51, 0x4C, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x20, 0x33, 0x00], 16), // SQLite
(
&[
0x53, 0x51, 0x4C, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x20,
0x33, 0x00,
],
16,
), // SQLite
(&[0x00, 0x01, 0x00, 0x00], 4), // Palm Database
// Font formats
(&[0x00, 0x01, 0x00, 0x00, 0x00], 5), // TrueType
(&[0x4F, 0x54, 0x54, 0x4F], 4), // OpenType
(&[0x77, 0x4F, 0x46, 0x46], 4), // WOFF
(&[0x77, 0x4F, 0x46, 0x32], 4), // WOFF2
// Virtual machine formats
(&[0x76, 0x6D, 0x64, 0x6B], 4), // VMDK
(&[0x3C, 0x3C, 0x3C, 0x20, 0x4F, 0x72, 0x61, 0x63, 0x6C, 0x65, 0x20, 0x56, 0x4D, 0x20, 0x56, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6C, 0x42, 0x6F, 0x78, 0x20, 0x44, 0x69, 0x73, 0x6B, 0x20, 0x49, 0x6D, 0x61, 0x67, 0x65, 0x20, 0x3E, 0x3E, 0x3E], 39), // VirtualBox VDI
(
&[
0x3C, 0x3C, 0x3C, 0x20, 0x4F, 0x72, 0x61, 0x63, 0x6C, 0x65, 0x20, 0x56, 0x4D, 0x20,
0x56, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6C, 0x42, 0x6F, 0x78, 0x20, 0x44, 0x69, 0x73,
0x6B, 0x20, 0x49, 0x6D, 0x61, 0x67, 0x65, 0x20, 0x3E, 0x3E, 0x3E,
],
39,
), // VirtualBox VDI
// Disk image formats
(&[0xEB, 0x3C, 0x90], 3), // FAT12/16/32
(&[0xEB, 0x58, 0x90], 3), // FAT32
(&[0x55, 0xAA], 2), // Boot sector (at offset 510)
// Other binary formats
(&[0x21, 0x3C, 0x61, 0x72, 0x63, 0x68, 0x3E, 0x0A], 8), // AR archive
(&[0x78, 0x01], 2), // zlib (default compression)
@@ -196,8 +199,8 @@ fn looks_like_tar(data: &[u8]) -> bool {
}
// Check checksum field (should be octal digits or spaces)
for i in 148..156 {
if data[i] != 0 && (data[i] < b'0' || data[i] > b'7') && data[i] != b' ' {
for &b in &data[148..156] {
if b != 0 && (b < b'0' || b > b'7') && b != b' ' {
return false;
}
}
@@ -211,18 +214,18 @@ fn looks_like_tar(data: &[u8]) -> bool {
}
// Additional heuristic: check if the structure looks reasonable
let has_reasonable_structure =
data[0] != 0 && // Filename starts
data[100..108].iter().all(|&b| b == 0 || (b >= b'0' && b <= b'7') || b == b' '); // Mode field
// Mode field
has_reasonable_structure
data[0] != 0 && // Filename starts
data[100..108].iter().all(|&b| b == 0 || (b'0'..=b'7').contains(&b) || b == b' ')
}
/// Calculate the ratio of printable characters in the data
fn calculate_printable_ratio(data: &[u8]) -> f64 {
let printable_count = data.iter().filter(|&&b| {
b.is_ascii_graphic() || b.is_ascii_whitespace()
}).count();
let printable_count = data
.iter()
.filter(|&&b| b.is_ascii_graphic() || b.is_ascii_whitespace())
.count();
printable_count as f64 / data.len() as f64
}

View File

@@ -3,7 +3,7 @@ use strum::IntoEnumIterator;
#[cfg(feature = "server")]
use utoipa::ToSchema;
use crate::compression_engine::{get_compression_engine, CompressionType};
use crate::compression_engine::{CompressionType, get_compression_engine};
use crate::meta_plugin::MetaPluginType;
use crate::filter_plugin::FilterOption;
@@ -56,13 +56,19 @@ pub struct MetaPluginInfo {
pub fn generate_status_info(
data_path: PathBuf,
db_path: PathBuf,
enabled_meta_plugins: &Vec<MetaPluginType>,
enabled_meta_plugins: &[MetaPluginType],
enabled_compression_type: Option<CompressionType>,
) -> StatusInfo {
log::debug!("STATUS: Starting status info generation");
let path_info = PathInfo {
data: data_path.into_os_string().into_string().expect("Unable to convert data path to string"),
database: db_path.into_os_string().into_string().expect("Unable to convert DB path to string"),
data: data_path
.into_os_string()
.into_string()
.expect("Unable to convert data path to string"),
database: db_path
.into_os_string()
.into_string()
.expect("Unable to convert DB path to string"),
};
let _default_type = crate::compression_engine::default_compression_type();
@@ -73,22 +79,40 @@ pub fn generate_status_info(
sorted_compression_types.sort_by_key(|ct| ct.to_string());
for compression_type in sorted_compression_types {
let (binary, compress, decompress, supported) = match get_compression_engine(compression_type.clone()) {
let (binary, compress, decompress, supported) =
match get_compression_engine(compression_type.clone()) {
Ok(engine) => {
let supp = engine.is_supported();
if supp && engine.is_internal() {
("<INTERNAL>".to_string(), "".to_string(), "".to_string(), supp)
(
"<INTERNAL>".to_string(),
"".to_string(),
"".to_string(),
supp,
)
} else if supp {
let (b, c, d) = engine.get_status_info();
(b, c, d, supp)
} else {
("<UNSUPPORTED>".to_string(), "".to_string(), "".to_string(), supp)
(
"<UNSUPPORTED>".to_string(),
"".to_string(),
"".to_string(),
supp,
)
}
}
Err(_) => ("<UNSUPPORTED>".to_string(), "".to_string(), "".to_string(), false),
Err(_) => (
"<UNSUPPORTED>".to_string(),
"".to_string(),
"".to_string(),
false,
),
};
let is_enabled = enabled_compression_type.as_ref().map_or(false, |ct| *ct == compression_type);
let is_enabled = enabled_compression_type
.as_ref()
.is_some_and(|ct| *ct == compression_type);
compression_info.push(CompressionInfo {
compression_type: compression_type.to_string(),
@@ -108,7 +132,10 @@ pub fn generate_status_info(
sorted_meta_plugins.sort_by_key(|meta_plugin_type| meta_plugin_type.to_string());
for meta_plugin_type in sorted_meta_plugins {
log::debug!("STATUS: Processing meta plugin type: {:?}", meta_plugin_type);
log::debug!(
"STATUS: Processing meta plugin type: {:?}",
meta_plugin_type
);
log::debug!("STATUS: About to call get_meta_plugin");
let meta_plugin = crate::meta_plugin::get_meta_plugin(meta_plugin_type.clone(), None, None);
log::debug!("STATUS: Created meta plugin instance");
@@ -140,11 +167,14 @@ pub fn generate_status_info(
// Get options
let options = meta_plugin.options().clone();
meta_plugins_map.insert(meta_name.clone(), MetaPluginInfo {
meta_plugins_map.insert(
meta_name.clone(),
MetaPluginInfo {
meta_name,
outputs: outputs_display,
options,
});
},
);
}
StatusInfo {

View File

@@ -3,7 +3,7 @@ use std::io;
use std::io::{Read, Write};
use std::path::PathBuf;
use strum::IntoEnumIterator;
use strum::{Display, EnumString, EnumIter};
use strum::{Display, EnumIter, EnumString};
use log::*;
@@ -203,7 +203,9 @@ lazy_static! {
#[cfg(feature = "gzip")]
{
em[CompressionType::GZip] = Box::new(crate::compression_engine::gzip::CompressionEngineGZip::new()) as Box<dyn CompressionEngine>;
em[CompressionType::GZip] =
Box::new(crate::compression_engine::gzip::CompressionEngineGZip::new())
as Box<dyn CompressionEngine>;
}
em
@@ -219,6 +221,9 @@ pub fn get_compression_engine(ct: CompressionType) -> Result<Box<dyn Compression
if engine.is_supported() {
Ok(engine.clone())
} else {
Err(anyhow!("Compression engine for {} is not supported", ct.to_string()))
Err(anyhow!(
"Compression engine for {} is not supported",
ct.to_string()
))
}
}

View File

@@ -68,7 +68,8 @@ impl CompressionEngineProgram {
let supported = program_path.is_ok();
CompressionEngineProgram {
program: program_path.map_or_else(|_| program.to_string(), |p| p.to_string_lossy().to_string()),
program: program_path
.map_or_else(|_| program.to_string(), |p| p.to_string_lossy().to_string()),
compress: compress.iter().map(|s| s.to_string()).collect(),
decompress: decompress.iter().map(|s| s.to_string()).collect(),
supported,
@@ -117,9 +118,10 @@ impl CompressionEngine for CompressionEngineProgram {
args
))?;
let stdout = process.stdout.take().ok_or_else(|| {
anyhow!("Failed to capture stdout from child process")
})?;
let stdout = process
.stdout
.take()
.ok_or_else(|| anyhow!("Failed to capture stdout from child process"))?;
Ok(Box::new(ProgramReader {
process,
@@ -151,9 +153,10 @@ impl CompressionEngine for CompressionEngineProgram {
args
))?;
let stdin = process.stdin.take().ok_or_else(|| {
anyhow!("Failed to capture stdin from child process")
})?;
let stdin = process
.stdin
.take()
.ok_or_else(|| anyhow!("Failed to capture stdin from child process"))?;
Ok(Box::new(ProgramWriter {
process,

View File

@@ -1,10 +1,10 @@
use std::path::PathBuf;
use std::fs;
use anyhow::{Result, Context};
use serde::{Deserialize, Serialize};
use log::{debug, error};
use crate::args::{Args};
use crate::args::Args;
use anyhow::{Context, Result};
use dirs;
use log::{debug, error};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
@@ -188,14 +188,17 @@ pub struct Settings {
impl Settings {
/// Create unified settings from config and args with proper priority
pub fn new(args: &Args, default_dir: PathBuf) -> Result<Self> {
debug!("CONFIG: Creating settings with default dir: {:?}", default_dir);
debug!(
"CONFIG: Creating settings with default dir: {:?}",
default_dir
);
let config_path = if let Some(config_path) = &args.options.config {
config_path.clone()
} else if let Ok(env_config) = std::env::var("KEEP_CONFIG") {
PathBuf::from(env_config)
} else {
let default_path = if let Some(home_dir) = std::env::var("HOME").ok() {
let default_path = if let Ok(home_dir) = std::env::var("HOME") {
let mut path = PathBuf::from(home_dir);
path.push(".config");
path.push("keep");
@@ -215,14 +218,17 @@ impl Settings {
// Load config file if it exists
if config_path.exists() {
debug!("CONFIG: Loading config file: {:?}", config_path);
config_builder = config_builder.add_source(config::File::from(config_path.clone()).required(false));
config_builder =
config_builder.add_source(config::File::from(config_path.clone()).required(false));
} else {
debug!("CONFIG: Config file does not exist: {:?}", config_path);
}
// Add environment variables
debug!("CONFIG: Adding environment variables");
let env_source = config::Environment::with_prefix("KEEP").separator("__").ignore_empty(true);
let env_source = config::Environment::with_prefix("KEEP")
.separator("__")
.ignore_empty(true);
config_builder = config_builder.add_source(env_source);
// Override with CLI args
@@ -231,13 +237,13 @@ impl Settings {
config_builder = config_builder.set_override("dir", dir.to_str().unwrap())?;
}
if args.options.human_readable {
config_builder = config_builder.set_override("human_readable", true)?;
}
if let Some(output_format) = &args.options.output_format {
config_builder = config_builder.set_override("output_format", output_format.as_str())?;
config_builder =
config_builder.set_override("output_format", output_format.as_str())?;
}
if args.options.verbose > 0 {
@@ -253,15 +259,18 @@ impl Settings {
}
if let Some(server_password) = &args.options.server_password {
config_builder = config_builder.set_override("server.password", server_password.as_str())?;
config_builder =
config_builder.set_override("server.password", server_password.as_str())?;
}
if let Some(server_password_hash) = &args.options.server_password_hash {
config_builder = config_builder.set_override("server.password_hash", server_password_hash.as_str())?;
config_builder = config_builder
.set_override("server.password_hash", server_password_hash.as_str())?;
}
if let Some(server_address) = &args.mode.server_address {
config_builder = config_builder.set_override("server.address", server_address.as_str())?;
config_builder =
config_builder.set_override("server.address", server_address.as_str())?;
}
if let Some(server_port) = args.mode.server_port {
@@ -269,12 +278,14 @@ impl Settings {
}
if let Some(compression) = &args.item.compression {
config_builder = config_builder.set_override("compression_plugin.name", compression.as_str())?;
config_builder =
config_builder.set_override("compression_plugin.name", compression.as_str())?;
}
if !args.item.meta_plugins.is_empty() {
let meta_plugins: Vec<std::collections::HashMap<String, String>> = args.item.meta_plugins
let meta_plugins: Vec<std::collections::HashMap<String, String>> = args
.item
.meta_plugins
.iter()
.map(|name| {
let mut map = std::collections::HashMap::new();
@@ -372,13 +383,11 @@ impl Settings {
// Set default meta_plugins to include 'env' if not provided
if settings.meta_plugins.is_none() {
debug!("CONFIG: Setting default meta_plugins to include 'env'");
settings.meta_plugins = Some(vec![
MetaPluginConfig {
settings.meta_plugins = Some(vec![MetaPluginConfig {
name: "env".to_string(),
options: std::collections::HashMap::new(),
outputs: std::collections::HashMap::new(),
}
]);
}]);
}
// Set dir to default if not provided or is empty
@@ -398,8 +407,8 @@ impl Settings {
}
pub fn default_dir() -> anyhow::Result<PathBuf> {
let mut path = dirs::home_dir()
.ok_or_else(|| anyhow::anyhow!("No home directory found"))?;
let mut path =
dirs::home_dir().ok_or_else(|| anyhow::anyhow!("No home directory found"))?;
path.push(".keep");
if !path.exists() {
std::fs::create_dir_all(&path)?;
@@ -451,7 +460,8 @@ impl Settings {
}
pub fn meta_plugins_names(&self) -> Vec<String> {
self.meta_plugins.as_ref()
self.meta_plugins
.as_ref()
.map(|plugins| plugins.iter().map(|p| p.name.clone()).collect())
.unwrap_or_default()
}

View File

@@ -1,10 +1,10 @@
use anyhow::{Context, Error, Result};
use chrono::prelude::*;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use log::*;
use rusqlite::{Connection, OpenFlags, params};
use rusqlite_migration::{M, Migrations};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::rc::Rc;
@@ -245,7 +245,10 @@ pub fn insert_item(conn: &Connection, item: Item) -> Result<i64> {
/// let item = db::create_item(&conn, compression)?;
/// assert!(item.id.is_some());
/// ```
pub fn create_item(conn: &Connection, compression_type: crate::compression_engine::CompressionType) -> Result<Item> {
pub fn create_item(
conn: &Connection,
compression_type: crate::compression_engine::CompressionType,
) -> Result<Item> {
let item = Item {
id: None,
ts: chrono::Utc::now(),
@@ -353,11 +356,7 @@ pub fn update_item(conn: &Connection, item: Item) -> Result<()> {
debug!("DB: Updating item: {:?}", item);
conn.execute(
"UPDATE items SET size=?2, compression=?3 WHERE id=?1",
params![
item.id,
item.size,
item.compression,
],
params![item.id, item.size, item.compression,],
)?;
Ok(())
}
@@ -1181,7 +1180,10 @@ pub fn get_item_meta_value(conn: &Connection, item: &Item, name: String) -> Resu
/// let ids = vec![1, 2, 3];
/// let tags_map = db::get_tags_for_items(&conn, &ids)?;
/// ```
pub fn get_tags_for_items(conn: &Connection, item_ids: &[i64]) -> Result<std::collections::HashMap<i64, Vec<Tag>>> {
pub fn get_tags_for_items(
conn: &Connection,
item_ids: &[i64],
) -> Result<std::collections::HashMap<i64, Vec<Tag>>> {
debug!("DB: Getting tags for items: {:?}", item_ids);
if item_ids.is_empty() {
@@ -1192,7 +1194,10 @@ pub fn get_tags_for_items(conn: &Connection, item_ids: &[i64]) -> Result<std::co
let placeholders: Vec<String> = item_ids.iter().map(|_| "?".to_string()).collect();
let placeholders_str = placeholders.join(",");
let sql = format!("SELECT id, name FROM tags WHERE id IN ({}) ORDER BY id ASC, name ASC", placeholders_str);
let sql = format!(
"SELECT id, name FROM tags WHERE id IN ({}) ORDER BY id ASC, name ASC",
placeholders_str
);
let mut statement = conn
.prepare_cached(&sql)
@@ -1206,7 +1211,7 @@ pub fn get_tags_for_items(conn: &Connection, item_ids: &[i64]) -> Result<std::co
let id: i64 = row.get(0)?;
let name: String = row.get(1)?;
tags_map.entry(id).or_insert_with(Vec::new).push(Tag { id, name });
tags_map.entry(id).or_default().push(Tag { id, name });
}
Ok(tags_map)
@@ -1235,7 +1240,10 @@ pub fn get_tags_for_items(conn: &Connection, item_ids: &[i64]) -> Result<std::co
/// let ids = vec![1, 2, 3];
/// let meta_map = db::get_meta_for_items(&conn, &ids)?;
/// ```
pub fn get_meta_for_items(conn: &Connection, item_ids: &[i64]) -> Result<std::collections::HashMap<i64, std::collections::HashMap<String, String>>> {
pub fn get_meta_for_items(
conn: &Connection,
item_ids: &[i64],
) -> Result<std::collections::HashMap<i64, std::collections::HashMap<String, String>>> {
debug!("DB: Getting meta for items: {:?}", item_ids);
if item_ids.is_empty() {
@@ -1246,7 +1254,10 @@ pub fn get_meta_for_items(conn: &Connection, item_ids: &[i64]) -> Result<std::co
let placeholders: Vec<String> = item_ids.iter().map(|_| "?".to_string()).collect();
let placeholders_str = placeholders.join(",");
let sql = format!("SELECT id, name, value FROM metas WHERE id IN ({}) ORDER BY id ASC, name ASC", placeholders_str);
let sql = format!(
"SELECT id, name, value FROM metas WHERE id IN ({}) ORDER BY id ASC, name ASC",
placeholders_str
);
let mut statement = conn
.prepare_cached(&sql)
@@ -1254,14 +1265,15 @@ pub fn get_meta_for_items(conn: &Connection, item_ids: &[i64]) -> Result<std::co
let mut rows = statement.query(rusqlite::params_from_iter(item_ids))?;
let mut meta_map: std::collections::HashMap<i64, std::collections::HashMap<String, String>> = std::collections::HashMap::new();
let mut meta_map: std::collections::HashMap<i64, std::collections::HashMap<String, String>> =
std::collections::HashMap::new();
while let Some(row) = rows.next()? {
let id: i64 = row.get(0)?;
let name: String = row.get(1)?;
let value: String = row.get(2)?;
meta_map.entry(id).or_insert_with(std::collections::HashMap::new).insert(name, value);
meta_map.entry(id).or_default().insert(name, value);
}
Ok(meta_map)

View File

@@ -1,6 +1,6 @@
use super::{FilterPlugin, FilterOption};
use std::io::{Result, Read, Write, BufRead};
use super::{FilterOption, FilterPlugin};
use regex::Regex;
use std::io::{BufRead, Read, Result, Write};
/// A filter that matches lines against a regular expression pattern.
///
@@ -40,9 +40,7 @@ impl GrepFilter {
pub fn new(pattern: String) -> Result<Self> {
let regex = Regex::new(&pattern)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
Ok(Self {
regex,
})
Ok(Self { regex })
}
}
@@ -116,12 +114,10 @@ impl FilterPlugin for GrepFilter {
/// assert!(opts[0].required);
/// ```
fn options(&self) -> Vec<FilterOption> {
vec![
FilterOption {
vec![FilterOption {
name: "pattern".to_string(),
default: None,
required: true,
}
]
}]
}
}

View File

@@ -1,7 +1,7 @@
use super::{FilterPlugin, FilterOption};
use std::io::{Result, Read, Write, BufRead};
use super::{FilterOption, FilterPlugin};
use crate::common::PIPESIZE;
use crate::services::filter_service::register_filter_plugin;
use std::io::{BufRead, Read, Result, Write};
/// A filter that reads the first N bytes from the input stream.
///
@@ -41,9 +41,7 @@ impl HeadBytesFilter {
/// assert_eq!(filter.remaining, 1024);
/// ```
pub fn new(count: usize) -> Self {
Self {
remaining: count,
}
Self { remaining: count }
}
}
@@ -111,13 +109,11 @@ impl FilterPlugin for HeadBytesFilter {
///
/// Vector of `FilterOption` describing parameters.
fn options(&self) -> Vec<FilterOption> {
vec![
FilterOption {
vec![FilterOption {
name: "count".to_string(),
default: None,
required: true,
}
]
}]
}
}
@@ -152,9 +148,7 @@ impl HeadLinesFilter {
/// assert_eq!(filter.remaining, 3);
/// ```
pub fn new(count: usize) -> Self {
Self {
remaining: count,
}
Self { remaining: count }
}
}
@@ -181,7 +175,6 @@ impl HeadLinesFilter {
/// // Assuming a filter chain with head_lines(2)
/// // Input: "Line1\nLine2\nLine3" becomes "Line1\nLine2\n"
/// ```
impl FilterPlugin for HeadLinesFilter {
fn filter(&mut self, reader: &mut dyn Read, writer: &mut dyn Write) -> Result<()> {
if self.remaining == 0 {
@@ -221,13 +214,11 @@ impl FilterPlugin for HeadLinesFilter {
///
/// Vector of `FilterOption` describing parameters.
fn options(&self) -> Vec<FilterOption> {
vec![
FilterOption {
vec![FilterOption {
name: "count".to_string(),
default: None,
required: true,
}
]
}]
}
}

View File

@@ -1,7 +1,8 @@
use std::io::{Result, Read, Write};
use std::io::{Read, Result, Write};
use std::str::FromStr;
use strum::EnumString;
pub mod grep;
/// Filter plugin module for processing input streams.
///
/// This module defines the `FilterPlugin` trait and `FilterChain` for chaining filters,
@@ -17,19 +18,18 @@ use strum::EnumString;
/// chain.filter(&mut reader, &mut writer)?;
/// ```
pub mod head;
pub mod tail;
pub mod skip;
pub mod grep;
pub mod strip_ansi;
pub mod tail;
pub mod utils;
use std::collections::HashMap;
pub use head::{HeadBytesFilter, HeadLinesFilter};
pub use tail::{TailBytesFilter, TailLinesFilter};
pub use skip::{SkipBytesFilter, SkipLinesFilter};
pub use grep::GrepFilter;
pub use head::{HeadBytesFilter, HeadLinesFilter};
pub use skip::{SkipBytesFilter, SkipLinesFilter};
pub use strip_ansi::StripAnsiFilter;
pub use tail::{TailBytesFilter, TailLinesFilter};
/// Represents an option for a filter plugin.
///
@@ -195,7 +195,6 @@ pub struct FilterChain {
/// chain.add_plugin(Box::new(HeadLinesFilter::new(10)));
/// chain.filter(&mut reader, &mut writer)?;
/// ```
impl Clone for FilterChain {
/// Clones this filter chain.
///
@@ -222,6 +221,12 @@ impl Clone for Box<dyn FilterPlugin> {
}
}
impl Default for FilterChain {
fn default() -> Self {
Self::new()
}
}
impl FilterChain {
/// Creates a new empty filter chain.
///
@@ -359,7 +364,8 @@ pub fn parse_filter_string(filter_str: &str) -> Result<FilterChain> {
// Create the appropriate filter plugin
if let Ok(filter_type) = FilterType::from_str(filter_name) {
let plugin = create_filter_with_options(filter_type, &unnamed_params, &options)?;
let plugin =
create_filter_with_options(filter_type, &unnamed_params, &options)?;
chain.add_plugin(plugin);
continue;
}
@@ -375,7 +381,7 @@ pub fn parse_filter_string(filter_str: &str) -> Result<FilterChain> {
_ => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Filter '{}' requires parameters", part)
format!("Filter '{}' requires parameters", part),
));
}
}
@@ -385,7 +391,7 @@ pub fn parse_filter_string(filter_str: &str) -> Result<FilterChain> {
// If we get here, the filter wasn't recognized
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Unknown filter: {}", part)
format!("Unknown filter: {}", part),
));
}
@@ -427,7 +433,10 @@ fn create_filter_with_options(
if unnamed_params.len() > option_defs.len() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Too many unnamed parameters (expected at most {})", option_defs.len())
format!(
"Too many unnamed parameters (expected at most {})",
option_defs.len()
),
));
}
@@ -445,7 +454,7 @@ fn create_filter_with_options(
if !option_defs.iter().any(|opt| &opt.name == key) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Unknown option '{}'", key)
format!("Unknown option '{}'", key),
));
}
options.insert(key.clone(), value.clone());
@@ -459,7 +468,7 @@ fn create_filter_with_options(
} else if opt_def.required {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Missing required option '{}'", opt_def.name)
format!("Missing required option '{}'", opt_def.name),
));
}
}
@@ -485,72 +494,93 @@ fn create_specific_filter(
) -> Result<Box<dyn FilterPlugin>> {
match filter_type {
FilterType::Grep => {
let pattern = options.get("pattern")
let pattern = options
.get("pattern")
.and_then(|v| v.as_str())
.ok_or_else(|| std::io::Error::new(
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"grep filter requires 'pattern' parameter"
))?;
"grep filter requires 'pattern' parameter",
)
})?;
grep::GrepFilter::new(pattern.to_string()).map(|f| Box::new(f) as Box<dyn FilterPlugin>)
}
FilterType::HeadBytes => {
let count = options.get("count")
let count = options
.get("count")
.and_then(|v| v.as_u64())
.map(|n| n as usize)
.ok_or_else(|| std::io::Error::new(
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"head_bytes filter requires 'count' parameter"
))?;
"head_bytes filter requires 'count' parameter",
)
})?;
Ok(Box::new(head::HeadBytesFilter::new(count)))
}
FilterType::HeadLines => {
let count = options.get("count")
let count = options
.get("count")
.and_then(|v| v.as_u64())
.map(|n| n as usize)
.ok_or_else(|| std::io::Error::new(
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"head_lines filter requires 'count' parameter"
))?;
"head_lines filter requires 'count' parameter",
)
})?;
Ok(Box::new(head::HeadLinesFilter::new(count)))
}
FilterType::TailBytes => {
let count = options.get("count")
let count = options
.get("count")
.and_then(|v| v.as_u64())
.map(|n| n as usize)
.ok_or_else(|| std::io::Error::new(
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"tail_bytes filter requires 'count' parameter"
))?;
"tail_bytes filter requires 'count' parameter",
)
})?;
Ok(Box::new(tail::TailBytesFilter::new(count)))
}
FilterType::TailLines => {
let count = options.get("count")
let count = options
.get("count")
.and_then(|v| v.as_u64())
.map(|n| n as usize)
.ok_or_else(|| std::io::Error::new(
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"tail_lines filter requires 'count' parameter"
))?;
"tail_lines filter requires 'count' parameter",
)
})?;
Ok(Box::new(tail::TailLinesFilter::new(count)))
}
FilterType::SkipBytes => {
let count = options.get("count")
let count = options
.get("count")
.and_then(|v| v.as_u64())
.map(|n| n as usize)
.ok_or_else(|| std::io::Error::new(
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"skip_bytes filter requires 'count' parameter"
))?;
"skip_bytes filter requires 'count' parameter",
)
})?;
Ok(Box::new(skip::SkipBytesFilter::new(count)))
}
FilterType::SkipLines => {
let count = options.get("count")
let count = options
.get("count")
.and_then(|v| v.as_u64())
.map(|n| n as usize)
.ok_or_else(|| std::io::Error::new(
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"skip_lines filter requires 'count' parameter"
))?;
"skip_lines filter requires 'count' parameter",
)
})?;
Ok(Box::new(skip::SkipLinesFilter::new(count)))
}
FilterType::StripAnsi => {
@@ -558,7 +588,7 @@ fn create_specific_filter(
if !options.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"strip_ansi filter doesn't take parameters"
"strip_ansi filter doesn't take parameters",
));
}
Ok(Box::new(strip_ansi::StripAnsiFilter::new()))
@@ -583,11 +613,11 @@ fn parse_option_value(input: &str) -> Result<serde_json::Value> {
if let Ok(num) = input.parse::<i64>() {
return Ok(serde_json::Value::Number(num.into()));
}
if let Ok(num) = input.parse::<f64>() {
if let Some(number) = serde_json::Number::from_f64(num) {
if let Ok(num) = input.parse::<f64>()
&& let Some(number) = serde_json::Number::from_f64(num)
{
return Ok(serde_json::Value::Number(number));
}
}
// Try to parse as boolean
if input.eq_ignore_ascii_case("true") {

View File

@@ -1,7 +1,7 @@
use super::{FilterPlugin, FilterOption};
use std::io::{Result, Read, Write, BufRead};
use super::{FilterOption, FilterPlugin};
use crate::common::PIPESIZE;
use crate::services::filter_service::register_filter_plugin;
use std::io::{BufRead, Read, Result, Write};
/// A filter that skips the first N bytes from the input stream.
pub struct SkipBytesFilter {
@@ -15,9 +15,7 @@ impl SkipBytesFilter {
///
/// * `count` - The number of bytes to skip from the beginning of the input.
pub fn new(count: usize) -> Self {
Self {
remaining: count,
}
Self { remaining: count }
}
}
@@ -68,13 +66,11 @@ impl FilterPlugin for SkipBytesFilter {
///
/// A vector of `FilterOption` describing the filter's configurable parameters.
fn options(&self) -> Vec<FilterOption> {
vec![
FilterOption {
vec![FilterOption {
name: "count".to_string(),
default: None,
required: true,
}
]
}]
}
}
@@ -90,9 +86,7 @@ impl SkipLinesFilter {
///
/// * `count` - The number of lines to skip from the beginning of the input.
pub fn new(count: usize) -> Self {
Self {
remaining: count,
}
Self { remaining: count }
}
}
@@ -137,13 +131,11 @@ impl FilterPlugin for SkipLinesFilter {
///
/// A vector of `FilterOption` describing the filter's configurable parameters.
fn options(&self) -> Vec<FilterOption> {
vec![
FilterOption {
vec![FilterOption {
name: "count".to_string(),
default: None,
required: true,
}
]
}]
}
}

View File

@@ -1,6 +1,6 @@
use std::io::{Result, Read, Write};
use super::{FilterOption, FilterPlugin};
use std::io::{Read, Result, Write};
use strip_ansi_escapes::Writer;
use super::{FilterPlugin, FilterOption};
/// A filter that removes ANSI escape sequences from the input.
///

View File

@@ -1,8 +1,8 @@
use super::{FilterPlugin, FilterOption};
use std::io::{Result, Read, Write, BufRead};
use std::collections::VecDeque;
use super::{FilterOption, FilterPlugin};
use crate::common::PIPESIZE;
use crate::services::filter_service::register_filter_plugin;
use std::collections::VecDeque;
use std::io::{BufRead, Read, Result, Write};
/// A filter that reads the last N bytes from the input stream.
pub struct TailBytesFilter {
@@ -76,13 +76,11 @@ impl FilterPlugin for TailBytesFilter {
///
/// A vector of `FilterOption` describing the filter's configurable parameters.
fn options(&self) -> Vec<FilterOption> {
vec![
FilterOption {
vec![FilterOption {
name: "count".to_string(),
default: None,
required: true,
}
]
}]
}
}
@@ -152,13 +150,11 @@ impl FilterPlugin for TailLinesFilter {
///
/// A vector of `FilterOption` describing the filter's configurable parameters.
fn options(&self) -> Vec<FilterOption> {
vec![
FilterOption {
vec![FilterOption {
name: "count".to_string(),
default: None,
required: true,
}
]
}]
}
}

View File

@@ -29,15 +29,15 @@
//! - `magic`: File type detection via libmagic.
// Re-export modules for testing
pub mod args;
pub mod common;
pub mod compression_engine;
pub mod config;
pub mod services;
pub mod db;
pub mod filter_plugin;
pub mod meta_plugin;
pub mod modes;
pub mod filter_plugin;
pub mod args;
pub mod services;
// Re-export Args struct for library usage
pub use args::Args;
@@ -46,13 +46,10 @@ pub use common::PIPESIZE;
// Import all filter plugins to ensure they register themselves
#[allow(unused_imports)]
use filter_plugin::{
head, tail, skip, grep, strip_ansi
};
use filter_plugin::{grep, head, skip, strip_ansi, tail};
use crate::meta_plugin::{
cwd, user, shell, shell_pid, keep_pid, digest,
read_time, read_rate, hostname, exec, env
cwd, digest, env, exec, hostname, keep_pid, read_rate, read_time, shell, shell_pid, user,
};
#[cfg(feature = "magic")]

View File

@@ -1,6 +1,6 @@
use anyhow::{Context, Error, Result, anyhow};
use clap::*;
use clap::error::ErrorKind;
use clap::*;
use log::*;
use directories::ProjectDirs;
@@ -56,7 +56,7 @@ fn main() -> Result<(), Error> {
NumberOrString::Number(num) => {
debug!("MAIN: Adding to ids: {}", num);
ids.push(num)
},
}
NumberOrString::Str(str) => {
// For --info and --get, try to parse strings as numbers to treat them as IDs
if args.mode.info || args.mode.get {
@@ -68,14 +68,15 @@ fn main() -> Result<(), Error> {
// --info only accepts numeric IDs
cmd.error(
ErrorKind::InvalidValue,
format!("--info requires numeric IDs, found: '{}'", str)
).exit();
format!("--info requires numeric IDs, found: '{}'", str),
)
.exit();
}
}
// If not a number, or not using --info/--get, treat as tag
debug!("MAIN: Adding to tags: {}", str);
tags.push(str)
},
}
}
}
tags.sort();
@@ -130,29 +131,35 @@ fn main() -> Result<(), Error> {
}
// Validate output format usage
if let Some(output_format_str) = &settings.output_format {
if output_format_str != "table" && mode != KeepModes::Info && mode != KeepModes::Status && mode != KeepModes::StatusPlugins && mode != KeepModes::List {
if let Some(output_format_str) = &settings.output_format
&& output_format_str != "table"
&& mode != KeepModes::Info
&& mode != KeepModes::Status
&& mode != KeepModes::StatusPlugins
&& mode != KeepModes::List
{
cmd.error(
ErrorKind::InvalidValue,
"--output-format can only be used with --info, --status, --status-plugins, or --list modes"
).exit();
}
}
// Validate human-readable usage
if settings.human_readable && mode != KeepModes::List && mode != KeepModes::Info {
cmd.error(
ErrorKind::InvalidValue,
"--human-readable can only be used with --list and --info modes"
).exit();
"--human-readable can only be used with --list and --info modes",
)
.exit();
}
// Validate server password usage
if settings.server_password().is_some() && mode != KeepModes::Server {
cmd.error(
ErrorKind::InvalidValue,
"--server-password can only be used with --server mode"
).exit();
"--server-password can only be used with --server mode",
)
.exit();
}
debug!("MAIN: args: {:?}", args);
@@ -186,8 +193,9 @@ fn main() -> Result<(), Error> {
Err(e) => {
cmd.error(
ErrorKind::InvalidValue,
format!("Invalid filter string: {}", e)
).exit();
format!("Invalid filter string: {}", e),
)
.exit();
}
}
} else {
@@ -195,14 +203,32 @@ fn main() -> Result<(), Error> {
};
match mode {
KeepModes::Save => modes::save::mode_save(&mut cmd, &settings, ids, tags, &mut conn, data_path),
KeepModes::Get => modes::get::mode_get(&mut cmd, &settings, ids, tags, &mut conn, data_path, filter_chain),
KeepModes::Save => {
modes::save::mode_save(&mut cmd, &settings, ids, tags, &mut conn, data_path)
}
KeepModes::Get => modes::get::mode_get(
&mut cmd,
&settings,
ids,
tags,
&mut conn,
data_path,
filter_chain,
),
KeepModes::Diff => modes::diff::mode_diff(&mut cmd, &args, &mut conn),
KeepModes::List => modes::list::mode_list(&mut cmd, &settings, ids, tags, &mut conn, data_path),
KeepModes::Delete => modes::delete::mode_delete(&mut cmd, &settings, &settings, ids, tags, &mut conn, data_path),
KeepModes::Info => modes::info::mode_info(&mut cmd, &settings, ids, tags, &mut conn, data_path),
KeepModes::List => {
modes::list::mode_list(&mut cmd, &settings, ids, tags, &mut conn, data_path)
}
KeepModes::Delete => modes::delete::mode_delete(
&mut cmd, &settings, &settings, ids, tags, &mut conn, data_path,
),
KeepModes::Info => {
modes::info::mode_info(&mut cmd, &settings, ids, tags, &mut conn, data_path)
}
KeepModes::Status => modes::status::mode_status(&mut cmd, &settings, data_path, db_path),
KeepModes::StatusPlugins => modes::status_plugins::mode_status_plugins(&mut cmd, &settings, data_path, db_path),
KeepModes::StatusPlugins => {
modes::status_plugins::mode_status_plugins(&mut cmd, &settings, data_path, db_path)
}
KeepModes::Server => {
#[cfg(feature = "server")]
{
@@ -215,8 +241,10 @@ fn main() -> Result<(), Error> {
"This binary was not compiled with server support. Recompile with --features server"
).exit();
}
},
KeepModes::GenerateConfig => modes::generate_config::mode_generate_config(&mut cmd, &settings),
}
KeepModes::GenerateConfig => {
modes::generate_config::mode_generate_config(&mut cmd, &settings)
}
KeepModes::Unknown => unreachable!(),
}
}

View File

@@ -1,5 +1,5 @@
use std::env;
use crate::meta_plugin::{MetaPlugin, MetaPluginType};
use std::env;
#[derive(Debug, Clone, Default)]
pub struct CwdMetaPlugin {
@@ -17,7 +17,8 @@ impl CwdMetaPlugin {
// Set default outputs
let default_outputs = vec!["cwd".to_string()];
for output_name in default_outputs {
base.outputs.insert(output_name.clone(), serde_yaml::Value::String(output_name));
base.outputs
.insert(output_name.clone(), serde_yaml::Value::String(output_name));
}
// Apply provided options and outputs
@@ -37,7 +38,6 @@ impl CwdMetaPlugin {
base,
}
}
}
impl MetaPlugin for CwdMetaPlugin {
@@ -90,7 +90,7 @@ impl MetaPlugin for CwdMetaPlugin {
if let Some(meta_data) = crate::meta_plugin::process_metadata_outputs(
"cwd",
serde_yaml::Value::String(cwd),
self.base.outputs()
self.base.outputs(),
) {
metadata.push(meta_data);
}
@@ -109,8 +109,6 @@ impl MetaPlugin for CwdMetaPlugin {
self.base.outputs_mut()
}
fn options(&self) -> &std::collections::HashMap<String, serde_yaml::Value> {
self.base.options()
}

View File

@@ -1,6 +1,6 @@
use sha2::{Digest, Sha256, Sha512};
use crate::meta_plugin::{BaseMetaPlugin, MetaPlugin, MetaPluginType};
use md5;
use crate::meta_plugin::{MetaPlugin, MetaPluginType, BaseMetaPlugin};
use sha2::{Digest, Sha256, Sha512};
use std::io::Write;
#[derive(Clone)]
@@ -33,7 +33,7 @@ impl Hasher {
Hasher::Sha256(hasher) => hasher.update(data),
Hasher::Md5(hasher) => {
let _ = hasher.write(data);
},
}
Hasher::Sha512(hasher) => hasher.update(data),
}
}
@@ -71,7 +71,6 @@ pub struct DigestMetaPlugin {
base: BaseMetaPlugin,
}
impl DigestMetaPlugin {
pub fn new(
options: Option<std::collections::HashMap<String, serde_yaml::Value>>,
@@ -111,16 +110,23 @@ impl DigestMetaPlugin {
};
// Add the method to options so it shows up in the status
base.options.insert("method".to_string(), serde_yaml::Value::String(method.to_string()));
base.options.insert(
"method".to_string(),
serde_yaml::Value::String(method.to_string()),
);
// Set outputs based on the selected hash method
// Only the selected method's output should be enabled, others should be None
let all_outputs = vec!["digest_md5", "digest_sha256", "digest_sha512"];
for output_name in &all_outputs {
if output_name == &format!("digest_{}", method) {
base.outputs.insert(output_name.to_string(), serde_yaml::Value::String(output_name.to_string()));
base.outputs.insert(
output_name.to_string(),
serde_yaml::Value::String(output_name.to_string()),
);
} else {
base.outputs.insert(output_name.to_string(), serde_yaml::Value::Null);
base.outputs
.insert(output_name.to_string(), serde_yaml::Value::Null);
}
}
@@ -129,7 +135,8 @@ impl DigestMetaPlugin {
for (key, value) in outs {
// Only update if the output is not disabled (not None)
if let Some(current_value) = base.outputs.get_mut(&key)
&& !current_value.is_null() {
&& !current_value.is_null()
{
*current_value = value;
}
}
@@ -178,7 +185,7 @@ impl MetaPlugin for DigestMetaPlugin {
if let Some(meta_data) = crate::meta_plugin::process_metadata_outputs(
output_name,
serde_yaml::Value::String(hash_value),
self.base.outputs()
self.base.outputs(),
) {
metadata.push(meta_data);
}
@@ -187,7 +194,9 @@ impl MetaPlugin for DigestMetaPlugin {
let all_outputs = vec!["digest_md5", "digest_sha256", "digest_sha512"];
for output_name in all_outputs {
if output_name != hasher.output_name() {
self.base.outputs.insert(output_name.to_string(), serde_yaml::Value::Null);
self.base
.outputs
.insert(output_name.to_string(), serde_yaml::Value::Null);
}
}
}

View File

@@ -1,4 +1,4 @@
use super::{MetaPlugin, MetaPluginType, process_metadata_outputs, BaseMetaPlugin};
use super::{BaseMetaPlugin, MetaPlugin, MetaPluginType, process_metadata_outputs};
#[derive(Debug, Clone)]
/// Meta plugin that extracts environment variables prefixed with KEEP_META_ as metadata.
@@ -36,7 +36,7 @@ impl EnvMetaPlugin {
// Add to outputs with default mapping to the stripped name
outputs_map.insert(
stripped_key.to_string(),
serde_yaml::Value::String(stripped_key.to_string())
serde_yaml::Value::String(stripped_key.to_string()),
);
}
}
@@ -109,7 +109,7 @@ impl MetaPlugin for EnvMetaPlugin {
if let Some(meta_data) = process_metadata_outputs(
name,
serde_yaml::Value::String(value.clone()),
self.base.outputs()
self.base.outputs(),
) {
metadata.push(meta_data);
}
@@ -193,9 +193,7 @@ impl MetaPlugin for EnvMetaPlugin {
///
/// A vector of environment variable names (stripped of KEEP_META_ prefix).
fn default_outputs(&self) -> Vec<String> {
self.env_vars.iter()
.map(|(name, _)| name.clone())
.collect()
self.env_vars.iter().map(|(name, _)| name.clone()).collect()
}
/// Returns a reference to the options mapping (empty for this plugin).

View File

@@ -1,9 +1,9 @@
use log::*;
use std::io::{self, Write};
use std::process::{Command, Stdio, Child};
use std::process::{Child, Command, Stdio};
use which::which;
use crate::meta_plugin::{MetaPlugin, MetaPluginResponse, MetaPluginType, BaseMetaPlugin};
use crate::meta_plugin::{BaseMetaPlugin, MetaPlugin, MetaPluginResponse, MetaPluginType};
/// External program execution meta plugin.
///
@@ -44,7 +44,6 @@ impl std::fmt::Debug for MetaPluginExec {
}
}
impl MetaPluginExec {
/// Creates a new MetaPluginExec instance.
///
@@ -113,7 +112,10 @@ impl MetaPluginExec {
}
if !self.supported {
debug!("META: Exec plugin: program '{}' not supported", self.program);
debug!(
"META: Exec plugin: program '{}' not supported",
self.program
);
return MetaPluginResponse {
metadata: Vec::new(),
is_finalized: true,
@@ -138,7 +140,10 @@ impl MetaPluginExec {
}
}
Err(e) => {
error!("META: Exec plugin: failed to start '{}': {}", self.program, e);
error!(
"META: Exec plugin: failed to start '{}': {}",
self.program, e
);
MetaPluginResponse {
metadata: Vec::new(),
is_finalized: true,
@@ -166,11 +171,11 @@ impl MetaPlugin for MetaPluginExec {
}
fn update(&mut self, data: &[u8]) -> MetaPluginResponse {
if let Some(writer) = self.writer.as_mut() {
if let Err(e) = writer.write_all(data) {
if let Some(writer) = self.writer.as_mut()
&& let Err(e) = writer.write_all(data)
{
error!("META: Exec plugin: failed to write to stdin: {}", e);
}
}
MetaPluginResponse {
metadata: Vec::new(),
is_finalized: false,
@@ -190,7 +195,11 @@ impl MetaPlugin for MetaPluginExec {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let result = if self.split_whitespace {
stdout.split_whitespace().next().unwrap_or(&stdout).to_string()
stdout
.split_whitespace()
.next()
.unwrap_or(&stdout)
.to_string()
} else {
stdout.trim().to_string()
};
@@ -198,7 +207,11 @@ impl MetaPlugin for MetaPluginExec {
self.result = Some(result.clone());
if let Some(meta_data) = crate::meta_plugin::process_metadata_outputs(
self.base.outputs().keys().next().unwrap_or(&"exec".to_string()),
self.base
.outputs()
.keys()
.next()
.unwrap_or(&"exec".to_string()),
serde_yaml::Value::String(result),
self.base.outputs(),
) {
@@ -261,7 +274,8 @@ fn register_exec_plugin() {
if let Some(opts) = &options {
if let Some(command_value) = opts.get("command")
&& let Some(command_str) = command_value.as_str() {
&& let Some(command_str) = command_value.as_str()
{
let parts: Vec<&str> = command_str.split_whitespace().collect();
if !parts.is_empty() {
program_name = parts[0].to_string();
@@ -269,11 +283,13 @@ fn register_exec_plugin() {
}
}
if let Some(split_value) = opts.get("split_whitespace")
&& let Some(split_bool) = split_value.as_bool() {
&& let Some(split_bool) = split_value.as_bool()
{
split_whitespace = split_bool;
}
if let Some(name_value) = opts.get("name")
&& let Some(name_str) = name_value.as_str() {
&& let Some(name_str) = name_value.as_str()
{
meta_name = name_str.to_string();
}
}

View File

@@ -1,4 +1,4 @@
use crate::meta_plugin::{MetaPlugin, MetaPluginType, BaseMetaPlugin};
use crate::meta_plugin::{BaseMetaPlugin, MetaPlugin, MetaPluginType};
use smart_default::SmartDefault;
@@ -21,21 +21,27 @@ impl HostnameMetaPlugin {
base.initialize_plugin(default_outputs, &options, &outputs);
// Start with default options - hostname is now boolean only
base.options.insert("hostname".to_string(), serde_yaml::Value::Bool(true));
base.options.insert("hostname_full".to_string(), serde_yaml::Value::Bool(true));
base.options.insert("hostname_short".to_string(), serde_yaml::Value::Bool(true));
base.options
.insert("hostname".to_string(), serde_yaml::Value::Bool(true));
base.options
.insert("hostname_full".to_string(), serde_yaml::Value::Bool(true));
base.options
.insert("hostname_short".to_string(), serde_yaml::Value::Bool(true));
// Override with provided options
if let Some(opts) = &options {
for (key, value) in opts {
// Convert string "true"/"false" to boolean for hostname option
if key == "hostname"
&& let serde_yaml::Value::String(s) = value {
&& let serde_yaml::Value::String(s) = value
{
if s == "false" {
base.options.insert(key.clone(), serde_yaml::Value::Bool(false));
base.options
.insert(key.clone(), serde_yaml::Value::Bool(false));
continue;
} else if s == "true" {
base.options.insert(key.clone(), serde_yaml::Value::Bool(true));
base.options
.insert(key.clone(), serde_yaml::Value::Bool(true));
continue;
}
}
@@ -44,15 +50,21 @@ impl HostnameMetaPlugin {
}
// Determine which outputs are enabled based on options
let hostname_enabled = base.options.get("hostname")
let hostname_enabled = base
.options
.get("hostname")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let hostname_full_enabled = base.options.get("hostname_full")
let hostname_full_enabled = base
.options
.get("hostname_full")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let hostname_short_enabled = base.options.get("hostname_short")
let hostname_short_enabled = base
.options
.get("hostname_short")
.and_then(|v| v.as_bool())
.unwrap_or(true);
@@ -61,21 +73,30 @@ impl HostnameMetaPlugin {
// Handle hostname output
if hostname_enabled {
final_outputs.insert("hostname".to_string(), serde_yaml::Value::String("hostname".to_string()));
final_outputs.insert(
"hostname".to_string(),
serde_yaml::Value::String("hostname".to_string()),
);
} else {
final_outputs.insert("hostname".to_string(), serde_yaml::Value::Null);
}
// Handle hostname_full output
if hostname_full_enabled {
final_outputs.insert("hostname_full".to_string(), serde_yaml::Value::String("hostname_full".to_string()));
final_outputs.insert(
"hostname_full".to_string(),
serde_yaml::Value::String("hostname_full".to_string()),
);
} else {
final_outputs.insert("hostname_full".to_string(), serde_yaml::Value::Null);
}
// Handle hostname_short output
if hostname_short_enabled {
final_outputs.insert("hostname_short".to_string(), serde_yaml::Value::String("hostname_short".to_string()));
final_outputs.insert(
"hostname_short".to_string(),
serde_yaml::Value::String("hostname_short".to_string()),
);
} else {
final_outputs.insert("hostname_short".to_string(), serde_yaml::Value::Null);
}
@@ -85,15 +106,21 @@ impl HostnameMetaPlugin {
for (key, value) in outs {
// Only add if the output is enabled
match key.as_str() {
"hostname" => if hostname_enabled {
"hostname" => {
if hostname_enabled {
final_outputs.insert(key.clone(), value.clone());
},
"hostname_full" => if hostname_full_enabled {
}
}
"hostname_full" => {
if hostname_full_enabled {
final_outputs.insert(key.clone(), value.clone());
},
"hostname_short" => if hostname_short_enabled {
}
}
"hostname_short" => {
if hostname_short_enabled {
final_outputs.insert(key.clone(), value.clone());
},
}
}
_ => {
final_outputs.insert(key.clone(), value.clone());
}
@@ -109,7 +136,6 @@ impl HostnameMetaPlugin {
}
}
fn get_hostname(&self) -> String {
// First get the short hostname
let short_hostname = match gethostname::gethostname().into_string() {
@@ -148,7 +174,8 @@ impl HostnameMetaPlugin {
// For local addresses, we might not get a reverse lookup, so try to infer
// from the system's domain name
if let Ok(domain) = std::process::Command::new("domainname").output()
&& domain.status.success() {
&& domain.status.success()
{
let domain_str = String::from_utf8_lossy(&domain.stdout).trim().to_string();
if !domain_str.is_empty() && domain_str != "(none)" {
return format!("{}.{}", short_hostname, domain_str);
@@ -159,11 +186,12 @@ impl HostnameMetaPlugin {
// Fallback: try to get the FQDN using the system's hostname resolution
// This should give us the full hostname if configured
if let Ok(full_hostname) = std::process::Command::new("hostname")
.arg("-f")
.output()
&& full_hostname.status.success() {
let full_hostname_str = String::from_utf8_lossy(&full_hostname.stdout).trim().to_string();
if let Ok(full_hostname) = std::process::Command::new("hostname").arg("-f").output()
&& full_hostname.status.success()
{
let full_hostname_str = String::from_utf8_lossy(&full_hostname.stdout)
.trim()
.to_string();
if !full_hostname_str.is_empty() && full_hostname_str != short_hostname {
return full_hostname_str;
}
@@ -231,24 +259,39 @@ impl MetaPlugin for HostnameMetaPlugin {
// Get the full hostname
let full_hostname = self.get_hostname();
let short_hostname = full_hostname.split('.').next().unwrap_or(&full_hostname).to_string();
let short_hostname = full_hostname
.split('.')
.next()
.unwrap_or(&full_hostname)
.to_string();
// Determine which hostnames to include based on options
let hostname_enabled = self.base.options.get("hostname")
let hostname_enabled = self
.base
.options
.get("hostname")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let hostname_full_enabled = self.base.options.get("hostname_full")
let hostname_full_enabled = self
.base
.options
.get("hostname_full")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let hostname_short_enabled = self.base.options.get("hostname_short")
let hostname_short_enabled = self
.base
.options
.get("hostname_short")
.and_then(|v| v.as_bool())
.unwrap_or(true);
// Always use gethostname() for the 'hostname' output when enabled
let hostname_value = if hostname_enabled {
gethostname::gethostname().into_string().unwrap_or_else(|_| "unknown".to_string())
gethostname::gethostname()
.into_string()
.unwrap_or_else(|_| "unknown".to_string())
} else {
String::new()
};
@@ -261,24 +304,27 @@ impl MetaPlugin for HostnameMetaPlugin {
&& let Some(meta_data) = crate::meta_plugin::process_metadata_outputs(
"hostname",
serde_yaml::Value::String(hostname_value.clone()),
self.base.outputs()
) {
self.base.outputs(),
)
{
metadata.push(meta_data);
}
if hostname_full_enabled
&& let Some(meta_data) = crate::meta_plugin::process_metadata_outputs(
"hostname_full",
serde_yaml::Value::String(full_hostname.clone()),
self.base.outputs()
) {
self.base.outputs(),
)
{
metadata.push(meta_data);
}
if hostname_short_enabled
&& let Some(meta_data) = crate::meta_plugin::process_metadata_outputs(
"hostname_short",
serde_yaml::Value::String(short_hostname.clone()),
self.base.outputs()
) {
self.base.outputs(),
)
{
metadata.push(meta_data);
}
@@ -289,7 +335,9 @@ impl MetaPlugin for HostnameMetaPlugin {
*output_value = serde_yaml::Value::String(hostname_value);
}
} else {
self.base.outputs_mut().insert("hostname".to_string(), serde_yaml::Value::Null);
self.base
.outputs_mut()
.insert("hostname".to_string(), serde_yaml::Value::Null);
}
// Handle hostname_full output
@@ -298,7 +346,9 @@ impl MetaPlugin for HostnameMetaPlugin {
*output_value = serde_yaml::Value::String(full_hostname);
}
} else {
self.base.outputs_mut().insert("hostname_full".to_string(), serde_yaml::Value::Null);
self.base
.outputs_mut()
.insert("hostname_full".to_string(), serde_yaml::Value::Null);
}
// Handle hostname_short output
@@ -307,7 +357,9 @@ impl MetaPlugin for HostnameMetaPlugin {
*output_value = serde_yaml::Value::String(short_hostname);
}
} else {
self.base.outputs_mut().insert("hostname_short".to_string(), serde_yaml::Value::Null);
self.base
.outputs_mut()
.insert("hostname_short".to_string(), serde_yaml::Value::Null);
}
// Mark as finalized since this plugin only needs to run once
@@ -335,7 +387,6 @@ impl MetaPlugin for HostnameMetaPlugin {
]
}
fn options(&self) -> &std::collections::HashMap<String, serde_yaml::Value> {
self.base.options()
}
@@ -343,7 +394,6 @@ impl MetaPlugin for HostnameMetaPlugin {
fn options_mut(&mut self) -> &mut std::collections::HashMap<String, serde_yaml::Value> {
self.base.options_mut()
}
}
use crate::meta_plugin::register_meta_plugin;

View File

@@ -1,5 +1,5 @@
use crate::meta_plugin::{BaseMetaPlugin, MetaPlugin, MetaPluginType};
use std::process;
use crate::meta_plugin::{MetaPlugin, MetaPluginType, BaseMetaPlugin};
#[derive(Debug, Clone, Default)]
pub struct KeepPidMetaPlugin {
@@ -33,7 +33,6 @@ impl KeepPidMetaPlugin {
base,
}
}
}
impl MetaPlugin for KeepPidMetaPlugin {
@@ -135,7 +134,7 @@ impl MetaPlugin for KeepPidMetaPlugin {
if let Some(meta_data) = crate::meta_plugin::process_metadata_outputs(
"keep_pid",
serde_yaml::Value::String(pid),
self.base.outputs()
self.base.outputs(),
) {
metadata.push(meta_data);
}

View File

@@ -3,11 +3,14 @@ use magic::{Cookie, CookieFlags};
#[cfg(not(feature = "magic"))]
use std::process::{Command, Stdio};
use log::debug;
use std::io::{self, Write};
use std::path::Path;
use log::debug;
use crate::meta_plugin::{MetaPlugin, MetaPluginType, BaseMetaPlugin, MetaPluginResponse, MetaData, process_metadata_outputs};
use crate::meta_plugin::{
BaseMetaPlugin, MetaData, MetaPlugin, MetaPluginResponse, MetaPluginType,
process_metadata_outputs,
};
#[cfg(feature = "magic")]
#[derive(Debug)]
@@ -32,7 +35,8 @@ impl MagicFileMetaPluginImpl {
base.initialize_plugin(default_outputs, &options, &outputs);
// Get max_buffer_size from options, default to PIPESIZE
let max_buffer_size = base.options
let max_buffer_size = base
.options
.get("max_buffer_size")
.and_then(|v| v.as_u64())
.unwrap_or(crate::common::PIPESIZE as u64) as usize;
@@ -48,18 +52,20 @@ impl MagicFileMetaPluginImpl {
fn get_magic_result(&self, flags: CookieFlags) -> io::Result<String> {
if let Some(cookie) = &self.cookie {
cookie.set_flags(flags)
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("Failed to set magic flags: {}", e)))?;
cookie
.set_flags(flags)
.map_err(|e| io::Error::other(format!("Failed to set magic flags: {}", e)))?;
let result = cookie.buffer(&self.buffer)
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("Failed to analyze buffer: {}", e)))?;
let result = cookie
.buffer(&self.buffer)
.map_err(|e| io::Error::other(format!("Failed to analyze buffer: {}", e)))?;
// Clean up the result - remove extra whitespace
let trimmed = result.trim().to_string();
Ok(trimmed)
} else {
Err(io::Error::new(io::ErrorKind::Other, "Magic cookie not initialized"))
Err(io::Error::other("Magic cookie not initialized"))
}
}
@@ -73,18 +79,17 @@ impl MagicFileMetaPluginImpl {
];
for (name, flags) in types_to_process.iter() {
if let Ok(result) = self.get_magic_result(*flags) {
if !result.is_empty() {
if let Some(meta_data) = process_metadata_outputs(
if let Ok(result) = self.get_magic_result(*flags)
&& !result.is_empty()
&& let Some(meta_data) = process_metadata_outputs(
name,
serde_yaml::Value::String(result),
self.base.outputs(),
) {
)
{
metadata.push(meta_data);
}
}
}
}
metadata
}
@@ -113,7 +118,10 @@ impl MetaPlugin for MagicFileMetaPluginImpl {
};
if let Err(e) = cookie.load(&[] as &[&Path]) {
debug!("META: MagicFile plugin: failed to load magic database: {}", e);
debug!(
"META: MagicFile plugin: failed to load magic database: {}",
e
);
return MetaPluginResponse {
metadata: Vec::new(),
is_finalized: true,
@@ -187,7 +195,11 @@ impl MetaPlugin for MagicFileMetaPluginImpl {
}
fn default_outputs(&self) -> Vec<String> {
vec!["mime_type".to_string(), "mime_encoding".to_string(), "file_type".to_string()]
vec![
"mime_type".to_string(),
"mime_encoding".to_string(),
"file_type".to_string(),
]
}
fn options(&self) -> &std::collections::HashMap<String, serde_yaml::Value> {
@@ -221,7 +233,8 @@ impl FallbackMagicFileMetaPlugin {
base.initialize_plugin(default_outputs, &options, &outputs);
// Get max_buffer_size from options, default to PIPESIZE
let max_buffer_size = base.options
let max_buffer_size = base
.options
.get("max_buffer_size")
.and_then(|v| v.as_u64())
.unwrap_or(crate::common::PIPESIZE as u64) as usize;
@@ -244,7 +257,12 @@ impl FallbackMagicFileMetaPlugin {
.arg("all")
.arg(temp_file.path())
.output()
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("Failed to run file command: {}", e)))?;
.map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("Failed to run file command: {}", e),
)
})?;
if !output.status.success() {
return Err(io::Error::new(io::ErrorKind::Other, "File command failed"));
@@ -261,7 +279,8 @@ impl FallbackMagicFileMetaPlugin {
// file -m all output format is typically: type; charset=encoding
let parts: Vec<&str> = result.split(';').map(|s| s.trim()).collect();
let file_type = parts.first().cloned().unwrap_or(result);
let mime_encoding = parts.get(1)
let mime_encoding = parts
.get(1)
.and_then(|s| s.strip_prefix("charset="))
.cloned()
.unwrap_or("");
@@ -392,7 +411,11 @@ impl MetaPlugin for FallbackMagicFileMetaPlugin {
}
fn default_outputs(&self) -> Vec<String> {
vec!["mime_type".to_string(), "mime_encoding".to_string(), "file_type".to_string()]
vec![
"mime_type".to_string(),
"mime_encoding".to_string(),
"file_type".to_string(),
]
}
fn options(&self) -> &std::collections::HashMap<String, serde_yaml::Value> {
@@ -418,4 +441,3 @@ fn register_magic_file_plugin() {
Box::new(MagicFileMetaPlugin::new(options, outputs))
});
}

View File

@@ -1,44 +1,47 @@
use log::debug;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Mutex;
use once_cell::sync::Lazy;
pub mod cwd;
pub mod digest;
pub mod env;
pub mod exec;
pub mod hostname;
pub mod keep_pid;
#[cfg(feature = "magic")]
pub mod magic_file;
pub mod exec;
pub mod digest;
pub mod read_time;
pub mod read_rate;
pub mod hostname;
pub mod cwd;
pub mod user;
pub mod read_time;
pub mod shell;
pub mod shell_pid;
pub mod keep_pid;
pub mod env;
pub mod text;
pub mod user;
// pub mod text; // Removed duplicate
pub use digest::DigestMetaPlugin;
pub use exec::MetaPluginExec;
#[cfg(feature = "magic")]
pub use magic_file::MagicFileMetaPlugin;
pub use exec::MetaPluginExec;
pub use digest::DigestMetaPlugin;
// pub use text::TextMetaPlugin; // Removed duplicate
pub use read_time::ReadTimeMetaPlugin;
pub use read_rate::ReadRateMetaPlugin;
pub use hostname::HostnameMetaPlugin;
pub use cwd::CwdMetaPlugin;
pub use user::UserMetaPlugin;
pub use env::EnvMetaPlugin;
pub use hostname::HostnameMetaPlugin;
pub use keep_pid::KeepPidMetaPlugin;
pub use read_rate::ReadRateMetaPlugin;
pub use read_time::ReadTimeMetaPlugin;
pub use shell::ShellMetaPlugin;
pub use shell_pid::ShellPidMetaPlugin;
pub use keep_pid::KeepPidMetaPlugin;
pub use env::EnvMetaPlugin;
pub use user::UserMetaPlugin;
#[cfg(not(feature = "magic"))]
pub use magic_file::FallbackMagicFileMetaPlugin as MagicFileMetaPlugin;
type PluginConstructor = fn(Option<HashMap<String, serde_yaml::Value>>, Option<HashMap<String, serde_yaml::Value>>) -> Box<dyn MetaPlugin>;
type PluginConstructor = fn(
Option<HashMap<String, serde_yaml::Value>>,
Option<HashMap<String, serde_yaml::Value>>,
) -> Box<dyn MetaPlugin>;
/// Represents metadata to be stored.
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -130,7 +133,10 @@ impl BaseMetaPlugin {
) {
// Set default outputs
for output_name in default_outputs {
self.outputs.insert(output_name.to_string(), serde_yaml::Value::String(output_name.to_string()));
self.outputs.insert(
output_name.to_string(),
serde_yaml::Value::String(output_name.to_string()),
);
}
// Apply provided options and outputs
@@ -196,7 +202,18 @@ impl MetaPlugin for BaseMetaPlugin {
}
}
#[derive(Debug, Eq, PartialEq, Clone, Hash, strum::EnumIter, strum::Display, strum::EnumString, Serialize, Deserialize)]
#[derive(
Debug,
Eq,
PartialEq,
Clone,
Hash,
strum::EnumIter,
strum::Display,
strum::EnumString,
Serialize,
Deserialize,
)]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
pub enum MetaPluginType {
MagicFile,
@@ -225,7 +242,11 @@ pub enum MetaPluginType {
/// # Returns
///
/// An optional `MetaData` if the output is enabled, `None` if disabled.
pub fn process_metadata_outputs(internal_name: &str, value: serde_yaml::Value, outputs: &std::collections::HashMap<String, serde_yaml::Value>) -> Option<MetaData> {
pub fn process_metadata_outputs(
internal_name: &str,
value: serde_yaml::Value,
outputs: &std::collections::HashMap<String, serde_yaml::Value>,
) -> Option<MetaData> {
// Check if this output is disabled
if let Some(mapping) = outputs.get(internal_name) {
// Check for null to disable the output
@@ -235,7 +256,8 @@ pub fn process_metadata_outputs(internal_name: &str, value: serde_yaml::Value, o
}
// Check for boolean false to disable the output
if let Some(false_val) = mapping.as_bool()
&& !false_val {
&& !false_val
{
debug!("META: Skipping disabled output: {}", internal_name);
return None;
}
@@ -246,11 +268,20 @@ pub fn process_metadata_outputs(internal_name: &str, value: serde_yaml::Value, o
serde_yaml::Value::Bool(b) => b.to_string(),
serde_yaml::Value::Number(n) => n.to_string(),
serde_yaml::Value::String(s) => s.clone(),
serde_yaml::Value::Sequence(_) => serde_yaml::to_string(&value).unwrap_or_else(|_| "".to_string()),
serde_yaml::Value::Mapping(_) => serde_yaml::to_string(&value).unwrap_or_else(|_| "".to_string()),
serde_yaml::Value::Tagged(_) => serde_yaml::to_string(&value).unwrap_or_else(|_| "".to_string()),
serde_yaml::Value::Sequence(_) => {
serde_yaml::to_string(&value).unwrap_or_else(|_| "".to_string())
}
serde_yaml::Value::Mapping(_) => {
serde_yaml::to_string(&value).unwrap_or_else(|_| "".to_string())
}
serde_yaml::Value::Tagged(_) => {
serde_yaml::to_string(&value).unwrap_or_else(|_| "".to_string())
}
};
debug!("META: Processing metadata: internal_name={}, custom_name={}, value={}", internal_name, custom_name, value_str);
debug!(
"META: Processing metadata: internal_name={}, custom_name={}, value={}",
internal_name, custom_name, value_str
);
return Some(MetaData {
name: custom_name.to_string(),
value: value_str,
@@ -264,20 +295,32 @@ pub fn process_metadata_outputs(internal_name: &str, value: serde_yaml::Value, o
serde_yaml::Value::Bool(b) => b.to_string(),
serde_yaml::Value::Number(n) => n.to_string(),
serde_yaml::Value::String(s) => s.clone(),
serde_yaml::Value::Sequence(_) => serde_yaml::to_string(&value).unwrap_or_else(|_| "".to_string()),
serde_yaml::Value::Mapping(_) => serde_yaml::to_string(&value).unwrap_or_else(|_| "".to_string()),
serde_yaml::Value::Tagged(_) => serde_yaml::to_string(&value).unwrap_or_else(|_| "".to_string()),
serde_yaml::Value::Sequence(_) => {
serde_yaml::to_string(&value).unwrap_or_else(|_| "".to_string())
}
serde_yaml::Value::Mapping(_) => {
serde_yaml::to_string(&value).unwrap_or_else(|_| "".to_string())
}
serde_yaml::Value::Tagged(_) => {
serde_yaml::to_string(&value).unwrap_or_else(|_| "".to_string())
}
};
// Default: use internal name as output name
debug!("META: Processing metadata: name={}, value={}", internal_name, value_str);
debug!(
"META: Processing metadata: name={}, value={}",
internal_name, value_str
);
Some(MetaData {
name: internal_name.to_string(),
value: value_str,
})
}
pub trait MetaPlugin where Self: 'static {
pub trait MetaPlugin
where
Self: 'static,
{
/// Returns the type of this meta plugin.
///
/// # Returns
@@ -358,7 +401,6 @@ pub trait MetaPlugin where Self: 'static {
None
}
/// Initializes the plugin.
///
/// # Returns
@@ -380,7 +422,7 @@ pub trait MetaPlugin where Self: 'static {
fn outputs(&self) -> &std::collections::HashMap<String, serde_yaml::Value> {
use once_cell::sync::Lazy;
static EMPTY: Lazy<std::collections::HashMap<String, serde_yaml::Value>> =
Lazy::new(|| std::collections::HashMap::new());
Lazy::new(std::collections::HashMap::new);
&EMPTY
}
@@ -401,7 +443,7 @@ pub trait MetaPlugin where Self: 'static {
fn options(&self) -> &std::collections::HashMap<String, serde_yaml::Value> {
use once_cell::sync::Lazy;
static EMPTY: Lazy<std::collections::HashMap<String, serde_yaml::Value>> =
Lazy::new(|| std::collections::HashMap::new());
Lazy::new(std::collections::HashMap::new);
&EMPTY
}
@@ -424,13 +466,15 @@ pub trait MetaPlugin where Self: 'static {
vec![self.meta_type().to_string()]
}
/// Method to downcast to concrete type (for checking finalization state).
///
/// # Returns
///
/// A mutable reference to `self` as `dyn Any`.
fn as_any_mut(&mut self) -> &mut dyn std::any::Any where Self: Sized {
fn as_any_mut(&mut self) -> &mut dyn std::any::Any
where
Self: Sized,
{
self
}
}
@@ -445,11 +489,11 @@ static META_PLUGIN_REGISTRY: Lazy<Mutex<HashMap<MetaPluginType, PluginConstructo
///
/// * `meta_plugin_type` - The type of the meta plugin to register.
/// * `constructor` - The constructor function for creating plugin instances.
pub fn register_meta_plugin(
meta_plugin_type: MetaPluginType,
constructor: PluginConstructor
) {
META_PLUGIN_REGISTRY.lock().unwrap().insert(meta_plugin_type, constructor);
pub fn register_meta_plugin(meta_plugin_type: MetaPluginType, constructor: PluginConstructor) {
META_PLUGIN_REGISTRY
.lock()
.unwrap()
.insert(meta_plugin_type, constructor);
}
pub fn get_meta_plugin(

View File

@@ -1,6 +1,6 @@
use std::time::Instant;
use crate::meta_plugin::{MetaPlugin, MetaPluginType, BaseMetaPlugin};
use crate::meta_plugin::{BaseMetaPlugin, MetaPlugin, MetaPluginType};
#[derive(Debug, Clone, Default)]
/// Meta plugin that calculates the read rate (KB/s) of input data.
@@ -60,7 +60,6 @@ impl ReadRateMetaPlugin {
base,
}
}
}
impl MetaPlugin for ReadRateMetaPlugin {
@@ -110,7 +109,10 @@ impl MetaPlugin for ReadRateMetaPlugin {
if let Some(start_time) = self.start_time {
let duration = start_time.elapsed();
let rate = if duration.as_secs_f64() > 0.0 {
format!("{:.2} KB/s", (self.bytes_read as f64 / 1024.0) / duration.as_secs_f64())
format!(
"{:.2} KB/s",
(self.bytes_read as f64 / 1024.0) / duration.as_secs_f64()
)
} else {
"N/A".to_string()
};
@@ -119,7 +121,7 @@ impl MetaPlugin for ReadRateMetaPlugin {
if let Some(meta_data) = crate::meta_plugin::process_metadata_outputs(
"read_rate",
serde_yaml::Value::String(rate),
self.base.outputs()
self.base.outputs(),
) {
metadata.push(meta_data);
}

View File

@@ -1,6 +1,6 @@
use std::time::Instant;
use crate::meta_plugin::{MetaPlugin, MetaPluginType, BaseMetaPlugin};
use crate::meta_plugin::{BaseMetaPlugin, MetaPlugin, MetaPluginType};
#[derive(Debug, Clone, Default)]
pub struct ReadTimeMetaPlugin {
@@ -26,7 +26,6 @@ impl ReadTimeMetaPlugin {
base,
}
}
}
impl MetaPlugin for ReadTimeMetaPlugin {
@@ -57,7 +56,7 @@ impl MetaPlugin for ReadTimeMetaPlugin {
if let Some(meta_data) = crate::meta_plugin::process_metadata_outputs(
"read_time",
serde_yaml::Value::String(duration_str),
self.base.outputs()
self.base.outputs(),
) {
metadata.push(meta_data);
}

View File

@@ -1,6 +1,6 @@
use std::env;
use crate::meta_plugin::{MetaPlugin, MetaPluginType, BaseMetaPlugin};
use crate::meta_plugin::{BaseMetaPlugin, MetaPlugin, MetaPluginType};
#[derive(Debug, Clone, Default)]
/// Meta plugin for capturing shell environment information.
@@ -48,7 +48,6 @@ impl ShellMetaPlugin {
base,
}
}
}
impl MetaPlugin for ShellMetaPlugin {
@@ -165,7 +164,7 @@ impl MetaPlugin for ShellMetaPlugin {
if let Some(meta_data) = crate::meta_plugin::process_metadata_outputs(
"shell",
serde_yaml::Value::String(shell),
self.base.outputs()
self.base.outputs(),
) {
metadata.push(meta_data);
}

View File

@@ -1,6 +1,6 @@
use crate::meta_plugin::{BaseMetaPlugin, MetaPlugin, MetaPluginType};
use std::env;
use std::process;
use crate::meta_plugin::{MetaPlugin, MetaPluginType, BaseMetaPlugin};
#[derive(Debug, Clone, Default)]
pub struct ShellPidMetaPlugin {
@@ -24,7 +24,6 @@ impl ShellPidMetaPlugin {
base,
}
}
}
impl MetaPlugin for ShellPidMetaPlugin {
@@ -92,7 +91,7 @@ impl MetaPlugin for ShellPidMetaPlugin {
if let Some(meta_data) = crate::meta_plugin::process_metadata_outputs(
"shell_pid",
serde_yaml::Value::String(pid),
self.base.outputs()
self.base.outputs(),
) {
metadata.push(meta_data);
}
@@ -114,8 +113,6 @@ impl MetaPlugin for ShellPidMetaPlugin {
self.base.outputs_mut()
}
fn options(&self) -> &std::collections::HashMap<String, serde_yaml::Value> {
self.base.options()
}

View File

@@ -1,5 +1,5 @@
use crate::common::is_binary::is_binary;
use crate::common::PIPESIZE;
use crate::common::is_binary::is_binary;
use crate::meta_plugin::{MetaPlugin, MetaPluginResponse, MetaPluginType};
#[derive(Debug, Clone)]
@@ -41,8 +41,14 @@ impl TextMetaPlugin {
// Initialize with helper function
base.initialize_plugin(
&["text", "text_word_count", "text_line_count",
"text_line_max_len", "text_line_mean_len", "text_line_median_len"],
&[
"text",
"text_word_count",
"text_line_count",
"text_line_max_len",
"text_line_mean_len",
"text_line_median_len",
],
&options,
&outputs,
);
@@ -65,14 +71,18 @@ impl TextMetaPlugin {
_ => false,
};
if should_disable {
base.outputs.insert(output_name.to_string(), serde_yaml::Value::Null);
base.outputs
.insert(output_name.to_string(), serde_yaml::Value::Null);
}
}
}
// Set default options if not provided
let default_options = vec![
("text_detect_size", serde_yaml::Value::Number(PIPESIZE.into())),
(
"text_detect_size",
serde_yaml::Value::Number(PIPESIZE.into()),
),
("text_word_count", serde_yaml::Value::Bool(true)),
("text_line_count", serde_yaml::Value::Bool(true)),
("text_line_max_len", serde_yaml::Value::Bool(true)),
@@ -87,25 +97,37 @@ impl TextMetaPlugin {
}
// Get text_detect_size (previously max_buffer_size)
let max_buffer_size = base.options.get("text_detect_size")
let max_buffer_size = base
.options
.get("text_detect_size")
.or_else(|| base.options.get("max_buffer_size")) // Handle backward compatibility
.and_then(|v| v.as_u64())
.unwrap_or(PIPESIZE as u64) as usize;
// Get which statistics to track
let track_word_count = base.options.get("text_word_count")
let track_word_count = base
.options
.get("text_word_count")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let track_line_count = base.options.get("text_line_count")
let track_line_count = base
.options
.get("text_line_count")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let track_line_max_len = base.options.get("text_line_max_len")
let track_line_max_len = base
.options
.get("text_line_max_len")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let track_line_mean_len = base.options.get("text_line_mean_len")
let track_line_mean_len = base
.options
.get("text_line_mean_len")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let track_line_median_len = base.options.get("text_line_median_len")
let track_line_median_len = base
.options
.get("text_line_median_len")
.and_then(|v| v.as_bool())
.unwrap_or(false);
@@ -130,7 +152,11 @@ impl TextMetaPlugin {
output_line_max_len: track_line_max_len,
output_line_mean_len: track_line_mean_len,
output_line_median_len: track_line_median_len,
line_lengths: if track_line_lengths { Some(Vec::new()) } else { None },
line_lengths: if track_line_lengths {
Some(Vec::new())
} else {
None
},
current_line_length: 0,
// Initialize incremental tracking for max and mean
max_line_length: 0,
@@ -139,7 +165,6 @@ impl TextMetaPlugin {
}
}
/// Count words and lines in a text chunk, handling block boundaries correctly.
///
/// Processes UTF-8 data, tracks word transitions, and updates line length statistics.
@@ -172,7 +197,8 @@ impl TextMetaPlugin {
// If we have incomplete UTF-8 at the end, buffer those bytes for next chunk
let valid_up_to = e.valid_up_to();
if valid_up_to < combined_data.len() {
self.utf8_buffer.extend_from_slice(&combined_data[valid_up_to..]);
self.utf8_buffer
.extend_from_slice(&combined_data[valid_up_to..]);
}
match std::str::from_utf8(&combined_data[..valid_up_to]) {
Ok(text) => text,
@@ -234,19 +260,26 @@ impl TextMetaPlugin {
/// # Returns
///
/// * `(Vec<MetaData>, bool)` - Metadata updates and whether content is binary.
fn perform_binary_detection(&mut self, buffer: &[u8]) -> (Vec<crate::meta_plugin::MetaData>, bool) {
fn perform_binary_detection(
&mut self,
buffer: &[u8],
) -> (Vec<crate::meta_plugin::MetaData>, bool) {
let mut metadata = Vec::new();
let is_binary_result = is_binary(buffer);
self.is_binary_content = Some(is_binary_result);
// Output text status
let text_value = if is_binary_result { "false".to_string() } else { "true".to_string() };
let text_value = if is_binary_result {
"false".to_string()
} else {
"true".to_string()
};
// Use process_metadata_outputs to handle output mapping
if let Some(meta_data) = crate::meta_plugin::process_metadata_outputs(
"text",
serde_yaml::Value::String(text_value),
self.base.outputs()
self.base.outputs(),
) {
metadata.push(meta_data);
}
@@ -264,7 +297,7 @@ impl TextMetaPlugin {
if let Some(meta_data) = crate::meta_plugin::process_metadata_outputs(
output_name,
serde_yaml::Value::Null,
self.base.outputs()
self.base.outputs(),
) {
metadata.push(meta_data);
}
@@ -314,7 +347,7 @@ impl TextMetaPlugin {
crate::meta_plugin::process_metadata_outputs(
"text_word_count",
serde_yaml::Value::String(self.word_count.to_string()),
self.base.outputs()
self.base.outputs(),
)
} else {
None
@@ -331,7 +364,7 @@ impl TextMetaPlugin {
crate::meta_plugin::process_metadata_outputs(
"text_line_count",
serde_yaml::Value::String(self.line_count.to_string()),
self.base.outputs()
self.base.outputs(),
)
} else {
None
@@ -348,7 +381,7 @@ impl TextMetaPlugin {
crate::meta_plugin::process_metadata_outputs(
"text_line_max_len",
serde_yaml::Value::String(self.max_line_length.to_string()),
self.base.outputs()
self.base.outputs(),
)
} else {
None
@@ -370,7 +403,7 @@ impl TextMetaPlugin {
crate::meta_plugin::process_metadata_outputs(
"text_line_mean_len",
serde_yaml::Value::String(mean_len_int.to_string()),
self.base.outputs()
self.base.outputs(),
)
} else {
None
@@ -386,12 +419,14 @@ impl TextMetaPlugin {
/// * `Option<MetaData>` - Metadata entry if enabled and data exists.
fn output_median_line_length_metadata(&self) -> Option<crate::meta_plugin::MetaData> {
if self.output_line_median_len
&& let Some(lengths) = &self.line_lengths {
if !lengths.is_empty() {
&& let Some(lengths) = &self.line_lengths
&& !lengths.is_empty()
{
let mut sorted_lengths = lengths.clone();
sorted_lengths.sort();
let median_len = if lengths.len() % 2 == 0 {
(sorted_lengths[lengths.len() / 2 - 1] + sorted_lengths[lengths.len() / 2]) as f64 / 2.0
(sorted_lengths[lengths.len() / 2 - 1] + sorted_lengths[lengths.len() / 2]) as f64
/ 2.0
} else {
sorted_lengths[lengths.len() / 2] as f64
};
@@ -399,10 +434,9 @@ impl TextMetaPlugin {
return crate::meta_plugin::process_metadata_outputs(
"text_line_median_len",
serde_yaml::Value::String(median_len.to_string()),
self.base.outputs()
self.base.outputs(),
);
}
}
None
}
@@ -440,7 +474,10 @@ impl TextMetaPlugin {
let line_stats_outputs = vec![
(self.output_max_line_length_metadata(), "max line length"),
(self.output_mean_line_length_metadata(), "mean line length"),
(self.output_median_line_length_metadata(), "median line length"),
(
self.output_median_line_length_metadata(),
"median line length",
),
];
for (output, _) in line_stats_outputs {
@@ -473,7 +510,6 @@ impl MetaPlugin for TextMetaPlugin {
self.is_finalized = finalized;
}
/// Updates the plugin with new data chunk.
///
/// Accumulates data for binary detection (if pending) or text statistics.
@@ -588,23 +624,36 @@ impl MetaPlugin for TextMetaPlugin {
let mut metadata = Vec::new();
// Check if we have head/tail options
let head_bytes = self.base.options.get("head_bytes")
let head_bytes = self
.base
.options
.get("head_bytes")
.and_then(|v| v.as_u64())
.map(|v| v as usize);
let head_lines = self.base.options.get("head_lines")
let head_lines = self
.base
.options
.get("head_lines")
.and_then(|v| v.as_u64())
.map(|v| v as usize);
let tail_bytes = self.base.options.get("tail_bytes")
let tail_bytes = self
.base
.options
.get("tail_bytes")
.and_then(|v| v.as_u64())
.map(|v| v as usize);
let tail_lines = self.base.options.get("tail_lines")
let tail_lines = self
.base
.options
.get("tail_lines")
.and_then(|v| v.as_u64())
.map(|v| v as usize);
// If we haven't determined binary status yet, do it now with whatever we have
if self.is_binary_content.is_none() {
if let Some(buffer) = &self.buffer {
if !buffer.is_empty() {
if self.is_binary_content.is_none()
&& let Some(buffer) = &self.buffer
&& !buffer.is_empty()
{
// Build filter string from individual parameters
let mut filter_parts = Vec::new();
if let Some(bytes) = head_bytes {
@@ -643,24 +692,22 @@ impl MetaPlugin for TextMetaPlugin {
];
for (output_name, is_enabled) in text_outputs {
if is_enabled {
if let Some(meta_data) = crate::meta_plugin::process_metadata_outputs(
if is_enabled
&& let Some(meta_data) = crate::meta_plugin::process_metadata_outputs(
output_name,
serde_yaml::Value::Null,
self.base.outputs()
) {
self.base.outputs(),
)
{
metadata.push(meta_data);
}
}
}
return MetaPluginResponse {
metadata,
is_finalized: true,
};
}
}
}
}
// If content is text, output word and line counts
if self.is_binary_content == Some(false) {
@@ -722,7 +769,7 @@ impl MetaPlugin for TextMetaPlugin {
"text_line_count".to_string(),
"text_line_max_len".to_string(),
"text_line_mean_len".to_string(),
"text_line_median_len".to_string()
"text_line_median_len".to_string(),
]
}
@@ -743,7 +790,6 @@ impl MetaPlugin for TextMetaPlugin {
fn options_mut(&mut self) -> &mut std::collections::HashMap<String, serde_yaml::Value> {
self.base.options_mut()
}
}
use crate::meta_plugin::register_meta_plugin;

View File

@@ -33,11 +33,8 @@ impl UserMetaPlugin {
&outputs,
);
UserMetaPlugin {
base,
UserMetaPlugin { base }
}
}
/// Gets the current username.
///
@@ -87,7 +84,7 @@ impl MetaPlugin for UserMetaPlugin {
if let Some(meta_data) = crate::meta_plugin::process_metadata_outputs(
name,
serde_yaml::Value::String(value),
self.base.outputs()
self.base.outputs(),
) {
metadata.push(meta_data);
}
@@ -132,10 +129,14 @@ impl MetaPlugin for UserMetaPlugin {
///
/// A vector of default output names.
fn default_outputs(&self) -> Vec<String> {
vec!["user_uid".to_string(), "user_gid".to_string(), "user_name".to_string(), "user_group".to_string()]
vec![
"user_uid".to_string(),
"user_gid".to_string(),
"user_name".to_string(),
"user_group".to_string(),
]
}
/// Returns a reference to the options mapping.
///
/// # Returns

View File

@@ -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,7 +185,10 @@ 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
@@ -215,7 +204,8 @@ pub fn settings_meta_plugin_types(cmd: &mut Command, settings: &config::Settings
// 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;
@@ -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)
@@ -382,9 +379,15 @@ pub fn create_table_with_config(table_config: &crate::config::TableConfig) -> Ta
// 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
@@ -439,4 +442,3 @@ pub fn create_table_with_config(table_config: &crate::config::TableConfig) -> Ta
table
}

View File

@@ -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)));
}
},
}
}

View File

@@ -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();

View File

@@ -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,26 +71,26 @@ 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<()> {
/// 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);
@@ -195,7 +195,7 @@ struct MetaPluginConfig {
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(),
);
}
}
}

View File

@@ -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,21 +29,30 @@ 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();

View File

@@ -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(())
}

View File

@@ -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;
@@ -97,7 +97,11 @@ 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 {
@@ -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(),
@@ -245,7 +252,8 @@ pub fn mode_list(
};
// 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
@@ -284,15 +292,21 @@ pub fn mode_list(
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);
}
}
_ => {}
@@ -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(())
}

View File

@@ -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,

View File

@@ -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)
}
}
@@ -86,7 +93,6 @@ impl ResponseBuilder {
}
}
/// Helper function to get mime type from metadata
fn get_mime_type(metadata: &HashMap<String, String>) -> String {
metadata
@@ -130,7 +136,7 @@ fn create_item_service(state: &AppState) -> AsyncItemService {
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 {
@@ -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;
@@ -316,7 +324,6 @@ async fn handle_as_meta_response_with_metadata(
}
}
#[utoipa::path(
post,
path = "/api/item/",
@@ -342,7 +349,6 @@ 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
@@ -356,7 +362,6 @@ pub async fn handle_post_item(
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(&params);
@@ -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,7 +559,10 @@ 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
@@ -520,14 +570,12 @@ async fn stream_item_content_response_with_metadata(
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,15 +591,15 @@ 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)
}
@@ -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)),
}
}

View File

@@ -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};

View File

@@ -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,13 +59,24 @@ 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")]
{
@@ -80,8 +88,7 @@ pub fn add_routes(router: Router<AppState>) -> Router<AppState> {
#[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"))]

View File

@@ -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,9 +48,14 @@ 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();

View File

@@ -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,7 +586,11 @@ 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;
}
@@ -619,7 +625,11 @@ 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;
}
@@ -667,7 +677,11 @@ 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;
@@ -675,8 +689,8 @@ pub fn check_auth(headers: &HeaderMap, password: &Option<String>, password_hash:
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
@@ -709,7 +723,8 @@ pub async fn logging_middleware(
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("-")
@@ -720,14 +735,23 @@ pub async fn logging_middleware(
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,7 +780,14 @@ 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();
@@ -771,7 +802,9 @@ 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);
}

View File

@@ -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,32 +26,34 @@ 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(
/// 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 {
) -> 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) {
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,
@@ -73,8 +70,7 @@ pub struct McpRequest {
};
(StatusCode::OK, Json(response))
}
}
}
},
Err(e) => {
let response = ApiResponse {
success: false,
@@ -84,4 +80,4 @@ pub struct McpRequest {
(StatusCode::BAD_REQUEST, Json(response))
}
}
}
}

View File

@@ -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,31 +36,38 @@ 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());

View File

@@ -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")
@@ -74,7 +75,7 @@ impl KeepTools {
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 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 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.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;
@@ -133,7 +145,11 @@ 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(
@@ -141,20 +157,33 @@ impl KeepTools {
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;
@@ -176,7 +205,11 @@ 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
@@ -194,18 +227,26 @@ impl KeepTools {
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);
@@ -236,16 +277,22 @@ 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(
@@ -253,9 +300,12 @@ impl KeepTools {
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));
@@ -263,7 +313,8 @@ impl KeepTools {
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);

View File

@@ -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,7 +30,10 @@ 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()
};
@@ -63,7 +63,14 @@ pub fn mode_server(
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(
@@ -108,8 +115,9 @@ async fn run_server(
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()
@@ -124,7 +132,7 @@ async fn run_server(
.layer(
ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(CorsLayer::permissive())
.layer(CorsLayer::permissive()),
);
let addr: SocketAddr = bind_address.parse()?;
@@ -134,8 +142,9 @@ async fn run_server(
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>()
).await?;
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await?;
Ok(())
}

View File

@@ -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))
}
@@ -96,13 +95,18 @@ 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();
@@ -185,7 +189,10 @@ fn build_item_list(conn: &Connection, params: &ListQueryParams, columns: &[Colum
} 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>");
}
@@ -205,9 +212,11 @@ fn build_item_list(conn: &Connection, params: &ListQueryParams, columns: &[Colum
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()
})
@@ -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..];
@@ -265,7 +275,10 @@ fn build_item_list(conn: &Connection, params: &ListQueryParams, columns: &[Colum
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
@@ -280,10 +293,12 @@ fn build_item_list(conn: &Connection, params: &ListQueryParams, columns: &[Colum
html.push_str("</table>");
// Add pagination info
html.push_str(&format!("<p>Showing {} to {} of {} items</p>",
html.push_str(&format!(
"<p>Showing {} to {} of {} items</p>",
start + 1,
std::cmp::min(end, sorted_items.len()),
sorted_items.len()));
sorted_items.len()
));
html.push_str("</body></html>");
@@ -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))),
}
}
@@ -378,17 +393,30 @@ fn build_item_details(conn: &Connection, id: i64) -> Result<String> {
// 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(", "));
@@ -400,14 +428,20 @@ fn build_item_details(conn: &Connection, id: i64) -> Result<String> {
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>");

View File

@@ -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);
@@ -79,11 +78,7 @@ fn build_meta_plugins_configured_table(status_info: &StatusInfo) -> Option<Table
};
// 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();
@@ -94,7 +89,8 @@ fn build_meta_plugins_configured_table(status_info: &StatusInfo) -> Option<Table
}
// 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();
@@ -107,11 +103,7 @@ fn build_meta_plugins_configured_table(status_info: &StatusInfo) -> Option<Table
);
// 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();
@@ -123,7 +115,8 @@ fn build_meta_plugins_configured_table(status_info: &StatusInfo) -> Option<Table
// 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()
@@ -175,23 +168,19 @@ fn build_meta_plugins_configured_table(status_info: &StatusInfo) -> Option<Table
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,
@@ -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(())

View File

@@ -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
@@ -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,7 +335,7 @@ 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;

View File

@@ -92,11 +92,13 @@ impl AsyncItemService {
}
pub async fn get_item(&self, id: i64) -> Result<ItemWithMeta, CoreError> {
self.execute_blocking(move |conn, item_service| item_service.get_item(conn, id)).await
self.execute_blocking(move |conn, item_service| item_service.get_item(conn, id))
.await
}
pub async fn get_item_content(&self, id: i64) -> Result<ItemWithContent, CoreError> {
self.execute_blocking(move |conn, item_service| item_service.get_item_content(conn, id)).await
self.execute_blocking(move |conn, item_service| item_service.get_item_content(conn, id))
.await
}
pub async fn get_item_content_info(
@@ -104,7 +106,10 @@ impl AsyncItemService {
id: i64,
filter: Option<String>,
) -> Result<(Vec<u8>, String, bool), CoreError> {
self.execute_blocking(move |conn, item_service| item_service.get_item_content_info(conn, id, filter)).await
self.execute_blocking(move |conn, item_service| {
item_service.get_item_content_info(conn, id, filter)
})
.await
}
pub async fn stream_item_content_by_id(
@@ -113,11 +118,25 @@ impl AsyncItemService {
allow_binary: bool,
offset: u64,
length: u64,
) -> Result<(std::pin::Pin<Box<dyn tokio_stream::Stream<Item = Result<tokio_util::bytes::Bytes, std::io::Error>> + Send>>, String), CoreError> {
let content = self.execute_blocking(move |conn, item_service| {
) -> Result<
(
std::pin::Pin<
Box<
dyn tokio_stream::Stream<
Item = Result<tokio_util::bytes::Bytes, std::io::Error>,
> + Send,
>,
>,
String,
),
CoreError,
> {
let content = self
.execute_blocking(move |conn, item_service| {
let item_with_content = item_service.get_item_content(conn, item_id)?;
Ok::<_, CoreError>(item_with_content.content)
}).await?;
})
.await?;
// Clone content for use in the binary check closure
let content_clone = content.clone();
@@ -150,7 +169,9 @@ impl AsyncItemService {
// Check if content is binary when allow_binary is false
if !allow_binary && is_binary {
return Err(CoreError::InvalidInput("Binary content not allowed".to_string()));
return Err(CoreError::InvalidInput(
"Binary content not allowed".to_string(),
));
}
// Create a stream that reads only the requested portion
@@ -165,7 +186,8 @@ impl AsyncItemService {
};
let stream = if start < content_len {
let chunk = tokio_util::bytes::Bytes::from(content[start as usize..end as usize].to_vec());
let chunk =
tokio_util::bytes::Bytes::from(content[start as usize..end as usize].to_vec());
Box::pin(tokio_stream::iter(vec![Ok(chunk)]))
} else {
Box::pin(tokio_stream::iter(vec![]))
@@ -182,7 +204,19 @@ impl AsyncItemService {
offset: u64,
length: u64,
filter: Option<String>,
) -> Result<(std::pin::Pin<Box<dyn tokio_stream::Stream<Item = Result<tokio_util::bytes::Bytes, std::io::Error>> + Send>>, String), CoreError> {
) -> Result<
(
std::pin::Pin<
Box<
dyn tokio_stream::Stream<
Item = Result<tokio_util::bytes::Bytes, std::io::Error>,
> + Send,
>,
>,
String,
),
CoreError,
> {
// Use provided metadata to determine MIME type and binary status
let mime_type = metadata
.get("mime_type")
@@ -195,15 +229,14 @@ impl AsyncItemService {
text_val == "false"
} else {
// Get binary status using streaming approach
let (_, _, is_binary) = self.get_item_content_info_streaming(
item_id,
None
).await?;
let (_, _, is_binary) = self.get_item_content_info_streaming(item_id, None).await?;
is_binary
};
if is_binary {
return Err(CoreError::InvalidInput("Binary content not allowed".to_string()));
return Err(CoreError::InvalidInput(
"Binary content not allowed".to_string(),
));
}
}
@@ -215,11 +248,9 @@ impl AsyncItemService {
let filter = filter.clone();
tokio::task::spawn_blocking(move || {
let conn = db.blocking_lock();
item_service.get_item_content_info_streaming(
&conn,
item_id,
filter
).map(|(reader, _, _)| reader)
item_service
.get_item_content_info_streaming(&conn, item_id, filter)
.map(|(reader, _, _)| reader)
})
.await
.map_err(|e| CoreError::Other(anyhow::anyhow!("Blocking task failed: {}", e)))?
@@ -302,7 +333,10 @@ impl AsyncItemService {
item_id: i64,
filter: Option<String>,
) -> Result<(Box<dyn Read + Send>, String, bool), CoreError> {
self.execute_blocking(move |conn, item_service| item_service.get_item_content_info_streaming(conn, item_id, filter)).await
self.execute_blocking(move |conn, item_service| {
item_service.get_item_content_info_streaming(conn, item_id, filter)
})
.await
}
pub async fn find_item(
@@ -314,7 +348,10 @@ impl AsyncItemService {
let ids_clone = ids.clone();
let tags_clone = tags.clone();
let meta_clone = meta.clone();
self.execute_blocking(move |conn, item_service| item_service.find_item(conn, &ids_clone, &tags_clone, &meta_clone)).await
self.execute_blocking(move |conn, item_service| {
item_service.find_item(conn, &ids_clone, &tags_clone, &meta_clone)
})
.await
}
pub async fn list_items(
@@ -324,7 +361,10 @@ impl AsyncItemService {
) -> Result<Vec<ItemWithMeta>, CoreError> {
let tags_clone = tags.clone();
let meta_clone = meta.clone();
self.execute_blocking(move |conn, item_service| item_service.list_items(conn, &tags_clone, &meta_clone)).await
self.execute_blocking(move |conn, item_service| {
item_service.list_items(conn, &tags_clone, &meta_clone)
})
.await
}
pub async fn delete_item(&self, id: i64) -> Result<(), CoreError> {
@@ -354,7 +394,8 @@ impl AsyncItemService {
let mut conn = db.blocking_lock();
let mut cmd = cmd.blocking_lock();
let settings = settings.as_ref();
item_service.save_item_from_mcp(&content, &tags, &metadata, &mut cmd, settings, &mut conn)
item_service
.save_item_from_mcp(&content, &tags, &metadata, &mut cmd, settings, &mut conn)
})
.await
.unwrap()

View File

@@ -1,9 +1,9 @@
use crate::compression_engine::{get_compression_engine, CompressionType};
use crate::compression_engine::{CompressionType, get_compression_engine};
use crate::services::error::CoreError;
use anyhow::anyhow;
use std::io::Read;
use std::path::PathBuf;
use std::str::FromStr;
use anyhow::anyhow;
pub struct CompressionService;
@@ -28,7 +28,6 @@ pub struct CompressionService;
/// let service = CompressionService::new();
/// let content = service.get_item_content(path, "gzip")?;
/// ```
impl CompressionService {
/// Creates a new CompressionService instance.
///
@@ -72,14 +71,19 @@ impl CompressionService {
/// let content = service.get_item_content(item_path, "lz4")?;
/// assert_eq!(content.len(), expected_size);
/// ```
pub fn get_item_content(&self, item_path: PathBuf, compression: &str) -> Result<Vec<u8>, CoreError> {
pub fn get_item_content(
&self,
item_path: PathBuf,
compression: &str,
) -> Result<Vec<u8>, CoreError> {
let compression_type = CompressionType::from_str(compression)
.map_err(|e| CoreError::Compression(e.to_string()))?;
let engine = get_compression_engine(compression_type)
.map_err(|e| CoreError::Other(anyhow!(e.to_string())))?;
let mut reader = engine.open(item_path.clone())
.map_err(|e| CoreError::Other(anyhow!("Failed to open item file {:?}: {}", item_path, e)))?;
let mut reader = engine.open(item_path.clone()).map_err(|e| {
CoreError::Other(anyhow!("Failed to open item file {:?}: {}", item_path, e))
})?;
let mut content = Vec::new();
reader.read_to_end(&mut content)?;
Ok(content)
@@ -122,8 +126,9 @@ impl CompressionService {
let engine = get_compression_engine(compression_type)
.map_err(|e| CoreError::Other(anyhow!(e.to_string())))?;
let reader = engine.open(item_path.clone())
.map_err(|e| CoreError::Other(anyhow!("Failed to open item file {:?}: {}", item_path, e)))?;
let reader = engine.open(item_path.clone()).map_err(|e| {
CoreError::Other(anyhow!("Failed to open item file {:?}: {}", item_path, e))
})?;
// Since we can't guarantee the reader implements Send, we need to wrap it
// We'll read the content into a buffer and return a Cursor which is Send
// This is not ideal for large files, but it ensures Send is implemented

View File

@@ -1,9 +1,11 @@
use crate::filter_plugin::{FilterChain, parse_filter_string};
use std::collections::HashMap;
use std::io::{Result, Read, Write};
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::io::{Read, Result, Write};
use std::sync::Mutex;
type FilterConstructor = fn() -> Box<dyn crate::filter_plugin::FilterPlugin>;
/// Service for managing filter chains and plugin registration.
///
/// The `FilterService` provides functionality to parse filter strings, create filter chains,
@@ -20,6 +22,12 @@ use std::sync::Mutex;
/// ```
pub struct FilterService;
impl Default for FilterService {
fn default() -> Self {
Self::new()
}
}
impl FilterService {
/// Creates a new `FilterService` instance.
///
@@ -99,7 +107,7 @@ impl FilterService {
&self,
chain: &mut Option<FilterChain>,
reader: &mut R,
writer: &mut W
writer: &mut W,
) -> Result<()> {
if let Some(chain) = chain {
chain.filter(reader, writer)
@@ -158,7 +166,7 @@ impl FilterService {
/// # Panics
///
/// Lock acquisition failures (rare) cause panics in accessors.
static FILTER_PLUGIN_REGISTRY: Lazy<Mutex<HashMap<String, fn() -> Box<dyn crate::filter_plugin::FilterPlugin>>>> =
static FILTER_PLUGIN_REGISTRY: Lazy<Mutex<HashMap<String, FilterConstructor>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
/// Registers a filter plugin in the global registry.
@@ -180,8 +188,11 @@ static FILTER_PLUGIN_REGISTRY: Lazy<Mutex<HashMap<String, fn() -> Box<dyn crate:
/// ```rust
/// register_filter_plugin("custom_filter", || Box::new(CustomFilter::default()));
/// ```
pub fn register_filter_plugin(name: &str, constructor: fn() -> Box<dyn crate::filter_plugin::FilterPlugin>) {
FILTER_PLUGIN_REGISTRY.lock().unwrap().insert(name.to_string(), constructor);
pub fn register_filter_plugin(name: &str, constructor: FilterConstructor) {
FILTER_PLUGIN_REGISTRY
.lock()
.unwrap()
.insert(name.to_string(), constructor);
}
/// Retrieves a snapshot of all registered filter plugins.
@@ -202,6 +213,6 @@ pub fn register_filter_plugin(name: &str, constructor: fn() -> Box<dyn crate::fi
/// let plugins = get_available_filter_plugins();
/// assert!(plugins.contains_key("head_bytes"));
/// ```
pub fn get_available_filter_plugins() -> HashMap<String, fn() -> Box<dyn crate::filter_plugin::FilterPlugin>> {
pub fn get_available_filter_plugins() -> HashMap<String, FilterConstructor> {
FILTER_PLUGIN_REGISTRY.lock().unwrap().clone()
}

View File

@@ -1,14 +1,14 @@
use crate::common::PIPESIZE;
use crate::compression_engine::{CompressionType, get_compression_engine};
use crate::config::Settings;
use crate::db::{self, Meta};
use crate::filter_plugin;
use crate::modes::common::settings_compression_type;
use crate::services::compression_service::CompressionService;
use crate::services::error::CoreError;
use crate::services::filter_service::FilterService;
use crate::services::meta_service::MetaService;
use crate::services::types::{ItemWithContent, ItemWithMeta};
use crate::db::{self, Meta};
use crate::compression_engine::{get_compression_engine, CompressionType};
use crate::modes::common::settings_compression_type;
use crate::filter_plugin;
use clap::Command;
use log::debug;
use rusqlite::Connection;
@@ -53,7 +53,10 @@ impl ItemService {
/// let service = ItemService::new(PathBuf::from("/data"));
/// ```
pub fn new(data_path: PathBuf) -> Self {
debug!("ITEM_SERVICE: Creating new ItemService with data_path: {:?}", data_path);
debug!(
"ITEM_SERVICE: Creating new ItemService with data_path: {:?}",
data_path
);
Self {
data_path,
compression_service: CompressionService::new(),
@@ -93,7 +96,11 @@ impl ItemService {
let tags = db::get_item_tags(conn, &item)?;
debug!("ITEM_SERVICE: Found {} tags for item {}", tags.len(), id);
let meta = db::get_item_meta(conn, &item)?;
debug!("ITEM_SERVICE: Found {} meta entries for item {}", meta.len(), id);
debug!(
"ITEM_SERVICE: Found {} meta entries for item {}",
meta.len(),
id
);
Ok(ItemWithMeta { item, tags, meta })
}
@@ -121,13 +128,23 @@ impl ItemService {
/// let item_with_content = item_service.get_item_content(&conn, 1)?;
/// assert!(!item_with_content.content.is_empty());
/// ```
pub fn get_item_content(&self, conn: &Connection, id: i64) -> Result<ItemWithContent, CoreError> {
pub fn get_item_content(
&self,
conn: &Connection,
id: i64,
) -> Result<ItemWithContent, CoreError> {
debug!("ITEM_SERVICE: Getting item content for id: {}", id);
let item_with_meta = self.get_item(conn, id)?;
let item_id = item_with_meta.item.id.ok_or_else(|| CoreError::InvalidInput("Item missing ID".to_string()))?;
let item_id = item_with_meta
.item
.id
.ok_or_else(|| CoreError::InvalidInput("Item missing ID".to_string()))?;
if item_id <= 0 {
return Err(CoreError::InvalidInput(format!("Invalid item ID: {}", item_id)));
return Err(CoreError::InvalidInput(format!(
"Invalid item ID: {}",
item_id
)));
}
let mut item_path = self.data_path.clone();
@@ -137,7 +154,11 @@ impl ItemService {
let content = self
.compression_service
.get_item_content(item_path, &item_with_meta.item.compression)?;
debug!("ITEM_SERVICE: Read {} bytes of content for item {}", content.len(), id);
debug!(
"ITEM_SERVICE: Read {} bytes of content for item {}",
content.len(),
id
);
Ok(ItemWithContent {
item_with_meta,
@@ -176,9 +197,8 @@ impl ItemService {
filter: Option<String>,
) -> Result<(Vec<u8>, String, bool), CoreError> {
// Use streaming approach to handle all filtering options consistently
let (mut reader, mime_type, is_binary) = self.get_item_content_info_streaming(
conn, id, filter
)?;
let (mut reader, mime_type, is_binary) =
self.get_item_content_info_streaming(conn, id, filter)?;
// Read all the filtered content into a buffer
let mut content = Vec::new();
@@ -222,13 +242,14 @@ impl ItemService {
}
// Read only the first 8192 bytes for binary detection
let mut sample_reader = self.compression_service.stream_item_content(
item_path,
compression
)?;
let mut sample_reader = self
.compression_service
.stream_item_content(item_path, compression)?;
let mut sample_buffer = vec![0; 8192];
let bytes_read = sample_reader.read(&mut sample_buffer)?;
Ok(crate::common::is_binary::is_binary(&sample_buffer[..bytes_read]))
Ok(crate::common::is_binary::is_binary(
&sample_buffer[..bytes_read],
))
}
/// Retrieves a streaming reader for item content with optional filtering.
@@ -263,8 +284,11 @@ impl ItemService {
) -> Result<(Box<dyn Read + Send>, String, bool), CoreError> {
// Convert filter string to FilterChain if provided
let filter_chain = if let Some(filter_str) = filter {
self.filter_service.create_filter_chain(Some(&filter_str))
.map_err(|e| CoreError::InvalidInput(format!("Failed to create filter chain: {}", e)))?
self.filter_service
.create_filter_chain(Some(&filter_str))
.map_err(|e| {
CoreError::InvalidInput(format!("Failed to create filter chain: {}", e))
})?
} else {
None
};
@@ -302,19 +326,24 @@ impl ItemService {
filter_chain: Option<&filter_plugin::FilterChain>,
) -> Result<(Box<dyn Read + Send>, String, bool), CoreError> {
let item_with_meta = self.get_item(conn, id)?;
let item_id = item_with_meta.item.id.ok_or_else(|| CoreError::InvalidInput("Item missing ID".to_string()))?;
let item_id = item_with_meta
.item
.id
.ok_or_else(|| CoreError::InvalidInput("Item missing ID".to_string()))?;
if item_id <= 0 {
return Err(CoreError::InvalidInput(format!("Invalid item ID: {}", item_id)));
return Err(CoreError::InvalidInput(format!(
"Invalid item ID: {}",
item_id
)));
}
let mut item_path = self.data_path.clone();
item_path.push(item_id.to_string());
let reader = self.compression_service.stream_item_content(
item_path.clone(),
&item_with_meta.item.compression
)?;
let reader = self
.compression_service
.stream_item_content(item_path.clone(), &item_with_meta.item.compression)?;
// Wrap the reader with filtering
let filtered_reader = Box::new(FilteringReader::new(reader, filter_chain.cloned()));
@@ -326,11 +355,8 @@ impl ItemService {
.unwrap_or_else(|| "application/octet-stream".to_string());
// Check if content is binary
let is_binary = self.is_content_binary(
item_path,
&item_with_meta.item.compression,
&metadata
)?;
let is_binary =
self.is_content_binary(item_path, &item_with_meta.item.compression, &metadata)?;
Ok((filtered_reader, mime_type, is_binary))
}
@@ -360,17 +386,26 @@ impl ItemService {
/// ```
/// let item = item_service.find_item(&conn, vec![1], &vec![], &HashMap::new())?;
/// ```
pub fn find_item(&self, conn: &Connection, ids: &[i64], tags: &[String], meta: &HashMap<String, String>) -> Result<ItemWithMeta, CoreError> {
debug!("ITEM_SERVICE: Finding item with ids: {:?}, tags: {:?}, meta: {:?}", ids, tags, meta);
pub fn find_item(
&self,
conn: &Connection,
ids: &[i64],
tags: &[String],
meta: &HashMap<String, String>,
) -> Result<ItemWithMeta, CoreError> {
debug!(
"ITEM_SERVICE: Finding item with ids: {:?}, tags: {:?}, meta: {:?}",
ids, tags, meta
);
let item_maybe = match (ids.is_empty(), tags.is_empty() && meta.is_empty()) {
(false, _) => {
debug!("ITEM_SERVICE: Finding by ID: {}", ids[0]);
db::get_item(conn, ids[0])?
},
}
(true, true) => {
debug!("ITEM_SERVICE: Finding last item");
db::get_item_last(conn)?
},
}
(true, false) => {
debug!("ITEM_SERVICE: Finding by tags/meta");
db::get_item_matching(conn, &tags.to_vec(), meta)?
@@ -381,11 +416,21 @@ impl ItemService {
debug!("ITEM_SERVICE: Found matching item: {:?}", item);
// Get tags and meta directly instead of calling get_item which makes redundant queries
let item_id = item.id.ok_or_else(|| CoreError::InvalidInput("Item missing ID".to_string()))?;
let item_id = item
.id
.ok_or_else(|| CoreError::InvalidInput("Item missing ID".to_string()))?;
let tags = db::get_item_tags(conn, &item)?;
debug!("ITEM_SERVICE: Found {} tags for item {}", tags.len(), item_id);
debug!(
"ITEM_SERVICE: Found {} tags for item {}",
tags.len(),
item_id
);
let meta = db::get_item_meta(conn, &item)?;
debug!("ITEM_SERVICE: Found {} meta entries for item {}", meta.len(), item_id);
debug!(
"ITEM_SERVICE: Found {} meta entries for item {}",
meta.len(),
item_id
);
Ok(ItemWithMeta { item, tags, meta })
}
@@ -413,8 +458,16 @@ impl ItemService {
/// ```
/// let items = item_service.list_items(&conn, &vec!["work"], &HashMap::new())?;
/// ```
pub fn list_items(&self, conn: &Connection, tags: &[String], meta: &HashMap<String, String>) -> Result<Vec<ItemWithMeta>, CoreError> {
debug!("ITEM_SERVICE: Listing items with tags: {:?}, meta: {:?}", tags, meta);
pub fn list_items(
&self,
conn: &Connection,
tags: &[String],
meta: &HashMap<String, String>,
) -> Result<Vec<ItemWithMeta>, CoreError> {
debug!(
"ITEM_SERVICE: Listing items with tags: {:?}, meta: {:?}",
tags, meta
);
let items = db::get_items_matching(conn, &tags.to_vec(), meta)?;
debug!("ITEM_SERVICE: Found {} matching items", items.len());
@@ -424,7 +477,10 @@ impl ItemService {
return Ok(Vec::new());
}
debug!("ITEM_SERVICE: Getting tags and meta for {} items", item_ids.len());
debug!(
"ITEM_SERVICE: Getting tags and meta for {} items",
item_ids.len()
);
let tags_map = db::get_tags_for_items(conn, &item_ids)?;
let meta_map_db = db::get_meta_for_items(conn, &item_ids)?;
@@ -433,12 +489,22 @@ impl ItemService {
let item_id = item.id.unwrap();
let tags = tags_map.get(&item_id).cloned().unwrap_or_default();
let meta_hm = meta_map_db.get(&item_id).cloned().unwrap_or_default();
let meta = meta_hm.into_iter().map(|(name, value)| Meta { id: item_id, name, value }).collect();
let meta = meta_hm
.into_iter()
.map(|(name, value)| Meta {
id: item_id,
name,
value,
})
.collect();
result.push(ItemWithMeta { item, tags, meta });
}
debug!("ITEM_SERVICE: Returning {} items with full metadata", result.len());
debug!(
"ITEM_SERVICE: Returning {} items with full metadata",
result.len()
);
Ok(result)
}
@@ -478,7 +544,13 @@ impl ItemService {
debug!("ITEM_SERVICE: Deleting file at path: {:?}", item_path);
db::delete_item(conn, item)?;
fs::remove_file(&item_path).or_else(|e| if e.kind() == std::io::ErrorKind::NotFound { Ok(()) } else { Err(e) })?;
fs::remove_file(&item_path).or_else(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
Ok(())
} else {
Err(e)
}
})?;
debug!("ITEM_SERVICE: Successfully deleted item {}", id);
Ok(())
@@ -527,7 +599,10 @@ impl ItemService {
}
let compression_type = settings_compression_type(cmd, settings);
debug!("ITEM_SERVICE: Using compression type: {:?}", compression_type);
debug!(
"ITEM_SERVICE: Using compression type: {:?}",
compression_type
);
let compression_engine = get_compression_engine(compression_type.clone())?;
let item_id;
@@ -539,7 +614,10 @@ impl ItemService {
db::set_item_tags(conn, item.clone(), tags)?;
debug!("ITEM_SERVICE: Set tags for item {}", item_id);
let item_meta = self.meta_service.collect_initial_meta();
debug!("ITEM_SERVICE: Collected {} initial meta entries", item_meta.len());
debug!(
"ITEM_SERVICE: Collected {} initial meta entries",
item_meta.len()
);
for (k, v) in item_meta.iter() {
db::add_meta(conn, item_id, k, v)?;
}
@@ -571,7 +649,8 @@ impl ItemService {
let mut plugins = self.meta_service.get_plugins(cmd, settings);
debug!("ITEM_SERVICE: Got {} meta plugins", plugins.len());
self.meta_service.initialize_plugins(&mut plugins, conn, item_id);
self.meta_service
.initialize_plugins(&mut plugins, conn, item_id);
let mut item_path = self.data_path.clone();
item_path.push(item_id.to_string());
@@ -585,11 +664,14 @@ impl ItemService {
debug!("ITEM_SERVICE: Starting to read and process input data");
loop {
let n = input.read(&mut buffer)?;
if n == 0 { break; }
if n == 0 {
break;
}
total_bytes += n as i64;
item_out.write_all(&buffer[..n])?;
self.meta_service.process_chunk(&mut plugins, &buffer[..n], conn, item_id);
self.meta_service
.process_chunk(&mut plugins, &buffer[..n], conn, item_id);
}
debug!("ITEM_SERVICE: Processed {} bytes total", total_bytes);
@@ -597,7 +679,8 @@ impl ItemService {
drop(item_out);
debug!("ITEM_SERVICE: Finalizing meta plugins");
self.meta_service.finalize_plugins(&mut plugins, conn, item_id);
self.meta_service
.finalize_plugins(&mut plugins, conn, item_id);
item.size = Some(total_bytes);
db::update_item(conn, item.clone())?;
@@ -646,8 +729,12 @@ impl ItemService {
settings: &Settings,
conn: &mut Connection,
) -> Result<ItemWithMeta, CoreError> {
debug!("ITEM_SERVICE: Starting save_item_from_mcp with {} bytes, {} tags, {} metadata entries",
content.len(), tags.len(), metadata.len());
debug!(
"ITEM_SERVICE: Starting save_item_from_mcp with {} bytes, {} tags, {} metadata entries",
content.len(),
tags.len(),
metadata.len()
);
let compression_type = CompressionType::LZ4;
let compression_engine = get_compression_engine(compression_type.clone())?;
@@ -669,7 +756,10 @@ impl ItemService {
for (key, value) in metadata {
db::add_meta(conn, item_id, key, value)?;
}
debug!("ITEM_SERVICE: Added {} custom metadata entries to MCP item", metadata.len());
debug!(
"ITEM_SERVICE: Added {} custom metadata entries to MCP item",
metadata.len()
);
}
let mut item_path = self.data_path.clone();
@@ -681,11 +771,17 @@ impl ItemService {
drop(writer);
let mut plugins = self.meta_service.get_plugins(cmd, settings);
debug!("ITEM_SERVICE: Got {} configured meta plugins for MCP item", plugins.len());
debug!(
"ITEM_SERVICE: Got {} configured meta plugins for MCP item",
plugins.len()
);
self.meta_service.initialize_plugins(&mut plugins, conn, item_id);
self.meta_service.process_chunk(&mut plugins, content, conn, item_id);
self.meta_service.finalize_plugins(&mut plugins, conn, item_id);
self.meta_service
.initialize_plugins(&mut plugins, conn, item_id);
self.meta_service
.process_chunk(&mut plugins, content, conn, item_id);
self.meta_service
.finalize_plugins(&mut plugins, conn, item_id);
debug!("ITEM_SERVICE: Processed MCP item through configured meta plugins");
item.size = Some(content.len() as i64);
@@ -713,7 +809,6 @@ impl ItemService {
pub fn get_data_path(&self) -> &PathBuf {
&self.data_path
}
}
/// A reader that applies a filter chain to the data as it's read.
@@ -793,7 +888,8 @@ impl<R: Read> Read for FilteringReader<R> {
// If we have data in our buffer, serve that first
if self.buffer_pos < self.buffer.len() {
let bytes_to_copy = std::cmp::min(buf.len(), self.buffer.len() - self.buffer_pos);
buf[..bytes_to_copy].copy_from_slice(&self.buffer[self.buffer_pos..self.buffer_pos + bytes_to_copy]);
buf[..bytes_to_copy]
.copy_from_slice(&self.buffer[self.buffer_pos..self.buffer_pos + bytes_to_copy]);
self.buffer_pos += bytes_to_copy;
return Ok(bytes_to_copy);
}

View File

@@ -16,7 +16,10 @@ impl MetaService {
pub fn get_plugins(&self, cmd: &mut Command, settings: &Settings) -> Vec<Box<dyn MetaPlugin>> {
debug!("META_SERVICE: get_plugins called");
let meta_plugin_types: Vec<MetaPluginType> = settings_meta_plugin_types(cmd, settings);
debug!("META_SERVICE: Meta plugin types from settings: {:?}", meta_plugin_types);
debug!(
"META_SERVICE: Meta plugin types from settings: {:?}",
meta_plugin_types
);
// Create plugins with their configuration
let meta_plugins: Vec<Box<dyn MetaPlugin>> = meta_plugin_types
@@ -29,7 +32,8 @@ impl MetaService {
// Get options and outputs from settings
let (options, outputs) = if let Some(meta_plugin_configs) = &settings.meta_plugins {
if let Some(config) = meta_plugin_configs.iter().find(|c| c.name == plugin_name) {
if let Some(config) = meta_plugin_configs.iter().find(|c| c.name == plugin_name)
{
// Convert options and outputs to the appropriate types
let options: std::collections::HashMap<String, serde_yaml::Value> = config
.options
@@ -65,7 +69,8 @@ impl MetaService {
item_id: i64,
) {
// Check for duplicate output names before initializing plugins
let mut output_names: std::collections::HashMap<String, Vec<String>> = std::collections::HashMap::new();
let mut output_names: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
for plugin in plugins.iter() {
let plugin_name = plugin.meta_type().to_string();
@@ -80,8 +85,9 @@ impl MetaService {
// Only track outputs that will actually be written
if !matches!(output_config, serde_yaml::Value::Bool(false)) {
output_names.entry(output_name)
.or_insert_with(Vec::new)
output_names
.entry(output_name)
.or_default()
.push(plugin_name.clone());
}
}
@@ -90,15 +96,17 @@ impl MetaService {
// Print warnings for duplicate output names
for (output_name, plugin_names) in &output_names {
if plugin_names.len() > 1 {
log::warn!("META_SERVICE: Output name '{}' is provided by multiple plugins: {}",
log::warn!(
"META_SERVICE: Output name '{}' is provided by multiple plugins: {}",
output_name,
plugin_names.join(", "));
plugin_names.join(", ")
);
}
}
for meta_plugin in plugins.iter_mut() {
let response = meta_plugin.initialize();
self.process_plugin_response(conn, item_id, meta_plugin, response);
self.process_plugin_response(conn, item_id, &mut **meta_plugin, response);
}
}
@@ -116,7 +124,7 @@ impl MetaService {
}
let response = meta_plugin.update(chunk);
self.process_plugin_response(conn, item_id, meta_plugin, response.clone());
self.process_plugin_response(conn, item_id, &mut **meta_plugin, response.clone());
// Set finalized flag if response indicates finalization
if response.is_finalized {
@@ -125,7 +133,12 @@ impl MetaService {
}
}
pub fn finalize_plugins(&self, plugins: &mut [Box<dyn MetaPlugin>], conn: &Connection, item_id: i64) {
pub fn finalize_plugins(
&self,
plugins: &mut [Box<dyn MetaPlugin>],
conn: &Connection,
item_id: i64,
) {
for meta_plugin in plugins.iter_mut() {
// Skip plugins that are already finalized
if meta_plugin.is_finalized() {
@@ -133,7 +146,7 @@ impl MetaService {
}
let response = meta_plugin.finalize();
self.process_plugin_response(conn, item_id, meta_plugin, response.clone());
self.process_plugin_response(conn, item_id, &mut **meta_plugin, response.clone());
// Set finalized flag if response indicates finalization
if response.is_finalized {
@@ -161,7 +174,7 @@ impl MetaService {
&self,
conn: &Connection,
item_id: i64,
_plugin: &Box<dyn MetaPlugin>,
_plugin: &mut dyn MetaPlugin,
response: crate::meta_plugin::MetaPluginResponse,
) {
for meta_data in response.metadata {
@@ -196,11 +209,11 @@ impl MetaService {
pub fn collect_initial_meta(&self) -> HashMap<String, String> {
let mut item_meta: HashMap<String, String> = crate::modes::common::get_meta_from_env();
if let Ok(hostname) = gethostname::gethostname().into_string() {
if !item_meta.contains_key("hostname") {
if let Ok(hostname) = gethostname::gethostname().into_string()
&& !item_meta.contains_key("hostname")
{
item_meta.insert("hostname".to_string(), hostname);
}
}
item_meta
}
}

View File

@@ -1,7 +1,7 @@
use crate::common::status::{generate_status_info, StatusInfo};
use crate::common::status::{StatusInfo, generate_status_info};
use crate::compression_engine::CompressionType;
use crate::config::Settings;
use crate::meta_plugin::MetaPluginType;
use crate::compression_engine::CompressionType;
use crate::services::filter_service::get_available_filter_plugins;
use clap::Command;
use std::path::PathBuf;
@@ -75,7 +75,8 @@ impl StatusService {
db_path: PathBuf,
) -> StatusInfo {
// Get meta plugins directly from config
let meta_plugin_types: Vec<MetaPluginType> = crate::modes::common::settings_meta_plugin_types(cmd, settings);
let meta_plugin_types: Vec<MetaPluginType> =
crate::modes::common::settings_meta_plugin_types(cmd, settings);
// Determine which compression type would be enabled for a save operation
let enabled_compression_type = if let Some(compression_name) = &settings.compression() {
@@ -84,7 +85,12 @@ impl StatusService {
Some(crate::compression_engine::default_compression_type())
};
let mut status_info = generate_status_info(data_path, db_path, &meta_plugin_types, enabled_compression_type);
let mut status_info = generate_status_info(
data_path,
db_path,
&meta_plugin_types,
enabled_compression_type,
);
// Add detailed filter plugins information
let filter_plugins_map = get_available_filter_plugins();

View File

@@ -34,7 +34,11 @@ impl ItemWithMeta {
/// assert_eq!(meta_map.get("hostname"), Some(&"example.com".to_string()));
/// ```
pub fn meta_as_map(&self) -> HashMap<String, String> {
self.meta.iter().cloned().map(|m| (m.name, m.value)).collect()
self.meta
.iter()
.cloned()
.map(|m| (m.name, m.value))
.collect()
}
}

View File

@@ -1,11 +1,11 @@
//! Common test utilities and helper functions to reduce duplication in tests
use tempfile::TempDir;
use crate::db;
use rusqlite::Connection;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use rusqlite::Connection;
use crate::db;
use tempfile::TempDir;
/// Create a temporary directory for testing
pub fn create_temp_dir() -> TempDir {
@@ -64,13 +64,18 @@ pub fn create_test_item(conn: &Connection) -> i64 {
}
/// Test compression and decompression with an engine
pub fn test_compression_engine(engine: &dyn crate::compression_engine::CompressionEngine, test_data: &[u8]) {
pub fn test_compression_engine(
engine: &dyn crate::compression_engine::CompressionEngine,
test_data: &[u8],
) {
let dir = create_temp_dir();
let file_path = dir.path().join("test_compression.dat");
// Test compression
{
let mut writer = engine.create(file_path.clone()).expect("Failed to create writer");
let mut writer = engine
.create(file_path.clone())
.expect("Failed to create writer");
writer.write_all(test_data).expect("Failed to write data");
}
@@ -95,5 +100,9 @@ pub fn assert_file_exists(file_path: &PathBuf) {
/// Assert that a file does not exist
pub fn assert_file_not_exists(file_path: &PathBuf) {
assert!(!file_path.exists(), "File {:?} should not exist but it does", file_path);
assert!(
!file_path.exists(),
"File {:?} should not exist but it does",
file_path
);
}

View File

@@ -1,7 +1,7 @@
#[cfg(test)]
mod tests {
use crate::compression_engine::gzip::CompressionEngineGZip;
use crate::compression_engine::CompressionEngine;
use crate::compression_engine::gzip::CompressionEngineGZip;
use crate::tests::common::test_helpers::test_compression_engine;
#[test]

View File

@@ -1,7 +1,7 @@
#[cfg(test)]
mod tests {
use crate::compression_engine::program::CompressionEngineProgram;
use crate::compression_engine::CompressionEngine;
use crate::compression_engine::program::CompressionEngineProgram;
#[test]
fn test_compression_engine_program_creation() {

View File

@@ -12,13 +12,31 @@ mod tests {
#[test]
fn test_compression_type_from_str() {
assert_eq!(CompressionType::from_str("lz4").unwrap(), CompressionType::LZ4);
assert_eq!(CompressionType::from_str("gzip").unwrap(), CompressionType::GZip);
assert_eq!(CompressionType::from_str("none").unwrap(), CompressionType::None);
assert_eq!(
CompressionType::from_str("lz4").unwrap(),
CompressionType::LZ4
);
assert_eq!(
CompressionType::from_str("gzip").unwrap(),
CompressionType::GZip
);
assert_eq!(
CompressionType::from_str("none").unwrap(),
CompressionType::None
);
// Test case insensitivity
assert_eq!(CompressionType::from_str("LZ4").unwrap(), CompressionType::LZ4);
assert_eq!(CompressionType::from_str("GZIP").unwrap(), CompressionType::GZip);
assert_eq!(CompressionType::from_str("NONE").unwrap(), CompressionType::None);
assert_eq!(
CompressionType::from_str("LZ4").unwrap(),
CompressionType::LZ4
);
assert_eq!(
CompressionType::from_str("GZIP").unwrap(),
CompressionType::GZip
);
assert_eq!(
CompressionType::from_str("NONE").unwrap(),
CompressionType::None
);
}
#[test]

View File

@@ -5,19 +5,16 @@ mod tests {
#[test]
fn test_compression_engine_factory() {
// Test getting different compression engines
let lz4_engine = compression_engine::get_compression_engine(
CompressionType::LZ4
).expect("Failed to get LZ4 engine");
let lz4_engine = compression_engine::get_compression_engine(CompressionType::LZ4)
.expect("Failed to get LZ4 engine");
assert!(lz4_engine.is_supported());
let gzip_engine = compression_engine::get_compression_engine(
CompressionType::GZip
).expect("Failed to get GZip engine");
let gzip_engine = compression_engine::get_compression_engine(CompressionType::GZip)
.expect("Failed to get GZip engine");
assert!(gzip_engine.is_supported());
let none_engine = compression_engine::get_compression_engine(
CompressionType::None
).expect("Failed to get None engine");
let none_engine = compression_engine::get_compression_engine(CompressionType::None)
.expect("Failed to get None engine");
assert!(none_engine.is_supported());
}

View File

@@ -1,2 +1,2 @@
pub mod factory_tests;
pub mod conversion_tests;
pub mod factory_tests;

View File

@@ -1,7 +1,7 @@
#[cfg(test)]
mod tests {
use crate::tests::common::test_helpers::create_temp_db;
use crate::db;
use crate::tests::common::test_helpers::create_temp_db;
#[test]
fn test_database_connection() {

View File

@@ -1,8 +1,8 @@
#[cfg(test)]
mod tests {
use crate::tests::common::test_helpers::{create_temp_db, create_test_item};
use crate::db;
use crate::db::Meta;
use crate::tests::common::test_helpers::{create_temp_db, create_test_item};
#[test]
fn test_database_meta_operations() {

View File

@@ -3,6 +3,6 @@
#[cfg(test)]
pub mod item_tests;
#[cfg(test)]
pub mod tag_tests;
#[cfg(test)]
pub mod meta_tests;
#[cfg(test)]
pub mod tag_tests;

View File

@@ -1,8 +1,8 @@
#[cfg(test)]
mod tests {
use crate::tests::common::test_helpers::{create_temp_db, create_test_item};
use crate::db;
use crate::db::Tag;
use crate::tests::common::test_helpers::{create_temp_db, create_test_item};
#[test]
fn test_database_tag_operations() {

View File

@@ -1,7 +1,7 @@
#[cfg(test)]
mod tests {
use crate::meta_plugin::digest::*;
use crate::meta_plugin::MetaPlugin;
use crate::meta_plugin::digest::*;
use std::io::Write;
#[test]

View File

@@ -1,8 +1,8 @@
// Meta plugin tests module
#[cfg(test)]
pub mod system_tests;
#[cfg(test)]
pub mod digest_tests;
#[cfg(test)]
pub mod program_tests;
#[cfg(test)]
pub mod system_tests;

View File

@@ -1,16 +1,12 @@
#[cfg(test)]
mod tests {
use crate::meta_plugin::program::MetaPluginProgram;
use crate::meta_plugin::MetaPlugin;
use crate::meta_plugin::program::MetaPluginProgram;
#[test]
fn test_meta_plugin_program_creation() {
let mut plugin = MetaPluginProgram::new(
"echo",
vec!["test"],
"test_plugin".to_string(),
false,
);
let mut plugin =
MetaPluginProgram::new("echo", vec!["test"], "test_plugin".to_string(), false);
assert_eq!(plugin.meta_name(), "test_plugin");
// If echo is available, it should be supported
@@ -19,12 +15,7 @@ mod tests {
#[test]
fn test_meta_plugin_program_create_writer() {
let plugin = MetaPluginProgram::new(
"cat",
vec![],
"cat_plugin".to_string(),
false,
);
let plugin = MetaPluginProgram::new("cat", vec![], "cat_plugin".to_string(), false);
// Creating a writer should work for valid programs
let result = plugin.create();

View File

@@ -1,7 +1,7 @@
#[cfg(test)]
mod tests {
use crate::meta_plugin::system::*;
use crate::meta_plugin::MetaPlugin;
use crate::meta_plugin::system::*;
#[test]
fn test_cwd_meta_plugin() {

View File

@@ -1,8 +1,8 @@
pub mod common;
pub mod compression;
pub mod compression_types;
pub mod compression_engine;
pub mod compression_types;
pub mod db;
pub mod meta_plugin;
pub mod modes;
pub mod server;
pub mod common;
pub mod db;

View File

@@ -1,6 +1,8 @@
#[cfg(test)]
mod tests {
use crate::tests::common::test_helpers::{create_temp_dir, create_empty_temp_file, assert_file_exists};
use crate::tests::common::test_helpers::{
assert_file_exists, create_empty_temp_file, create_temp_dir,
};
#[test]
fn test_delete_mode_setup() {

View File

@@ -1,6 +1,8 @@
#[cfg(test)]
mod tests {
use crate::tests::common::test_helpers::{create_temp_dir, test_temp_dir_setup, assert_file_not_exists};
use crate::tests::common::test_helpers::{
assert_file_not_exists, create_temp_dir, test_temp_dir_setup,
};
#[test]
fn test_get_mode_basic_setup() {

View File

@@ -1,6 +1,8 @@
#[cfg(test)]
mod tests {
use crate::tests::common::test_helpers::{create_temp_dir, test_file_creation, assert_file_not_exists, get_file_size};
use crate::tests::common::test_helpers::{
assert_file_not_exists, create_temp_dir, get_file_size, test_file_creation,
};
use std::path::PathBuf;
#[test]

View File

@@ -1,18 +1,18 @@
// Modes tests module
#[cfg(test)]
pub mod save_tests;
pub mod delete_tests;
#[cfg(test)]
pub mod diff_tests;
#[cfg(test)]
pub mod get_tests;
#[cfg(test)]
pub mod info_tests;
#[cfg(test)]
pub mod list_tests;
#[cfg(test)]
pub mod delete_tests;
#[cfg(test)]
pub mod update_tests;
#[cfg(test)]
pub mod info_tests;
pub mod save_tests;
#[cfg(test)]
pub mod status_tests;
#[cfg(test)]
pub mod diff_tests;
pub mod update_tests;

View File

@@ -1,12 +1,16 @@
#[cfg(test)]
mod tests {
use crate::tests::common::test_helpers::{create_temp_dir, create_temp_file_with_content, create_empty_temp_file, assert_file_exists, get_file_size};
use crate::tests::common::test_helpers::{
assert_file_exists, create_empty_temp_file, create_temp_dir, create_temp_file_with_content,
get_file_size,
};
#[test]
fn test_save_mode_basic_functionality() {
// Create a temporary directory for testing
let dir = create_temp_dir();
let file_path = create_temp_file_with_content(&dir, "test_input.txt", "test content for save mode\n");
let file_path =
create_temp_file_with_content(&dir, "test_input.txt", "test content for save mode\n");
// Verify file was created
assert_file_exists(&file_path);

View File

@@ -1,7 +1,7 @@
#[cfg(test)]
mod tests {
use axum::http::{HeaderMap, HeaderValue};
use crate::modes::server::common::check_auth;
use axum::http::{HeaderMap, HeaderValue};
#[test]
fn test_auth_with_no_password_required() {
@@ -15,7 +15,10 @@ mod tests {
#[test]
fn test_auth_with_bearer_token() {
let mut headers = HeaderMap::new();
headers.insert("authorization", HeaderValue::from_static("Bearer secret123"));
headers.insert(
"authorization",
HeaderValue::from_static("Bearer secret123"),
);
let password = Some("secret123".to_string());
@@ -26,7 +29,10 @@ mod tests {
#[test]
fn test_auth_with_invalid_bearer_token() {
let mut headers = HeaderMap::new();
headers.insert("authorization", HeaderValue::from_static("Bearer wrongtoken"));
headers.insert(
"authorization",
HeaderValue::from_static("Bearer wrongtoken"),
);
let password = Some("secret123".to_string());
@@ -38,7 +44,10 @@ mod tests {
fn test_auth_with_basic_auth() {
let mut headers = HeaderMap::new();
// Basic auth for "keep:secret123" base64 encoded
headers.insert("authorization", HeaderValue::from_static("Basic a2VlcDpzZWNyZXQxMjM="));
headers.insert(
"authorization",
HeaderValue::from_static("Basic a2VlcDpzZWNyZXQxMjM="),
);
let password = Some("secret123".to_string());
@@ -50,7 +59,10 @@ mod tests {
fn test_auth_with_invalid_basic_auth() {
let mut headers = HeaderMap::new();
// Basic auth for "keep:wrongpass" base64 encoded
headers.insert("authorization", HeaderValue::from_static("Basic a2VlcDp3cm9uZ3Bhc3M="));
headers.insert(
"authorization",
HeaderValue::from_static("Basic a2VlcDp3cm9uZ3Bhc3M="),
);
let password = Some("secret123".to_string());