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>
96 lines
2.9 KiB
Rust
96 lines
2.9 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)?;
|
|
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(())
|
|
}
|