Skip to content

Commit a86609a

Browse files
author
openviking
committed
feat: rust cli add-resource support zip
1 parent 16e1335 commit a86609a

File tree

3 files changed

+122
-10
lines changed

3 files changed

+122
-10
lines changed

crates/ov_cli/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,7 @@ thiserror = "1.0"
2424
unicode-width = "0.1"
2525
ratatui = "0.30"
2626
crossterm = "0.28"
27+
zip = "2.2"
28+
tempfile = "3.12"
29+
url = "2.5"
30+
walkdir = "2.5"

crates/ov_cli/src/client.rs

Lines changed: 113 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
use reqwest::{Client as ReqwestClient, StatusCode};
22
use serde::de::DeserializeOwned;
33
use serde_json::Value;
4+
use std::fs::File;
5+
use std::path::Path;
6+
use tempfile::NamedTempFile;
7+
use url::Url;
8+
use zip::write::FileOptions;
9+
use zip::CompressionMethod;
410

511
use crate::error::{Error, Result};
612

@@ -27,6 +33,82 @@ impl HttpClient {
2733
}
2834
}
2935

36+
/// Check if the server is localhost or 127.0.0.1
37+
fn is_local_server(&self) -> bool {
38+
if let Ok(url) = Url::parse(&self.base_url) {
39+
if let Some(host) = url.host_str() {
40+
return host == "localhost" || host == "127.0.0.1";
41+
}
42+
}
43+
false
44+
}
45+
46+
/// Zip a directory to a temporary file
47+
fn zip_directory(&self, dir_path: &Path) -> Result<NamedTempFile> {
48+
if !dir_path.is_dir() {
49+
return Err(Error::Network(format!(
50+
"Path {} is not a directory",
51+
dir_path.display()
52+
)));
53+
}
54+
55+
let temp_file = NamedTempFile::new()?;
56+
let file = File::create(temp_file.path())?;
57+
let mut zip = zip::ZipWriter::new(file);
58+
let options: FileOptions<'_, ()> = FileOptions::default().compression_method(CompressionMethod::Deflated);
59+
60+
let walkdir = walkdir::WalkDir::new(dir_path);
61+
for entry in walkdir.into_iter().filter_map(|e| e.ok()) {
62+
let path = entry.path();
63+
if path.is_file() {
64+
let name = path.strip_prefix(dir_path).unwrap_or(path);
65+
zip.start_file(name.to_string_lossy(), options)?;
66+
let mut file = File::open(path)?;
67+
std::io::copy(&mut file, &mut zip)?;
68+
}
69+
}
70+
71+
zip.finish()?;
72+
Ok(temp_file)
73+
}
74+
75+
/// Upload a temporary file and return the temp_path
76+
async fn upload_temp_file(&self, file_path: &Path) -> Result<String> {
77+
let url = format!("{}/api/v1/resources/temp_upload", self.base_url);
78+
let file_name = file_path
79+
.file_name()
80+
.and_then(|n| n.to_str())
81+
.unwrap_or("temp_upload.zip");
82+
83+
// Read file content
84+
let file_content = tokio::fs::read(file_path).await?;
85+
86+
// Create multipart form
87+
let part = reqwest::multipart::Part::bytes(file_content)
88+
.file_name(file_name.to_string());
89+
90+
let part = part.mime_str("application/octet-stream").map_err(|e| {
91+
Error::Network(format!("Failed to set mime type: {}", e))
92+
})?;
93+
94+
let form = reqwest::multipart::Form::new().part("file", part);
95+
96+
let response = self
97+
.http
98+
.post(&url)
99+
.multipart(form)
100+
.send()
101+
.await
102+
.map_err(|e| Error::Network(format!("HTTP request failed: {}", e)))?;
103+
104+
let result: Value = self.handle_response(response).await?;
105+
result
106+
.get("temp_path")
107+
.and_then(|v| v.as_str())
108+
.map(|s| s.to_string())
109+
.ok_or_else(|| Error::Parse("Missing temp_path in response".to_string()))
110+
}
111+
30112
fn build_headers(&self) -> reqwest::header::HeaderMap {
31113
let mut headers = reqwest::header::HeaderMap::new();
32114
headers.insert(
@@ -327,15 +409,37 @@ impl HttpClient {
327409
wait: bool,
328410
timeout: Option<f64>,
329411
) -> Result<serde_json::Value> {
330-
let body = serde_json::json!({
331-
"path": path,
332-
"target": target,
333-
"reason": reason,
334-
"instruction": instruction,
335-
"wait": wait,
336-
"timeout": timeout,
337-
});
338-
self.post("/api/v1/resources", &body).await
412+
let path_obj = Path::new(path);
413+
414+
// Check if it's a local directory and not a local server
415+
if path_obj.exists() && path_obj.is_dir() && !self.is_local_server() {
416+
// Zip the directory
417+
let zip_file = self.zip_directory(path_obj)?;
418+
let temp_path = self.upload_temp_file(zip_file.path()).await?;
419+
420+
let body = serde_json::json!({
421+
"temp_path": temp_path,
422+
"target": target,
423+
"reason": reason,
424+
"instruction": instruction,
425+
"wait": wait,
426+
"timeout": timeout,
427+
});
428+
429+
self.post("/api/v1/resources", &body).await
430+
} else {
431+
// Regular case - use path directly
432+
let body = serde_json::json!({
433+
"path": path,
434+
"target": target,
435+
"reason": reason,
436+
"instruction": instruction,
437+
"wait": wait,
438+
"timeout": timeout,
439+
});
440+
441+
self.post("/api/v1/resources", &body).await
442+
}
339443
}
340444

341445
pub async fn add_skill(

crates/ov_cli/src/error.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ pub enum Error {
2525

2626
#[error("Serialization error: {0}")]
2727
Serialization(#[from] serde_json::Error),
28+
29+
#[error("Zip error: {0}")]
30+
Zip(#[from] zip::result::ZipError),
2831
}
2932

3033
pub type Result<T> = std::result::Result<T, Error>;
@@ -75,6 +78,7 @@ impl From<Error> for CliError {
7578
Error::Output(msg) => CliError::new(format!("Output error: {}", msg)),
7679
Error::Io(e) => CliError::new(format!("IO error: {}", e)),
7780
Error::Serialization(e) => CliError::new(format!("Serialization error: {}", e)),
81+
Error::Zip(e) => CliError::new(format!("Zip error: {}", e)),
7882
}
7983
}
8084
}
@@ -84,7 +88,7 @@ impl From<reqwest::Error> for CliError {
8488
if err.is_connect() || err.is_timeout() {
8589
CliError::network(format!(
8690
"Failed to connect to OpenViking server. \
87-
Check the url in ovcli.conf and ensure the server is running. ({ })",
91+
Check the url in ovcli.conf and ensure the server is running. ({})",
8892
err
8993
))
9094
} else {

0 commit comments

Comments
 (0)