mirror of
https://github.com/localsend/localsend.git
synced 2026-08-07 07:14:52 +00:00
refactor: restructure http server
This commit is contained in:
@@ -6,6 +6,7 @@ use crate::http::dto_v2::{
|
|||||||
PrepareUploadResponseDtoV2, PrepareUploadResultV2, RegisterDtoV2, RegisterResponseDtoV2,
|
PrepareUploadResponseDtoV2, PrepareUploadResultV2, RegisterDtoV2, RegisterResponseDtoV2,
|
||||||
};
|
};
|
||||||
use crate::model;
|
use crate::model;
|
||||||
|
use bytes::Bytes;
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use reqwest::{Response, StatusCode};
|
use reqwest::{Response, StatusCode};
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
@@ -227,7 +228,7 @@ impl LsHttpClientV2 {
|
|||||||
}
|
}
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let stream = ReceiverStream::new(content.into_receiver()).map(Ok::<Vec<u8>, anyhow::Error>);
|
let stream = ReceiverStream::new(content.into_receiver()).map(Ok::<Bytes, anyhow::Error>);
|
||||||
let body = reqwest::Body::wrap_stream(stream);
|
let body = reqwest::Body::wrap_stream(stream);
|
||||||
|
|
||||||
let res = tokio::select! {
|
let res = tokio::select! {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use crate::http::client::url::{ApiVersion, TargetUrl};
|
|||||||
use crate::http::dto::ProtocolType;
|
use crate::http::dto::ProtocolType;
|
||||||
use crate::{crypto, util};
|
use crate::{crypto, util};
|
||||||
use crate::{http, model};
|
use crate::{http, model};
|
||||||
|
use bytes::Bytes;
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use lru::LruCache;
|
use lru::LruCache;
|
||||||
use reqwest::{Response, StatusCode};
|
use reqwest::{Response, StatusCode};
|
||||||
@@ -215,7 +216,7 @@ impl LsHttpClientV3 {
|
|||||||
)
|
)
|
||||||
.body({
|
.body({
|
||||||
let stream =
|
let stream =
|
||||||
ReceiverStream::new(content.into_receiver()).map(Ok::<Vec<u8>, anyhow::Error>);
|
ReceiverStream::new(content.into_receiver()).map(Ok::<Bytes, anyhow::Error>);
|
||||||
reqwest::Body::wrap_stream(stream)
|
reqwest::Body::wrap_stream(stream)
|
||||||
})
|
})
|
||||||
.send();
|
.send();
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
use crate::http::server::error::AppError;
|
use crate::http::server::common::error::AppError;
|
||||||
use http_body_util::BodyExt;
|
use http_body_util::BodyExt;
|
||||||
use hyper::body::Incoming;
|
use hyper::body::Incoming;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::http::dto::ErrorResponse;
|
use crate::http::dto::ErrorResponse;
|
||||||
use crate::http::server::response::{BoxedBody, JsonResponse};
|
use crate::http::server::common::response::{BoxedBody, JsonResponse};
|
||||||
use hyper::{Response, StatusCode};
|
use hyper::{Response, StatusCode};
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
pub mod client_cert_verifier;
|
||||||
|
pub mod collect_to_json;
|
||||||
|
pub mod error;
|
||||||
|
pub mod pin;
|
||||||
|
pub mod query;
|
||||||
|
pub mod response;
|
||||||
|
pub mod save;
|
||||||
|
pub mod session;
|
||||||
@@ -1,8 +1,4 @@
|
|||||||
pub(crate) mod v2;
|
use crate::http::server::common::error::AppError;
|
||||||
pub(crate) mod v3;
|
|
||||||
pub(crate) mod web;
|
|
||||||
|
|
||||||
use crate::http::server::error::AppError;
|
|
||||||
use hyper::StatusCode;
|
use hyper::StatusCode;
|
||||||
use lru::LruCache;
|
use lru::LruCache;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
use bytes::Bytes;
|
||||||
|
use http_body_util::BodyExt;
|
||||||
|
use hyper::body::Incoming;
|
||||||
|
use hyper::Request;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tokio::sync::{mpsc, oneshot};
|
||||||
|
|
||||||
|
/// Channel capacity for file upload chunks (provides backpressure).
|
||||||
|
const UPLOAD_CHANNEL_CAPACITY: usize = 16;
|
||||||
|
|
||||||
|
/// Where the content of an uploaded file should go, decided by the application.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum FileUploadTarget {
|
||||||
|
/// The application consumes the binary chunks itself.
|
||||||
|
///
|
||||||
|
/// The server forwards chunks into `binary_tx` and closes it at end of file.
|
||||||
|
/// The application should compare the number of received bytes with `file.size`
|
||||||
|
/// and report the result on the sender side of `result_rx` which determines
|
||||||
|
/// the HTTP response (200 on `Ok`, 500 on `Err` or when the sender is dropped).
|
||||||
|
Stream {
|
||||||
|
/// Channel the server sends the binary chunks of the file into.
|
||||||
|
binary_tx: mpsc::Sender<Bytes>,
|
||||||
|
|
||||||
|
/// Channel on which the application reports whether the file was
|
||||||
|
/// processed successfully.
|
||||||
|
result_rx: oneshot::Receiver<Result<(), String>>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// The server writes the file to this path (created or truncated)
|
||||||
|
/// and reports the result on `result_tx`.
|
||||||
|
Path {
|
||||||
|
/// The path to write the file to.
|
||||||
|
path: PathBuf,
|
||||||
|
|
||||||
|
/// Channel on which the server reports whether the file was saved successfully.
|
||||||
|
result_tx: oneshot::Sender<Result<(), String>>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// The server writes the file to this raw file descriptor (Android only)
|
||||||
|
/// and reports the result on `result_tx`.
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
Fd {
|
||||||
|
/// The raw file descriptor to write the file to.
|
||||||
|
/// Ownership is transferred; the descriptor is closed after writing.
|
||||||
|
fd: std::os::fd::RawFd,
|
||||||
|
|
||||||
|
/// Channel on which the server reports whether the file was saved successfully.
|
||||||
|
result_tx: oneshot::Sender<Result<(), String>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn save_req_to_target(
|
||||||
|
req: Request<Incoming>,
|
||||||
|
target: FileUploadTarget,
|
||||||
|
file_size: u64,
|
||||||
|
) -> bool {
|
||||||
|
// Resolve the target into a chunk sender and a result receiver.
|
||||||
|
let (binary_tx, result_rx) = match target {
|
||||||
|
FileUploadTarget::Stream {
|
||||||
|
binary_tx,
|
||||||
|
result_rx,
|
||||||
|
} => (binary_tx, result_rx),
|
||||||
|
FileUploadTarget::Path { path, result_tx } => spawn_file_writer(
|
||||||
|
async move {
|
||||||
|
tokio::fs::File::create(&path)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to create {}: {e}", path.display()))
|
||||||
|
},
|
||||||
|
file_size,
|
||||||
|
result_tx,
|
||||||
|
),
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
FileUploadTarget::Fd { fd, result_tx } => spawn_file_writer(
|
||||||
|
async move {
|
||||||
|
use std::os::fd::FromRawFd;
|
||||||
|
|
||||||
|
// SAFETY: the descriptor is owned by this transfer; wrapping it in
|
||||||
|
// a File transfers that ownership so it is closed once writing finishes.
|
||||||
|
let std_file = unsafe { std::fs::File::from_raw_fd(fd) };
|
||||||
|
Ok(tokio::fs::File::from_std(std_file))
|
||||||
|
},
|
||||||
|
file_size,
|
||||||
|
result_tx,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Forward the request body to the target.
|
||||||
|
let mut body = req.into_body();
|
||||||
|
let mut stream_error = false;
|
||||||
|
while let Some(frame) = body.frame().await {
|
||||||
|
match frame {
|
||||||
|
Ok(frame) => {
|
||||||
|
let Ok(data) = frame.into_data() else {
|
||||||
|
continue; // ignore non-data frames (e.g. trailers)
|
||||||
|
};
|
||||||
|
if data.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if binary_tx.send(data).await.is_err() {
|
||||||
|
// The receiver is gone (dropped by the application or
|
||||||
|
// closed by the file writer after an error).
|
||||||
|
stream_error = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::warn!("Error reading upload body of file: {err:#}");
|
||||||
|
stream_error = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal end of file to the receiving side.
|
||||||
|
drop(binary_tx);
|
||||||
|
|
||||||
|
match stream_error {
|
||||||
|
true => false,
|
||||||
|
false => match result_rx.await {
|
||||||
|
Ok(Ok(())) => true,
|
||||||
|
Ok(Err(err)) => {
|
||||||
|
tracing::warn!("Failed to process file: {err}");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
Err(_) => false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawns a task that writes incoming chunks to a file provided by `open`.
|
||||||
|
///
|
||||||
|
/// Returns the sender for the binary chunks and a receiver for the final result.
|
||||||
|
/// The result is additionally reported to the application on `result_tx`.
|
||||||
|
fn spawn_file_writer(
|
||||||
|
open: impl Future<Output = Result<tokio::fs::File, String>> + Send + 'static,
|
||||||
|
expected_size: u64,
|
||||||
|
result_tx: oneshot::Sender<Result<(), String>>,
|
||||||
|
) -> (mpsc::Sender<Bytes>, oneshot::Receiver<Result<(), String>>) {
|
||||||
|
let (binary_tx, mut binary_rx) = mpsc::channel::<Bytes>(UPLOAD_CHANNEL_CAPACITY);
|
||||||
|
let (internal_tx, internal_rx) = oneshot::channel::<Result<(), String>>();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let result = write_file_from_receiver(open, expected_size, &mut binary_rx).await;
|
||||||
|
// Unblock the request handler if it is still sending chunks.
|
||||||
|
binary_rx.close();
|
||||||
|
let _ = result_tx.send(result.clone());
|
||||||
|
let _ = internal_tx.send(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
(binary_tx, internal_rx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes all chunks received on `rx` to the file provided by `open`.
|
||||||
|
///
|
||||||
|
/// Fails if the total number of written bytes does not match `expected_size`
|
||||||
|
/// (e.g. the sender disconnected mid-transfer).
|
||||||
|
async fn write_file_from_receiver(
|
||||||
|
open: impl Future<Output = Result<tokio::fs::File, String>>,
|
||||||
|
expected_size: u64,
|
||||||
|
rx: &mut mpsc::Receiver<Bytes>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
|
||||||
|
let mut file = open.await?;
|
||||||
|
let mut written: u64 = 0;
|
||||||
|
while let Some(chunk) = rx.recv().await {
|
||||||
|
file.write_all(&chunk)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to write file: {e}"))?;
|
||||||
|
written += chunk.len() as u64;
|
||||||
|
}
|
||||||
|
file.flush()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to flush file: {e}"))?;
|
||||||
|
|
||||||
|
if written != expected_size {
|
||||||
|
return Err(format!(
|
||||||
|
"Expected {expected_size} bytes, received {written}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
+32
-43
@@ -1,23 +1,17 @@
|
|||||||
mod client_cert_verifier;
|
pub mod common;
|
||||||
mod collect_to_json;
|
pub mod v2;
|
||||||
mod controller;
|
pub mod v3;
|
||||||
mod error;
|
pub mod web;
|
||||||
mod query;
|
|
||||||
mod response;
|
|
||||||
mod session;
|
|
||||||
|
|
||||||
pub use crate::http::server::controller::v2::{
|
|
||||||
FileUploadTargetV2, PrepareUploadDecisionV2, ServerEventV2, SessionEndReasonV2,
|
|
||||||
};
|
|
||||||
pub use crate::http::server::controller::web::{WebSendConfig, WebSendEvent, WebSendI18n};
|
|
||||||
|
|
||||||
use crate::crypto::cert::public_key_from_cert_der;
|
use crate::crypto::cert::public_key_from_cert_der;
|
||||||
use crate::http::server::client_cert_verifier::CustomClientCertVerifier;
|
use crate::http::server::v2::ServerEventV2;
|
||||||
use crate::http::server::controller::web::WebPageState;
|
use crate::http::server::web::WebSendConfig;
|
||||||
use crate::http::server::error::AppError;
|
|
||||||
use crate::http::server::response::BoxedBody;
|
|
||||||
use crate::http::server::session::SessionStateV2;
|
|
||||||
use crate::http::state::ClientInfo;
|
use crate::http::state::ClientInfo;
|
||||||
|
use common::client_cert_verifier::CustomClientCertVerifier;
|
||||||
|
use common::error::AppError;
|
||||||
|
use common::response;
|
||||||
|
use common::response::BoxedBody;
|
||||||
|
use common::session::SessionStateV2;
|
||||||
use hyper::body::Incoming;
|
use hyper::body::Incoming;
|
||||||
use hyper::{Method, Request, Response, StatusCode};
|
use hyper::{Method, Request, Response, StatusCode};
|
||||||
use hyper_util::rt::{TokioExecutor, TokioIo};
|
use hyper_util::rt::{TokioExecutor, TokioIo};
|
||||||
@@ -31,6 +25,7 @@ use std::num::NonZeroUsize;
|
|||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::{mpsc, oneshot, Mutex};
|
use tokio::sync::{mpsc, oneshot, Mutex};
|
||||||
|
use web::WebPageState;
|
||||||
|
|
||||||
/// Configuration for the v2 (legacy) protocol endpoints.
|
/// Configuration for the v2 (legacy) protocol endpoints.
|
||||||
pub struct ServerConfigV2 {
|
pub struct ServerConfigV2 {
|
||||||
@@ -57,7 +52,7 @@ pub(crate) struct V2State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct AppState {
|
pub struct AppState {
|
||||||
/// Information about server's device.
|
/// Information about server's device.
|
||||||
info: Arc<Mutex<ClientInfo>>,
|
info: Arc<Mutex<ClientInfo>>,
|
||||||
|
|
||||||
@@ -277,7 +272,7 @@ fn create_tls_config(tls_config: &TlsConfig) -> anyhow::Result<tokio_rustls::Tls
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct RequestClientInfo {
|
pub struct RequestClientInfo {
|
||||||
/// The IP address of the client.
|
/// The IP address of the client.
|
||||||
ip: IpAddr,
|
ip: IpAddr,
|
||||||
|
|
||||||
@@ -324,67 +319,61 @@ async fn handle_request_inner(mut req: Request<Incoming>) -> Result<Response<Box
|
|||||||
let v2_enabled = state.v2.is_some();
|
let v2_enabled = state.v2.is_some();
|
||||||
|
|
||||||
match (req.method(), req.uri().path()) {
|
match (req.method(), req.uri().path()) {
|
||||||
(&Method::GET, "/") => Ok(controller::web::index(&state)),
|
(&Method::GET, "/") => Ok(web::index(&state)),
|
||||||
(&Method::GET, "/main.js") => Ok(controller::web::main_js(&state)),
|
(&Method::GET, "/main.js") => Ok(web::main_js(&state)),
|
||||||
(&Method::GET, "/i18n.json") => controller::web::i18n(&state),
|
(&Method::GET, "/i18n.json") => web::i18n(&state),
|
||||||
(&Method::POST, "/api/localsend/v2/prepare-download") => {
|
(&Method::POST, "/api/localsend/v2/prepare-download") => {
|
||||||
controller::web::prepare_download(req, state, client_info).await
|
web::prepare_download(req, state, client_info).await
|
||||||
}
|
}
|
||||||
(&Method::GET, "/api/localsend/v2/download") => {
|
(&Method::GET, "/api/localsend/v2/download") => {
|
||||||
controller::web::download(req, state, client_info).await
|
web::download(req, state, client_info).await
|
||||||
}
|
}
|
||||||
(&Method::POST, "/api/localsend/v2/register") => {
|
(&Method::POST, "/api/localsend/v2/register") => {
|
||||||
if !v2_enabled {
|
if !v2_enabled {
|
||||||
return Err(AppError::Status(StatusCode::NOT_FOUND));
|
return Err(AppError::Status(StatusCode::NOT_FOUND));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(
|
Ok(v2::register(req.into_body(), state, client_info)
|
||||||
controller::v2::register(req.into_body(), state, client_info)
|
.await?
|
||||||
.await?
|
.into_response())
|
||||||
.into_response(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
(&Method::GET, "/api/localsend/v2/info") => {
|
(&Method::GET, "/api/localsend/v2/info") => {
|
||||||
if !v2_enabled {
|
if !v2_enabled {
|
||||||
return Err(AppError::Status(StatusCode::NOT_FOUND));
|
return Err(AppError::Status(StatusCode::NOT_FOUND));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(controller::v2::info(state).await?.into_response())
|
Ok(v2::info(state).await?.into_response())
|
||||||
}
|
}
|
||||||
(&Method::POST, "/api/localsend/v2/prepare-upload") => {
|
(&Method::POST, "/api/localsend/v2/prepare-upload") => {
|
||||||
if !v2_enabled {
|
if !v2_enabled {
|
||||||
return Err(AppError::Status(StatusCode::NOT_FOUND));
|
return Err(AppError::Status(StatusCode::NOT_FOUND));
|
||||||
}
|
}
|
||||||
|
|
||||||
controller::v2::prepare_upload(req, state, client_info).await
|
v2::prepare_upload(req, state, client_info).await
|
||||||
}
|
}
|
||||||
(&Method::POST, "/api/localsend/v2/upload") => {
|
(&Method::POST, "/api/localsend/v2/upload") => {
|
||||||
if !v2_enabled {
|
if !v2_enabled {
|
||||||
return Err(AppError::Status(StatusCode::NOT_FOUND));
|
return Err(AppError::Status(StatusCode::NOT_FOUND));
|
||||||
}
|
}
|
||||||
|
|
||||||
controller::v2::upload(req, state, client_info).await
|
v2::upload(req, state, client_info).await
|
||||||
}
|
}
|
||||||
(&Method::POST, "/api/localsend/v2/cancel") => {
|
(&Method::POST, "/api/localsend/v2/cancel") => {
|
||||||
if !v2_enabled {
|
if !v2_enabled {
|
||||||
return Err(AppError::Status(StatusCode::NOT_FOUND));
|
return Err(AppError::Status(StatusCode::NOT_FOUND));
|
||||||
}
|
}
|
||||||
|
|
||||||
controller::v2::cancel(req, state).await
|
v2::cancel(req, state).await
|
||||||
}
|
}
|
||||||
(&Method::POST, "/api/localsend/v3/nonce") => {
|
(&Method::POST, "/api/localsend/v3/nonce") => {
|
||||||
Ok(
|
Ok(v3::nonce_exchange(req.into_body(), state, client_info)
|
||||||
controller::v3::nonce_exchange(req.into_body(), state, client_info)
|
.await?
|
||||||
.await?
|
.into_response())
|
||||||
.into_response(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
(&Method::POST, "/api/localsend/v3/register") => {
|
(&Method::POST, "/api/localsend/v3/register") => {
|
||||||
Ok(
|
Ok(v3::register(req.into_body(), state, client_info)
|
||||||
controller::v3::register(req.into_body(), state, client_info)
|
.await?
|
||||||
.await?
|
.into_response())
|
||||||
.into_response(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let mut res = Response::new(response::empty_body());
|
let mut res = Response::new(response::empty_body());
|
||||||
|
|||||||
@@ -2,24 +2,23 @@ use crate::http::dto_v2::{
|
|||||||
InfoResponseDtoV2, PrepareUploadRequestDtoV2, PrepareUploadResponseDtoV2, RegisterDtoV2,
|
InfoResponseDtoV2, PrepareUploadRequestDtoV2, PrepareUploadResponseDtoV2, RegisterDtoV2,
|
||||||
RegisterResponseDtoV2, PROTOCOL_VERSION_V2,
|
RegisterResponseDtoV2, PROTOCOL_VERSION_V2,
|
||||||
};
|
};
|
||||||
use crate::http::server::collect_to_json::CollectToJson;
|
use crate::http::server::common::collect_to_json::CollectToJson;
|
||||||
use crate::http::server::controller::check_pin;
|
use crate::http::server::common::error::AppError;
|
||||||
use crate::http::server::error::AppError;
|
use crate::http::server::common::pin::check_pin;
|
||||||
use crate::http::server::query::parse_query;
|
use crate::http::server::common::query::parse_query;
|
||||||
use crate::http::server::response::{empty_body, BoxedBody, JsonResponse};
|
use crate::http::server::common::response::{empty_body, BoxedBody, JsonResponse};
|
||||||
use crate::http::server::session::{FileStatusV2, SessionFileV2, SessionStateV2, UploadSessionV2};
|
use crate::http::server::common::save::FileUploadTarget;
|
||||||
use crate::http::server::{AppState, RequestClientInfo, V2State};
|
use crate::http::server::common::session::{
|
||||||
|
FileStatusV2, SessionFileV2, SessionStateV2, UploadSessionV2,
|
||||||
|
};
|
||||||
|
use crate::http::server::{common, AppState, RequestClientInfo, V2State};
|
||||||
use crate::model::transfer::FileDto;
|
use crate::model::transfer::FileDto;
|
||||||
use bytes::Bytes;
|
|
||||||
use http_body_util::BodyExt;
|
|
||||||
use hyper::body::Incoming;
|
use hyper::body::Incoming;
|
||||||
use hyper::{Request, Response, StatusCode};
|
use hyper::{Request, Response, StatusCode};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::future::Future;
|
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::{mpsc, oneshot};
|
use tokio::sync::oneshot;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Events emitted by the v2 HTTP server that must be handled by the application.
|
/// Events emitted by the v2 HTTP server that must be handled by the application.
|
||||||
@@ -68,7 +67,7 @@ pub enum ServerEventV2 {
|
|||||||
file: FileDto,
|
file: FileDto,
|
||||||
|
|
||||||
/// Channel to send the target the file content should be written to.
|
/// Channel to send the target the file content should be written to.
|
||||||
target_tx: oneshot::Sender<FileUploadTargetV2>,
|
target_tx: oneshot::Sender<FileUploadTarget>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// An upload session ended.
|
/// An upload session ended.
|
||||||
@@ -81,47 +80,6 @@ pub enum ServerEventV2 {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where the content of an uploaded file should go, decided by the application.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum FileUploadTargetV2 {
|
|
||||||
/// The application consumes the binary chunks itself.
|
|
||||||
///
|
|
||||||
/// The server forwards chunks into `binary_tx` and closes it at end of file.
|
|
||||||
/// The application should compare the number of received bytes with `file.size`
|
|
||||||
/// and report the result on the sender side of `result_rx` which determines
|
|
||||||
/// the HTTP response (200 on `Ok`, 500 on `Err` or when the sender is dropped).
|
|
||||||
Stream {
|
|
||||||
/// Channel the server sends the binary chunks of the file into.
|
|
||||||
binary_tx: mpsc::Sender<Bytes>,
|
|
||||||
|
|
||||||
/// Channel on which the application reports whether the file was
|
|
||||||
/// processed successfully.
|
|
||||||
result_rx: oneshot::Receiver<Result<(), String>>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// The server writes the file to this path (created or truncated)
|
|
||||||
/// and reports the result on `result_tx`.
|
|
||||||
Path {
|
|
||||||
/// The path to write the file to.
|
|
||||||
path: PathBuf,
|
|
||||||
|
|
||||||
/// Channel on which the server reports whether the file was saved successfully.
|
|
||||||
result_tx: oneshot::Sender<Result<(), String>>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// The server writes the file to this raw file descriptor (Android only)
|
|
||||||
/// and reports the result on `result_tx`.
|
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
Fd {
|
|
||||||
/// The raw file descriptor to write the file to.
|
|
||||||
/// Ownership is transferred; the descriptor is closed after writing.
|
|
||||||
fd: std::os::fd::RawFd,
|
|
||||||
|
|
||||||
/// Channel on which the server reports whether the file was saved successfully.
|
|
||||||
result_tx: oneshot::Sender<Result<(), String>>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The application's decision for a prepare-upload request.
|
/// The application's decision for a prepare-upload request.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PrepareUploadDecisionV2 {
|
pub enum PrepareUploadDecisionV2 {
|
||||||
@@ -143,9 +101,6 @@ pub enum SessionEndReasonV2 {
|
|||||||
Cancelled,
|
Cancelled,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Channel capacity for file upload chunks (provides backpressure).
|
|
||||||
const UPLOAD_CHANNEL_CAPACITY: usize = 16;
|
|
||||||
|
|
||||||
pub(crate) async fn register(
|
pub(crate) async fn register(
|
||||||
body: Incoming,
|
body: Incoming,
|
||||||
state: AppState,
|
state: AppState,
|
||||||
@@ -348,7 +303,7 @@ pub(crate) async fn upload(
|
|||||||
let mut upload_guard = UploadGuard::new(v2.clone(), session_id.clone(), file_id.clone());
|
let mut upload_guard = UploadGuard::new(v2.clone(), session_id.clone(), file_id.clone());
|
||||||
|
|
||||||
let file_size = file_dto.size;
|
let file_size = file_dto.size;
|
||||||
let (target_tx, target_rx) = oneshot::channel::<FileUploadTargetV2>();
|
let (target_tx, target_rx) = oneshot::channel::<FileUploadTarget>();
|
||||||
|
|
||||||
let event = ServerEventV2::FileUpload {
|
let event = ServerEventV2::FileUpload {
|
||||||
session_id: session_id.clone(),
|
session_id: session_id.clone(),
|
||||||
@@ -366,77 +321,7 @@ pub(crate) async fn upload(
|
|||||||
return Err(AppError::Status(StatusCode::INTERNAL_SERVER_ERROR));
|
return Err(AppError::Status(StatusCode::INTERNAL_SERVER_ERROR));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Resolve the target into a chunk sender and a result receiver.
|
let success = common::save::save_req_to_target(req, target, file_size).await;
|
||||||
let (binary_tx, result_rx) = match target {
|
|
||||||
FileUploadTargetV2::Stream {
|
|
||||||
binary_tx,
|
|
||||||
result_rx,
|
|
||||||
} => (binary_tx, result_rx),
|
|
||||||
FileUploadTargetV2::Path { path, result_tx } => spawn_file_writer(
|
|
||||||
async move {
|
|
||||||
tokio::fs::File::create(&path)
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to create {}: {e}", path.display()))
|
|
||||||
},
|
|
||||||
file_size,
|
|
||||||
result_tx,
|
|
||||||
),
|
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
FileUploadTargetV2::Fd { fd, result_tx } => spawn_file_writer(
|
|
||||||
async move {
|
|
||||||
use std::os::fd::FromRawFd;
|
|
||||||
|
|
||||||
// SAFETY: the descriptor is owned by this transfer; wrapping it in
|
|
||||||
// a File transfers that ownership so it is closed once writing finishes.
|
|
||||||
let std_file = unsafe { std::fs::File::from_raw_fd(fd) };
|
|
||||||
Ok(tokio::fs::File::from_std(std_file))
|
|
||||||
},
|
|
||||||
file_size,
|
|
||||||
result_tx,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Forward the request body to the target.
|
|
||||||
let mut body = req.into_body();
|
|
||||||
let mut stream_error = false;
|
|
||||||
while let Some(frame) = body.frame().await {
|
|
||||||
match frame {
|
|
||||||
Ok(frame) => {
|
|
||||||
let Ok(data) = frame.into_data() else {
|
|
||||||
continue; // ignore non-data frames (e.g. trailers)
|
|
||||||
};
|
|
||||||
if data.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if binary_tx.send(data).await.is_err() {
|
|
||||||
// The receiver is gone (dropped by the application or
|
|
||||||
// closed by the file writer after an error).
|
|
||||||
stream_error = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
tracing::warn!("Error reading upload body of file {file_id}: {err:#}");
|
|
||||||
stream_error = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signal end of file to the receiving side.
|
|
||||||
drop(binary_tx);
|
|
||||||
|
|
||||||
let success = match stream_error {
|
|
||||||
true => false,
|
|
||||||
false => match result_rx.await {
|
|
||||||
Ok(Ok(())) => true,
|
|
||||||
Ok(Err(err)) => {
|
|
||||||
tracing::warn!("Failed to process file {file_id}: {err}");
|
|
||||||
false
|
|
||||||
}
|
|
||||||
Err(_) => false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
upload_guard.finish(success).await;
|
upload_guard.finish(success).await;
|
||||||
|
|
||||||
@@ -446,60 +331,6 @@ pub(crate) async fn upload(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawns a task that writes incoming chunks to a file provided by `open`.
|
|
||||||
///
|
|
||||||
/// Returns the sender for the binary chunks and a receiver for the final result.
|
|
||||||
/// The result is additionally reported to the application on `result_tx`.
|
|
||||||
fn spawn_file_writer(
|
|
||||||
open: impl Future<Output = Result<tokio::fs::File, String>> + Send + 'static,
|
|
||||||
expected_size: u64,
|
|
||||||
result_tx: oneshot::Sender<Result<(), String>>,
|
|
||||||
) -> (mpsc::Sender<Bytes>, oneshot::Receiver<Result<(), String>>) {
|
|
||||||
let (binary_tx, mut binary_rx) = mpsc::channel::<Bytes>(UPLOAD_CHANNEL_CAPACITY);
|
|
||||||
let (internal_tx, internal_rx) = oneshot::channel::<Result<(), String>>();
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let result = write_file_from_receiver(open, expected_size, &mut binary_rx).await;
|
|
||||||
// Unblock the request handler if it is still sending chunks.
|
|
||||||
binary_rx.close();
|
|
||||||
let _ = result_tx.send(result.clone());
|
|
||||||
let _ = internal_tx.send(result);
|
|
||||||
});
|
|
||||||
|
|
||||||
(binary_tx, internal_rx)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes all chunks received on `rx` to the file provided by `open`.
|
|
||||||
///
|
|
||||||
/// Fails if the total number of written bytes does not match `expected_size`
|
|
||||||
/// (e.g. the sender disconnected mid-transfer).
|
|
||||||
async fn write_file_from_receiver(
|
|
||||||
open: impl Future<Output = Result<tokio::fs::File, String>>,
|
|
||||||
expected_size: u64,
|
|
||||||
rx: &mut mpsc::Receiver<Bytes>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
use tokio::io::AsyncWriteExt;
|
|
||||||
|
|
||||||
let mut file = open.await?;
|
|
||||||
let mut written: u64 = 0;
|
|
||||||
while let Some(chunk) = rx.recv().await {
|
|
||||||
file.write_all(&chunk)
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to write file: {e}"))?;
|
|
||||||
written += chunk.len() as u64;
|
|
||||||
}
|
|
||||||
file.flush()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to flush file: {e}"))?;
|
|
||||||
|
|
||||||
if written != expected_size {
|
|
||||||
return Err(format!(
|
|
||||||
"Expected {expected_size} bytes, received {written}"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn cancel(
|
pub(crate) async fn cancel(
|
||||||
req: Request<Incoming>,
|
req: Request<Incoming>,
|
||||||
state: AppState,
|
state: AppState,
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::http::dto::{NonceRequest, NonceResponse, RegisterDto, RegisterResponseDto};
|
use crate::http::dto::{NonceRequest, NonceResponse, RegisterDto, RegisterResponseDto};
|
||||||
use crate::http::server::collect_to_json::CollectToJson;
|
use crate::http::server::common::collect_to_json::CollectToJson;
|
||||||
use crate::http::server::error::AppError;
|
use crate::http::server::common::error::AppError;
|
||||||
use crate::http::server::response::JsonResponse;
|
use crate::http::server::common::response::JsonResponse;
|
||||||
use crate::http::server::{AppState, RequestClientInfo};
|
use crate::http::server::{AppState, RequestClientInfo};
|
||||||
use crate::{crypto, util};
|
use crate::{crypto, util};
|
||||||
use hyper::body::Incoming;
|
use hyper::body::Incoming;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
use crate::http::dto_v2::{InfoResponseDtoV2, PrepareDownloadResponseDtoV2, PROTOCOL_VERSION_V2};
|
use crate::http::dto_v2::{InfoResponseDtoV2, PrepareDownloadResponseDtoV2, PROTOCOL_VERSION_V2};
|
||||||
use crate::http::server::controller::check_pin;
|
use crate::http::server::common::error::AppError;
|
||||||
use crate::http::server::error::AppError;
|
use crate::http::server::common::pin::check_pin;
|
||||||
use crate::http::server::query::parse_query;
|
use crate::http::server::common::query::parse_query;
|
||||||
use crate::http::server::response::{full_body, BoxedBody, JsonResponse};
|
use crate::http::server::common::response::{full_body, BoxedBody, JsonResponse};
|
||||||
use crate::http::server::{AppState, RequestClientInfo};
|
use crate::http::server::{AppState, RequestClientInfo};
|
||||||
use crate::model::transfer::{FileContent, FileDto};
|
use crate::model::transfer::{FileContent, FileDto};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
@@ -65,9 +65,9 @@ pub enum WebSendEvent {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const INDEX_HTML: &str = include_str!("../../../../assets/web/index.html");
|
const INDEX_HTML: &str = include_str!("../../../assets/web/index.html");
|
||||||
const MAIN_JS: &str = include_str!("../../../../assets/web/main.js");
|
const MAIN_JS: &str = include_str!("../../../assets/web/main.js");
|
||||||
const ERROR_403_HTML: &str = include_str!("../../../../assets/web/error-403.html");
|
const ERROR_403_HTML: &str = include_str!("../../../assets/web/error-403.html");
|
||||||
|
|
||||||
/// Characters that are percent-encoded in the content-disposition file name.
|
/// Characters that are percent-encoded in the content-disposition file name.
|
||||||
/// Matches the component encoding of RFC 2396 (letters, digits and marks are kept).
|
/// Matches the component encoding of RFC 2396 (letters, digits and marks are kept).
|
||||||
@@ -415,7 +415,7 @@ async fn file_list_response(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Streams application-provided chunks as a response body.
|
/// Streams application-provided chunks as a response body.
|
||||||
fn receiver_stream_body(binary_rx: mpsc::Receiver<Vec<u8>>) -> BoxedBody {
|
fn receiver_stream_body(binary_rx: mpsc::Receiver<Bytes>) -> BoxedBody {
|
||||||
let stream = ReceiverStream::new(binary_rx)
|
let stream = ReceiverStream::new(binary_rx)
|
||||||
.map(|chunk| Ok::<_, std::io::Error>(Frame::data(Bytes::from(chunk))));
|
.map(|chunk| Ok::<_, std::io::Error>(Frame::data(Bytes::from(chunk))));
|
||||||
StreamBody::new(stream).boxed()
|
StreamBody::new(stream).boxed()
|
||||||
+3
-2
@@ -7,7 +7,8 @@ mod webrtc;
|
|||||||
use crate::crypto::token;
|
use crate::crypto::token;
|
||||||
use crate::http::client::LsHttpClientV3;
|
use crate::http::client::LsHttpClientV3;
|
||||||
use crate::http::dto::{PrepareUploadRequestDto, ProtocolType, RegisterDto};
|
use crate::http::dto::{PrepareUploadRequestDto, ProtocolType, RegisterDto};
|
||||||
use crate::http::server::{FileUploadTargetV2, PrepareUploadDecisionV2, ServerEventV2};
|
use crate::http::server::common::save::FileUploadTarget;
|
||||||
|
use crate::http::server::v2::{PrepareUploadDecisionV2, ServerEventV2};
|
||||||
use crate::http::server::{ServerConfigV2, TlsConfig};
|
use crate::http::server::{ServerConfigV2, TlsConfig};
|
||||||
use crate::model::discovery::DeviceType;
|
use crate::model::discovery::DeviceType;
|
||||||
use crate::webrtc::signaling::{ClientInfo, WsServerMessage};
|
use crate::webrtc::signaling::{ClientInfo, WsServerMessage};
|
||||||
@@ -169,7 +170,7 @@ async fn server_test() -> Result<()> {
|
|||||||
} => {
|
} => {
|
||||||
let (binary_tx, mut binary_rx) = mpsc::channel::<Bytes>(16);
|
let (binary_tx, mut binary_rx) = mpsc::channel::<Bytes>(16);
|
||||||
let (result_tx, result_rx) = oneshot::channel::<Result<(), String>>();
|
let (result_tx, result_rx) = oneshot::channel::<Result<(), String>>();
|
||||||
let _ = target_tx.send(FileUploadTargetV2::Stream {
|
let _ = target_tx.send(FileUploadTarget::Stream {
|
||||||
binary_tx,
|
binary_tx,
|
||||||
result_rx,
|
result_rx,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use bytes::Bytes;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
@@ -14,7 +15,7 @@ const FILE_CHANNEL_CAPACITY: usize = 16;
|
|||||||
pub enum FileContent {
|
pub enum FileContent {
|
||||||
/// A stream of binary chunks. The channel is closed once the file has been
|
/// A stream of binary chunks. The channel is closed once the file has been
|
||||||
/// fully provided.
|
/// fully provided.
|
||||||
Stream(mpsc::Receiver<Vec<u8>>),
|
Stream(mpsc::Receiver<Bytes>),
|
||||||
|
|
||||||
/// A path to a regular file the content is read from.
|
/// A path to a regular file the content is read from.
|
||||||
Path(PathBuf),
|
Path(PathBuf),
|
||||||
@@ -30,7 +31,7 @@ impl FileContent {
|
|||||||
/// [`FileContent::Stream`] is returned as-is. For [`FileContent::Path`] and
|
/// [`FileContent::Stream`] is returned as-is. For [`FileContent::Path`] and
|
||||||
/// [`FileContent::Fd`], a background task reads the file and forwards the
|
/// [`FileContent::Fd`], a background task reads the file and forwards the
|
||||||
/// chunks; the channel is closed on EOF or on an I/O error.
|
/// chunks; the channel is closed on EOF or on an I/O error.
|
||||||
pub fn into_receiver(self) -> mpsc::Receiver<Vec<u8>> {
|
pub fn into_receiver(self) -> mpsc::Receiver<Bytes> {
|
||||||
match self {
|
match self {
|
||||||
FileContent::Stream(rx) => rx,
|
FileContent::Stream(rx) => rx,
|
||||||
FileContent::Path(path) => {
|
FileContent::Path(path) => {
|
||||||
@@ -64,15 +65,15 @@ impl FileContent {
|
|||||||
/// Reads `file` to EOF, forwarding chunks on `tx`.
|
/// Reads `file` to EOF, forwarding chunks on `tx`.
|
||||||
///
|
///
|
||||||
/// Stops early if the receiver is gone or a read error occurs.
|
/// Stops early if the receiver is gone or a read error occurs.
|
||||||
async fn read_file_into_sender(mut file: tokio::fs::File, tx: mpsc::Sender<Vec<u8>>) {
|
async fn read_file_into_sender(mut file: tokio::fs::File, tx: mpsc::Sender<Bytes>) {
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
let mut buffer = vec![0u8; 64 * 1024];
|
let mut buffer = bytes::BytesMut::with_capacity(64 * 1024);
|
||||||
loop {
|
loop {
|
||||||
match file.read(&mut buffer).await {
|
match file.read_buf(&mut buffer).await {
|
||||||
Ok(0) => break,
|
Ok(0) => break,
|
||||||
Ok(n) => {
|
Ok(_) => {
|
||||||
if tx.send(buffer[..n].to_vec()).await.is_err() {
|
if tx.send(buffer.split().freeze()).await.is_err() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
#![cfg(feature = "http")]
|
#![cfg(feature = "http")]
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
use localsend::http::client::{ClientError, LsHttpClientV2};
|
use localsend::http::client::{ClientError, LsHttpClientV2};
|
||||||
use localsend::http::dto::ProtocolType;
|
use localsend::http::dto::ProtocolType;
|
||||||
use localsend::http::dto_v2::{PrepareUploadRequestDtoV2, ProtocolTypeV2, RegisterDtoV2};
|
use localsend::http::dto_v2::{PrepareUploadRequestDtoV2, ProtocolTypeV2, RegisterDtoV2};
|
||||||
|
use localsend::http::server::common::save::FileUploadTarget;
|
||||||
|
use localsend::http::server::v2::{PrepareUploadDecisionV2, ServerEventV2, SessionEndReasonV2};
|
||||||
use localsend::http::server::{start_with_port, ServerConfigV2};
|
use localsend::http::server::{start_with_port, ServerConfigV2};
|
||||||
use localsend::http::server::{
|
|
||||||
FileUploadTargetV2, PrepareUploadDecisionV2, ServerEventV2, SessionEndReasonV2,
|
|
||||||
};
|
|
||||||
use localsend::http::state::ClientInfo;
|
use localsend::http::state::ClientInfo;
|
||||||
use localsend::model::transfer::{FileContent, FileDto};
|
use localsend::model::transfer::{FileContent, FileDto};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -69,7 +69,7 @@ async fn start_test_server(
|
|||||||
None => {
|
None => {
|
||||||
let (binary_tx, mut binary_rx) = mpsc::channel(16);
|
let (binary_tx, mut binary_rx) = mpsc::channel(16);
|
||||||
let (result_tx, result_rx) = oneshot::channel();
|
let (result_tx, result_rx) = oneshot::channel();
|
||||||
let _ = target_tx.send(FileUploadTargetV2::Stream {
|
let _ = target_tx.send(FileUploadTarget::Stream {
|
||||||
binary_tx,
|
binary_tx,
|
||||||
result_rx,
|
result_rx,
|
||||||
});
|
});
|
||||||
@@ -85,7 +85,7 @@ async fn start_test_server(
|
|||||||
Some(dir) => {
|
Some(dir) => {
|
||||||
let path = dir.join(&file_id);
|
let path = dir.join(&file_id);
|
||||||
let (result_tx, result_rx) = oneshot::channel();
|
let (result_tx, result_rx) = oneshot::channel();
|
||||||
let _ = target_tx.send(FileUploadTargetV2::Path {
|
let _ = target_tx.send(FileUploadTarget::Path {
|
||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
result_tx,
|
result_tx,
|
||||||
});
|
});
|
||||||
@@ -206,11 +206,11 @@ async fn upload_bytes(
|
|||||||
token: &str,
|
token: &str,
|
||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
) -> Result<(), ClientError> {
|
) -> Result<(), ClientError> {
|
||||||
let (tx, rx) = mpsc::channel::<Vec<u8>>(4);
|
let (tx, rx) = mpsc::channel::<Bytes>(4);
|
||||||
let chunks: Vec<Vec<u8>> = bytes.chunks(1024).map(|chunk| chunk.to_vec()).collect();
|
let chunks: Vec<Vec<u8>> = bytes.chunks(1024).map(|chunk| chunk.to_vec()).collect();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
for chunk in chunks {
|
for chunk in chunks {
|
||||||
if tx.send(chunk).await.is_err() {
|
if tx.send(Bytes::from(chunk)).await.is_err() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use localsend::http::client::{ClientError, LsHttpClientV2};
|
use localsend::http::client::{ClientError, LsHttpClientV2};
|
||||||
use localsend::http::dto::ProtocolType;
|
use localsend::http::dto::ProtocolType;
|
||||||
use localsend::http::server::{start_with_port, ServerConfigV2, WebSendConfig, WebSendI18n};
|
use localsend::http::server::v2::ServerEventV2;
|
||||||
use localsend::http::server::{ServerEventV2, WebSendEvent};
|
use localsend::http::server::web::WebSendConfig;
|
||||||
|
use localsend::http::server::web::{WebSendEvent, WebSendI18n};
|
||||||
|
use localsend::http::server::{start_with_port, ServerConfigV2};
|
||||||
use localsend::http::state::ClientInfo;
|
use localsend::http::state::ClientInfo;
|
||||||
use localsend::model::transfer::{FileContent, FileDto};
|
use localsend::model::transfer::{FileContent, FileDto};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -34,10 +36,10 @@ enum TestFileContent {
|
|||||||
impl TestFileContent {
|
impl TestFileContent {
|
||||||
/// Streams the content into `tx`, mimicking how an application would
|
/// Streams the content into `tx`, mimicking how an application would
|
||||||
/// serve in-memory content or a file from disk.
|
/// serve in-memory content or a file from disk.
|
||||||
async fn stream(self, tx: mpsc::Sender<Vec<u8>>) {
|
async fn stream(self, tx: mpsc::Sender<Bytes>) {
|
||||||
match self {
|
match self {
|
||||||
TestFileContent::Bytes(bytes) => {
|
TestFileContent::Bytes(bytes) => {
|
||||||
let _ = tx.send(bytes.to_vec()).await;
|
let _ = tx.send(bytes).await;
|
||||||
}
|
}
|
||||||
TestFileContent::Path(path) => {
|
TestFileContent::Path(path) => {
|
||||||
let mut file = tokio::fs::File::open(&path)
|
let mut file = tokio::fs::File::open(&path)
|
||||||
@@ -49,7 +51,11 @@ impl TestFileContent {
|
|||||||
if bytes_read == 0 {
|
if bytes_read == 0 {
|
||||||
break; // EOF
|
break; // EOF
|
||||||
}
|
}
|
||||||
if tx.send(buffer[..bytes_read].to_vec()).await.is_err() {
|
if tx
|
||||||
|
.send(Bytes::copy_from_slice(&buffer[..bytes_read]))
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
break; // client disconnected
|
break; // client disconnected
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,7 +99,7 @@ async fn start_test_server(
|
|||||||
.expect("FileDownload for unknown file")
|
.expect("FileDownload for unknown file")
|
||||||
.clone();
|
.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let (tx, rx) = mpsc::channel::<Vec<u8>>(16);
|
let (tx, rx) = mpsc::channel::<Bytes>(16);
|
||||||
if content_tx.send(FileContent::Stream(rx)).is_err() {
|
if content_tx.send(FileContent::Stream(rx)).is_err() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user