Plumb metadata filter from client CLI through the HTTP API to the server's data_service.list_items(). The server accepts a JSON-encoded meta query parameter where null values mean 'key exists' and string values mean 'exact match'. Also fix LZ4 compression round-trip for client mode: - Explicit flush FrameEncoder before drop to avoid sending only the frame header when compress=false - Send _client_compression metadata so client knows actual compression on retrieval (server records compression=None when compress=false) - Use FrameDecoder (frame format) instead of decompress_size_prepended (size-prepended format) to match server storage format
110 lines
3.4 KiB
Rust
110 lines
3.4 KiB
Rust
use crate::client::KeepClient;
|
|
use crate::compression_engine::CompressionType;
|
|
use crate::filter_plugin::FilterChain;
|
|
use anyhow::Result;
|
|
use clap::Command;
|
|
use is_terminal::IsTerminal;
|
|
use log::debug;
|
|
use std::io::{Read, Write};
|
|
use std::str::FromStr;
|
|
|
|
pub fn mode(
|
|
client: &KeepClient,
|
|
_cmd: &mut Command,
|
|
settings: &crate::config::Settings,
|
|
ids: &[i64],
|
|
tags: &[String],
|
|
filter_chain: Option<FilterChain>,
|
|
) -> Result<(), anyhow::Error> {
|
|
debug!("CLIENT_GET: Getting item via remote server");
|
|
|
|
// Find the item ID
|
|
let item_id = if !ids.is_empty() {
|
|
ids[0]
|
|
} else if !tags.is_empty() {
|
|
// Find item by tags
|
|
let items = client.list_items(tags, "newest", 0, 1, &std::collections::HashMap::new())?;
|
|
if items.is_empty() {
|
|
return Err(anyhow::anyhow!("No items found matching tags: {:?}", tags));
|
|
}
|
|
items[0].id
|
|
} else {
|
|
// Get latest item
|
|
let items = client.list_items(&[], "newest", 0, 1, &std::collections::HashMap::new())?;
|
|
if items.is_empty() {
|
|
return Err(anyhow::anyhow!("No items found"));
|
|
}
|
|
items[0].id
|
|
};
|
|
|
|
// Get item info to determine compression type
|
|
let item_info = client.get_item_info(item_id)?;
|
|
|
|
// Get raw content from server
|
|
let (raw_bytes, compression) = client.get_item_content_raw(item_id)?;
|
|
|
|
// Check if binary content would be sent to TTY
|
|
let is_text = item_info
|
|
.metadata
|
|
.get("text")
|
|
.map(|v| v == "true")
|
|
.unwrap_or(false);
|
|
|
|
if std::io::stdout().is_terminal() && !is_text && !settings.force {
|
|
// Check if content is binary
|
|
let sample_len = std::cmp::min(raw_bytes.len(), 8192);
|
|
if crate::common::is_binary::is_binary(&raw_bytes[..sample_len]) {
|
|
return Err(anyhow::anyhow!(
|
|
"Refusing to output binary data to a terminal. Use --force to override."
|
|
));
|
|
}
|
|
}
|
|
|
|
// Decompress locally.
|
|
// Prefer _client_compression metadata (set by client save) over the server-reported
|
|
// compression header, because when compress=false the server stores compressed bytes
|
|
// but records compression=None.
|
|
let effective_compression = item_info
|
|
.metadata
|
|
.get("_client_compression")
|
|
.map(|s| s.as_str())
|
|
.unwrap_or(&compression);
|
|
let compression_type =
|
|
CompressionType::from_str(effective_compression).unwrap_or(CompressionType::None);
|
|
|
|
let decompressed = match compression_type {
|
|
CompressionType::GZip => {
|
|
use flate2::read::GzDecoder;
|
|
let mut decoder = GzDecoder::new(&raw_bytes[..]);
|
|
let mut content = Vec::new();
|
|
decoder.read_to_end(&mut content)?;
|
|
content
|
|
}
|
|
CompressionType::LZ4 => {
|
|
use lz4_flex::frame::FrameDecoder;
|
|
let mut decoder = FrameDecoder::new(&raw_bytes[..]);
|
|
let mut content = Vec::new();
|
|
decoder.read_to_end(&mut content)?;
|
|
content
|
|
}
|
|
_ => raw_bytes,
|
|
};
|
|
|
|
// Apply filters if present
|
|
let output = if let Some(mut chain) = filter_chain {
|
|
let mut filtered = Vec::new();
|
|
chain.filter(&mut &decompressed[..], &mut filtered)?;
|
|
filtered
|
|
} else {
|
|
decompressed
|
|
};
|
|
|
|
// Stream to stdout
|
|
let stdout = std::io::stdout();
|
|
let mut stdout = stdout.lock();
|
|
stdout.write_all(&output)?;
|
|
stdout.flush()?;
|
|
|
|
Ok(())
|
|
}
|