refactor: restructure http server

This commit is contained in:
Tien Do Nam
2026-07-13 21:47:32 +02:00
parent 1c6728f71c
commit 3aedb1a67f
19 changed files with 285 additions and 268 deletions
+2 -1
View File
@@ -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! {
+2 -1
View File
@@ -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,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)]
+8
View File
@@ -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;
+183
View File
@@ -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
View File
@@ -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
View File
@@ -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,
}); });
+8 -7
View File
@@ -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;
} }
} }
+7 -7
View File
@@ -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;
} }
} }
+12 -6
View File
@@ -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;
} }