refactor: move WebSendConfig out of v2

This commit is contained in:
Tien Do Nam
2026-07-13 03:08:57 +02:00
parent e8fd062ec3
commit 3e451114c8
6 changed files with 68 additions and 55 deletions
+10 -5
View File
@@ -3,7 +3,7 @@ use crate::http::dto_v2::{
};
use crate::http::server::controller::check_pin;
use crate::http::server::error::AppError;
use crate::http::server::event::ServerEventV2;
use crate::http::server::event::WebSendEvent;
use crate::http::server::query::parse_query;
use crate::http::server::response::{full_body, BoxedBody, JsonResponse};
use crate::http::server::{AppState, RequestClientInfo};
@@ -48,6 +48,8 @@ const FILE_NAME_ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC
.remove(b')');
/// Configuration for web send (download API): files offered for download by web browsers.
///
/// Web send can be enabled independently of the v2/v3 protocol endpoints.
pub struct WebSendConfig {
/// The files offered for download, mapped by file ID.
pub files: HashMap<String, WebSendFile>,
@@ -57,6 +59,9 @@ pub struct WebSendConfig {
/// Translations for the web page, served via `/i18n.json`.
pub i18n: WebSendI18n,
/// Channel on which the server emits events that must be handled by the application.
pub event_tx: mpsc::Sender<WebSendEvent>,
}
/// A file offered for download.
@@ -118,7 +123,7 @@ pub(crate) struct WebPageState {
pub(crate) i18n: WebSendI18n,
/// Channel on which server events are emitted to the application.
pub(crate) event_tx: mpsc::Sender<ServerEventV2>,
pub(crate) event_tx: mpsc::Sender<WebSendEvent>,
/// Download sessions, keyed by session ID (the client's IP address).
pub(crate) sessions: Mutex<HashMap<String, WebSendSession>>,
@@ -128,12 +133,12 @@ pub(crate) struct WebPageState {
}
impl WebPageState {
pub(crate) fn new(config: WebSendConfig, event_tx: mpsc::Sender<ServerEventV2>) -> Self {
pub(crate) fn new(config: WebSendConfig) -> Self {
Self {
files: config.files,
pin: config.pin,
i18n: config.i18n,
event_tx,
event_tx: config.event_tx,
sessions: Mutex::new(HashMap::new()),
pin_attempts: Mutex::new(LruCache::new(NonZeroUsize::new(200).unwrap())),
}
@@ -225,7 +230,7 @@ pub(crate) async fn prepare_download(
let mut pending_guard = PendingWebSessionGuard::new(web.clone(), session_id.clone());
let (decision_tx, decision_rx) = oneshot::channel();
let event = ServerEventV2::PrepareDownload {
let event = WebSendEvent::PrepareDownload {
ip: client_info.ip,
session_id: session_id.clone(),
user_agent,
+14 -9
View File
@@ -58,6 +58,20 @@ pub enum ServerEventV2 {
result_tx: oneshot::Sender<Result<(), String>>,
},
/// An upload session ended.
SessionEnd {
/// The session ID of the ended session.
session_id: String,
/// Why the session ended.
reason: SessionEndReasonV2,
},
}
/// Events emitted by the web send (download API) endpoints that must be handled
/// by the application. Web send can be enabled independently of the v2 endpoints.
#[derive(Debug)]
pub enum WebSendEvent {
/// A web client requests to download the shared files
/// via `POST /api/localsend/v2/prepare-download`.
///
@@ -76,15 +90,6 @@ pub enum ServerEventV2 {
/// Channel to send the decision (`true` to accept, `false` to decline).
decision_tx: oneshot::Sender<bool>,
},
/// An upload session ended.
SessionEnd {
/// The session ID of the ended session.
session_id: String,
/// Why the session ended.
reason: SessionEndReasonV2,
},
}
/// The application's decision for a prepare-upload request.
+17 -20
View File
@@ -40,10 +40,6 @@ pub struct ServerConfigV2 {
/// Channel on which the server emits events that must be handled by the application.
pub event_tx: mpsc::Sender<ServerEventV2>,
/// Configuration for web send (download API).
/// `None` disables the web page and download routes.
pub web_send: Option<WebSendConfig>,
}
/// Runtime state of the v2 protocol endpoints.
@@ -80,21 +76,21 @@ struct AppState {
}
impl AppState {
fn new(info: Arc<Mutex<ClientInfo>>, v2_config: Option<ServerConfigV2>) -> Self {
let (v2, web) = match v2_config {
Some(config) => (
Some(Arc::new(V2State {
pin: config.pin,
event_tx: config.event_tx.clone(),
session: Mutex::new(None),
pin_attempts: Mutex::new(LruCache::new(NonZeroUsize::new(200).unwrap())),
})),
config
.web_send
.map(|web_config| Arc::new(WebPageState::new(web_config, config.event_tx))),
),
None => (None, None),
};
fn new(
info: Arc<Mutex<ClientInfo>>,
v2_config: Option<ServerConfigV2>,
web_send_config: Option<WebSendConfig>,
) -> Self {
let v2 = v2_config.map(|config| {
Arc::new(V2State {
pin: config.pin,
event_tx: config.event_tx,
session: Mutex::new(None),
pin_attempts: Mutex::new(LruCache::new(NonZeroUsize::new(200).unwrap())),
})
});
let web = web_send_config.map(|config| Arc::new(WebPageState::new(config)));
Self {
info,
@@ -116,12 +112,13 @@ pub async fn start_with_port(
tls_config: Option<TlsConfig>,
info: ClientInfo,
v2_config: Option<ServerConfigV2>,
web_send_config: Option<WebSendConfig>,
stop_rx: oneshot::Receiver<()>,
) -> anyhow::Result<()> {
let ipv4_socket_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), port);
let ipv6_socket_addr = SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), port);
let info = Arc::new(Mutex::new(info));
let state = AppState::new(info.clone(), v2_config);
let state = AppState::new(info.clone(), v2_config, web_send_config);
let ipv4_listener = tokio::net::TcpListener::bind(ipv4_socket_addr).await?;
let ipv6_listener = match bind_ipv6_only(ipv6_socket_addr) {
+1 -10
View File
@@ -184,15 +184,6 @@ async fn server_test() -> Result<()> {
let _ = result_tx.send(Ok(()));
});
}
ServerEventV2::PrepareDownload {
ip,
session_id,
decision_tx,
..
} => {
tracing::info!("Prepare download from {ip} (session {session_id})");
let _ = decision_tx.send(true);
}
ServerEventV2::SessionEnd { session_id, reason } => {
tracing::info!("Session {session_id} ended: {reason:?}");
}
@@ -210,8 +201,8 @@ async fn server_test() -> Result<()> {
Some(ServerConfigV2 {
pin: None,
event_tx,
web_send: None,
}),
None,
stop_rx,
)
.await?;
+1 -2
View File
@@ -68,7 +68,6 @@ async fn start_test_server(pin: Option<String>, accept: bool) -> TestServer {
ServerEventV2::SessionEnd { session_id, reason } => {
session_ends.lock().await.push((session_id, reason));
}
ServerEventV2::PrepareDownload { .. } => {}
}
}
}
@@ -89,8 +88,8 @@ async fn start_test_server(pin: Option<String>, accept: bool) -> TestServer {
Some(ServerConfigV2 {
pin,
event_tx,
web_send: None,
}),
None,
stop_rx,
)
.await
+25 -9
View File
@@ -3,7 +3,7 @@
use bytes::Bytes;
use localsend::http::client::{ClientError, LsHttpClientV2};
use localsend::http::dto::ProtocolType;
use localsend::http::server::event::ServerEventV2;
use localsend::http::server::event::{ServerEventV2, WebSendEvent};
use localsend::http::server::{
start_with_port, ServerConfigV2, WebSendConfig, WebSendFile, WebSendFileContent, WebSendI18n,
};
@@ -29,22 +29,33 @@ async fn start_test_server(web_send: Option<WebSendConfig>, accept: bool) -> Tes
let port = free_port();
let prepare_download_events = Arc::new(AtomicU32::new(0));
let (event_tx, mut event_rx) = tokio::sync::mpsc::channel::<ServerEventV2>(16);
// Web send emits its own event type, independent of the v2 protocol events.
let (web_event_tx, mut web_event_rx) = tokio::sync::mpsc::channel::<WebSendEvent>(16);
tokio::spawn({
let prepare_download_events = prepare_download_events.clone();
async move {
while let Some(event) = event_rx.recv().await {
if let ServerEventV2::PrepareDownload { decision_tx, .. } = event {
prepare_download_events.fetch_add(1, Ordering::SeqCst);
let _ = decision_tx.send(accept);
}
while let Some(WebSendEvent::PrepareDownload { decision_tx, .. }) =
web_event_rx.recv().await
{
prepare_download_events.fetch_add(1, Ordering::SeqCst);
let _ = decision_tx.send(accept);
}
}
});
// v2 stays enabled so the `/info` endpoint (which advertises `download`) can be
// exercised. Its event channel is unused by these tests.
let (v2_event_tx, _v2_event_rx) = tokio::sync::mpsc::channel::<ServerEventV2>(16);
let (stop_tx, stop_rx) = oneshot::channel::<()>();
// Web send is configured independently of the v2 endpoints.
let web_send = web_send.map(|mut config| {
config.event_tx = web_event_tx;
config
});
start_with_port(
port,
None, // plain HTTP
@@ -57,9 +68,9 @@ async fn start_test_server(web_send: Option<WebSendConfig>, accept: bool) -> Tes
},
Some(ServerConfigV2 {
pin: None,
event_tx,
web_send,
event_tx: v2_event_tx,
}),
web_send,
stop_rx,
)
.await
@@ -139,11 +150,16 @@ fn web_send_config(pin: Option<String>) -> (WebSendConfig, PathBuf, Vec<u8>) {
),
]);
// The event channel is a placeholder; `start_test_server` replaces it with
// the one whose receiver counts `PrepareDownload` events.
let (event_tx, _event_rx) = tokio::sync::mpsc::channel::<WebSendEvent>(16);
(
WebSendConfig {
files,
pin,
i18n: WebSendI18n::default(),
event_tx,
},
disk_path,
disk_content,