Kould
commited on
Commit
·
db8cae3
1
Parent(s):
d0db329
feat: impl websocket upload file for doc_info (#20)
Browse files- Cargo.toml +8 -1
- migration/src/m20220101_000001_create_table.rs +2 -1
- src/api/doc_info.rs +14 -10
- src/main.rs +4 -0
- src/web_socket/doc_info.rs +97 -0
- src/web_socket/mod.rs +1 -0
Cargo.toml
CHANGED
@@ -13,9 +13,15 @@ actix-multipart = "0.4"
|
|
13 |
actix-session = { version = "0.5" }
|
14 |
actix-identity = { version = "0.4" }
|
15 |
actix-web-httpauth = { version = "0.6" }
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
thiserror = "1.0"
|
17 |
postgres = "0.19.7"
|
18 |
-
sea-orm = {version = "0.12.9", features = ["sqlx-postgres", "runtime-tokio-native-tls", "macros"]}
|
19 |
serde = { version = "1", features = ["derive"] }
|
20 |
serde_json = "1.0"
|
21 |
tracing-subscriber = "0.3.18"
|
@@ -27,6 +33,7 @@ minio = "0.1.0"
|
|
27 |
futures-util = "0.3.29"
|
28 |
actix-multipart-extract = "0.1.5"
|
29 |
regex = "1.10.2"
|
|
|
30 |
|
31 |
[[bin]]
|
32 |
name = "doc_gpt"
|
|
|
13 |
actix-session = { version = "0.5" }
|
14 |
actix-identity = { version = "0.4" }
|
15 |
actix-web-httpauth = { version = "0.6" }
|
16 |
+
actix-ws = "0.2.5"
|
17 |
+
uuid = { version = "1.6.1", features = [
|
18 |
+
"v4",
|
19 |
+
"fast-rng",
|
20 |
+
"macro-diagnostics",
|
21 |
+
] }
|
22 |
thiserror = "1.0"
|
23 |
postgres = "0.19.7"
|
24 |
+
sea-orm = { version = "0.12.9", features = ["sqlx-postgres", "runtime-tokio-native-tls", "macros"] }
|
25 |
serde = { version = "1", features = ["derive"] }
|
26 |
serde_json = "1.0"
|
27 |
tracing-subscriber = "0.3.18"
|
|
|
33 |
futures-util = "0.3.29"
|
34 |
actix-multipart-extract = "0.1.5"
|
35 |
regex = "1.10.2"
|
36 |
+
tokio = { version = "1.35.1", features = ["rt", "time", "macros"] }
|
37 |
|
38 |
[[bin]]
|
39 |
name = "doc_gpt"
|
migration/src/m20220101_000001_create_table.rs
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
-
use sea_orm_migration::
|
2 |
use chrono::{ FixedOffset, Utc };
|
3 |
|
|
|
4 |
fn now() -> chrono::DateTime<FixedOffset> {
|
5 |
Utc::now().with_timezone(&FixedOffset::east_opt(3600 * 8).unwrap())
|
6 |
}
|
|
|
1 |
+
use sea_orm_migration::prelude::*;
|
2 |
use chrono::{ FixedOffset, Utc };
|
3 |
|
4 |
+
#[allow(dead_code)]
|
5 |
fn now() -> chrono::DateTime<FixedOffset> {
|
6 |
Utc::now().with_timezone(&FixedOffset::east_opt(3600 * 8).unwrap())
|
7 |
}
|
src/api/doc_info.rs
CHANGED
@@ -107,8 +107,12 @@ async fn upload(
|
|
107 |
payload: Multipart<UploadForm>,
|
108 |
data: web::Data<AppState>
|
109 |
) -> Result<HttpResponse, AppError> {
|
110 |
-
|
111 |
-
|
|
|
|
|
|
|
|
|
112 |
async fn add_number_to_filename(
|
113 |
file_name: &str,
|
114 |
conn: &DbConn,
|
@@ -138,9 +142,9 @@ async fn upload(
|
|
138 |
}
|
139 |
new_file_name
|
140 |
}
|
141 |
-
let fnm = add_number_to_filename(file_name, &data.conn, uid,
|
142 |
|
143 |
-
let bucket_name = format!("{}-upload",
|
144 |
let s3_client: &minio::s3::client::Client = &data.s3_client;
|
145 |
let buckets_exists = s3_client
|
146 |
.bucket_exists(&BucketExistsArgs::new(&bucket_name).unwrap()).await
|
@@ -152,7 +156,7 @@ async fn upload(
|
|
152 |
print!("Existing bucket: {}", bucket_name.clone());
|
153 |
}
|
154 |
|
155 |
-
let location = format!("/{}/{}",
|
156 |
.as_bytes()
|
157 |
.to_vec()
|
158 |
.iter()
|
@@ -164,8 +168,8 @@ async fn upload(
|
|
164 |
&mut PutObjectArgs::new(
|
165 |
&bucket_name,
|
166 |
&location,
|
167 |
-
&mut BufReader::new(
|
168 |
-
Some(
|
169 |
None
|
170 |
)?
|
171 |
).await?;
|
@@ -174,7 +178,7 @@ async fn upload(
|
|
174 |
did: Default::default(),
|
175 |
uid: uid,
|
176 |
doc_name: fnm.clone(),
|
177 |
-
size:
|
178 |
location,
|
179 |
r#type: file_type(&fnm),
|
180 |
thumbnail_base64: Default::default(),
|
@@ -183,9 +187,9 @@ async fn upload(
|
|
183 |
is_deleted: Default::default(),
|
184 |
}).await?;
|
185 |
|
186 |
-
let _ = Mutation::place_doc(&data.conn,
|
187 |
|
188 |
-
Ok(
|
189 |
}
|
190 |
|
191 |
#[derive(Deserialize, Debug)]
|
|
|
107 |
payload: Multipart<UploadForm>,
|
108 |
data: web::Data<AppState>
|
109 |
) -> Result<HttpResponse, AppError> {
|
110 |
+
|
111 |
+
|
112 |
+
Ok(HttpResponse::Ok().body("File uploaded successfully"))
|
113 |
+
}
|
114 |
+
|
115 |
+
pub(crate) async fn _upload_file(uid: i64, did: i64, file_name: &str, bytes: &[u8], data: &web::Data<AppState>) -> Result<(), AppError> {
|
116 |
async fn add_number_to_filename(
|
117 |
file_name: &str,
|
118 |
conn: &DbConn,
|
|
|
142 |
}
|
143 |
new_file_name
|
144 |
}
|
145 |
+
let fnm = add_number_to_filename(file_name, &data.conn, uid, did).await;
|
146 |
|
147 |
+
let bucket_name = format!("{}-upload", uid);
|
148 |
let s3_client: &minio::s3::client::Client = &data.s3_client;
|
149 |
let buckets_exists = s3_client
|
150 |
.bucket_exists(&BucketExistsArgs::new(&bucket_name).unwrap()).await
|
|
|
156 |
print!("Existing bucket: {}", bucket_name.clone());
|
157 |
}
|
158 |
|
159 |
+
let location = format!("/{}/{}", did, fnm)
|
160 |
.as_bytes()
|
161 |
.to_vec()
|
162 |
.iter()
|
|
|
168 |
&mut PutObjectArgs::new(
|
169 |
&bucket_name,
|
170 |
&location,
|
171 |
+
&mut BufReader::new(bytes),
|
172 |
+
Some(bytes.len()),
|
173 |
None
|
174 |
)?
|
175 |
).await?;
|
|
|
178 |
did: Default::default(),
|
179 |
uid: uid,
|
180 |
doc_name: fnm.clone(),
|
181 |
+
size: bytes.len() as i64,
|
182 |
location,
|
183 |
r#type: file_type(&fnm),
|
184 |
thumbnail_base64: Default::default(),
|
|
|
187 |
is_deleted: Default::default(),
|
188 |
}).await?;
|
189 |
|
190 |
+
let _ = Mutation::place_doc(&data.conn, did, doc.did.unwrap()).await?;
|
191 |
|
192 |
+
Ok(())
|
193 |
}
|
194 |
|
195 |
#[derive(Deserialize, Debug)]
|
src/main.rs
CHANGED
@@ -2,6 +2,7 @@ mod api;
|
|
2 |
mod entity;
|
3 |
mod service;
|
4 |
mod errors;
|
|
|
5 |
|
6 |
use std::env;
|
7 |
use actix_files::Files;
|
@@ -19,6 +20,7 @@ use minio::s3::http::BaseUrl;
|
|
19 |
use sea_orm::{ Database, DatabaseConnection };
|
20 |
use migration::{ Migrator, MigratorTrait };
|
21 |
use crate::errors::{ AppError, UserError };
|
|
|
22 |
|
23 |
#[derive(Debug, Clone)]
|
24 |
struct AppState {
|
@@ -138,4 +140,6 @@ fn init(cfg: &mut web::ServiceConfig) {
|
|
138 |
cfg.service(api::user_info::login);
|
139 |
cfg.service(api::user_info::register);
|
140 |
cfg.service(api::user_info::setting);
|
|
|
|
|
141 |
}
|
|
|
2 |
mod entity;
|
3 |
mod service;
|
4 |
mod errors;
|
5 |
+
mod web_socket;
|
6 |
|
7 |
use std::env;
|
8 |
use actix_files::Files;
|
|
|
20 |
use sea_orm::{ Database, DatabaseConnection };
|
21 |
use migration::{ Migrator, MigratorTrait };
|
22 |
use crate::errors::{ AppError, UserError };
|
23 |
+
use crate::web_socket::doc_info::upload_file_ws;
|
24 |
|
25 |
#[derive(Debug, Clone)]
|
26 |
struct AppState {
|
|
|
140 |
cfg.service(api::user_info::login);
|
141 |
cfg.service(api::user_info::register);
|
142 |
cfg.service(api::user_info::setting);
|
143 |
+
|
144 |
+
cfg.service(web::resource("/ws-upload-doc").route(web::get().to(upload_file_ws)));
|
145 |
}
|
src/web_socket/doc_info.rs
ADDED
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
use std::io::{Cursor, Write};
|
2 |
+
use std::time::{Duration, Instant};
|
3 |
+
use actix_rt::time::interval;
|
4 |
+
use actix_web::{HttpRequest, HttpResponse, rt, web};
|
5 |
+
use actix_web::web::Buf;
|
6 |
+
use actix_ws::Message;
|
7 |
+
use futures_util::{future, StreamExt};
|
8 |
+
use futures_util::future::Either;
|
9 |
+
use uuid::Uuid;
|
10 |
+
use crate::api::doc_info::_upload_file;
|
11 |
+
use crate::AppState;
|
12 |
+
use crate::errors::AppError;
|
13 |
+
|
14 |
+
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
|
15 |
+
|
16 |
+
/// How long before lack of client response causes a timeout.
|
17 |
+
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
|
18 |
+
|
19 |
+
pub async fn upload_file_ws(req: HttpRequest, stream: web::Payload, data: web::Data<AppState>) -> Result<HttpResponse, AppError> {
|
20 |
+
let (res, session, msg_stream) = actix_ws::handle(&req, stream)?;
|
21 |
+
|
22 |
+
// spawn websocket handler (and don't await it) so that the response is returned immediately
|
23 |
+
rt::spawn(upload_file_handler(data, session, msg_stream));
|
24 |
+
|
25 |
+
Ok(res)
|
26 |
+
}
|
27 |
+
|
28 |
+
async fn upload_file_handler(
|
29 |
+
data: web::Data<AppState>,
|
30 |
+
mut session: actix_ws::Session,
|
31 |
+
mut msg_stream: actix_ws::MessageStream,
|
32 |
+
) {
|
33 |
+
let mut bytes = Cursor::new(vec![]);
|
34 |
+
let mut last_heartbeat = Instant::now();
|
35 |
+
let mut interval = interval(HEARTBEAT_INTERVAL);
|
36 |
+
|
37 |
+
let reason = loop {
|
38 |
+
let tick = interval.tick();
|
39 |
+
tokio::pin!(tick);
|
40 |
+
|
41 |
+
match future::select(msg_stream.next(), tick).await {
|
42 |
+
// received message from WebSocket client
|
43 |
+
Either::Left((Some(Ok(msg)), _)) => {
|
44 |
+
match msg {
|
45 |
+
Message::Text(text) => {
|
46 |
+
session.text(text).await.unwrap();
|
47 |
+
}
|
48 |
+
|
49 |
+
Message::Binary(bin) => {
|
50 |
+
let mut pos = 0; // notice the name of the file that will be written
|
51 |
+
while pos < bin.len() {
|
52 |
+
let bytes_written = bytes.write(&bin[pos..]).unwrap();
|
53 |
+
pos += bytes_written
|
54 |
+
};
|
55 |
+
session.binary(bin).await.unwrap();
|
56 |
+
}
|
57 |
+
|
58 |
+
Message::Close(reason) => {
|
59 |
+
break reason;
|
60 |
+
}
|
61 |
+
|
62 |
+
Message::Ping(bytes) => {
|
63 |
+
last_heartbeat = Instant::now();
|
64 |
+
let _ = session.pong(&bytes).await;
|
65 |
+
}
|
66 |
+
|
67 |
+
Message::Pong(_) => {
|
68 |
+
last_heartbeat = Instant::now();
|
69 |
+
}
|
70 |
+
|
71 |
+
Message::Continuation(_) | Message::Nop => {}
|
72 |
+
};
|
73 |
+
}
|
74 |
+
Either::Left((Some(Err(_)), _)) => {
|
75 |
+
break None;
|
76 |
+
}
|
77 |
+
Either::Left((None, _)) => break None,
|
78 |
+
Either::Right((_inst, _)) => {
|
79 |
+
if Instant::now().duration_since(last_heartbeat) > CLIENT_TIMEOUT {
|
80 |
+
break None;
|
81 |
+
}
|
82 |
+
|
83 |
+
let _ = session.ping(b"").await;
|
84 |
+
}
|
85 |
+
}
|
86 |
+
};
|
87 |
+
let _ = session.close(reason).await;
|
88 |
+
|
89 |
+
if !bytes.has_remaining() {
|
90 |
+
return;
|
91 |
+
}
|
92 |
+
|
93 |
+
let uid = bytes.get_i64();
|
94 |
+
let did = bytes.get_i64();
|
95 |
+
|
96 |
+
_upload_file(uid, did, &Uuid::new_v4().to_string(), &bytes.into_inner(), &data).await.unwrap();
|
97 |
+
}
|
src/web_socket/mod.rs
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
pub mod doc_info;
|