Compare commits
67 Commits
096b907a23
...
d3e0b86a91
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3e0b86a91 | ||
|
|
2098d163e5 | ||
|
|
55d97a7ea1 | ||
|
|
86dabbdbc0 | ||
|
|
6f27530b3b | ||
|
|
ab48d19dcd | ||
|
|
ab7dc4c34f | ||
|
|
40f07b6915 | ||
|
|
50de138e23 | ||
|
|
f8c896fa25 | ||
|
|
ff588b9db2 | ||
|
|
da0a771683 | ||
|
|
d5fb446763 | ||
|
|
c4e037d9c6 | ||
|
|
e2bcdd2acf | ||
|
|
f5149cfb68 | ||
|
|
e8eaf7aeb1 | ||
|
|
6588c827be | ||
|
|
f58336e67c | ||
|
|
b97e79ed2f | ||
|
|
68d182ee0b | ||
|
|
dc550c3f35 | ||
|
|
ac531354d5 | ||
|
|
29aa477417 | ||
|
|
37654eb911 | ||
|
|
96aef4f02c | ||
|
|
118e02c56e | ||
|
|
e292bfa886 | ||
|
|
f7ee3a0796 | ||
|
|
bb28b4f41a | ||
|
|
7ed3291e97 | ||
|
|
ed8bf0f7fc | ||
|
|
0a06098796 | ||
|
|
e9f97b1ffd | ||
|
|
00b34cb3f7 | ||
|
|
35ae5776c0 | ||
|
|
08f3c3e9e5 | ||
|
|
7b9d127ff0 | ||
|
|
b5b101ee35 | ||
|
|
91af08b48c | ||
|
|
badcd66217 | ||
|
|
4be4831334 | ||
|
|
64789ef48b | ||
|
|
741d0f19cc | ||
|
|
380eb59094 | ||
|
|
e6dad42c6e | ||
|
|
7c1c5bd9c9 | ||
|
|
0d1ae9ff12 | ||
|
|
9f93d6965f | ||
|
|
e390139425 | ||
|
|
a5bc9373a9 | ||
|
|
0f06d31423 | ||
|
|
7210aa08d0 | ||
|
|
d4370563c3 | ||
|
|
0e68e5ff03 | ||
|
|
469e3640b8 | ||
|
|
cacf843da7 | ||
|
|
38f2caaf1b | ||
|
|
e1c0c81445 | ||
|
|
58f047ba6d | ||
|
|
498f3e0b9d | ||
|
|
cb408bafa1 | ||
|
|
a3eb9e7056 | ||
|
|
2be895fea5 | ||
|
|
6804429c9f | ||
|
|
f88897000f | ||
|
|
84fdffd97d |
@@ -40,6 +40,14 @@ sha2 = "0.10.0"
|
||||
local-ip-address = "0.5.5"
|
||||
dns-lookup = "2.0.2"
|
||||
uzers = "0.11.3"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.142"
|
||||
serde_yaml = "0.9.34"
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
axum = "0.7"
|
||||
tower = "0.4"
|
||||
tower-http = { version = "0.5", features = ["cors", "fs", "trace"] }
|
||||
hyper = { version = "1.0", features = ["full"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.3.0"
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
- `modes/info.rs` - Show detailed item information
|
||||
- `modes/diff.rs` - Compare two items
|
||||
- `modes/status.rs` - Show system status and capabilities
|
||||
- `modes/server.rs` - REST HTTP server mode with OpenAPI documentation
|
||||
- `modes/common.rs` - Shared utilities for all modes
|
||||
|
||||
### Database Module
|
||||
|
||||
81
PLAN.md
Normal file
81
PLAN.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Code Quality Issues and Fixes
|
||||
|
||||
## Critical Issues
|
||||
|
||||
### 1. Memory Safety & Resource Leaks - DONE
|
||||
**Files affected:** `src/modes/diff.rs`, `src/compression_engine/program.rs`
|
||||
**Functions affected:** `mode_diff()`, `CompressionEngineProgram::open()`, `CompressionEngineProgram::create()`
|
||||
**Problem example:** Raw file descriptors converted with `unsafe { std::fs::File::from_raw_fd(fd_write) }` without proper cleanup on errors
|
||||
**Fix example:** Use RAII wrappers or ensure proper cleanup in Drop implementations and error paths
|
||||
|
||||
### 2. Error Handling Problems - DONE
|
||||
**Files affected:** `src/modes/save.rs`, `src/modes/update.rs`, `src/db.rs`
|
||||
**Functions affected:** `mode_save()`, `mode_update()`, `get_item()`, `insert_item()`
|
||||
**Problem example:** `item.id.unwrap()` can panic if item.id is None
|
||||
**Fix example:** Replace with `item.id.ok_or_else(|| anyhow!("Item missing ID"))?`
|
||||
|
||||
### 3. Concurrency Issues - DONE
|
||||
**Files affected:** `src/modes/diff.rs`, `src/meta_plugin/digest.rs`
|
||||
**Functions affected:** `mode_diff()`, meta plugin `update()` methods
|
||||
**Problem example:** In `mode_diff()`, if writer threads panic, resources may not be cleaned up properly: `writer_thread_a.join()` only propagates panic but doesn't ensure file descriptors are closed
|
||||
**Fix example:** Use RAII guards or ensure cleanup in panic handlers: `let _fd_guard = FileDescriptorGuard::new(fd_write);`
|
||||
|
||||
## Design Problems
|
||||
|
||||
### 4. Database Design Issues - DONE
|
||||
**Files affected:** `src/db.rs`, `src/modes/save.rs`, `src/modes/update.rs`
|
||||
**Functions affected:** `insert_item()`, `update_item()`, `store_meta()`, `set_item_tags()`
|
||||
**Problem example:** Multiple database operations without transactions can leave partial state
|
||||
**Fix example:** Wrap related operations in `conn.transaction()` blocks
|
||||
|
||||
### 5. Plugin Architecture Flaws
|
||||
**Files affected:** `src/meta_plugin.rs`, `src/meta_plugin/digest.rs`, `src/meta_plugin/program.rs`
|
||||
**Functions affected:** `MetaPlugin::create()`, `MetaPlugin::update()`, `MetaPlugin::finalize()`
|
||||
**Problem example:**
|
||||
- `create()` returns dummy writer that's never used, inconsistent with actual usage pattern
|
||||
- `MetaPluginProgram::finalize()` spawns new process instead of reusing existing one
|
||||
- No validation that meta plugins produce valid output formats
|
||||
- Plugin errors are silently ignored in save operations
|
||||
**Fix example:**
|
||||
- Remove `create()` method and rely only on `update()`/`finalize()` pattern
|
||||
- Reuse single process per plugin instance for better performance
|
||||
- Add output validation and proper error propagation
|
||||
|
||||
### 6. Security Concerns - DONE
|
||||
**Files affected:** `src/main.rs`, `src/modes/get.rs`, `src/modes/delete.rs`
|
||||
**Functions affected:** `main()`, `mode_get()`, `mode_delete()`
|
||||
**Problem example:** Item IDs used directly in file paths without validation: `item_path.push(item_id.to_string())`
|
||||
**Fix example:** Validate item IDs are positive integers and sanitize file paths
|
||||
|
||||
## Performance Issues
|
||||
|
||||
### 7. Inefficient Operations
|
||||
**Files affected:** `src/modes/save.rs`, `src/compression_engine.rs`
|
||||
**Functions affected:** `mode_save()`, `CompressionEngine::size()`
|
||||
**Problem example:** Fixed BUFSIZ buffer (typically 8KB) may not be optimal for all scenarios, especially large files or fast storage
|
||||
**Fix example:** Use adaptive buffer sizing based on file size or storage characteristics, or allow configuration via environment variable
|
||||
|
||||
### 8. I/O Problems
|
||||
**Files affected:** `src/meta_plugin/program.rs`, `src/compression_engine/program.rs`
|
||||
**Functions affected:** `MetaPluginProgram::finalize()`, `CompressionEngineProgram::open()`, `CompressionEngineProgram::create()`
|
||||
**Problem example:** Meta plugin processes can block indefinitely if they hang or produce large output without proper timeouts
|
||||
**Fix example:** Add timeouts to process operations and non-blocking I/O for meta plugins: `process.wait_timeout(Duration::from_secs(30))`
|
||||
|
||||
## Code Quality Issues
|
||||
|
||||
### 9. Error Messages
|
||||
**Files affected:** `src/modes/common.rs`, `src/main.rs`
|
||||
**Functions affected:** `cmd_args_digest_type()`, `cmd_args_compression_type()`, `main()`
|
||||
**Problem example:** `format!("Unknown digest type: {}", digest_name)` exposes internal terminology
|
||||
**Fix example:** `format!("Invalid digest algorithm '{}'. Use 'sha256' or 'md5'", digest_name)`
|
||||
|
||||
### 10. Code Organization
|
||||
**Files affected:** `src/modes/save.rs`, `src/modes/diff.rs`
|
||||
**Functions affected:** `mode_save()`, `mode_diff()`
|
||||
**Problem example:** Large functions doing multiple responsibilities
|
||||
**Fix example:** Split into smaller functions:
|
||||
- `src/modes/save.rs: mode_save()` → `setup_compression_and_plugins()`, `process_input_stream()`, `finalize_meta_plugins()`, `save_item_to_database()`
|
||||
- `src/modes/diff.rs: mode_diff()` → `validate_diff_args()`, `setup_diff_pipes()`, `spawn_writer_threads()`, `execute_diff_command()`, `handle_diff_output()`
|
||||
- `src/modes/diff.rs: write_item_to_pipe()` → `open_item_reader()`, `copy_item_data()`
|
||||
|
||||
|
||||
217
src/common.rs
Normal file
217
src/common.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
use std::io::Read;
|
||||
|
||||
/// 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 {
|
||||
if data.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// First check for known binary file signatures
|
||||
if has_binary_signature(data) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for UTF-16 BOM (text)
|
||||
if data.len() >= 2 {
|
||||
if (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 {
|
||||
return false; // UTF-8 with BOM is text
|
||||
}
|
||||
|
||||
// Check if it's valid UTF-8
|
||||
if std::str::from_utf8(data).is_ok() {
|
||||
// Valid UTF-8, check printable character ratio
|
||||
return calculate_printable_ratio(data) < 0.7;
|
||||
}
|
||||
|
||||
// Not valid UTF-8, check if it might be UTF-16 without BOM
|
||||
if looks_like_utf16(data) {
|
||||
return false; // Likely UTF-16 text
|
||||
}
|
||||
|
||||
// Check for TAR format (special case with no magic number)
|
||||
if looks_like_tar(data) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Final fallback: check printable character ratio
|
||||
// For 1KB of random data, we expect very few printable characters
|
||||
calculate_printable_ratio(data) < 0.7
|
||||
}
|
||||
|
||||
/// Check for known binary file signatures
|
||||
fn has_binary_signature(data: &[u8]) -> bool {
|
||||
// Define binary file signatures with their minimum required lengths
|
||||
let signatures: &[(&[u8], usize)] = &[
|
||||
// Image formats
|
||||
(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A], 8), // PNG
|
||||
(&[0xFF, 0xD8, 0xFF], 3), // JPEG (various subtypes)
|
||||
(&[0x47, 0x49, 0x46, 0x38, 0x37, 0x61], 6), // GIF87a
|
||||
(&[0x47, 0x49, 0x46, 0x38, 0x39, 0x61], 6), // GIF89a
|
||||
(&[0x42, 0x4D], 2), // BMP
|
||||
(&[0x00, 0x00, 0x01, 0x00], 4), // ICO
|
||||
(&[0x49, 0x49, 0x2A, 0x00], 4), // TIFF (little endian)
|
||||
(&[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
|
||||
(&[0xFF, 0xF3], 2), // MP3
|
||||
(&[0xFF, 0xF2], 2), // MP3
|
||||
(&[0x4F, 0x67, 0x67, 0x53], 4), // OGG
|
||||
(&[0x66, 0x74, 0x79, 0x70], 4), // MP4/M4A/MOV (at offset 4)
|
||||
(&[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)
|
||||
(&[0x50, 0x4B, 0x07, 0x08], 4), // ZIP (spanned)
|
||||
(&[0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00], 7), // RAR v1.5+
|
||||
(&[0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01, 0x00], 8), // RAR v5.0+
|
||||
(&[0x1F, 0x8B], 2), // GZIP
|
||||
(&[0x42, 0x5A, 0x68], 3), // BZIP2
|
||||
(&[0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00], 6), // XZ
|
||||
(&[0x28, 0xB5, 0x2F, 0xFD], 4), // Zstandard
|
||||
(&[0x04, 0x22, 0x4D, 0x18], 4), // LZ4
|
||||
(&[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
|
||||
(&[0xCA, 0xFE, 0xBA, 0xBE], 4), // Mach-O (big endian)
|
||||
(&[0xFE, 0xED, 0xFA, 0xCE], 4), // Mach-O 32-bit (little endian)
|
||||
(&[0xFE, 0xED, 0xFA, 0xCF], 4), // Mach-O 64-bit (little endian)
|
||||
(&[0xCE, 0xFA, 0xED, 0xFE], 4), // Mach-O 32-bit (big endian)
|
||||
(&[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
|
||||
(&[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
|
||||
|
||||
// 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)
|
||||
(&[0x78, 0x9C], 2), // zlib (best compression)
|
||||
(&[0x78, 0xDA], 2), // zlib (fast compression)
|
||||
(&[0x62, 0x76, 0x78, 0x32], 4), // LZFSE
|
||||
];
|
||||
|
||||
for (signature, min_len) in signatures {
|
||||
if data.len() >= *min_len && data.starts_with(signature) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Special case: check for ftyp box in MP4/MOV files (at offset 4)
|
||||
if data.len() >= 8 && &data[4..8] == b"ftyp" {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Check if data looks like UTF-16 without BOM
|
||||
fn looks_like_utf16(data: &[u8]) -> bool {
|
||||
if data.len() < 4 || data.len() % 2 != 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut zero_count = 0;
|
||||
let pairs = data.len() / 2;
|
||||
|
||||
// Check if every other byte is zero (indicating UTF-16)
|
||||
for i in 0..pairs {
|
||||
if data[i * 2 + 1] == 0 {
|
||||
zero_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// If more than 50% of odd positions are zero, might be UTF-16
|
||||
zero_count as f64 / pairs as f64 > 0.5
|
||||
}
|
||||
|
||||
/// Check if data looks like a TAR archive
|
||||
fn looks_like_tar(data: &[u8]) -> bool {
|
||||
if data.len() < 512 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TAR header structure validation
|
||||
// Filename should not start with null
|
||||
if data[0] == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check file mode field (should be octal digits)
|
||||
for i in 100..108 {
|
||||
if data[i] != 0 && (data[i] < b'0' || data[i] > b'7') && data[i] != b' ' {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 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' ' {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check magic field for POSIX TAR
|
||||
if data.len() >= 265 {
|
||||
let magic = &data[257..262];
|
||||
if magic == b"ustar" {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
has_reasonable_structure
|
||||
}
|
||||
|
||||
/// 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();
|
||||
|
||||
printable_count as f64 / data.len() as f64
|
||||
}
|
||||
@@ -6,10 +6,52 @@ use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
|
||||
use crate::compression_engine::CompressionEngine;
|
||||
|
||||
pub struct ProgramReader {
|
||||
process: Child,
|
||||
stdout: Option<std::process::ChildStdout>,
|
||||
}
|
||||
|
||||
impl Read for ProgramReader {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
self.stdout.as_mut().unwrap().read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ProgramReader {
|
||||
fn drop(&mut self) {
|
||||
// Ensure the process is waited on to prevent zombie processes
|
||||
let _ = self.process.wait();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProgramWriter {
|
||||
process: Child,
|
||||
stdin: Option<std::process::ChildStdin>,
|
||||
}
|
||||
|
||||
impl Write for ProgramWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
self.stdin.as_mut().unwrap().write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
self.stdin.as_mut().unwrap().flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ProgramWriter {
|
||||
fn drop(&mut self) {
|
||||
// Close stdin to signal EOF to the child process
|
||||
drop(self.stdin.take());
|
||||
// Ensure the process is waited on to prevent zombie processes
|
||||
let _ = self.process.wait();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub struct CompressionEngineProgram {
|
||||
pub program: String,
|
||||
@@ -72,7 +114,7 @@ impl CompressionEngine for CompressionEngineProgram {
|
||||
|
||||
let file = File::open(file_path).context("Unable to open file for reading")?;
|
||||
|
||||
let process = Command::new(program.clone())
|
||||
let mut process = Command::new(program.clone())
|
||||
.args(args.clone())
|
||||
.stdin(file)
|
||||
.stdout(Stdio::piped())
|
||||
@@ -82,11 +124,19 @@ impl CompressionEngine for CompressionEngineProgram {
|
||||
program,
|
||||
args
|
||||
))?;
|
||||
Ok(Box::new(process.stdout.unwrap()))
|
||||
|
||||
let stdout = process.stdout.take().ok_or_else(|| {
|
||||
anyhow!("Failed to capture stdout from child process")
|
||||
})?;
|
||||
|
||||
Ok(Box::new(ProgramReader {
|
||||
process,
|
||||
stdout: Some(stdout),
|
||||
}))
|
||||
}
|
||||
|
||||
fn create(&self, file_path: PathBuf) -> Result<Box<dyn Write>> {
|
||||
debug!("COMPRESSION: Writting to {:?} using {:?}", file_path, *self);
|
||||
debug!("COMPRESSION: Writing to {:?} using {:?}", file_path, *self);
|
||||
|
||||
let program = self.program.clone();
|
||||
let args = self.compress.clone();
|
||||
@@ -98,7 +148,7 @@ impl CompressionEngine for CompressionEngineProgram {
|
||||
|
||||
let file = File::create(file_path).context("Unable to open file for writing")?;
|
||||
|
||||
let process = Command::new(program.clone())
|
||||
let mut process = Command::new(program.clone())
|
||||
.args(args.clone())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(file)
|
||||
@@ -109,6 +159,13 @@ impl CompressionEngine for CompressionEngineProgram {
|
||||
args
|
||||
))?;
|
||||
|
||||
Ok(Box::new(process.stdin.unwrap()))
|
||||
let stdin = process.stdin.take().ok_or_else(|| {
|
||||
anyhow!("Failed to capture stdin from child process")
|
||||
})?;
|
||||
|
||||
Ok(Box::new(ProgramWriter {
|
||||
process,
|
||||
stdin: Some(stdin),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
64
src/main.rs
64
src/main.rs
@@ -2,6 +2,7 @@ use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Context, Error, Result, anyhow};
|
||||
use clap::*;
|
||||
use clap::error::ErrorKind;
|
||||
use log::*;
|
||||
mod modes;
|
||||
|
||||
@@ -21,11 +22,16 @@ pub mod meta_plugin;
|
||||
//pub mod item;
|
||||
|
||||
extern crate term;
|
||||
extern crate serde_json;
|
||||
extern crate serde_yaml;
|
||||
extern crate serde;
|
||||
|
||||
mod common;
|
||||
|
||||
/**
|
||||
* Main struct for command-line arguments.
|
||||
*/
|
||||
#[derive(Parser, Debug)]
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
pub struct Args {
|
||||
#[command(flatten)]
|
||||
@@ -42,7 +48,7 @@ pub struct Args {
|
||||
/**
|
||||
* Struct for mode-specific arguments.
|
||||
*/
|
||||
#[derive(Parser, Debug)]
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
struct ModeArgs {
|
||||
#[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["get", "diff", "list", "update", "delete", "info", "status"]))]
|
||||
#[arg(help("Save an item using any tags or metadata provided"))]
|
||||
@@ -76,15 +82,19 @@ struct ModeArgs {
|
||||
))]
|
||||
info: bool,
|
||||
|
||||
#[arg(group("mode"), help_heading("Mode Options"), short('S'), long, conflicts_with_all(["save", "get", "diff", "list", "update", "delete", "info"]))]
|
||||
#[arg(group("mode"), help_heading("Mode Options"), short('S'), long, conflicts_with_all(["save", "get", "diff", "list", "update", "delete", "info", "server"]))]
|
||||
#[arg(help("Show status of directories and supported compression algorithms"))]
|
||||
status: bool,
|
||||
|
||||
#[arg(group("mode"), help_heading("Mode Options"), long, conflicts_with_all(["save", "get", "diff", "list", "update", "delete", "info", "status"]))]
|
||||
#[arg(help("Start REST HTTP server on specified address:port or socket path"))]
|
||||
server: Option<String>,
|
||||
}
|
||||
|
||||
/**
|
||||
* Struct for item-specific arguments.
|
||||
*/
|
||||
#[derive(Parser, Debug)]
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
struct ItemArgs {
|
||||
#[arg(help_heading("Item Options"), short, long, conflicts_with_all(["get", "delete", "status"]))]
|
||||
#[arg(help(
|
||||
@@ -105,10 +115,11 @@ struct ItemArgs {
|
||||
meta_plugins: Vec<String>,
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Struct for general options.
|
||||
*/
|
||||
#[derive(Parser, Debug)]
|
||||
#[derive(Parser, Debug, Default, Clone)]
|
||||
struct OptionsArgs {
|
||||
#[arg(long, env("KEEP_DIR"))]
|
||||
#[arg(help("Specify the directory to use for storage"))]
|
||||
@@ -133,6 +144,17 @@ struct OptionsArgs {
|
||||
#[arg(short, long)]
|
||||
#[arg(help("Do not show any messages"))]
|
||||
quiet: bool,
|
||||
|
||||
#[arg(long, value_enum, default_value("table"))]
|
||||
#[arg(help("Output format (only works with --info, --status, --list)"))]
|
||||
output_format: Option<String>,
|
||||
|
||||
#[arg(long, env("KEEP_SERVER_PASSWORD"))]
|
||||
#[arg(help("Password for server authentication (requires --server)"))]
|
||||
server_password: Option<String>,
|
||||
|
||||
#[arg(long, help("Force output even when binary data would be sent to a TTY"))]
|
||||
force: bool,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,6 +171,7 @@ enum KeepModes {
|
||||
Delete,
|
||||
Info,
|
||||
Status,
|
||||
Server,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,6 +265,8 @@ fn main() -> Result<(), Error> {
|
||||
mode = KeepModes::Info;
|
||||
} else if args.mode.status {
|
||||
mode = KeepModes::Status;
|
||||
} else if args.mode.server.is_some() {
|
||||
mode = KeepModes::Server;
|
||||
}
|
||||
|
||||
if mode == KeepModes::Unknown {
|
||||
@@ -252,6 +277,32 @@ fn main() -> Result<(), Error> {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate output format usage
|
||||
if let Some(output_format_str) = &args.options.output_format {
|
||||
if output_format_str != "table" && mode != KeepModes::Info && mode != KeepModes::Status && mode != KeepModes::List {
|
||||
cmd.error(
|
||||
ErrorKind::InvalidValue,
|
||||
"--output-format can only be used with --info, --status, or --list modes"
|
||||
).exit();
|
||||
}
|
||||
}
|
||||
|
||||
// Validate human-readable usage
|
||||
if args.options.human_readable && mode != KeepModes::List && mode != KeepModes::Info {
|
||||
cmd.error(
|
||||
ErrorKind::InvalidValue,
|
||||
"--human-readable can only be used with --list and --info modes"
|
||||
).exit();
|
||||
}
|
||||
|
||||
// Validate server password usage
|
||||
if args.options.server_password.is_some() && mode != KeepModes::Server {
|
||||
cmd.error(
|
||||
ErrorKind::InvalidValue,
|
||||
"--server-password can only be used with --server mode"
|
||||
).exit();
|
||||
}
|
||||
|
||||
debug!("MAIN: args: {:?}", args);
|
||||
debug!("MAIN: ids: {:?}", ids);
|
||||
debug!("MAIN: tags: {:?}", tags);
|
||||
@@ -306,6 +357,9 @@ fn main() -> Result<(), Error> {
|
||||
KeepModes::Status => {
|
||||
crate::modes::status::mode_status(&mut cmd, &args, data_path, db_path)?
|
||||
}
|
||||
KeepModes::Server => {
|
||||
crate::modes::server::mode_server(&mut cmd, &args, &mut conn, data_path)?
|
||||
}
|
||||
_ => todo!(),
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ pub mod system;
|
||||
|
||||
use crate::meta_plugin::program::MetaPluginProgram;
|
||||
use crate::meta_plugin::digest::{DigestSha256MetaPlugin, ReadTimeMetaPlugin, ReadRateMetaPlugin};
|
||||
use crate::meta_plugin::system::{CwdMetaPlugin, UidMetaPlugin, UserMetaPlugin, GidMetaPlugin, GroupMetaPlugin, ShellMetaPlugin, ShellPidMetaPlugin, KeepPidMetaPlugin, HostnameMetaPlugin, FullHostnameMetaPlugin};
|
||||
use crate::meta_plugin::system::{CwdMetaPlugin, BinaryMetaPlugin, UidMetaPlugin, UserMetaPlugin, GidMetaPlugin, GroupMetaPlugin, ShellMetaPlugin, ShellPidMetaPlugin, KeepPidMetaPlugin, HostnameMetaPlugin, FullHostnameMetaPlugin};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, strum::EnumIter, strum::Display, strum::EnumString)]
|
||||
#[strum(ascii_case_insensitive)]
|
||||
@@ -20,6 +20,7 @@ pub enum MetaPluginType {
|
||||
LineCount,
|
||||
WordCount,
|
||||
Cwd,
|
||||
Binary,
|
||||
Uid,
|
||||
User,
|
||||
Gid,
|
||||
@@ -40,6 +41,10 @@ pub trait MetaPlugin {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_internal(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn create(&self) -> Result<Box<dyn Write>>;
|
||||
fn finalize(&mut self) -> io::Result<String>;
|
||||
|
||||
@@ -62,6 +67,7 @@ pub fn get_meta_plugin(meta_plugin_type: MetaPluginType) -> Box<dyn MetaPlugin>
|
||||
MetaPluginType::LineCount => Box::new(MetaPluginProgram::new("wc", vec!["-l"], "line_count".to_string(), true)),
|
||||
MetaPluginType::WordCount => Box::new(MetaPluginProgram::new("wc", vec!["-w"], "word_count".to_string(), true)),
|
||||
MetaPluginType::Cwd => Box::new(CwdMetaPlugin::new()),
|
||||
MetaPluginType::Binary => Box::new(BinaryMetaPlugin::new()),
|
||||
MetaPluginType::Uid => Box::new(UidMetaPlugin::new()),
|
||||
MetaPluginType::User => Box::new(UserMetaPlugin::new()),
|
||||
MetaPluginType::Gid => Box::new(GidMetaPlugin::new()),
|
||||
|
||||
@@ -22,6 +22,10 @@ impl DigestSha256MetaPlugin {
|
||||
}
|
||||
|
||||
impl MetaPlugin for DigestSha256MetaPlugin {
|
||||
fn is_internal(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn create(&self) -> Result<Box<dyn Write>> {
|
||||
// For meta plugins, we don't actually create a writer since we're buffering data internally
|
||||
// This method is required by the trait but not used in the same way as digest engines
|
||||
@@ -72,6 +76,10 @@ impl ReadTimeMetaPlugin {
|
||||
}
|
||||
|
||||
impl MetaPlugin for ReadTimeMetaPlugin {
|
||||
fn is_internal(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn create(&self) -> Result<Box<dyn Write>> {
|
||||
// For meta plugins, we don't actually create a writer since we're buffering data internally
|
||||
Ok(Box::new(DummyWriter))
|
||||
@@ -115,6 +123,10 @@ impl ReadRateMetaPlugin {
|
||||
}
|
||||
|
||||
impl MetaPlugin for ReadRateMetaPlugin {
|
||||
fn is_internal(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn create(&self) -> Result<Box<dyn Write>> {
|
||||
// For meta plugins, we don't actually create a writer since we're buffering data internally
|
||||
Ok(Box::new(DummyWriter))
|
||||
|
||||
@@ -41,6 +41,10 @@ impl MetaPlugin for MetaPluginProgram {
|
||||
self.supported
|
||||
}
|
||||
|
||||
fn is_internal(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn create(&self) -> Result<Box<dyn Write>> {
|
||||
debug!("META: Writing using {:?}", *self);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use std::env;
|
||||
use std::process;
|
||||
use uzers::{get_current_uid, get_current_gid, get_current_username, get_current_groupname};
|
||||
|
||||
use crate::common::is_binary;
|
||||
use crate::meta_plugin::MetaPlugin;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
@@ -15,6 +16,52 @@ pub struct CwdMetaPlugin {
|
||||
meta_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct BinaryMetaPlugin {
|
||||
meta_name: String,
|
||||
buffer: Vec<u8>,
|
||||
max_buffer_size: usize,
|
||||
}
|
||||
|
||||
impl BinaryMetaPlugin {
|
||||
pub fn new() -> BinaryMetaPlugin {
|
||||
BinaryMetaPlugin {
|
||||
meta_name: "binary".to_string(),
|
||||
buffer: Vec::new(),
|
||||
max_buffer_size: 4096, // 4KB
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl MetaPlugin for BinaryMetaPlugin {
|
||||
fn is_internal(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn create(&self) -> Result<Box<dyn Write>> {
|
||||
Ok(Box::new(io::sink()))
|
||||
}
|
||||
|
||||
fn finalize(&mut self) -> io::Result<String> {
|
||||
let is_binary = is_binary(&self.buffer);
|
||||
Ok(if is_binary { "true".to_string() } else { "false".to_string() })
|
||||
}
|
||||
|
||||
fn update(&mut self, data: &[u8]) {
|
||||
// Only collect up to max_buffer_size
|
||||
let remaining_capacity = self.max_buffer_size.saturating_sub(self.buffer.len());
|
||||
if remaining_capacity > 0 {
|
||||
let bytes_to_copy = std::cmp::min(data.len(), remaining_capacity);
|
||||
self.buffer.extend_from_slice(&data[..bytes_to_copy]);
|
||||
}
|
||||
}
|
||||
|
||||
fn meta_name(&mut self) -> String {
|
||||
self.meta_name.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl CwdMetaPlugin {
|
||||
pub fn new() -> CwdMetaPlugin {
|
||||
CwdMetaPlugin {
|
||||
@@ -24,6 +71,10 @@ impl CwdMetaPlugin {
|
||||
}
|
||||
|
||||
impl MetaPlugin for CwdMetaPlugin {
|
||||
fn is_internal(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn create(&self) -> Result<Box<dyn Write>> {
|
||||
Ok(Box::new(io::sink()))
|
||||
}
|
||||
@@ -58,6 +109,10 @@ impl UidMetaPlugin {
|
||||
}
|
||||
|
||||
impl MetaPlugin for UidMetaPlugin {
|
||||
fn is_internal(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn create(&self) -> Result<Box<dyn Write>> {
|
||||
Ok(Box::new(io::sink()))
|
||||
}
|
||||
@@ -89,6 +144,10 @@ impl UserMetaPlugin {
|
||||
}
|
||||
|
||||
impl MetaPlugin for UserMetaPlugin {
|
||||
fn is_internal(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn create(&self) -> Result<Box<dyn Write>> {
|
||||
Ok(Box::new(io::sink()))
|
||||
}
|
||||
@@ -123,6 +182,10 @@ impl GidMetaPlugin {
|
||||
}
|
||||
|
||||
impl MetaPlugin for GidMetaPlugin {
|
||||
fn is_internal(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn create(&self) -> Result<Box<dyn Write>> {
|
||||
Ok(Box::new(io::sink()))
|
||||
}
|
||||
@@ -154,6 +217,10 @@ impl GroupMetaPlugin {
|
||||
}
|
||||
|
||||
impl MetaPlugin for GroupMetaPlugin {
|
||||
fn is_internal(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn create(&self) -> Result<Box<dyn Write>> {
|
||||
Ok(Box::new(io::sink()))
|
||||
}
|
||||
@@ -188,6 +255,10 @@ impl ShellMetaPlugin {
|
||||
}
|
||||
|
||||
impl MetaPlugin for ShellMetaPlugin {
|
||||
fn is_internal(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn create(&self) -> Result<Box<dyn Write>> {
|
||||
Ok(Box::new(io::sink()))
|
||||
}
|
||||
@@ -222,6 +293,10 @@ impl ShellPidMetaPlugin {
|
||||
}
|
||||
|
||||
impl MetaPlugin for ShellPidMetaPlugin {
|
||||
fn is_internal(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn create(&self) -> Result<Box<dyn Write>> {
|
||||
Ok(Box::new(io::sink()))
|
||||
}
|
||||
@@ -256,6 +331,10 @@ impl KeepPidMetaPlugin {
|
||||
}
|
||||
|
||||
impl MetaPlugin for KeepPidMetaPlugin {
|
||||
fn is_internal(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn create(&self) -> Result<Box<dyn Write>> {
|
||||
Ok(Box::new(io::sink()))
|
||||
}
|
||||
@@ -287,6 +366,10 @@ impl HostnameMetaPlugin {
|
||||
}
|
||||
|
||||
impl MetaPlugin for HostnameMetaPlugin {
|
||||
fn is_internal(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn create(&self) -> Result<Box<dyn Write>> {
|
||||
Ok(Box::new(io::sink()))
|
||||
}
|
||||
@@ -321,6 +404,10 @@ impl FullHostnameMetaPlugin {
|
||||
}
|
||||
|
||||
impl MetaPlugin for FullHostnameMetaPlugin {
|
||||
fn is_internal(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn create(&self) -> Result<Box<dyn Write>> {
|
||||
Ok(Box::new(io::sink()))
|
||||
}
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
use crate::Args;
|
||||
use crate::compression_engine::CompressionType;
|
||||
use crate::db::Item;
|
||||
use crate::db::Meta;
|
||||
use crate::db::store_meta;
|
||||
use crate::meta_plugin::MetaPluginType;
|
||||
use clap::Command;
|
||||
use clap::error::ErrorKind;
|
||||
use humansize::{BINARY, FormatSizeOptions};
|
||||
use log::debug;
|
||||
use prettytable::format::TableFormat;
|
||||
use regex::Regex;
|
||||
use rusqlite::Connection;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::str::FromStr;
|
||||
use strum::IntoEnumIterator;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn get_meta_from_env() -> HashMap<String, String> {
|
||||
debug!("COMMON: Getting meta from KEEP_META_*");
|
||||
@@ -34,8 +30,28 @@ pub fn get_meta_from_env() -> HashMap<String, String> {
|
||||
}
|
||||
|
||||
pub fn format_size_human_readable(size: u64) -> String {
|
||||
let options = FormatSizeOptions::from(BINARY).decimal_places(1);
|
||||
humansize::format_size(size, options)
|
||||
const UNITS: &[&str] = &["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei"];
|
||||
const THRESHOLD: u64 = 1024;
|
||||
|
||||
if size == 0 {
|
||||
return "0".to_string();
|
||||
}
|
||||
|
||||
let mut size_f = size as f64;
|
||||
let mut unit_index = 0;
|
||||
|
||||
while size_f >= THRESHOLD as f64 && unit_index < UNITS.len() - 1 {
|
||||
size_f /= THRESHOLD as f64;
|
||||
unit_index += 1;
|
||||
}
|
||||
|
||||
if unit_index == 0 {
|
||||
format!("{}", size)
|
||||
} else if size_f.fract() == 0.0 {
|
||||
format!("{}{}", size_f as u64, UNITS[unit_index])
|
||||
} else {
|
||||
format!("{:.1}{}", size_f, UNITS[unit_index])
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_size(size: u64, human_readable: bool) -> String {
|
||||
@@ -107,38 +123,6 @@ pub fn get_digest_type_meta(digest_type: MetaPluginType) -> String {
|
||||
format!("digest_{}", digest_type.to_string().to_lowercase())
|
||||
}
|
||||
|
||||
pub fn store_item_meta_value(
|
||||
conn: &mut Connection,
|
||||
item: Item,
|
||||
meta_name: String,
|
||||
meta_value: String,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
// Save digest to meta
|
||||
let meta = Meta {
|
||||
id: item.id.unwrap(),
|
||||
name: meta_name,
|
||||
value: meta_value,
|
||||
};
|
||||
store_meta(conn, meta)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn store_item_digest_value(
|
||||
conn: &mut Connection,
|
||||
item: Item,
|
||||
digest_type: MetaPluginType,
|
||||
digest_value: String,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
// Save digest to meta
|
||||
let digest_meta_name = get_digest_type_meta(digest_type);
|
||||
let digest_meta = Meta {
|
||||
id: item.id.unwrap(),
|
||||
name: digest_meta_name,
|
||||
value: digest_value,
|
||||
};
|
||||
store_meta(conn, digest_meta)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cmd_args_digest_type(cmd: &mut Command, args: &Args) -> MetaPluginType {
|
||||
let digest_name = args
|
||||
@@ -151,7 +135,7 @@ pub fn cmd_args_digest_type(cmd: &mut Command, args: &Args) -> MetaPluginType {
|
||||
if digest_type_opt.is_err() {
|
||||
cmd.error(
|
||||
ErrorKind::InvalidValue,
|
||||
format!("Unknown digest type: {}", digest_name),
|
||||
format!("Invalid digest algorithm '{}'. Use 'sha256' or 'md5'", digest_name),
|
||||
)
|
||||
.exit();
|
||||
}
|
||||
@@ -170,7 +154,7 @@ pub fn cmd_args_compression_type(cmd: &mut Command, args: &Args) -> CompressionT
|
||||
if compression_type_opt.is_err() {
|
||||
cmd.error(
|
||||
ErrorKind::InvalidValue,
|
||||
format!("Unknown compression type: {}", compression_name),
|
||||
format!("Invalid compression algorithm '{}'. Supported algorithms: lz4, gzip, xz, zstd", compression_name),
|
||||
)
|
||||
.exit();
|
||||
}
|
||||
@@ -178,6 +162,33 @@ pub fn cmd_args_compression_type(cmd: &mut Command, args: &Args) -> CompressionT
|
||||
compression_type_opt.unwrap()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum OutputFormat {
|
||||
Table,
|
||||
Json,
|
||||
Yaml,
|
||||
}
|
||||
|
||||
impl FromStr for OutputFormat {
|
||||
type Err = anyhow::Error;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"table" => Ok(OutputFormat::Table),
|
||||
"json" => Ok(OutputFormat::Json),
|
||||
"yaml" => Ok(OutputFormat::Yaml),
|
||||
_ => Err(anyhow::anyhow!("Invalid output format. Supported formats: table, json, yaml")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_output_format(args: &Args) -> OutputFormat {
|
||||
args.options.output_format
|
||||
.as_ref()
|
||||
.and_then(|s| OutputFormat::from_str(s).ok())
|
||||
.unwrap_or(OutputFormat::Table)
|
||||
}
|
||||
|
||||
pub fn cmd_args_meta_plugin_types(cmd: &mut Command, args: &Args) -> Vec<MetaPluginType> {
|
||||
let mut meta_plugin_types = Vec::new();
|
||||
|
||||
|
||||
@@ -35,6 +35,11 @@ pub fn mode_delete(
|
||||
debug!("MAIN: Found item {:?}", item);
|
||||
db::delete_item(conn, item)?;
|
||||
|
||||
// Validate that item ID is positive to prevent path traversal issues
|
||||
if *item_id <= 0 {
|
||||
return Err(anyhow!("Invalid item ID: {}", item_id));
|
||||
}
|
||||
|
||||
let mut item_path = data_path.clone();
|
||||
item_path.push(item_id.to_string());
|
||||
|
||||
|
||||
@@ -1,25 +1,10 @@
|
||||
use crate::compression_engine::{CompressionType, get_compression_engine};
|
||||
use libc::c_int;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::Command;
|
||||
use nix::Error as NixError;
|
||||
use nix::fcntl::FdFlag;
|
||||
use nix::unistd::{close, pipe};
|
||||
use std::io::Read;
|
||||
use std::os::fd::FromRawFd;
|
||||
use std::process::Stdio;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub fn mode_diff(
|
||||
cmd: &mut Command,
|
||||
_args: &crate::Args,
|
||||
ids: &mut Vec<i64>,
|
||||
tags: &mut Vec<String>,
|
||||
conn: &mut rusqlite::Connection,
|
||||
data_path: PathBuf,
|
||||
) -> Result<()> {
|
||||
fn validate_diff_args(cmd: &mut Command, ids: &Vec<i64>, tags: &Vec<String>) {
|
||||
if !tags.is_empty() {
|
||||
cmd.error(
|
||||
clap::error::ErrorKind::InvalidValue,
|
||||
@@ -34,57 +19,107 @@ pub fn mode_diff(
|
||||
)
|
||||
.exit();
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_and_validate_items(
|
||||
conn: &mut rusqlite::Connection,
|
||||
ids: &Vec<i64>,
|
||||
) -> Result<(crate::db::Item, crate::db::Item), anyhow::Error> {
|
||||
// Fetch items, ensuring they exist.
|
||||
let item_a = crate::db::get_item(conn, ids[0])?
|
||||
.ok_or_else(|| anyhow!("Unable to find first item (ID: {}) in database", ids[0]))?;
|
||||
.ok_or_else(|| anyhow::anyhow!("Unable to find first item (ID: {}) in database", ids[0]))?;
|
||||
let item_b = crate::db::get_item(conn, ids[1])?
|
||||
.ok_or_else(|| anyhow!("Unable to find second item (ID: {}) in database", ids[1]))?;
|
||||
.ok_or_else(|| anyhow::anyhow!("Unable to find second item (ID: {}) in database", ids[1]))?;
|
||||
|
||||
log::debug!("MAIN: Found item A {:?}", item_a);
|
||||
log::debug!("MAIN: Found item B {:?}", item_b);
|
||||
|
||||
let item_a_tags: Vec<String> = crate::db::get_item_tags(conn, &item_a)?
|
||||
.into_iter()
|
||||
.map(|x| x.name)
|
||||
.collect();
|
||||
let item_a_id = item_a.id.ok_or_else(|| anyhow!("Item A missing ID"))?;
|
||||
let item_b_id = item_b.id.ok_or_else(|| anyhow!("Item B missing ID"))?;
|
||||
|
||||
let item_b_tags: Vec<String> = crate::db::get_item_tags(conn, &item_b)?
|
||||
// Validate that item IDs are positive to prevent path traversal issues
|
||||
if item_a_id <= 0 || item_b_id <= 0 {
|
||||
return Err(anyhow::anyhow!("Invalid item ID: {} or {}", item_a_id, item_b_id));
|
||||
}
|
||||
|
||||
Ok((item_a, item_b))
|
||||
}
|
||||
|
||||
fn get_item_tags(conn: &mut rusqlite::Connection, item: &crate::db::Item) -> Result<Vec<String>, anyhow::Error> {
|
||||
let tags: Vec<String> = crate::db::get_item_tags(conn, item)?
|
||||
.into_iter()
|
||||
.map(|x| x.name)
|
||||
.collect();
|
||||
Ok(tags)
|
||||
}
|
||||
|
||||
fn setup_diff_paths_and_compression(
|
||||
data_path: &std::path::PathBuf,
|
||||
item_a: &crate::db::Item,
|
||||
item_b: &crate::db::Item,
|
||||
) -> Result<(std::path::PathBuf, crate::compression_engine::CompressionType, std::path::PathBuf, crate::compression_engine::CompressionType), anyhow::Error> {
|
||||
let item_a_id = item_a.id.ok_or_else(|| anyhow::anyhow!("Item A missing ID"))?;
|
||||
let item_b_id = item_b.id.ok_or_else(|| anyhow::anyhow!("Item B missing ID"))?;
|
||||
|
||||
let mut item_path_a = data_path.clone();
|
||||
item_path_a.push(item_a.id.unwrap().to_string()); // id.unwrap() is safe due to ok_or_else
|
||||
let compression_type_a = CompressionType::from_str(&item_a.compression)?;
|
||||
item_path_a.push(item_a_id.to_string());
|
||||
let compression_type_a = crate::compression_engine::CompressionType::from_str(&item_a.compression)?;
|
||||
log::debug!("MAIN: Item A has compression type {:?}", compression_type_a);
|
||||
|
||||
let mut item_path_b = data_path.clone();
|
||||
item_path_b.push(item_b.id.unwrap().to_string());
|
||||
let compression_type_b = CompressionType::from_str(&item_b.compression)?;
|
||||
item_path_b.push(item_b_id.to_string());
|
||||
let compression_type_b = crate::compression_engine::CompressionType::from_str(&item_b.compression)?;
|
||||
log::debug!("MAIN: Item B has compression type {:?}", compression_type_b);
|
||||
|
||||
Ok((item_path_a, compression_type_a, item_path_b, compression_type_b))
|
||||
}
|
||||
|
||||
fn setup_diff_pipes() -> Result<((libc::c_int, libc::c_int), (libc::c_int, libc::c_int)), anyhow::Error> {
|
||||
use nix::unistd::pipe;
|
||||
use nix::Error as NixError;
|
||||
|
||||
// Create pipes for diff's input
|
||||
let (fd_a_read, fd_a_write) =
|
||||
pipe().map_err(|e: NixError| anyhow!("Failed to create pipe A: {}", e))?;
|
||||
pipe().map_err(|e: NixError| anyhow::anyhow!("Failed to create pipe A: {}", e))?;
|
||||
let (fd_b_read, fd_b_write) =
|
||||
pipe().map_err(|e: NixError| anyhow!("Failed to create pipe B: {}", e))?;
|
||||
pipe().map_err(|e: NixError| anyhow::anyhow!("Failed to create pipe B: {}", e))?;
|
||||
|
||||
// Set FD_CLOEXEC on write ends. While they are consumed by File::from_raw_fd,
|
||||
// it's good practice if the raw FDs were to be handled further before that.
|
||||
// For this specific code, since from_raw_fd takes ownership immediately, this is less critical
|
||||
// but doesn't hurt.
|
||||
nix::fcntl::fcntl(
|
||||
Ok(((fd_a_read, fd_a_write), (fd_b_read, fd_b_write)))
|
||||
}
|
||||
|
||||
fn setup_fd_guards(fd_a_read: libc::c_int, fd_b_read: libc::c_int) -> (FdGuard, FdGuard) {
|
||||
// Wrap file descriptors in RAII guards
|
||||
let fd_a_read_guard = FdGuard::new(fd_a_read);
|
||||
let fd_b_read_guard = FdGuard::new(fd_b_read);
|
||||
(fd_a_read_guard, fd_b_read_guard)
|
||||
}
|
||||
|
||||
fn set_fd_cloexec(fd_a_write: libc::c_int, fd_b_write: libc::c_int) -> Result<(), anyhow::Error> {
|
||||
use nix::fcntl::{fcntl, FcntlArg, FdFlag};
|
||||
|
||||
// Set FD_CLOEXEC on write ends
|
||||
fcntl(
|
||||
fd_a_write,
|
||||
nix::fcntl::FcntlArg::F_SETFD(FdFlag::FD_CLOEXEC),
|
||||
FcntlArg::F_SETFD(FdFlag::FD_CLOEXEC),
|
||||
)
|
||||
.map_err(|e| anyhow!("Failed to set FD_CLOEXEC on fd_a_write: {}", e))?;
|
||||
nix::fcntl::fcntl(
|
||||
.map_err(|e| anyhow::anyhow!("Failed to set FD_CLOEXEC on fd_a_write: {}", e))?;
|
||||
fcntl(
|
||||
fd_b_write,
|
||||
nix::fcntl::FcntlArg::F_SETFD(FdFlag::FD_CLOEXEC),
|
||||
FcntlArg::F_SETFD(FdFlag::FD_CLOEXEC),
|
||||
)
|
||||
.map_err(|e| anyhow!("Failed to set FD_CLOEXEC on fd_b_write: {}", e))?;
|
||||
.map_err(|e| anyhow::anyhow!("Failed to set FD_CLOEXEC on fd_b_write: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spawn_diff_process(
|
||||
item_a_id: i64,
|
||||
item_a_tags: Vec<String>,
|
||||
item_b_id: i64,
|
||||
item_b_tags: Vec<String>,
|
||||
fd_a_read: libc::c_int,
|
||||
fd_b_read: libc::c_int,
|
||||
) -> Result<std::process::Child, anyhow::Error> {
|
||||
log::debug!("MAIN: Creating child process for diff");
|
||||
let mut diff_command = std::process::Command::new("diff");
|
||||
diff_command
|
||||
@@ -92,28 +127,78 @@ pub fn mode_diff(
|
||||
.arg("--label")
|
||||
.arg(format!(
|
||||
"Keep item A: {} {}",
|
||||
item_a.id.unwrap(),
|
||||
item_a_id,
|
||||
item_a_tags.join(" ")
|
||||
))
|
||||
.arg(format!("/dev/fd/{}", fd_a_read))
|
||||
.arg("--label")
|
||||
.arg(format!(
|
||||
"Keep item B: {} {}",
|
||||
item_b.id.unwrap(),
|
||||
item_b_id,
|
||||
item_b_tags.join(" ")
|
||||
))
|
||||
.arg(format!("/dev/fd/{}", fd_b_read))
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped());
|
||||
|
||||
let mut child_process = diff_command
|
||||
let child_process = diff_command
|
||||
.spawn()
|
||||
.map_err(|e| anyhow!("Failed to execute diff command: {}", e))?;
|
||||
.map_err(|e| anyhow::anyhow!("Failed to execute diff command: {}", e))?;
|
||||
|
||||
close(fd_a_read).map_err(|e| anyhow!("Failed to close fd_a_read in parent: {}", e))?;
|
||||
close(fd_b_read).map_err(|e| anyhow!("Failed to close fd_b_read in parent: {}", e))?;
|
||||
Ok(child_process)
|
||||
}
|
||||
|
||||
// RAII guard for file descriptors to ensure they're closed
|
||||
struct FdGuard {
|
||||
fd: libc::c_int,
|
||||
}
|
||||
|
||||
impl FdGuard {
|
||||
fn new(fd: libc::c_int) -> Self {
|
||||
Self { fd }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FdGuard {
|
||||
fn drop(&mut self) {
|
||||
let _ = nix::unistd::close(self.fd);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a function to write item data to a pipe
|
||||
fn write_item_to_pipe(
|
||||
item_path: std::path::PathBuf,
|
||||
compression_type: crate::compression_engine::CompressionType,
|
||||
pipe_writer_raw: std::fs::File,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
use std::io::BufWriter;
|
||||
let mut buffered_pipe_writer = BufWriter::new(pipe_writer_raw);
|
||||
let engine =
|
||||
crate::compression_engine::get_compression_engine(compression_type).expect("Unable to get compression engine");
|
||||
log::debug!("THREAD: Sending item to diff");
|
||||
engine
|
||||
.copy(item_path, &mut buffered_pipe_writer)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to copy/compress item: {}", e))?;
|
||||
log::debug!("THREAD: Done sending item to diff");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Function to spawn a writer thread for an item
|
||||
fn spawn_writer_thread(
|
||||
item_path: std::path::PathBuf,
|
||||
compression_type: crate::compression_engine::CompressionType,
|
||||
fd_write: libc::c_int,
|
||||
) -> std::thread::JoinHandle<Result<(), anyhow::Error>> {
|
||||
let pipe_writer_raw = unsafe { std::fs::File::from_raw_fd(fd_write) };
|
||||
std::thread::spawn(move || {
|
||||
write_item_to_pipe(item_path, compression_type, pipe_writer_raw)
|
||||
})
|
||||
}
|
||||
|
||||
fn execute_diff_command(
|
||||
child_process: &mut std::process::Child,
|
||||
) -> Result<(Vec<u8>, Vec<u8>), anyhow::Error> {
|
||||
let mut child_stdout_pipe = child_process
|
||||
.stdout
|
||||
.take()
|
||||
@@ -125,42 +210,6 @@ pub fn mode_diff(
|
||||
|
||||
log::debug!("MAIN: Creating threads for diff I/O");
|
||||
|
||||
// Create a function to write item data to a pipe
|
||||
fn write_item_to_pipe(
|
||||
item_path: PathBuf,
|
||||
compression_type: CompressionType,
|
||||
pipe_writer_raw: std::fs::File,
|
||||
) {
|
||||
use std::io::BufWriter;
|
||||
let mut buffered_pipe_writer = BufWriter::new(pipe_writer_raw);
|
||||
let engine =
|
||||
get_compression_engine(compression_type).expect("Unable to get compression engine");
|
||||
log::debug!("THREAD: Sending item to diff");
|
||||
engine
|
||||
.copy(item_path, &mut buffered_pipe_writer)
|
||||
.expect("Failed to copy/compress item");
|
||||
log::debug!("THREAD: Done sending item to diff");
|
||||
}
|
||||
|
||||
// Function to spawn a writer thread for an item
|
||||
fn spawn_writer_thread(
|
||||
item_path: PathBuf,
|
||||
compression_type: CompressionType,
|
||||
fd_write: c_int,
|
||||
) -> std::thread::JoinHandle<()> {
|
||||
let pipe_writer_raw = unsafe { std::fs::File::from_raw_fd(fd_write) };
|
||||
std::thread::spawn(move || {
|
||||
write_item_to_pipe(item_path, compression_type, pipe_writer_raw);
|
||||
})
|
||||
}
|
||||
|
||||
// Spawn writer threads for both items
|
||||
let writer_thread_a =
|
||||
spawn_writer_thread(item_path_a.clone(), compression_type_a.clone(), fd_a_write);
|
||||
|
||||
let writer_thread_b =
|
||||
spawn_writer_thread(item_path_b.clone(), compression_type_b.clone(), fd_b_write);
|
||||
|
||||
// Thread to read diff's standard output
|
||||
let stdout_reader_thread = std::thread::spawn(move || {
|
||||
let mut output_buffer = Vec::new();
|
||||
@@ -168,7 +217,7 @@ pub fn mode_diff(
|
||||
// child_stdout_pipe is a ChildStdout, which implements std::io::Read
|
||||
child_stdout_pipe
|
||||
.read_to_end(&mut output_buffer)
|
||||
.map_err(|e| anyhow!("Failed to read diff stdout: {}", e))
|
||||
.map_err(|e| anyhow::anyhow!("Failed to read diff stdout: {}", e))
|
||||
.map(|_| output_buffer) // Return the Vec<u8> on success
|
||||
});
|
||||
|
||||
@@ -178,64 +227,33 @@ pub fn mode_diff(
|
||||
log::debug!("STDERR_READER: Reading diff stderr");
|
||||
child_stderr_pipe
|
||||
.read_to_end(&mut error_buffer)
|
||||
.map_err(|e| anyhow!("Failed to read diff stderr: {}", e))
|
||||
.map_err(|e| anyhow::anyhow!("Failed to read diff stderr: {}", e))
|
||||
.map(|_| error_buffer)
|
||||
});
|
||||
|
||||
// Wait for writer threads to complete (meaning all input has been sent to diff)
|
||||
log::debug!("MAIN: Waiting on writer thread for item A");
|
||||
if let Err(panic_payload) = writer_thread_a.join() {
|
||||
// Propagate panic from writer thread
|
||||
return Err(anyhow!(
|
||||
"Writer thread for item A (ID: {}) panicked: {:?}",
|
||||
ids[0],
|
||||
panic_payload
|
||||
));
|
||||
}
|
||||
log::debug!("MAIN: Writer thread for item A completed.");
|
||||
|
||||
log::debug!("MAIN: Waiting on writer thread for item B");
|
||||
if let Err(panic_payload) = writer_thread_b.join() {
|
||||
return Err(anyhow!(
|
||||
"Writer thread for item B (ID: {}) panicked: {:?}",
|
||||
ids[1],
|
||||
panic_payload
|
||||
));
|
||||
}
|
||||
log::debug!("MAIN: Writer thread for item B completed.");
|
||||
log::debug!("MAIN: Done waiting on input-writer threads.");
|
||||
|
||||
// Now that all input has been sent and input pipes will be closed by threads exiting,
|
||||
// wait for the diff child process to terminate.
|
||||
log::debug!("MAIN: Waiting for diff child process to finish...");
|
||||
let diff_status = child_process
|
||||
.wait()
|
||||
.map_err(|e| anyhow!("Failed to wait on diff command: {}", e))?;
|
||||
log::debug!(
|
||||
"MAIN: Diff child process finished with status: {}",
|
||||
diff_status
|
||||
);
|
||||
|
||||
// Retrieve the captured output from the reader threads.
|
||||
// .join().unwrap() here will panic if the reader thread itself panicked.
|
||||
// The inner Result is from the read_to_end operation within the thread.
|
||||
let stdout_capture_result = stdout_reader_thread
|
||||
.join()
|
||||
.unwrap_or_else(|panic_payload| {
|
||||
Err(anyhow!(
|
||||
"Stdout reader thread panicked: {:?}",
|
||||
panic_payload
|
||||
))
|
||||
})?;
|
||||
.map_err(|panic_payload| {
|
||||
anyhow::anyhow!("Stdout reader thread panicked: {:?}", panic_payload)
|
||||
})?
|
||||
.map_err(|e| anyhow::anyhow!("Failed to read diff stdout: {}", e))?;
|
||||
|
||||
let stderr_capture_result = stderr_reader_thread
|
||||
.join()
|
||||
.unwrap_or_else(|panic_payload| {
|
||||
Err(anyhow!(
|
||||
"Stderr reader thread panicked: {:?}",
|
||||
panic_payload
|
||||
))
|
||||
})?;
|
||||
.map_err(|panic_payload| {
|
||||
anyhow::anyhow!("Stderr reader thread panicked: {:?}", panic_payload)
|
||||
})?
|
||||
.map_err(|e| anyhow::anyhow!("Failed to read diff stderr: {}", e))?;
|
||||
|
||||
Ok((stdout_capture_result, stderr_capture_result))
|
||||
}
|
||||
|
||||
fn handle_diff_output(
|
||||
diff_status: std::process::ExitStatus,
|
||||
stdout_capture_result: Vec<u8>,
|
||||
stderr_capture_result: Vec<u8>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
// Handle diff's exit status and output
|
||||
match diff_status.code() {
|
||||
Some(0) => {
|
||||
@@ -267,7 +285,7 @@ pub fn mode_diff(
|
||||
String::from_utf8_lossy(&stderr_capture_result)
|
||||
);
|
||||
}
|
||||
return Err(anyhow!(
|
||||
return Err(anyhow::anyhow!(
|
||||
"Diff command reported an error (exit code {})",
|
||||
error_code
|
||||
));
|
||||
@@ -281,9 +299,107 @@ pub fn mode_diff(
|
||||
String::from_utf8_lossy(&stderr_capture_result)
|
||||
);
|
||||
}
|
||||
return Err(anyhow!("Diff command terminated by signal"));
|
||||
return Err(anyhow::anyhow!("Diff command terminated by signal"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn mode_diff(
|
||||
cmd: &mut Command,
|
||||
_args: &crate::Args,
|
||||
ids: &mut Vec<i64>,
|
||||
tags: &mut Vec<String>,
|
||||
conn: &mut rusqlite::Connection,
|
||||
data_path: std::path::PathBuf,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
validate_diff_args(cmd, ids, tags);
|
||||
let (item_a, item_b) = fetch_and_validate_items(conn, ids)?;
|
||||
|
||||
let item_a_tags = get_item_tags(conn, &item_a)?;
|
||||
let item_b_tags = get_item_tags(conn, &item_b)?;
|
||||
|
||||
let (item_path_a, compression_type_a, item_path_b, compression_type_b) =
|
||||
setup_diff_paths_and_compression(&data_path, &item_a, &item_b)?;
|
||||
|
||||
let ((fd_a_read, fd_a_write), (fd_b_read, fd_b_write)) = setup_diff_pipes()?;
|
||||
let (_fd_a_read_guard, _fd_b_read_guard) = setup_fd_guards(fd_a_read, fd_b_read);
|
||||
set_fd_cloexec(fd_a_write, fd_b_write)?;
|
||||
|
||||
let item_a_id = item_a.id.ok_or_else(|| anyhow::anyhow!("Item A missing ID"))?;
|
||||
let item_b_id = item_b.id.ok_or_else(|| anyhow::anyhow!("Item B missing ID"))?;
|
||||
|
||||
let mut child_process = spawn_diff_process(
|
||||
item_a_id,
|
||||
item_a_tags,
|
||||
item_b_id,
|
||||
item_b_tags,
|
||||
fd_a_read,
|
||||
fd_b_read,
|
||||
)?;
|
||||
|
||||
// Close read ends in parent process - they're now guarded by FdGuard
|
||||
drop(_fd_a_read_guard);
|
||||
drop(_fd_b_read_guard);
|
||||
|
||||
// Spawn writer threads for both items
|
||||
let writer_thread_a =
|
||||
spawn_writer_thread(item_path_a.clone(), compression_type_a.clone(), fd_a_write);
|
||||
|
||||
let writer_thread_b =
|
||||
spawn_writer_thread(item_path_b.clone(), compression_type_b.clone(), fd_b_write);
|
||||
|
||||
// Wait for writer threads to complete (meaning all input has been sent to diff)
|
||||
log::debug!("MAIN: Waiting on writer thread for item A");
|
||||
match writer_thread_a.join() {
|
||||
Ok(Ok(())) => {
|
||||
log::debug!("MAIN: Writer thread for item A completed successfully.");
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
return Err(anyhow::anyhow!("Writer thread for item A failed: {}", e));
|
||||
}
|
||||
Err(panic_payload) => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Writer thread for item A (ID: {}) panicked: {:?}",
|
||||
ids[0],
|
||||
panic_payload
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("MAIN: Waiting on writer thread for item B");
|
||||
match writer_thread_b.join() {
|
||||
Ok(Ok(())) => {
|
||||
log::debug!("MAIN: Writer thread for item B completed successfully.");
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
return Err(anyhow::anyhow!("Writer thread for item B failed: {}", e));
|
||||
}
|
||||
Err(panic_payload) => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Writer thread for item B (ID: {}) panicked: {:?}",
|
||||
ids[1],
|
||||
panic_payload
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("MAIN: Done waiting on input-writer threads.");
|
||||
|
||||
// Now that all input has been sent and input pipes will be closed by threads exiting,
|
||||
// wait for the diff child process to terminate.
|
||||
log::debug!("MAIN: Waiting for diff child process to finish...");
|
||||
let diff_status = child_process
|
||||
.wait()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to wait on diff command: {}", e))?;
|
||||
log::debug!(
|
||||
"MAIN: Diff child process finished with status: {}",
|
||||
diff_status
|
||||
);
|
||||
|
||||
let (stdout_capture_result, stderr_capture_result) = execute_diff_command(&mut child_process)?;
|
||||
handle_diff_output(diff_status, stdout_capture_result, stderr_capture_result)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use anyhow::anyhow;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use crate::compression_engine::{CompressionType, get_compression_engine};
|
||||
use crate::common::is_binary;
|
||||
use clap::Command;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
@@ -34,15 +36,79 @@ pub fn mode_get(
|
||||
};
|
||||
|
||||
if let Some(item) = item_maybe {
|
||||
let item_id = item.id.ok_or_else(|| anyhow!("Item missing ID"))?;
|
||||
// Validate that item ID is positive to prevent path traversal issues
|
||||
if item_id <= 0 {
|
||||
return Err(anyhow!("Invalid item ID: {}", item_id));
|
||||
}
|
||||
|
||||
let mut item_path = data_path.clone();
|
||||
item_path.push(item.id.unwrap().to_string());
|
||||
item_path.push(item_id.to_string());
|
||||
|
||||
// Determine if we should detect binary data
|
||||
let mut detect_binary = !args.options.force && is_stdout_tty();
|
||||
|
||||
// If we're detecting binary and there's binary metadata, check it
|
||||
if detect_binary {
|
||||
let item_meta = crate::db::get_item_meta(conn, &item)?;
|
||||
let binary_meta = item_meta.into_iter().find(|meta| meta.name == "binary");
|
||||
if let Some(binary_meta) = binary_meta {
|
||||
if binary_meta.value == "false" {
|
||||
// If metadata says it's not binary, don't detect
|
||||
detect_binary = false;
|
||||
} else if binary_meta.value == "true" {
|
||||
// If metadata says it's binary, error immediately
|
||||
return Err(anyhow!("Refusing to output binary data to TTY, use --force to override"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let compression_type = CompressionType::from_str(&item.compression)?;
|
||||
let compression_engine = get_compression_engine(compression_type)?;
|
||||
compression_engine.cat(item_path.clone())?;
|
||||
|
||||
// If we need to detect binary, read first 4KB and check
|
||||
if detect_binary {
|
||||
// Open the file through compression engine to read first 4KB
|
||||
let mut reader = compression_engine.open(item_path.clone())?;
|
||||
let mut buffer = [0u8; 4096];
|
||||
let bytes_read = reader.read(&mut buffer)?;
|
||||
|
||||
// Check if this data is binary
|
||||
if is_binary(&buffer[..bytes_read]) {
|
||||
return Err(anyhow!("Refusing to output binary data to TTY, use --force to override"));
|
||||
}
|
||||
|
||||
// If not binary, output the data we've read
|
||||
std::io::stdout().write_all(&buffer[..bytes_read])?;
|
||||
|
||||
// Continue reading and outputting the rest of the data
|
||||
let mut stdout = std::io::stdout();
|
||||
std::io::copy(&mut reader, &mut stdout)?;
|
||||
} else {
|
||||
// No binary detection needed, just output the data
|
||||
compression_engine.cat(item_path.clone())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("Unable to find matching item in database"))
|
||||
}
|
||||
}
|
||||
|
||||
fn is_stdout_tty() -> bool {
|
||||
#[cfg(unix)]
|
||||
unsafe {
|
||||
libc::isatty(libc::STDOUT_FILENO) != 0
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
unsafe {
|
||||
let stdout_handle = winapi::um::processenv::GetStdHandle(winapi::um::winbase::STD_OUTPUT_HANDLE);
|
||||
let mut console_mode: winapi::shared::minwindef::DWORD = 0;
|
||||
winapi::um::consoleapi::GetConsoleMode(stdout_handle, &mut console_mode) != 0
|
||||
}
|
||||
|
||||
// Fallback for non-unix platforms or if we can't determine
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
false
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use crate::db::Item;
|
||||
use crate::modes::common::format_size;
|
||||
use crate::modes::common::{format_size, get_output_format, OutputFormat};
|
||||
use anyhow::anyhow;
|
||||
use serde_json;
|
||||
use serde_yaml;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use clap::Command;
|
||||
use clap::error::ErrorKind;
|
||||
use std::path::PathBuf;
|
||||
@@ -48,6 +51,20 @@ pub fn mode_info(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ItemInfo {
|
||||
id: i64,
|
||||
timestamp: String,
|
||||
path: String,
|
||||
stream_size: Option<u64>,
|
||||
stream_size_formatted: String,
|
||||
compression: String,
|
||||
file_size: Option<u64>,
|
||||
file_size_formatted: String,
|
||||
tags: Vec<String>,
|
||||
meta: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
fn show_item(
|
||||
item: Item, // Using the provided struct definition
|
||||
args: &crate::Args,
|
||||
@@ -61,6 +78,12 @@ fn show_item(
|
||||
.map(|x| x.name)
|
||||
.collect();
|
||||
|
||||
let output_format = get_output_format(args);
|
||||
|
||||
if output_format != OutputFormat::Table {
|
||||
return show_item_structured(item, args, conn, data_path, output_format);
|
||||
}
|
||||
|
||||
let mut table = Table::new();
|
||||
if std::io::stdout().is_terminal() {
|
||||
table.set_format(get_format_box_chars_no_border_line_separator());
|
||||
@@ -136,3 +159,61 @@ fn show_item(
|
||||
table.printstd();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_item_structured(
|
||||
item: Item,
|
||||
args: &crate::Args,
|
||||
conn: &mut rusqlite::Connection,
|
||||
data_path: PathBuf,
|
||||
output_format: OutputFormat,
|
||||
) -> anyhow::Result<()> {
|
||||
let item_id = item.id.unwrap();
|
||||
let item_tags: Vec<String> = crate::db::get_item_tags(conn, &item)?
|
||||
.into_iter()
|
||||
.map(|x| x.name)
|
||||
.collect();
|
||||
|
||||
let mut item_path_buf = data_path.clone();
|
||||
item_path_buf.push(item_id.to_string());
|
||||
|
||||
let file_size = item_path_buf.metadata().map(|m| m.len()).ok();
|
||||
let file_size_formatted = match file_size {
|
||||
Some(size) => format_size(size, args.options.human_readable),
|
||||
None => "Missing".to_string(),
|
||||
};
|
||||
|
||||
let stream_size_formatted = match item.size {
|
||||
Some(size) => format_size(size as u64, args.options.human_readable),
|
||||
None => "Missing".to_string(),
|
||||
};
|
||||
|
||||
let mut meta_map = std::collections::HashMap::new();
|
||||
for meta in crate::db::get_item_meta(conn, &item)? {
|
||||
meta_map.insert(meta.name, meta.value);
|
||||
}
|
||||
|
||||
let item_info = ItemInfo {
|
||||
id: item_id,
|
||||
timestamp: item.ts.with_timezone(&chrono::Local).format("%F %T %Z").to_string(),
|
||||
path: item_path_buf.to_str().unwrap_or("").to_string(),
|
||||
stream_size: item.size.map(|s| s as u64),
|
||||
stream_size_formatted,
|
||||
compression: item.compression,
|
||||
file_size,
|
||||
file_size_formatted,
|
||||
tags: item_tags,
|
||||
meta: meta_map,
|
||||
};
|
||||
|
||||
match output_format {
|
||||
OutputFormat::Json => {
|
||||
println!("{}", serde_json::to_string_pretty(&item_info)?);
|
||||
}
|
||||
OutputFormat::Yaml => {
|
||||
println!("{}", serde_yaml::to_string(&item_info)?);
|
||||
}
|
||||
OutputFormat::Table => unreachable!(),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use crate::db::{get_items, get_items_matching};
|
||||
use crate::modes::common::ColumnType;
|
||||
use crate::modes::common::{size_column, string_column};
|
||||
use crate::modes::common::{size_column, string_column, get_output_format, OutputFormat};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use serde_yaml;
|
||||
use anyhow::anyhow;
|
||||
use log::debug;
|
||||
use prettytable::color;
|
||||
@@ -8,6 +11,20 @@ use prettytable::row;
|
||||
use prettytable::format::Alignment;
|
||||
use prettytable::{Attr, Cell, Row, Table};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ListItem {
|
||||
id: Option<i64>,
|
||||
time: String,
|
||||
size: Option<u64>,
|
||||
size_formatted: String,
|
||||
compression: String,
|
||||
file_size: Option<u64>,
|
||||
file_size_formatted: String,
|
||||
file_path: String,
|
||||
tags: Vec<String>,
|
||||
meta: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
pub fn mode_list(
|
||||
cmd: &mut clap::Command,
|
||||
args: &crate::Args,
|
||||
@@ -54,6 +71,12 @@ pub fn mode_list(
|
||||
// Fetch all metadata for all items in a single query
|
||||
let meta_by_item = crate::db::get_meta_for_items(conn, &item_ids)?;
|
||||
|
||||
let output_format = get_output_format(args);
|
||||
|
||||
if output_format != OutputFormat::Table {
|
||||
return show_list_structured(items, tags_by_item, meta_by_item, data_path, args, output_format);
|
||||
}
|
||||
|
||||
let mut table = Table::new();
|
||||
table.set_format(*prettytable::format::consts::FORMAT_CLEAN);
|
||||
|
||||
@@ -166,3 +189,61 @@ pub fn mode_list(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_list_structured(
|
||||
items: Vec<crate::db::Item>,
|
||||
tags_by_item: std::collections::HashMap<i64, Vec<String>>,
|
||||
meta_by_item: std::collections::HashMap<i64, std::collections::HashMap<String, String>>,
|
||||
data_path: std::path::PathBuf,
|
||||
args: &crate::Args,
|
||||
output_format: OutputFormat,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut list_items = Vec::new();
|
||||
|
||||
for item in items {
|
||||
let item_id = item.id.unwrap();
|
||||
let tags = tags_by_item.get(&item_id).cloned().unwrap_or_default();
|
||||
let meta = meta_by_item.get(&item_id).cloned().unwrap_or_default();
|
||||
|
||||
let mut item_path = data_path.clone();
|
||||
item_path.push(item_id.to_string());
|
||||
|
||||
let file_size = item_path.metadata().map(|m| m.len()).ok();
|
||||
let file_size_formatted = match file_size {
|
||||
Some(size) => crate::modes::common::format_size(size, args.options.human_readable),
|
||||
None => "Missing".to_string(),
|
||||
};
|
||||
|
||||
let size_formatted = match item.size {
|
||||
Some(size) => crate::modes::common::format_size(size as u64, args.options.human_readable),
|
||||
None => "Unknown".to_string(),
|
||||
};
|
||||
|
||||
let list_item = ListItem {
|
||||
id: item.id,
|
||||
time: item.ts.with_timezone(&chrono::Local).format("%F %T").to_string(),
|
||||
size: item.size.map(|s| s as u64),
|
||||
size_formatted,
|
||||
compression: item.compression,
|
||||
file_size,
|
||||
file_size_formatted,
|
||||
file_path: item_path.into_os_string().into_string().unwrap_or_default(),
|
||||
tags,
|
||||
meta,
|
||||
};
|
||||
|
||||
list_items.push(list_item);
|
||||
}
|
||||
|
||||
match output_format {
|
||||
OutputFormat::Json => {
|
||||
println!("{}", serde_json::to_string_pretty(&list_items)?);
|
||||
}
|
||||
OutputFormat::Yaml => {
|
||||
println!("{}", serde_yaml::to_string(&list_items)?);
|
||||
}
|
||||
OutputFormat::Table => unreachable!(),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@ pub mod get;
|
||||
pub mod info;
|
||||
pub mod list;
|
||||
pub mod save;
|
||||
pub mod server;
|
||||
pub mod status;
|
||||
pub mod update;
|
||||
|
||||
@@ -1,57 +1,47 @@
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use gethostname::gethostname;
|
||||
use is_terminal::IsTerminal;
|
||||
use std::collections::HashMap;
|
||||
use std::io::{self, Read, Write};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::Command;
|
||||
use clap::error::ErrorKind;
|
||||
use log::debug;
|
||||
use rusqlite::Connection;
|
||||
use std::path::PathBuf;
|
||||
use std::io::{Read, Write, IsTerminal};
|
||||
|
||||
use crate::compression_engine::get_compression_engine;
|
||||
use crate::db::{self};
|
||||
use crate::meta_plugin::{MetaPlugin, MetaPluginType, get_meta_plugin};
|
||||
use crate::modes::common::{cmd_args_compression_type, cmd_args_digest_type, cmd_args_meta_plugin_types, get_meta_from_env, store_item_meta_value};
|
||||
use chrono::Utc;
|
||||
// Import the missing functions from common module
|
||||
use crate::modes::common::{cmd_args_digest_type, cmd_args_compression_type, cmd_args_meta_plugin_types};
|
||||
|
||||
pub fn mode_save(
|
||||
cmd: &mut Command,
|
||||
args: &crate::Args,
|
||||
ids: &mut Vec<i64>,
|
||||
tags: &mut Vec<String>,
|
||||
conn: &mut Connection,
|
||||
data_path: PathBuf,
|
||||
) -> Result<()> {
|
||||
fn validate_save_args(cmd: &mut Command, ids: &Vec<i64>) {
|
||||
if !ids.is_empty() {
|
||||
cmd.error(
|
||||
ErrorKind::InvalidValue,
|
||||
clap::error::ErrorKind::InvalidValue,
|
||||
"ID given, you cannot supply IDs when using --save",
|
||||
)
|
||||
.exit();
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize_tags(tags: &mut Vec<String>) {
|
||||
if tags.is_empty() {
|
||||
tags.push("none".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_compression_and_plugins(
|
||||
cmd: &mut Command,
|
||||
args: &crate::Args,
|
||||
) -> (crate::compression_engine::CompressionType, Box<dyn crate::compression_engine::CompressionEngine>, Vec<Box<dyn crate::meta_plugin::MetaPlugin>>) {
|
||||
let digest_type = cmd_args_digest_type(cmd, &args);
|
||||
debug!("MAIN: Digest type: {:?}", digest_type);
|
||||
|
||||
let compression_type = cmd_args_compression_type(cmd, &args);
|
||||
debug!("MAIN: Compression type: {:?}", compression_type);
|
||||
let compression_engine =
|
||||
get_compression_engine(compression_type.clone()).expect("Unable to get compression engine");
|
||||
crate::compression_engine::get_compression_engine(compression_type.clone()).expect("Unable to get compression engine");
|
||||
|
||||
// Start with meta plugin types from command line
|
||||
let mut meta_plugin_types: Vec<MetaPluginType> = cmd_args_meta_plugin_types(cmd, &args);
|
||||
let mut meta_plugin_types: Vec<crate::meta_plugin::MetaPluginType> = cmd_args_meta_plugin_types(cmd, &args);
|
||||
debug!("MAIN: Meta plugin types: {:?}", meta_plugin_types);
|
||||
|
||||
// Convert digest type to meta plugin type and add to the list if needed
|
||||
let digest_meta_plugin_type = match digest_type {
|
||||
crate::meta_plugin::MetaPluginType::DigestSha256 => Some(MetaPluginType::DigestSha256),
|
||||
crate::meta_plugin::MetaPluginType::DigestMd5 => Some(MetaPluginType::DigestMd5),
|
||||
crate::meta_plugin::MetaPluginType::DigestSha256 => Some(crate::meta_plugin::MetaPluginType::DigestSha256),
|
||||
crate::meta_plugin::MetaPluginType::DigestMd5 => Some(crate::meta_plugin::MetaPluginType::DigestMd5),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
@@ -63,9 +53,9 @@ pub fn mode_save(
|
||||
}
|
||||
|
||||
// Initialize meta_plugins with MetaPlugin instances for each MetaPluginType
|
||||
let mut meta_plugins: Vec<Box<dyn MetaPlugin>> = meta_plugin_types
|
||||
let mut meta_plugins: Vec<Box<dyn crate::meta_plugin::MetaPlugin>> = meta_plugin_types
|
||||
.iter()
|
||||
.map(|meta_plugin_type| get_meta_plugin(meta_plugin_type.clone()))
|
||||
.map(|meta_plugin_type| crate::meta_plugin::get_meta_plugin(meta_plugin_type.clone()))
|
||||
.collect();
|
||||
|
||||
// Check for unsupported meta plugins, warn the user, and remove them from the list
|
||||
@@ -76,21 +66,30 @@ pub fn mode_save(
|
||||
// We need to get the meta name for the warning message
|
||||
// Since we can't mutably borrow meta_plugin here, we create a temporary one
|
||||
let meta_plugin_type = meta_plugin_types[i].clone();
|
||||
let mut temp_plugin = get_meta_plugin(meta_plugin_type);
|
||||
let mut temp_plugin = crate::meta_plugin::get_meta_plugin(meta_plugin_type);
|
||||
eprintln!("Warning: Meta plugin '{}' is enabled but not supported on this system", temp_plugin.meta_name());
|
||||
}
|
||||
i += 1;
|
||||
is_supported
|
||||
});
|
||||
|
||||
let mut item = db::Item {
|
||||
(compression_type, compression_engine, meta_plugins)
|
||||
}
|
||||
|
||||
fn create_and_log_item(
|
||||
conn: &mut rusqlite::Connection,
|
||||
args: &crate::Args,
|
||||
tags: &Vec<String>,
|
||||
compression_type: &crate::compression_engine::CompressionType,
|
||||
) -> Result<crate::db::Item, anyhow::Error> {
|
||||
let mut item = crate::db::Item {
|
||||
id: None,
|
||||
ts: Utc::now(),
|
||||
ts: chrono::Utc::now(),
|
||||
size: None,
|
||||
compression: compression_type.to_string(),
|
||||
};
|
||||
|
||||
let id = db::insert_item(conn, item.clone())?;
|
||||
let id = crate::db::insert_item(conn, item.clone())?;
|
||||
item.id = Some(id);
|
||||
debug!("MAIN: Added item {:?}", item.clone());
|
||||
|
||||
@@ -117,11 +116,23 @@ pub fn mode_save(
|
||||
}
|
||||
}
|
||||
|
||||
db::set_item_tags(conn, item.clone(), tags)?;
|
||||
Ok(item)
|
||||
}
|
||||
|
||||
let mut item_meta: HashMap<String, String> = get_meta_from_env();
|
||||
fn setup_item_metadata(
|
||||
conn: &mut rusqlite::Connection,
|
||||
_args: &crate::Args,
|
||||
item: &crate::db::Item,
|
||||
tags: &Vec<String>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
crate::db::set_item_tags(conn, item.clone(), tags)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
if let Ok(hostname) = gethostname().into_string() {
|
||||
fn collect_item_meta(args: &crate::Args) -> std::collections::HashMap<String, String> {
|
||||
let mut item_meta: std::collections::HashMap<String, String> = crate::modes::common::get_meta_from_env();
|
||||
|
||||
if let Ok(hostname) = gethostname::gethostname().into_string() {
|
||||
if !item_meta.contains_key("hostname") {
|
||||
item_meta.insert("hostname".to_string(), hostname);
|
||||
}
|
||||
@@ -132,30 +143,33 @@ pub fn mode_save(
|
||||
item_meta.insert(item.key, item.value);
|
||||
}
|
||||
|
||||
for kv in item_meta.iter() {
|
||||
let meta = db::Meta {
|
||||
id: item.id.unwrap(),
|
||||
name: kv.0.to_string(),
|
||||
value: kv.1.to_string(),
|
||||
};
|
||||
db::store_meta(conn, meta)?;
|
||||
}
|
||||
item_meta
|
||||
}
|
||||
|
||||
fn process_input_stream(
|
||||
compression_engine: &Box<dyn crate::compression_engine::CompressionEngine>,
|
||||
data_path: &std::path::PathBuf,
|
||||
item_id: i64,
|
||||
meta_plugins: &mut Vec<Box<dyn crate::meta_plugin::MetaPlugin>>,
|
||||
) -> Result<(Box<dyn std::io::Write>, crate::db::Item), anyhow::Error> {
|
||||
let mut item = crate::db::Item {
|
||||
id: Some(item_id),
|
||||
ts: chrono::Utc::now(),
|
||||
size: None,
|
||||
compression: String::new(), // Will be set later
|
||||
};
|
||||
|
||||
let mut item_path = data_path.clone();
|
||||
item_path.push(id.to_string());
|
||||
item_path.push(item_id.to_string());
|
||||
|
||||
let mut stdin = io::stdin().lock();
|
||||
let mut stdout = io::stdout().lock();
|
||||
let mut stdin = std::io::stdin().lock();
|
||||
let mut stdout = std::io::stdout().lock();
|
||||
let mut buffer = [0; libc::BUFSIZ as usize];
|
||||
|
||||
let mut item_out: Box<dyn Write> =
|
||||
let mut item_out: Box<dyn std::io::Write> =
|
||||
compression_engine
|
||||
.create(item_path.clone())
|
||||
.context(anyhow!(
|
||||
"Unable to write file {:?} using compression {:?}",
|
||||
item_path,
|
||||
compression_type
|
||||
))?;
|
||||
.map_err(|e| anyhow!("Unable to write file {:?}: {}", item_path, e))?;
|
||||
|
||||
debug!("MAIN: Starting IO loop");
|
||||
loop {
|
||||
@@ -184,12 +198,25 @@ pub fn mode_save(
|
||||
stdout.flush()?;
|
||||
item_out.flush()?;
|
||||
|
||||
Ok((item_out, item))
|
||||
}
|
||||
|
||||
fn finalize_meta_plugins(
|
||||
conn: &rusqlite::Connection,
|
||||
meta_plugins: &mut Vec<Box<dyn crate::meta_plugin::MetaPlugin>>,
|
||||
item: &crate::db::Item,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
for meta_plugin in meta_plugins.iter_mut() {
|
||||
let meta_name = meta_plugin.meta_name();
|
||||
|
||||
match meta_plugin.finalize() {
|
||||
Ok(meta_value) => {
|
||||
if let Err(e) = store_item_meta_value(conn, item.clone(), meta_name.clone(), meta_value) {
|
||||
let meta = crate::db::Meta {
|
||||
id: item.id.ok_or_else(|| anyhow!("Item missing ID"))?,
|
||||
name: meta_name.clone(),
|
||||
value: meta_value,
|
||||
};
|
||||
if let Err(e) = crate::db::store_meta(conn, meta) {
|
||||
eprintln!("Warning: Failed to store meta value for {}: {}", meta_name, e);
|
||||
}
|
||||
}
|
||||
@@ -198,8 +225,51 @@ pub fn mode_save(
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
db::update_item(conn, item.clone())?;
|
||||
pub fn mode_save(
|
||||
cmd: &mut Command,
|
||||
args: &crate::Args,
|
||||
ids: &mut Vec<i64>,
|
||||
tags: &mut Vec<String>,
|
||||
conn: &mut rusqlite::Connection,
|
||||
data_path: std::path::PathBuf,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
validate_save_args(cmd, ids);
|
||||
initialize_tags(tags);
|
||||
|
||||
let (compression_type, compression_engine, mut meta_plugins) = setup_compression_and_plugins(cmd, args);
|
||||
|
||||
let mut item = create_and_log_item(conn, args, tags, &compression_type)?;
|
||||
setup_item_metadata(conn, args, &item, tags)?; // Pass mutable reference
|
||||
|
||||
// Save as much as possible in case something breaks - don't use transactions
|
||||
// This allows partial saves to succeed even if some metadata operations fail
|
||||
let item_meta = collect_item_meta(args);
|
||||
let item_id = item.id.ok_or_else(|| anyhow!("Item missing ID"))?;
|
||||
|
||||
for kv in item_meta.iter() {
|
||||
let meta = crate::db::Meta {
|
||||
id: item_id,
|
||||
name: kv.0.to_string(),
|
||||
value: kv.1.to_string(),
|
||||
};
|
||||
crate::db::store_meta(conn, meta)?;
|
||||
}
|
||||
|
||||
let (_item_out, processed_item) = process_input_stream(
|
||||
&compression_engine,
|
||||
&data_path,
|
||||
item_id,
|
||||
&mut meta_plugins,
|
||||
)?;
|
||||
|
||||
item.size = processed_item.size;
|
||||
item.compression = compression_type.to_string();
|
||||
|
||||
finalize_meta_plugins(conn, &mut meta_plugins, &item)?;
|
||||
crate::db::update_item(conn, item.clone())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
101
src/modes/server.rs
Normal file
101
src/modes/server.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use anyhow::Result;
|
||||
use axum::{
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use clap::Command;
|
||||
use log::{debug, info, warn};
|
||||
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::trace::TraceLayer;
|
||||
|
||||
use crate::Args;
|
||||
|
||||
mod common;
|
||||
mod status;
|
||||
mod items;
|
||||
mod content;
|
||||
mod docs;
|
||||
|
||||
pub use common::{ServerConfig, AppState, logging_middleware};
|
||||
use status::handle_status;
|
||||
use items::{handle_list_items, handle_get_item, handle_put_item, handle_delete_item};
|
||||
use content::{handle_get_content_latest, handle_get_content};
|
||||
use docs::{handle_openapi, handle_swagger_ui};
|
||||
|
||||
pub fn mode_server(
|
||||
_cmd: &mut Command,
|
||||
args: &Args,
|
||||
conn: &mut rusqlite::Connection,
|
||||
data_path: PathBuf,
|
||||
) -> Result<()> {
|
||||
let server_address = args.mode.server.as_ref().unwrap();
|
||||
|
||||
let config = ServerConfig {
|
||||
address: server_address.clone(),
|
||||
password: args.options.server_password.clone(),
|
||||
};
|
||||
|
||||
// We need to move the connection into the async runtime
|
||||
let rt = tokio::runtime::Runtime::new()?;
|
||||
// Take ownership of the connection and move it into the async runtime
|
||||
let owned_conn = std::mem::replace(conn, rusqlite::Connection::open_in_memory()?);
|
||||
rt.block_on(run_server(config, owned_conn, data_path, args))
|
||||
}
|
||||
|
||||
async fn run_server(
|
||||
config: ServerConfig,
|
||||
conn: rusqlite::Connection,
|
||||
data_dir: PathBuf,
|
||||
args: &Args,
|
||||
) -> Result<()> {
|
||||
debug!("Starting REST HTTP server on {}", config.address);
|
||||
|
||||
// Use the existing database connection
|
||||
let db_conn = Arc::new(Mutex::new(conn));
|
||||
|
||||
let state = AppState {
|
||||
db: db_conn,
|
||||
data_dir: data_dir.clone(),
|
||||
password: config.password.clone(),
|
||||
args: Arc::new(args.clone()),
|
||||
};
|
||||
|
||||
let app = Router::new()
|
||||
.route("/status", get(handle_status))
|
||||
.route("/item/", get(handle_list_items).put(handle_put_item))
|
||||
.route("/item/:id", get(handle_get_item).delete(handle_delete_item))
|
||||
.route("/content", get(handle_get_content_latest))
|
||||
.route("/content/:id", get(handle_get_content))
|
||||
.route("/openapi.json", get(handle_openapi))
|
||||
.route("/swagger/", get(handle_swagger_ui))
|
||||
.layer(axum::middleware::from_fn(logging_middleware))
|
||||
.layer(
|
||||
ServiceBuilder::new()
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.layer(CorsLayer::permissive())
|
||||
)
|
||||
.with_state(state);
|
||||
|
||||
let addr: SocketAddr = if config.address.starts_with('/') || config.address.starts_with("./") {
|
||||
// Unix socket - not supported by axum directly, fall back to TCP
|
||||
warn!("Unix sockets not yet implemented, falling back to TCP on 127.0.0.1:8080");
|
||||
"127.0.0.1:8080".parse()?
|
||||
} else {
|
||||
config.address.parse()?
|
||||
};
|
||||
|
||||
info!("SERVER: HTTP server listening on {}", addr);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
axum::serve(
|
||||
listener,
|
||||
app.into_make_service_with_connect_info::<SocketAddr>()
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
94
src/modes/server/common.rs
Normal file
94
src/modes/server/common.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use anyhow::Result;
|
||||
use axum::http::HeaderMap;
|
||||
use log::info;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::Args;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServerConfig {
|
||||
pub address: String,
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
impl FromStr for ServerConfig {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(ServerConfig {
|
||||
address: s.to_string(),
|
||||
password: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub db: Arc<Mutex<rusqlite::Connection>>,
|
||||
pub data_dir: PathBuf,
|
||||
pub password: Option<String>,
|
||||
pub args: Arc<Args>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ApiResponse<T> {
|
||||
pub success: bool,
|
||||
pub data: Option<T>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ItemInfo {
|
||||
pub id: i64,
|
||||
pub ts: String,
|
||||
pub size: Option<i64>,
|
||||
pub compression: String,
|
||||
pub tags: Vec<String>,
|
||||
pub metadata: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct TagsQuery {
|
||||
pub tags: Option<String>,
|
||||
}
|
||||
|
||||
pub fn check_auth(headers: &HeaderMap, password: &Option<String>) -> bool {
|
||||
if let Some(expected_password) = password {
|
||||
if let Some(auth_header) = headers.get("authorization") {
|
||||
if let Ok(auth_str) = auth_header.to_str() {
|
||||
return auth_str.starts_with("Bearer ") && &auth_str[7..] == expected_password;
|
||||
}
|
||||
}
|
||||
false
|
||||
} else {
|
||||
true // No password required
|
||||
}
|
||||
}
|
||||
|
||||
// Custom middleware for logging requests and responses
|
||||
pub async fn logging_middleware(
|
||||
req: axum::http::Request<axum::body::Body>,
|
||||
next: axum::middleware::Next,
|
||||
) -> Result<axum::http::Response<axum::body::Body>, axum::response::Response> {
|
||||
let method = req.method().clone();
|
||||
let uri = req.uri().clone();
|
||||
let headers = req.headers().clone();
|
||||
|
||||
// Log incoming request
|
||||
info!("SERVER: {} {} - Headers: {:?}", method, uri, headers);
|
||||
|
||||
let start = Instant::now();
|
||||
let response = next.run(req).await;
|
||||
let duration = start.elapsed();
|
||||
|
||||
// Log response
|
||||
info!("SERVER: {} {} - Status: {} - Duration: {:?}", method, uri, response.status(), duration);
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
181
src/modes/server/content.rs
Normal file
181
src/modes/server/content.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use axum::{
|
||||
extract::{ConnectInfo, Path, Query, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::Json,
|
||||
};
|
||||
use log::warn;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::compression_engine::{CompressionType, get_compression_engine};
|
||||
use crate::db;
|
||||
use super::common::{AppState, ApiResponse, TagsQuery, check_auth};
|
||||
|
||||
pub async fn handle_get_content_latest(
|
||||
State(state): State<AppState>,
|
||||
Query(params): Query<TagsQuery>,
|
||||
headers: HeaderMap,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
) -> Result<Json<ApiResponse<String>>, StatusCode> {
|
||||
if !check_auth(&headers, &state.password) {
|
||||
warn!("Unauthorized request to /content from {}", addr);
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
let mut conn = state.db.lock().await;
|
||||
|
||||
let item = if let Some(tags_str) = params.tags {
|
||||
let tags: Vec<String> = tags_str.split(',').map(|t| t.trim().to_string()).collect();
|
||||
db::get_item_matching(&mut *conn, &tags, &HashMap::new())
|
||||
.map_err(|e| {
|
||||
warn!("Failed to get item matching tags {:?} for content: {}", tags, e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?
|
||||
} else {
|
||||
db::get_item_last(&mut *conn).map_err(|e| {
|
||||
warn!("Failed to get last item for content: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?
|
||||
};
|
||||
|
||||
if let Some(item) = item {
|
||||
match get_item_content(&item, &state.data_dir).await {
|
||||
Ok(content) => {
|
||||
let response = ApiResponse {
|
||||
success: true,
|
||||
data: Some(content),
|
||||
error: None,
|
||||
};
|
||||
Ok(Json(response))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to get content for item {}: {}", item.id.unwrap_or(0), e);
|
||||
let response = ApiResponse::<String> {
|
||||
success: false,
|
||||
data: None,
|
||||
error: Some(format!("Failed to retrieve content: {}", e)),
|
||||
};
|
||||
Ok(Json(response))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(StatusCode::NOT_FOUND)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_get_content(
|
||||
State(state): State<AppState>,
|
||||
Path(item_id): Path<String>,
|
||||
headers: HeaderMap,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
) -> Result<Json<ApiResponse<String>>, StatusCode> {
|
||||
if !check_auth(&headers, &state.password) {
|
||||
warn!("Unauthorized request to /content/{} from {}", item_id, addr);
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
if let Ok(id) = item_id.parse::<i64>() {
|
||||
// Validate that item ID is positive to prevent path traversal issues
|
||||
if id <= 0 {
|
||||
warn!("Invalid item ID {} from {}", id, addr);
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
let mut conn = state.db.lock().await;
|
||||
|
||||
if let Some(item) = db::get_item(&mut *conn, id).map_err(|e| {
|
||||
warn!("Failed to get item {} for content: {}", id, e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})? {
|
||||
match get_item_content(&item, &state.data_dir).await {
|
||||
Ok(content) => {
|
||||
let response = ApiResponse {
|
||||
success: true,
|
||||
data: Some(content),
|
||||
error: None,
|
||||
};
|
||||
Ok(Json(response))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to get content for item {}: {}", id, e);
|
||||
let response = ApiResponse::<String> {
|
||||
success: false,
|
||||
data: None,
|
||||
error: Some(format!("Failed to retrieve content: {}", e)),
|
||||
};
|
||||
Ok(Json(response))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(StatusCode::NOT_FOUND)
|
||||
}
|
||||
} else {
|
||||
Err(StatusCode::BAD_REQUEST)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_item_content(item: &db::Item, data_dir: &PathBuf) -> Result<String> {
|
||||
let item_id = item.id.ok_or_else(|| anyhow!("Item missing ID"))?;
|
||||
|
||||
// Validate that item ID is positive to prevent path traversal issues
|
||||
if item_id <= 0 {
|
||||
return Err(anyhow!("Invalid item ID: {}", item_id));
|
||||
}
|
||||
|
||||
let mut item_path = data_dir.clone();
|
||||
item_path.push(item_id.to_string());
|
||||
|
||||
let compression_type = CompressionType::from_str(&item.compression)?;
|
||||
let compression_engine = get_compression_engine(compression_type)?;
|
||||
|
||||
// Read the content using the compression engine
|
||||
let mut reader = compression_engine.open(item_path)?;
|
||||
let mut content = String::new();
|
||||
reader.read_to_string(&mut content)?;
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
pub fn get_content_openapi_spec() -> serde_json::Value {
|
||||
json!({
|
||||
"/content": {
|
||||
"get": {
|
||||
"summary": "Get content of latest item",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "tags",
|
||||
"in": "query",
|
||||
"schema": {"type": "string"},
|
||||
"description": "Comma-separated list of tags to filter by"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {"description": "Item content"},
|
||||
"404": {"description": "No items found"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/content/{id}": {
|
||||
"get": {
|
||||
"summary": "Get content by item ID",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {"type": "integer"}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {"description": "Item content"},
|
||||
"404": {"description": "Item not found"}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
110
src/modes/server/docs.rs
Normal file
110
src/modes/server/docs.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use axum::response::{Html, Json};
|
||||
use serde_json::json;
|
||||
|
||||
use super::status::get_status_openapi_spec;
|
||||
use super::items::get_items_openapi_spec;
|
||||
use super::content::get_content_openapi_spec;
|
||||
|
||||
pub async fn handle_openapi() -> Json<serde_json::Value> {
|
||||
let mut paths = json!({});
|
||||
|
||||
// Merge all endpoint specifications
|
||||
let status_paths = get_status_openapi_spec();
|
||||
let items_paths = get_items_openapi_spec();
|
||||
let content_paths = get_content_openapi_spec();
|
||||
|
||||
// Merge the path objects
|
||||
if let serde_json::Value::Object(ref mut paths_map) = paths {
|
||||
if let serde_json::Value::Object(status_map) = status_paths {
|
||||
for (key, value) in status_map {
|
||||
paths_map.insert(key, value);
|
||||
}
|
||||
}
|
||||
if let serde_json::Value::Object(items_map) = items_paths {
|
||||
for (key, value) in items_map {
|
||||
paths_map.insert(key, value);
|
||||
}
|
||||
}
|
||||
if let serde_json::Value::Object(content_map) = content_paths {
|
||||
for (key, value) in content_map {
|
||||
paths_map.insert(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let openapi_spec = json!({
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "Keep API",
|
||||
"version": "1.0.0",
|
||||
"description": "REST API for the Keep data storage system"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "/",
|
||||
"description": "Local server"
|
||||
}
|
||||
],
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"bearerAuth": {
|
||||
"type": "http",
|
||||
"scheme": "bearer"
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"ItemInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "integer"},
|
||||
"ts": {"type": "string", "format": "date-time"},
|
||||
"size": {"type": "integer", "nullable": true},
|
||||
"compression": {"type": "string"},
|
||||
"tags": {"type": "array", "items": {"type": "string"}},
|
||||
"metadata": {"type": "object"}
|
||||
}
|
||||
},
|
||||
"StatusInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {"type": "string"},
|
||||
"database_path": {"type": "string"},
|
||||
"data_directory": {"type": "string"},
|
||||
"compression_engines": {"type": "array", "items": {"type": "string"}},
|
||||
"meta_plugins": {"type": "array", "items": {"type": "string"}}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [{"bearerAuth": []}],
|
||||
"paths": paths
|
||||
});
|
||||
|
||||
Json(openapi_spec)
|
||||
}
|
||||
|
||||
pub async fn handle_swagger_ui() -> Html<&'static str> {
|
||||
let html = r#"<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Keep API Documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@3.52.5/swagger-ui.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script src="https://unpkg.com/swagger-ui-dist@3.52.5/swagger-ui-bundle.js"></script>
|
||||
<script>
|
||||
SwaggerUIBundle({
|
||||
url: '/openapi.json',
|
||||
dom_id: '#swagger-ui',
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIBundle.presets.standalone
|
||||
]
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>"#;
|
||||
|
||||
Html(html)
|
||||
}
|
||||
311
src/modes/server/items.rs
Normal file
311
src/modes/server/items.rs
Normal file
@@ -0,0 +1,311 @@
|
||||
use axum::{
|
||||
extract::{ConnectInfo, Path, Query, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::Json,
|
||||
};
|
||||
use log::warn;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use crate::db;
|
||||
use super::common::{AppState, ApiResponse, ItemInfo, TagsQuery, check_auth};
|
||||
|
||||
pub async fn handle_list_items(
|
||||
State(state): State<AppState>,
|
||||
Query(params): Query<TagsQuery>,
|
||||
headers: HeaderMap,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
) -> Result<Json<ApiResponse<Vec<ItemInfo>>>, StatusCode> {
|
||||
if !check_auth(&headers, &state.password) {
|
||||
warn!("Unauthorized request to /item/ from {}", addr);
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
let mut conn = state.db.lock().await;
|
||||
|
||||
let tags: Vec<String> = params.tags
|
||||
.map(|s| s.split(',').map(|t| t.trim().to_string()).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
let items = if tags.is_empty() {
|
||||
db::get_items(&mut *conn).map_err(|e| {
|
||||
warn!("Failed to get items: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?
|
||||
} else {
|
||||
db::get_items_matching(&mut *conn, &tags, &HashMap::new())
|
||||
.map_err(|e| {
|
||||
warn!("Failed to get items matching tags {:?}: {}", tags, e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?
|
||||
};
|
||||
|
||||
// Get item IDs for batch queries
|
||||
let item_ids: Vec<i64> = items.iter().filter_map(|item| item.id).collect();
|
||||
|
||||
// Get tags and metadata for all items
|
||||
let tags_map = db::get_tags_for_items(&mut *conn, &item_ids)
|
||||
.map_err(|e| {
|
||||
warn!("Failed to get tags for items: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
let meta_map = db::get_meta_for_items(&mut *conn, &item_ids)
|
||||
.map_err(|e| {
|
||||
warn!("Failed to get metadata for items: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
let item_infos: Vec<ItemInfo> = items
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
let item_id = item.id.unwrap_or(0);
|
||||
let item_tags = tags_map.get(&item_id)
|
||||
.map(|tags| tags.iter().map(|t| t.name.clone()).collect())
|
||||
.unwrap_or_default();
|
||||
let item_meta = meta_map.get(&item_id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
ItemInfo {
|
||||
id: item_id,
|
||||
ts: item.ts.to_rfc3339(),
|
||||
size: item.size,
|
||||
compression: item.compression,
|
||||
tags: item_tags,
|
||||
metadata: item_meta,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let response = ApiResponse {
|
||||
success: true,
|
||||
data: Some(item_infos),
|
||||
error: None,
|
||||
};
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
pub async fn handle_get_item(
|
||||
State(state): State<AppState>,
|
||||
Path(item_id): Path<String>,
|
||||
Query(params): Query<TagsQuery>,
|
||||
headers: HeaderMap,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
) -> Result<Json<ApiResponse<ItemInfo>>, StatusCode> {
|
||||
if !check_auth(&headers, &state.password) {
|
||||
warn!("Unauthorized request to /item/{} from {}", item_id, addr);
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
let mut conn = state.db.lock().await;
|
||||
|
||||
let item = if let Ok(id) = item_id.parse::<i64>() {
|
||||
db::get_item(&mut *conn, id).map_err(|e| {
|
||||
warn!("Failed to get item {}: {}", id, e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?
|
||||
} else {
|
||||
// Try to find by tags
|
||||
if let Some(tags_str) = params.tags {
|
||||
let tags: Vec<String> = tags_str.split(',').map(|t| t.trim().to_string()).collect();
|
||||
db::get_item_matching(&mut *conn, &tags, &HashMap::new())
|
||||
.map_err(|e| {
|
||||
warn!("Failed to get item matching tags {:?}: {}", tags, e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?
|
||||
} else {
|
||||
warn!("Invalid item ID '{}' and no tags provided", item_id);
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(item) = item {
|
||||
let item_tags = db::get_item_tags(&mut *conn, &item)
|
||||
.map_err(|e| {
|
||||
warn!("Failed to get tags for item {}: {}", item.id.unwrap_or(0), e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?
|
||||
.into_iter()
|
||||
.map(|t| t.name)
|
||||
.collect();
|
||||
let item_meta = db::get_item_meta(&mut *conn, &item)
|
||||
.map_err(|e| {
|
||||
warn!("Failed to get metadata for item {}: {}", item.id.unwrap_or(0), e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?
|
||||
.into_iter()
|
||||
.map(|m| (m.name, m.value))
|
||||
.collect();
|
||||
|
||||
let item_info = ItemInfo {
|
||||
id: item.id.unwrap_or(0),
|
||||
ts: item.ts.to_rfc3339(),
|
||||
size: item.size,
|
||||
compression: item.compression,
|
||||
tags: item_tags,
|
||||
metadata: item_meta,
|
||||
};
|
||||
|
||||
let response = ApiResponse {
|
||||
success: true,
|
||||
data: Some(item_info),
|
||||
error: None,
|
||||
};
|
||||
|
||||
Ok(Json(response))
|
||||
} else {
|
||||
Err(StatusCode::NOT_FOUND)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_put_item(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
) -> Result<Json<ApiResponse<ItemInfo>>, StatusCode> {
|
||||
if !check_auth(&headers, &state.password) {
|
||||
warn!("Unauthorized request to PUT /item/ from {}", addr);
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
let response = ApiResponse::<ItemInfo> {
|
||||
success: false,
|
||||
data: None,
|
||||
error: Some("PUT /item/ not yet implemented".to_string()),
|
||||
};
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
pub async fn handle_delete_item(
|
||||
State(state): State<AppState>,
|
||||
Path(item_id): Path<String>,
|
||||
headers: HeaderMap,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
) -> Result<Json<ApiResponse<()>>, StatusCode> {
|
||||
if !check_auth(&headers, &state.password) {
|
||||
warn!("Unauthorized request to DELETE /item/{} from {}", item_id, addr);
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
if let Ok(id) = item_id.parse::<i64>() {
|
||||
let mut conn = state.db.lock().await;
|
||||
|
||||
if let Some(item) = db::get_item(&mut *conn, id).map_err(|e| {
|
||||
warn!("Failed to get item {} for deletion: {}", id, e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})? {
|
||||
db::delete_item(&mut *conn, item).map_err(|e| {
|
||||
warn!("Failed to delete item {}: {}", id, e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
let response = ApiResponse::<()> {
|
||||
success: true,
|
||||
data: None,
|
||||
error: None,
|
||||
};
|
||||
Ok(Json(response))
|
||||
} else {
|
||||
Err(StatusCode::NOT_FOUND)
|
||||
}
|
||||
} else {
|
||||
Err(StatusCode::BAD_REQUEST)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_items_openapi_spec() -> serde_json::Value {
|
||||
json!({
|
||||
"/item/": {
|
||||
"get": {
|
||||
"summary": "List items",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "tags",
|
||||
"in": "query",
|
||||
"schema": {"type": "string"},
|
||||
"description": "Comma-separated list of tags to filter by"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of items",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ItemInfo"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"summary": "Add new item",
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Item created",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/ItemInfo"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/item/{id}": {
|
||||
"get": {
|
||||
"summary": "Get item by ID",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {"type": "string"},
|
||||
"description": "Item ID or use tags query parameter"
|
||||
},
|
||||
{
|
||||
"name": "tags",
|
||||
"in": "query",
|
||||
"schema": {"type": "string"},
|
||||
"description": "Comma-separated list of tags (when ID is not numeric)"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Item information",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/ItemInfo"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {"description": "Item not found"}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"summary": "Delete item by ID",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {"type": "integer"}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {"description": "Item deleted"},
|
||||
"404": {"description": "Item not found"}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
77
src/modes/server/status.rs
Normal file
77
src/modes/server/status.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use axum::{
|
||||
extract::{ConnectInfo, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::Json,
|
||||
};
|
||||
use clap::Command;
|
||||
use log::warn;
|
||||
use serde_json::json;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use crate::meta_plugin::MetaPluginType;
|
||||
use crate::modes::status::{StatusInfo, generate_status_info};
|
||||
use super::common::{AppState, ApiResponse, check_auth};
|
||||
|
||||
pub async fn handle_status(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
) -> Result<Json<ApiResponse<StatusInfo>>, StatusCode> {
|
||||
if !check_auth(&headers, &state.password) {
|
||||
warn!("Unauthorized request from {}", addr);
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
// Use the actual args that the server was started with
|
||||
let args = &state.args;
|
||||
|
||||
// Determine which meta plugins would be enabled for a save operation
|
||||
let mut meta_plugin_types: Vec<MetaPluginType> = crate::modes::common::cmd_args_meta_plugin_types(&mut Command::new("keep"), args);
|
||||
|
||||
// Add digest type if specified
|
||||
let digest_type = crate::modes::common::cmd_args_digest_type(&mut Command::new("keep"), args);
|
||||
let digest_meta_plugin_type = match digest_type {
|
||||
crate::meta_plugin::MetaPluginType::DigestSha256 => Some(MetaPluginType::DigestSha256),
|
||||
crate::meta_plugin::MetaPluginType::DigestMd5 => Some(MetaPluginType::DigestMd5),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(digest_plugin_type) = digest_meta_plugin_type {
|
||||
if !meta_plugin_types.contains(&digest_plugin_type) {
|
||||
meta_plugin_types.push(digest_plugin_type);
|
||||
}
|
||||
}
|
||||
|
||||
let mut db_path = state.data_dir.clone();
|
||||
db_path.push("keep-1.db");
|
||||
|
||||
let status_info = generate_status_info(state.data_dir.clone(), db_path, &meta_plugin_types);
|
||||
|
||||
let response = ApiResponse {
|
||||
success: true,
|
||||
data: Some(status_info),
|
||||
error: None,
|
||||
};
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
pub fn get_status_openapi_spec() -> serde_json::Value {
|
||||
json!({
|
||||
"/status": {
|
||||
"get": {
|
||||
"summary": "Get system status",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "System status",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/StatusInfo"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -8,8 +8,11 @@ use crate::compression_engine::COMPRESSION_PROGRAMS;
|
||||
use crate::compression_engine::CompressionType;
|
||||
use crate::compression_engine::program::CompressionEngineProgram;
|
||||
|
||||
use crate::modes::common::get_format_box_chars_no_border_line_separator;
|
||||
use crate::modes::common::{get_format_box_chars_no_border_line_separator, get_output_format, OutputFormat};
|
||||
use prettytable::color;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use serde_yaml;
|
||||
use prettytable::row;
|
||||
use prettytable::{Attr, Cell, Row, Table};
|
||||
use prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR;
|
||||
@@ -17,7 +20,130 @@ use prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR;
|
||||
use crate::meta_plugin;
|
||||
use crate::meta_plugin::MetaPluginType;
|
||||
|
||||
fn build_path_table(data_path: PathBuf, db_path: PathBuf) -> Table {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct StatusInfo {
|
||||
pub paths: PathInfo,
|
||||
pub compression: Vec<CompressionInfo>,
|
||||
pub meta_plugins: Vec<MetaPluginInfo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct PathInfo {
|
||||
pub data: String,
|
||||
pub database: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CompressionInfo {
|
||||
#[serde(rename = "type")]
|
||||
pub compression_type: String,
|
||||
pub found: bool,
|
||||
pub default: bool,
|
||||
pub binary: String,
|
||||
pub compress: String,
|
||||
pub decompress: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MetaPluginInfo {
|
||||
pub meta_name: String,
|
||||
pub found: bool,
|
||||
pub enabled: bool,
|
||||
pub binary: String,
|
||||
pub args: String,
|
||||
}
|
||||
|
||||
pub fn generate_status_info(
|
||||
data_path: PathBuf,
|
||||
db_path: PathBuf,
|
||||
enabled_meta_plugins: &Vec<MetaPluginType>,
|
||||
) -> StatusInfo {
|
||||
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"),
|
||||
};
|
||||
|
||||
let default_type = compression_engine::default_compression_type();
|
||||
let mut compression_info = Vec::new();
|
||||
|
||||
// Sort compression types by their string representation
|
||||
let mut sorted_compression_types: Vec<CompressionType> = CompressionType::iter().collect();
|
||||
sorted_compression_types.sort_by_key(|ct| ct.to_string());
|
||||
|
||||
for compression_type in sorted_compression_types {
|
||||
let compression_program: CompressionEngineProgram =
|
||||
match &COMPRESSION_PROGRAMS[compression_type.clone()] {
|
||||
Some(compression_program) => compression_program.clone(),
|
||||
None => CompressionEngineProgram {
|
||||
program: "".to_string(),
|
||||
compress: Vec::new(),
|
||||
decompress: Vec::new(),
|
||||
supported: true,
|
||||
},
|
||||
};
|
||||
|
||||
let is_default = compression_type == default_type;
|
||||
let binary = if compression_program.program.is_empty() {
|
||||
"<INTERNAL>".to_string()
|
||||
} else {
|
||||
compression_program.program
|
||||
};
|
||||
|
||||
compression_info.push(CompressionInfo {
|
||||
compression_type: compression_type.to_string(),
|
||||
found: compression_program.supported,
|
||||
default: is_default,
|
||||
binary,
|
||||
compress: compression_program.compress.join(" "),
|
||||
decompress: compression_program.decompress.join(" "),
|
||||
});
|
||||
}
|
||||
|
||||
let mut meta_plugin_info = Vec::new();
|
||||
|
||||
// Sort meta plugin types by their meta name
|
||||
let mut sorted_meta_plugins: Vec<MetaPluginType> = MetaPluginType::iter().collect();
|
||||
sorted_meta_plugins.sort_by_key(|meta_plugin_type| {
|
||||
let mut meta_plugin = meta_plugin::get_meta_plugin(meta_plugin_type.clone());
|
||||
meta_plugin.meta_name()
|
||||
});
|
||||
|
||||
for meta_plugin_type in sorted_meta_plugins {
|
||||
let mut meta_plugin = meta_plugin::get_meta_plugin(meta_plugin_type.clone());
|
||||
let is_supported = meta_plugin.is_supported();
|
||||
let is_enabled = enabled_meta_plugins.contains(&meta_plugin_type);
|
||||
|
||||
let (binary_display, args_display) = if !is_supported {
|
||||
("<NOT FOUND>".to_string(), "".to_string())
|
||||
} else {
|
||||
if meta_plugin.is_internal() {
|
||||
("<INTERNAL>".to_string(), "".to_string())
|
||||
} else {
|
||||
if let Some((program, args)) = meta_plugin.program_info() {
|
||||
(program.to_string(), args.join(" "))
|
||||
} else {
|
||||
("<NOT FOUND>".to_string(), "".to_string())
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
meta_plugin_info.push(MetaPluginInfo {
|
||||
meta_name: meta_plugin.meta_name(),
|
||||
found: is_supported,
|
||||
enabled: is_enabled,
|
||||
binary: binary_display,
|
||||
args: args_display,
|
||||
});
|
||||
}
|
||||
|
||||
StatusInfo {
|
||||
paths: path_info,
|
||||
compression: compression_info,
|
||||
meta_plugins: meta_plugin_info,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_path_table(path_info: &PathInfo) -> Table {
|
||||
let mut path_table = Table::new();
|
||||
|
||||
if std::io::stdout().is_terminal() {
|
||||
@@ -33,28 +159,18 @@ fn build_path_table(data_path: PathBuf, db_path: PathBuf) -> Table {
|
||||
|
||||
path_table.add_row(Row::new(vec![
|
||||
Cell::new("Data"),
|
||||
Cell::new(
|
||||
&data_path
|
||||
.into_os_string()
|
||||
.into_string()
|
||||
.expect("Unable to convert data path to string"),
|
||||
),
|
||||
Cell::new(&path_info.data),
|
||||
]));
|
||||
|
||||
path_table.add_row(Row::new(vec![
|
||||
Cell::new("Database"),
|
||||
Cell::new(
|
||||
&db_path
|
||||
.into_os_string()
|
||||
.into_string()
|
||||
.expect("Unable to convert DB path to string"),
|
||||
),
|
||||
Cell::new(&path_info.database),
|
||||
]));
|
||||
|
||||
path_table
|
||||
}
|
||||
|
||||
fn build_compression_table() -> Table {
|
||||
fn build_compression_table(compression_info: &Vec<CompressionInfo>) -> Table {
|
||||
let mut compression_table = Table::new();
|
||||
if std::io::stdout().is_terminal() {
|
||||
compression_table.set_format(get_format_box_chars_no_border_line_separator());
|
||||
@@ -70,48 +186,30 @@ fn build_compression_table() -> Table {
|
||||
b->"Compress",
|
||||
b->"Decompress"));
|
||||
|
||||
let default_type = compression_engine::default_compression_type();
|
||||
|
||||
for compression_type in CompressionType::iter() {
|
||||
let compression_program: CompressionEngineProgram =
|
||||
match &COMPRESSION_PROGRAMS[compression_type.clone()] {
|
||||
Some(compression_program) => compression_program.clone(),
|
||||
None => CompressionEngineProgram {
|
||||
program: "".to_string(),
|
||||
compress: Vec::new(),
|
||||
decompress: Vec::new(),
|
||||
supported: true,
|
||||
},
|
||||
};
|
||||
|
||||
let is_default = compression_type == default_type;
|
||||
|
||||
for info in compression_info {
|
||||
compression_table.add_row(Row::new(vec![
|
||||
Cell::new(&compression_type.to_string()),
|
||||
match compression_program.supported {
|
||||
Cell::new(&info.compression_type),
|
||||
match info.found {
|
||||
true => Cell::new("Yes").with_style(Attr::ForegroundColor(color::GREEN)),
|
||||
false => Cell::new("No").with_style(Attr::ForegroundColor(color::RED)),
|
||||
},
|
||||
match is_default {
|
||||
match info.default {
|
||||
true => Cell::new("Yes").with_style(Attr::ForegroundColor(color::GREEN)),
|
||||
false => Cell::new("No"),
|
||||
},
|
||||
match compression_program.program.is_empty() {
|
||||
true => {
|
||||
Cell::new("<INTERNAL>").with_style(Attr::ForegroundColor(color::BRIGHT_BLACK))
|
||||
}
|
||||
false => Cell::new(&compression_program.program),
|
||||
match info.binary.as_str() {
|
||||
"<INTERNAL>" => Cell::new(&info.binary).with_style(Attr::ForegroundColor(color::BRIGHT_BLACK)),
|
||||
_ => Cell::new(&info.binary),
|
||||
},
|
||||
Cell::new(&compression_program.compress.join(" ")),
|
||||
Cell::new(&compression_program.decompress.join(" ")),
|
||||
Cell::new(&info.compress),
|
||||
Cell::new(&info.decompress),
|
||||
]));
|
||||
}
|
||||
|
||||
compression_table
|
||||
}
|
||||
|
||||
|
||||
fn build_meta_plugin_table(enabled_meta_plugins: &Vec<MetaPluginType>) -> Table {
|
||||
fn build_meta_plugin_table(meta_plugin_info: &Vec<MetaPluginInfo>) -> Table {
|
||||
let mut meta_plugin_table = Table::new();
|
||||
if std::io::stdout().is_terminal() {
|
||||
meta_plugin_table.set_format(get_format_box_chars_no_border_line_separator());
|
||||
@@ -126,52 +224,23 @@ fn build_meta_plugin_table(enabled_meta_plugins: &Vec<MetaPluginType>) -> Table
|
||||
b->"Binary",
|
||||
b->"Args"));
|
||||
|
||||
for meta_plugin_type in MetaPluginType::iter() {
|
||||
let mut meta_plugin = meta_plugin::get_meta_plugin(meta_plugin_type.clone());
|
||||
let is_supported = meta_plugin.is_supported();
|
||||
let is_enabled = enabled_meta_plugins.contains(&meta_plugin_type);
|
||||
|
||||
// Determine what implementation will actually be used
|
||||
let (binary_display, args_display) = if !is_supported {
|
||||
("<NOT FOUND>".to_string(), "".to_string())
|
||||
} else {
|
||||
match meta_plugin_type {
|
||||
// For internal plugins, always show as internal
|
||||
MetaPluginType::DigestSha256 | MetaPluginType::ReadTime | MetaPluginType::ReadRate |
|
||||
MetaPluginType::Cwd | MetaPluginType::Uid | MetaPluginType::User |
|
||||
MetaPluginType::Gid | MetaPluginType::Group | MetaPluginType::Shell |
|
||||
MetaPluginType::ShellPid | MetaPluginType::KeepPid | MetaPluginType::Hostname |
|
||||
MetaPluginType::FullHostname => {
|
||||
("<INTERNAL>".to_string(), "".to_string())
|
||||
},
|
||||
// For program-based plugins, show program info
|
||||
_ => {
|
||||
// Get program info from the meta plugin itself
|
||||
if let Some((program, args)) = meta_plugin.program_info() {
|
||||
(program.to_string(), args.join(" "))
|
||||
} else {
|
||||
("<NOT FOUND>".to_string(), "".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for info in meta_plugin_info {
|
||||
meta_plugin_table.add_row(Row::new(vec![
|
||||
Cell::new(&meta_plugin.meta_name()),
|
||||
match is_supported {
|
||||
Cell::new(&info.meta_name),
|
||||
match info.found {
|
||||
true => Cell::new("Yes").with_style(Attr::ForegroundColor(color::GREEN)),
|
||||
false => Cell::new("No").with_style(Attr::ForegroundColor(color::RED)),
|
||||
},
|
||||
match is_enabled {
|
||||
match info.enabled {
|
||||
true => Cell::new("Yes").with_style(Attr::ForegroundColor(color::GREEN)),
|
||||
false => Cell::new("No"),
|
||||
},
|
||||
match binary_display.as_str() {
|
||||
"<INTERNAL>" => Cell::new(&binary_display).with_style(Attr::ForegroundColor(color::BRIGHT_BLACK)),
|
||||
"<NOT FOUND>" => Cell::new(&binary_display).with_style(Attr::ForegroundColor(color::RED)),
|
||||
_ => Cell::new(&binary_display),
|
||||
match info.binary.as_str() {
|
||||
"<INTERNAL>" => Cell::new(&info.binary).with_style(Attr::ForegroundColor(color::BRIGHT_BLACK)),
|
||||
"<NOT FOUND>" => Cell::new(&info.binary).with_style(Attr::ForegroundColor(color::RED)),
|
||||
_ => Cell::new(&info.binary),
|
||||
},
|
||||
Cell::new(&args_display),
|
||||
Cell::new(&info.args),
|
||||
]));
|
||||
}
|
||||
|
||||
@@ -201,13 +270,28 @@ pub fn mode_status(
|
||||
}
|
||||
}
|
||||
|
||||
println!("PATHS:");
|
||||
build_path_table(data_path, db_path).printstd();
|
||||
println!();
|
||||
println!("COMPRESSION:");
|
||||
build_compression_table().printstd();
|
||||
println!();
|
||||
println!("META PLUGINS:");
|
||||
build_meta_plugin_table(&meta_plugin_types).printstd();
|
||||
Ok(())
|
||||
let output_format = get_output_format(args);
|
||||
let status_info = generate_status_info(data_path, db_path, &meta_plugin_types);
|
||||
|
||||
match output_format {
|
||||
OutputFormat::Table => {
|
||||
println!("PATHS:");
|
||||
build_path_table(&status_info.paths).printstd();
|
||||
println!();
|
||||
println!("COMPRESSION:");
|
||||
build_compression_table(&status_info.compression).printstd();
|
||||
println!();
|
||||
println!("META PLUGINS:");
|
||||
build_meta_plugin_table(&status_info.meta_plugins).printstd();
|
||||
Ok(())
|
||||
},
|
||||
OutputFormat::Json => {
|
||||
println!("{}", serde_json::to_string_pretty(&status_info)?);
|
||||
Ok(())
|
||||
},
|
||||
OutputFormat::Yaml => {
|
||||
println!("{}", serde_yaml::to_string(&status_info)?);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::compression_engine::{CompressionType, get_compression_engine};
|
||||
use crate::db;
|
||||
use crate::meta_plugin;
|
||||
use crate::modes::common::{cmd_args_digest_type, get_digest_type_meta, store_item_digest_value};
|
||||
use crate::modes::common::{cmd_args_digest_type, get_digest_type_meta};
|
||||
use clap::Command;
|
||||
use clap::error::ErrorKind;
|
||||
use log::{debug, info};
|
||||
@@ -35,14 +35,18 @@ pub fn mode_update(
|
||||
let mut item = item_maybe.expect("Unable to find item in database");
|
||||
debug!("MAIN: Found item {:?}", item);
|
||||
|
||||
// Use a transaction for database operations to ensure atomicity
|
||||
let tx = conn.transaction()?;
|
||||
|
||||
if !tags.is_empty() {
|
||||
debug!("MAIN: Updating item tags");
|
||||
db::set_item_tags(conn, item.clone(), tags)?;
|
||||
db::set_item_tags(&tx, item.clone(), tags)?;
|
||||
}
|
||||
|
||||
let item_id = item.id.ok_or_else(|| anyhow!("Item missing ID"))?;
|
||||
let item_path = {
|
||||
let mut path = data_path.clone();
|
||||
path.push(item.id.unwrap().to_string());
|
||||
path.push(item_id.to_string());
|
||||
path
|
||||
};
|
||||
|
||||
@@ -58,7 +62,7 @@ pub fn mode_update(
|
||||
debug!("MAIN: Updating stream size of {:?}", item_path);
|
||||
let size = compression_engine.size(item_path.clone())?;
|
||||
item.size = Some(size as i64);
|
||||
db::update_item(conn, item.clone())?;
|
||||
db::update_item(&tx, item.clone())?;
|
||||
} else {
|
||||
debug!(
|
||||
"MAIN: Unable to update size of item due to missing file {:?}",
|
||||
@@ -69,7 +73,7 @@ pub fn mode_update(
|
||||
|
||||
let digest_type = cmd_args_digest_type(cmd, args);
|
||||
let digest_meta = get_digest_type_meta(digest_type.clone());
|
||||
let digest_value = db::get_item_meta_value(&conn, &item, digest_meta)?;
|
||||
let digest_value = db::get_item_meta_value(&tx, &item, digest_meta)?;
|
||||
|
||||
if digest_value.is_none() || digest_value.unwrap().is_empty() {
|
||||
let item_file_metadata = item_path.metadata();
|
||||
@@ -95,8 +99,14 @@ pub fn mode_update(
|
||||
let digest_value = digest_engine.finalize()?;
|
||||
debug!("DIGEST: {}", digest_value);
|
||||
|
||||
// Save digest to meta using the common function
|
||||
store_item_digest_value(conn, item.clone(), digest_type, digest_value)?;
|
||||
// Save digest to meta
|
||||
let digest_meta_name = get_digest_type_meta(digest_type);
|
||||
let digest_meta = db::Meta {
|
||||
id: item_id,
|
||||
name: digest_meta_name,
|
||||
value: digest_value,
|
||||
};
|
||||
db::store_meta(&tx, digest_meta)?;
|
||||
} else {
|
||||
debug!(
|
||||
"MAIN: Unable to update digest of item due to missing file {:?}",
|
||||
@@ -109,13 +119,16 @@ pub fn mode_update(
|
||||
debug!("MAIN: Updating item meta");
|
||||
for kv in args.item.meta.iter() {
|
||||
let meta = db::Meta {
|
||||
id: item.id.unwrap(),
|
||||
id: item_id,
|
||||
name: kv.key.to_string(),
|
||||
value: kv.value.to_string(),
|
||||
};
|
||||
db::store_meta(conn, meta)?;
|
||||
db::store_meta(&tx, meta)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
tx.commit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user