Skip to content

Commit d9dea0e

Browse files
authored
feat: support for uploading files to the server (#310)
* feat: support for uploading files to the server * feat: field Internationalization * refactor: encapsulation attachment-related requests * feat: support for getting a list of attachments that have been uploaded for a session * feat: the session displays the number and list of uploaded files * feat: internalization * feat: wrapping the Checkbox component * feat: add checkbox * feat: support for deleting uploaded files * feat: support for selecting uploaded files * refactor: optimize the display of file icons * refactor: hide file uploads when there is no sessionId
1 parent d2eed4a commit d9dea0e

File tree

23 files changed

+859
-74
lines changed

23 files changed

+859
-74
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"release-beta": "release-it --preRelease=beta --preReleaseBase=1"
1919
},
2020
"dependencies": {
21+
"@ant-design/icons": "^6.0.0",
2122
"@headlessui/react": "^2.2.0",
2223
"@tauri-apps/api": "^2.4.0",
2324
"@tauri-apps/plugin-autostart": "~2.2.0",

pnpm-lock.yaml

Lines changed: 59 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.lock

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ tokio-native-tls = "0.3" # For wss connections
4747
tokio = { version = "1", features = ["full"] }
4848
tokio-tungstenite = { version = "0.20", features = ["rustls-tls-webpki-roots"] }
4949
hyper = { version = "0.14", features = ["client"] }
50-
reqwest = "0.12.12"
50+
reqwest = { version = "0.12", features = ["json", "multipart"] }
5151
futures = "0.3.31"
5252
ordered-float = { version = "4.6.0", default-features = false }
5353
lazy_static = "1.5.0"
@@ -68,6 +68,7 @@ url = "2.5.2"
6868
http = "1.1.0"
6969
tungstenite = "0.24.0"
7070
env_logger = "0.11.5"
71+
tokio-util = "0.7.14"
7172

7273
[target."cfg(target_os = \"macos\")".dependencies]
7374
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }

src-tauri/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,10 @@ pub fn run() {
124124
// server::get_coco_server_connectors,
125125
server::websocket::connect_to_server,
126126
server::websocket::disconnect,
127-
get_app_search_source
127+
get_app_search_source,
128+
server::attachment::upload_attachment,
129+
server::attachment::get_attachment,
130+
server::attachment::delete_attachment,
128131
])
129132
.setup(|app| {
130133
let registry = SearchSourceRegistry::default();

src-tauri/src/server/attachment.rs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
use super::servers::{get_server_by_id, get_server_token};
2+
use crate::server::http_client::HttpClient;
3+
use reqwest::multipart::{Form, Part};
4+
use serde::{Deserialize, Serialize};
5+
use serde_json::Value;
6+
use std::{collections::HashMap, path::PathBuf};
7+
use tauri::command;
8+
use tokio::fs::File;
9+
use tokio_util::codec::{BytesCodec, FramedRead};
10+
11+
#[derive(Debug, Serialize, Deserialize)]
12+
pub struct UploadAttachmentResponse {
13+
pub acknowledged: bool,
14+
pub attachments: Vec<String>,
15+
}
16+
17+
#[derive(Debug, Serialize, Deserialize)]
18+
pub struct AttachmentSource {
19+
pub id: String,
20+
pub created: String,
21+
pub updated: String,
22+
pub session: String,
23+
pub name: String,
24+
pub icon: String,
25+
pub url: String,
26+
pub size: u64,
27+
}
28+
29+
#[derive(Debug, Serialize, Deserialize)]
30+
pub struct AttachmentHit {
31+
pub _index: String,
32+
pub _type: String,
33+
pub _id: String,
34+
pub _score: f64,
35+
pub _source: AttachmentSource,
36+
}
37+
38+
#[derive(Debug, Serialize, Deserialize)]
39+
pub struct AttachmentHits {
40+
pub total: Value,
41+
pub max_score: f64,
42+
pub hits: Vec<AttachmentHit>,
43+
}
44+
45+
#[derive(Debug, Serialize, Deserialize)]
46+
pub struct GetAttachmentResponse {
47+
pub took: u32,
48+
pub timed_out: bool,
49+
pub _shards: Value,
50+
pub hits: AttachmentHits,
51+
}
52+
53+
#[derive(Debug, Serialize, Deserialize)]
54+
pub struct DeleteAttachmentResponse {
55+
pub _id: String,
56+
pub result: String,
57+
}
58+
59+
#[command]
60+
pub async fn upload_attachment(
61+
server_id: String,
62+
session_id: String,
63+
file_paths: Vec<PathBuf>,
64+
) -> Result<UploadAttachmentResponse, String> {
65+
let mut form = Form::new();
66+
67+
for file_path in file_paths {
68+
let file = File::open(&file_path)
69+
.await
70+
.map_err(|err| err.to_string())?;
71+
72+
let stream = FramedRead::new(file, BytesCodec::new());
73+
let file_name = file_path
74+
.file_name()
75+
.and_then(|n| n.to_str())
76+
.ok_or("Invalid filename")?;
77+
78+
let part =
79+
Part::stream(reqwest::Body::wrap_stream(stream)).file_name(file_name.to_string());
80+
81+
form = form.part("files", part);
82+
}
83+
84+
let server = get_server_by_id(&server_id).ok_or("Server not found")?;
85+
let url = HttpClient::join_url(&server.endpoint, &format!("chat/{}/_upload", session_id));
86+
87+
let token = get_server_token(&server_id).await?;
88+
let mut headers = HashMap::new();
89+
if let Some(token) = token {
90+
headers.insert("X-API-TOKEN".to_string(), token.access_token);
91+
}
92+
93+
let client = reqwest::Client::new();
94+
let response = client
95+
.post(url)
96+
.multipart(form)
97+
.headers((&headers).try_into().map_err(|err| format!("{}", err))?)
98+
.send()
99+
.await
100+
.map_err(|err| err.to_string())?;
101+
102+
if response.status().is_success() {
103+
let result = response
104+
.json::<UploadAttachmentResponse>()
105+
.await
106+
.map_err(|err| err.to_string())?;
107+
108+
Ok(result)
109+
} else {
110+
Err(format!("Upload failed with status: {}", response.status()))
111+
}
112+
}
113+
114+
#[command]
115+
pub async fn get_attachment(
116+
server_id: String,
117+
session_id: String,
118+
) -> Result<GetAttachmentResponse, String> {
119+
let mut query_params = HashMap::new();
120+
query_params.insert("session".to_string(), serde_json::Value::String(session_id));
121+
122+
let response = HttpClient::get(&server_id, "/attachment/_search", Some(query_params)).await?;
123+
124+
if response.status().is_success() {
125+
response
126+
.json::<GetAttachmentResponse>()
127+
.await
128+
.map_err(|e| e.to_string())
129+
} else {
130+
Err(format!("Request failed with status: {}", response.status()))
131+
}
132+
}
133+
134+
#[command]
135+
pub async fn delete_attachment(server_id: String, id: String) -> Result<bool, String> {
136+
let response =
137+
HttpClient::delete(&server_id, &format!("/attachment/{}", id), None, None).await?;
138+
139+
if response.status().is_success() {
140+
response
141+
.json::<DeleteAttachmentResponse>()
142+
.await
143+
.map_err(|e| e.to_string())?
144+
.result
145+
.eq("deleted")
146+
.then_some(true)
147+
.ok_or("Delete operation was not successful".to_string())
148+
} else {
149+
Err(format!("Delete failed with status: {}", response.status()))
150+
}
151+
}

src-tauri/src/server/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
//! This file contains Rust APIs related to Coco Server management.
22
3+
pub mod attachment;
34
pub mod auth;
4-
pub mod servers;
55
pub mod connector;
66
pub mod datasource;
77
pub mod http_client;
88
pub mod profile;
99
pub mod search;
10+
pub mod servers;
1011
pub mod websocket;

0 commit comments

Comments
 (0)