feat: add client mode with streaming support
Add client mode enabling the keep CLI to connect to a remote keep server over HTTP. Local plugins (compression, meta, filters) run on the client; the server stores/retrieves binary blobs. Architecture: - Client save uses 3-thread streaming pipeline: reader thread (stdin → tee/stdout → hash → compress), OS pipe, streamer thread (pipe → chunked HTTP POST). Memory usage is O(PIPESIZE) regardless of data size. - Server accepts compress=false, meta=false, decompress=false query params for granular control of server-side processing. - Streaming body handling on server via async channel → sync reader bridge (ChannelReader). Key additions: - src/client.rs: KeepClient with post_stream() for chunked upload - src/modes/client/: save, get, list, info, delete, diff, status - --client-url / KEEP_CLIENT_URL configuration - --client-password / KEEP_CLIENT_PASSWORD for auth - os_pipe dependency for zero-copy pipe streaming Co-Authored-By: andrew/openrouter/hunter-alpha <noreply@opencode.ai>
This commit is contained in:
95
src/modes/client/get.rs
Normal file
95
src/modes/client/get.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
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)?;
|
||||
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)?;
|
||||
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
|
||||
let compression_type = CompressionType::from_str(&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 => lz4_flex::decompress_size_prepended(&raw_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("LZ4 decompression failed: {}", e))?,
|
||||
_ => 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user