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,
};
use crate::model;
use bytes::Bytes;
use futures_util::StreamExt;
use reqwest::{Response, StatusCode};
use tokio::io::AsyncWriteExt;
@@ -227,7 +228,7 @@ impl LsHttpClientV2 {
}
.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 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::{crypto, util};
use crate::{http, model};
use bytes::Bytes;
use futures_util::StreamExt;
use lru::LruCache;
use reqwest::{Response, StatusCode};
@@ -215,7 +216,7 @@ impl LsHttpClientV3 {
)
.body({
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)
})
.send();
@@ -1,4 +1,4 @@
use crate::http::server::error::AppError;
use crate::http::server::common::error::AppError;
use http_body_util::BodyExt;
use hyper::body::Incoming;
use serde::de::DeserializeOwned;
@@ -1,5 +1,5 @@
use crate::http::dto::ErrorResponse;
use crate::http::server::response::{BoxedBody, JsonResponse};
use crate::http::server::common::response::{BoxedBody, JsonResponse};
use hyper::{Response, StatusCode};
#[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;
pub(crate) mod v3;
pub(crate) mod web;
use crate::http::server::error::AppError;
use crate::http::server::common::error::AppError;
use hyper::StatusCode;
use lru::LruCache;
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;
mod collect_to_json;
mod controller;
mod error;
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};
pub mod common;
pub mod v2;
pub mod v3;
pub mod web;
use crate::crypto::cert::public_key_from_cert_der;
use crate::http::server::client_cert_verifier::CustomClientCertVerifier;
use crate::http::server::controller::web::WebPageState;
use crate::http::server::error::AppError;
use crate::http::server::response::BoxedBody;
use crate::http::server::session::SessionStateV2;
use crate::http::server::v2::ServerEventV2;
use crate::http::server::web::WebSendConfig;
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::{Method, Request, Response, StatusCode};
use hyper_util::rt::{TokioExecutor, TokioIo};
@@ -31,6 +25,7 @@ use std::num::NonZeroUsize;
use std::ops::Deref;
use std::sync::Arc;
use tokio::sync::{mpsc, oneshot, Mutex};
use web::WebPageState;
/// Configuration for the v2 (legacy) protocol endpoints.
pub struct ServerConfigV2 {
@@ -57,7 +52,7 @@ pub(crate) struct V2State {
}
#[derive(Clone)]
struct AppState {
pub struct AppState {
/// Information about server's device.
info: Arc<Mutex<ClientInfo>>,
@@ -277,7 +272,7 @@ fn create_tls_config(tls_config: &TlsConfig) -> anyhow::Result<tokio_rustls::Tls
}
#[derive(Clone, Debug)]
struct RequestClientInfo {
pub struct RequestClientInfo {
/// The IP address of the client.
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();
match (req.method(), req.uri().path()) {
(&Method::GET, "/") => Ok(controller::web::index(&state)),
(&Method::GET, "/main.js") => Ok(controller::web::main_js(&state)),
(&Method::GET, "/i18n.json") => controller::web::i18n(&state),
(&Method::GET, "/") => Ok(web::index(&state)),
(&Method::GET, "/main.js") => Ok(web::main_js(&state)),
(&Method::GET, "/i18n.json") => web::i18n(&state),
(&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") => {
controller::web::download(req, state, client_info).await
web::download(req, state, client_info).await
}
(&Method::POST, "/api/localsend/v2/register") => {
if !v2_enabled {
return Err(AppError::Status(StatusCode::NOT_FOUND));
}
Ok(
controller::v2::register(req.into_body(), state, client_info)
.await?
.into_response(),
)
Ok(v2::register(req.into_body(), state, client_info)
.await?
.into_response())
}
(&Method::GET, "/api/localsend/v2/info") => {
if !v2_enabled {
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") => {
if !v2_enabled {
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") => {
if !v2_enabled {
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") => {
if !v2_enabled {
return Err(AppError::Status(StatusCode::NOT_FOUND));
}
controller::v2::cancel(req, state).await
v2::cancel(req, state).await
}
(&Method::POST, "/api/localsend/v3/nonce") => {
Ok(
controller::v3::nonce_exchange(req.into_body(), state, client_info)
.await?
.into_response(),
)
Ok(v3::nonce_exchange(req.into_body(), state, client_info)
.await?
.into_response())
}
(&Method::POST, "/api/localsend/v3/register") => {
Ok(
controller::v3::register(req.into_body(), state, client_info)
.await?
.into_response(),
)
Ok(v3::register(req.into_body(), state, client_info)
.await?
.into_response())
}
_ => {
let mut res = Response::new(response::empty_body());
@@ -2,24 +2,23 @@ use crate::http::dto_v2::{
InfoResponseDtoV2, PrepareUploadRequestDtoV2, PrepareUploadResponseDtoV2, RegisterDtoV2,
RegisterResponseDtoV2, PROTOCOL_VERSION_V2,
};
use crate::http::server::collect_to_json::CollectToJson;
use crate::http::server::controller::check_pin;
use crate::http::server::error::AppError;
use crate::http::server::query::parse_query;
use crate::http::server::response::{empty_body, BoxedBody, JsonResponse};
use crate::http::server::session::{FileStatusV2, SessionFileV2, SessionStateV2, UploadSessionV2};
use crate::http::server::{AppState, RequestClientInfo, V2State};
use crate::http::server::common::collect_to_json::CollectToJson;
use crate::http::server::common::error::AppError;
use crate::http::server::common::pin::check_pin;
use crate::http::server::common::query::parse_query;
use crate::http::server::common::response::{empty_body, BoxedBody, JsonResponse};
use crate::http::server::common::save::FileUploadTarget;
use crate::http::server::common::session::{
FileStatusV2, SessionFileV2, SessionStateV2, UploadSessionV2,
};
use crate::http::server::{common, AppState, RequestClientInfo, V2State};
use crate::model::transfer::FileDto;
use bytes::Bytes;
use http_body_util::BodyExt;
use hyper::body::Incoming;
use hyper::{Request, Response, StatusCode};
use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::net::IpAddr;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::{mpsc, oneshot};
use tokio::sync::oneshot;
use uuid::Uuid;
/// Events emitted by the v2 HTTP server that must be handled by the application.
@@ -68,7 +67,7 @@ pub enum ServerEventV2 {
file: FileDto,
/// 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.
@@ -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.
#[derive(Debug)]
pub enum PrepareUploadDecisionV2 {
@@ -143,9 +101,6 @@ pub enum SessionEndReasonV2 {
Cancelled,
}
/// Channel capacity for file upload chunks (provides backpressure).
const UPLOAD_CHANNEL_CAPACITY: usize = 16;
pub(crate) async fn register(
body: Incoming,
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 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 {
session_id: session_id.clone(),
@@ -366,77 +321,7 @@ pub(crate) async fn upload(
return Err(AppError::Status(StatusCode::INTERNAL_SERVER_ERROR));
};
// Resolve the target into a chunk sender and a result receiver.
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,
},
};
let success = common::save::save_req_to_target(req, target, file_size).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(
req: Request<Incoming>,
state: AppState,
@@ -1,7 +1,7 @@
use crate::http::dto::{NonceRequest, NonceResponse, RegisterDto, RegisterResponseDto};
use crate::http::server::collect_to_json::CollectToJson;
use crate::http::server::error::AppError;
use crate::http::server::response::JsonResponse;
use crate::http::server::common::collect_to_json::CollectToJson;
use crate::http::server::common::error::AppError;
use crate::http::server::common::response::JsonResponse;
use crate::http::server::{AppState, RequestClientInfo};
use crate::{crypto, util};
use hyper::body::Incoming;
@@ -1,8 +1,8 @@
use crate::http::dto_v2::{InfoResponseDtoV2, PrepareDownloadResponseDtoV2, PROTOCOL_VERSION_V2};
use crate::http::server::controller::check_pin;
use crate::http::server::error::AppError;
use crate::http::server::query::parse_query;
use crate::http::server::response::{full_body, BoxedBody, JsonResponse};
use crate::http::server::common::error::AppError;
use crate::http::server::common::pin::check_pin;
use crate::http::server::common::query::parse_query;
use crate::http::server::common::response::{full_body, BoxedBody, JsonResponse};
use crate::http::server::{AppState, RequestClientInfo};
use crate::model::transfer::{FileContent, FileDto};
use bytes::Bytes;
@@ -65,9 +65,9 @@ pub enum WebSendEvent {
},
}
const INDEX_HTML: &str = include_str!("../../../../assets/web/index.html");
const MAIN_JS: &str = include_str!("../../../../assets/web/main.js");
const ERROR_403_HTML: &str = include_str!("../../../../assets/web/error-403.html");
const INDEX_HTML: &str = include_str!("../../../assets/web/index.html");
const MAIN_JS: &str = include_str!("../../../assets/web/main.js");
const ERROR_403_HTML: &str = include_str!("../../../assets/web/error-403.html");
/// Characters that are percent-encoded in the content-disposition file name.
/// 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.
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)
.map(|chunk| Ok::<_, std::io::Error>(Frame::data(Bytes::from(chunk))));
StreamBody::new(stream).boxed()
+3 -2
View File
@@ -7,7 +7,8 @@ mod webrtc;
use crate::crypto::token;
use crate::http::client::LsHttpClientV3;
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::model::discovery::DeviceType;
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 (result_tx, result_rx) = oneshot::channel::<Result<(), String>>();
let _ = target_tx.send(FileUploadTargetV2::Stream {
let _ = target_tx.send(FileUploadTarget::Stream {
binary_tx,
result_rx,
});
+8 -7
View File
@@ -1,3 +1,4 @@
use bytes::Bytes;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tokio::sync::mpsc;
@@ -14,7 +15,7 @@ const FILE_CHANNEL_CAPACITY: usize = 16;
pub enum FileContent {
/// A stream of binary chunks. The channel is closed once the file has been
/// fully provided.
Stream(mpsc::Receiver<Vec<u8>>),
Stream(mpsc::Receiver<Bytes>),
/// A path to a regular file the content is read from.
Path(PathBuf),
@@ -30,7 +31,7 @@ impl FileContent {
/// [`FileContent::Stream`] is returned as-is. For [`FileContent::Path`] and
/// [`FileContent::Fd`], a background task reads the file and forwards the
/// 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 {
FileContent::Stream(rx) => rx,
FileContent::Path(path) => {
@@ -64,15 +65,15 @@ impl FileContent {
/// Reads `file` to EOF, forwarding chunks on `tx`.
///
/// 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;
let mut buffer = vec![0u8; 64 * 1024];
let mut buffer = bytes::BytesMut::with_capacity(64 * 1024);
loop {
match file.read(&mut buffer).await {
match file.read_buf(&mut buffer).await {
Ok(0) => break,
Ok(n) => {
if tx.send(buffer[..n].to_vec()).await.is_err() {
Ok(_) => {
if tx.send(buffer.split().freeze()).await.is_err() {
break;
}
}
+7 -7
View File
@@ -1,12 +1,12 @@
#![cfg(feature = "http")]
use bytes::Bytes;
use localsend::http::client::{ClientError, LsHttpClientV2};
use localsend::http::dto::ProtocolType;
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::{
FileUploadTargetV2, PrepareUploadDecisionV2, ServerEventV2, SessionEndReasonV2,
};
use localsend::http::state::ClientInfo;
use localsend::model::transfer::{FileContent, FileDto};
use std::collections::HashMap;
@@ -69,7 +69,7 @@ async fn start_test_server(
None => {
let (binary_tx, mut binary_rx) = mpsc::channel(16);
let (result_tx, result_rx) = oneshot::channel();
let _ = target_tx.send(FileUploadTargetV2::Stream {
let _ = target_tx.send(FileUploadTarget::Stream {
binary_tx,
result_rx,
});
@@ -85,7 +85,7 @@ async fn start_test_server(
Some(dir) => {
let path = dir.join(&file_id);
let (result_tx, result_rx) = oneshot::channel();
let _ = target_tx.send(FileUploadTargetV2::Path {
let _ = target_tx.send(FileUploadTarget::Path {
path: path.clone(),
result_tx,
});
@@ -206,11 +206,11 @@ async fn upload_bytes(
token: &str,
bytes: &[u8],
) -> 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();
tokio::spawn(async move {
for chunk in chunks {
if tx.send(chunk).await.is_err() {
if tx.send(Bytes::from(chunk)).await.is_err() {
break;
}
}
+12 -6
View File
@@ -3,8 +3,10 @@
use bytes::Bytes;
use localsend::http::client::{ClientError, LsHttpClientV2};
use localsend::http::dto::ProtocolType;
use localsend::http::server::{start_with_port, ServerConfigV2, WebSendConfig, WebSendI18n};
use localsend::http::server::{ServerEventV2, WebSendEvent};
use localsend::http::server::v2::ServerEventV2;
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::model::transfer::{FileContent, FileDto};
use std::collections::HashMap;
@@ -34,10 +36,10 @@ enum TestFileContent {
impl TestFileContent {
/// Streams the content into `tx`, mimicking how an application would
/// 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 {
TestFileContent::Bytes(bytes) => {
let _ = tx.send(bytes.to_vec()).await;
let _ = tx.send(bytes).await;
}
TestFileContent::Path(path) => {
let mut file = tokio::fs::File::open(&path)
@@ -49,7 +51,11 @@ impl TestFileContent {
if bytes_read == 0 {
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
}
}
@@ -93,7 +99,7 @@ async fn start_test_server(
.expect("FileDownload for unknown file")
.clone();
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() {
return;
}