feat: add export/import modes, unify service layer, fix binary detection
Export/import:
- Add --export and --import modes for both local and client paths
- Use strfmt crate for --export-filename-format templates ({id}, {tags}, {ts}, {compression})
- Import preserves original timestamps via server ?ts= param
- --import-data-file for file-based import; stdin fallback streams with PIPESIZE buffers
Service unification:
- Merge SyncDataService unique methods into ItemService (delete_item now returns Result<Item>)
- Delete AsyncDataService, AsyncItemService, DataService trait (dead code / async-blocking anti-pattern)
- All server handlers use spawn_blocking + ItemService directly
- Extract shared types (ExportMeta, ImportMeta) and helpers (resolve_item_id(s), check_binary_tty)
Binary detection fix:
- Replace broken metadata.get("map") + is_binary(&[]) with actual content sampling
- Both as_meta and allow_binary paths read PIPESIZE sample before deciding
- Never load entire item into memory for binary check
Other fixes:
- Fix lock consistency: all handlers use blocking_lock() in spawn_blocking (no mixed lock().await)
- Use ISO 8601 format for {ts} in export filenames
- Fix resolve_item_ids returning only 1 item for tag lookups
- Fix client get.rs triple-buffering and export.rs whole-file buffering
- Add KeepClient::get_item_content_stream() for streaming reads
- Pass all clippy --features server lints (Path vs PathBuf, &mut conn, etc.)
This commit is contained in:
@@ -16,11 +16,14 @@ use crate::compression_engine::CompressionType;
|
||||
/// ```
|
||||
use crate::config;
|
||||
use crate::meta_plugin::MetaPluginType;
|
||||
use anyhow::{Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
use clap::Command;
|
||||
use clap::error::ErrorKind;
|
||||
use comfy_table::{Attribute, Cell, ContentArrangement, Table};
|
||||
use log::debug;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::io::IsTerminal;
|
||||
@@ -618,3 +621,111 @@ pub fn build_path_table(path_info: &PathInfo, table_config: &config::TableConfig
|
||||
|
||||
path_table
|
||||
}
|
||||
|
||||
/// Sanitize tags for use in filenames.
|
||||
///
|
||||
/// Replaces non-alphanumeric characters with underscores and joins with `_`.
|
||||
/// Empty tags are filtered out to avoid double underscores.
|
||||
pub fn sanitize_tags(tags: &[String]) -> String {
|
||||
tags.iter()
|
||||
.filter(|t| !t.is_empty())
|
||||
.map(|t| {
|
||||
t.chars()
|
||||
.map(|c| if c.is_alphanumeric() { c } else { '_' })
|
||||
.collect::<String>()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("_")
|
||||
}
|
||||
|
||||
/// Metadata structure for export to YAML. Shared by local and client export modes.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ExportMeta {
|
||||
pub ts: DateTime<Utc>,
|
||||
pub compression: String,
|
||||
pub size: Option<i64>,
|
||||
pub tags: Vec<String>,
|
||||
pub metadata: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Metadata structure for import from YAML. Shared by local and client import modes.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ImportMeta {
|
||||
pub ts: DateTime<Utc>,
|
||||
pub compression: String,
|
||||
#[serde(default)]
|
||||
pub size: Option<i64>,
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub metadata: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Resolve a single item ID from explicit IDs, tags, or latest item.
|
||||
///
|
||||
/// Returns the first ID if provided, the newest item matching tags,
|
||||
/// or the newest item overall if neither is specified.
|
||||
pub fn resolve_item_id(
|
||||
client: &crate::client::KeepClient,
|
||||
ids: &[i64],
|
||||
tags: &[String],
|
||||
) -> Result<i64> {
|
||||
if !ids.is_empty() {
|
||||
Ok(ids[0])
|
||||
} else if !tags.is_empty() {
|
||||
let items = client.list_items(tags, "newest", 0, 1, &HashMap::new())?;
|
||||
if items.is_empty() {
|
||||
return Err(anyhow!("No items found matching tags: {:?}", tags));
|
||||
}
|
||||
Ok(items[0].id)
|
||||
} else {
|
||||
let items = client.list_items(&[], "newest", 0, 1, &HashMap::new())?;
|
||||
if items.is_empty() {
|
||||
return Err(anyhow!("No items found"));
|
||||
}
|
||||
Ok(items[0].id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve item IDs from explicit IDs or tags (multi-item variant).
|
||||
pub fn resolve_item_ids(
|
||||
client: &crate::client::KeepClient,
|
||||
ids: &[i64],
|
||||
tags: &[String],
|
||||
) -> Result<Vec<i64>> {
|
||||
if !ids.is_empty() {
|
||||
Ok(ids.to_vec())
|
||||
} else if !tags.is_empty() {
|
||||
let items = client.list_items(tags, "newest", 0, 0, &HashMap::new())?;
|
||||
if items.is_empty() {
|
||||
return Err(anyhow!("No items found matching tags: {:?}", tags));
|
||||
}
|
||||
Ok(items.into_iter().map(|i| i.id).collect())
|
||||
} else {
|
||||
let items = client.list_items(&[], "newest", 0, 1, &HashMap::new())?;
|
||||
if items.is_empty() {
|
||||
return Err(anyhow!("No items found"));
|
||||
}
|
||||
Ok(vec![items[0].id])
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if binary content should be blocked from TTY output.
|
||||
///
|
||||
/// Uses metadata `text` field as fast path, then falls back to byte sampling.
|
||||
/// Returns Err if content is binary and should not be displayed.
|
||||
pub fn check_binary_tty(
|
||||
metadata: &HashMap<String, String>,
|
||||
data_sample: &[u8],
|
||||
force: bool,
|
||||
) -> Result<()> {
|
||||
if force || !std::io::stdout().is_terminal() {
|
||||
return Ok(());
|
||||
}
|
||||
if crate::common::is_binary::is_content_binary_from_metadata(metadata, data_sample) {
|
||||
return Err(anyhow!(
|
||||
"Refusing to output binary data to TTY, use --force to override"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user