Compare commits

..

4 commits

Author SHA1 Message Date
Joe Ardent
342b634388 try to shutdown nicely 2025-07-06 16:02:11 -07:00
Joe Ardent
f5af07f860 more module cleanup 2025-07-06 14:15:08 -07:00
Joe Ardent
3aaeb4394a collapse discovery module, remove Client struct 2025-07-06 13:56:21 -07:00
Joe Ardent
b4f22c5851 add license 2025-07-05 15:21:04 -07:00
17 changed files with 378 additions and 360 deletions

13
LICENSE.md Normal file
View file

@ -0,0 +1,13 @@
# Dual Licensed (combined terms are binding)
This software is governed under the combined terms of the following two licenses:
## The Chaos License (GLP)
This software is released under the terms of the Chaos License. In cases where the terms of the
license are unclear, refer to the [Fuck Around and Find Out
License](https://git.sr.ht/~boringcactus/fafol/tree/master/LICENSE-v0.2.md).
## The Butlerian Jihad License (DUN)
If you feed this code into an LLM, I will fuck you up.

121
src/discovery.rs Normal file
View file

@ -0,0 +1,121 @@
use std::{
net::{SocketAddr, SocketAddrV4},
sync::Arc,
};
use axum::{
Json,
extract::{ConnectInfo, State},
};
use tokio::net::UdpSocket;
use crate::{Config, JoecalState, RunningState, models::Device};
impl JoecalState {
pub async fn announce(
&self,
socket: Option<SocketAddr>,
config: &Config,
) -> crate::error::Result<()> {
announce_http(&self.device, socket, self.client.clone()).await?;
announce_multicast(&self.device, config.multicast_addr, self.socket.clone()).await?;
Ok(())
}
pub async fn listen_multicast(&self, config: &Config) -> crate::error::Result<()> {
let mut buf = [0; 65536];
println!("Socket local addr: {:?}", self.socket.local_addr()?);
println!("Listening on multicast addr: {}", config.multicast_addr);
loop {
match self.socket.recv_from(&mut buf).await {
Ok((size, src)) => {
let received_msg = String::from_utf8_lossy(&buf[..size]);
self.process_device(&received_msg, src, config).await;
}
Err(e) => {
eprintln!("Error receiving message: {e}");
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}
}
if let Ok(state) = self.running_state.try_lock()
&& *state == RunningState::Stopping
{
break;
}
}
Ok(())
}
async fn process_device(&self, message: &str, src: SocketAddr, config: &Config) {
if let Ok(device) = serde_json::from_str::<Device>(message) {
if device.fingerprint == self.device.fingerprint {
return;
}
let mut src = src;
src.set_port(device.port); // Update the port to the one the device sent
let mut peers = self.peers.lock().await;
peers.insert(device.fingerprint.clone(), (src, device.clone()));
if device.announce != Some(true) {
return;
}
// Announce in return upon receiving a valid device message and it wants
// announcements
if let Err(e) =
announce_multicast(&device, config.multicast_addr, self.socket.clone()).await
{
eprintln!("Error during multicast announcement: {e}");
}
if let Err(e) = announce_http(&device, Some(src), self.client.clone()).await {
eprintln!("Error during HTTP announcement: {e}");
};
} else {
eprintln!("Received invalid message: {message}");
}
}
}
/// Axum request handler for receiving other devices' registration requests.
pub async fn register_device(
State(state): State<JoecalState>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
Json(device): Json<Device>,
) -> Json<Device> {
let mut addr = addr;
addr.set_port(state.device.port);
state
.peers
.lock()
.await
.insert(device.fingerprint.clone(), (addr, device.clone()));
Json(device)
}
//-************************************************************************
// private helpers
//-************************************************************************
async fn announce_http(
device: &Device,
ip: Option<SocketAddr>,
client: reqwest::Client,
) -> crate::error::Result<()> {
if let Some(ip) = ip {
let url = format!("http://{ip}/api/localsend/v2/register");
client.post(&url).json(device).send().await?;
}
Ok(())
}
async fn announce_multicast(
device: &Device,
addr: SocketAddrV4,
socket: Arc<UdpSocket>,
) -> crate::error::Result<()> {
let msg = device.to_json()?;
socket.send_to(msg.as_bytes(), addr).await?;
Ok(())
}

View file

@ -1,55 +0,0 @@
use std::{net::SocketAddr, sync::Arc};
use axum::{
Json,
extract::{ConnectInfo, State},
};
use crate::{Client, JoecalState, models::device::DeviceInfo};
impl Client {
pub async fn announce_http(&self, ip: Option<SocketAddr>) -> crate::error::Result<()> {
if let Some(ip) = ip {
let url = format!("http://{ip}/api/localsend/v2/register");
let client = reqwest::Client::new();
client.post(&url).json(&self.device).send().await?;
}
Ok(())
}
pub async fn announce_http_legacy(&self) -> crate::error::Result<()> {
// send the reqwest to all local ip addresses from 192.168.0.0 to
// 192.168.255.255
let mut address_list = Vec::new();
for j in 0..256 {
for k in 0..256 {
address_list.push(format!("192.168.{j:03}.{k}:53317"));
}
}
for ip in address_list {
let url = format!("http://{ip}/api/localsend/v2/register");
self.http_client
.post(&url)
.json(&self.device)
.send()
.await?;
}
Ok(())
}
}
pub async fn register_device(
State(state): State<Arc<JoecalState>>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
Json(device): Json<DeviceInfo>,
) -> Json<DeviceInfo> {
let mut addr = addr;
addr.set_port(state.device.port);
state
.peers
.lock()
.await
.insert(device.fingerprint.clone(), (addr, device.clone()));
Json(device)
}

View file

@ -1,43 +0,0 @@
use std::net::SocketAddr;
use crate::{Client, models::device::DeviceInfo};
pub mod http;
pub mod multicast;
impl Client {
pub async fn announce(&self, socket: Option<SocketAddr>) -> crate::error::Result<()> {
self.announce_http(socket).await?;
self.announce_multicast().await?;
Ok(())
}
async fn process_device(&self, message: &str, src: SocketAddr) {
if let Ok(device) = serde_json::from_str::<DeviceInfo>(message) {
if device.fingerprint == self.device.fingerprint {
return;
}
let mut src = src;
src.set_port(device.port); // Update the port to the one the device sent
let mut peers = self.peers.lock().await;
peers.insert(device.fingerprint.clone(), (src, device.clone()));
if device.announce != Some(true) {
return;
}
// Announce in return upon receiving a valid device message and it wants
// announcements
if let Err(e) = self.announce_multicast().await {
eprintln!("Error during multicast announcement: {e}");
}
if let Err(e) = self.announce_http(Some(src)).await {
eprintln!("Error during HTTP announcement: {e}");
};
} else {
eprintln!("Received invalid message: {message}");
}
}
}

View file

@ -1,29 +0,0 @@
use crate::Client;
impl Client {
pub async fn announce_multicast(&self) -> crate::error::Result<()> {
let msg = self.device.to_json()?;
let addr = self.multicast_addr;
self.socket.send_to(msg.as_bytes(), addr).await?;
Ok(())
}
pub async fn listen_multicast(&self) -> crate::error::Result<()> {
let mut buf = [0; 65536];
println!("Socket local addr: {:?}", self.socket.local_addr()?);
println!("Listening on multicast addr: {}", self.multicast_addr);
loop {
match self.socket.recv_from(&mut buf).await {
Ok((size, src)) => {
let received_msg = String::from_utf8_lossy(&buf[..size]);
self.process_device(&received_msg, src).await;
}
Err(e) => {
eprintln!("Error receiving message: {e}");
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}
}
}
}
}

View file

@ -1,23 +1,27 @@
use std::{net::SocketAddr, sync::Arc};
use std::net::SocketAddr;
use axum::{
Extension, Json, Router,
extract::DefaultBodyLimit,
routing::{get, post},
};
use tokio::net::TcpListener;
use tokio::{net::TcpListener, sync::mpsc};
use tower_http::limit::RequestBodyLimitLayer;
use crate::{
Client, JoecalState,
discovery::http::register_device,
transfer::upload::{register_prepare_upload, register_upload},
Config, JoecalState,
discovery::register_device,
transfer::{register_prepare_upload, register_upload},
};
impl Client {
pub async fn start_http_server(&self) -> crate::error::Result<()> {
let app = self.create_router();
let addr = SocketAddr::from(([0, 0, 0, 0], self.port));
impl JoecalState {
pub async fn start_http_server(
&self,
stop_rx: mpsc::Receiver<()>,
config: &Config,
) -> crate::error::Result<()> {
let app = self.create_router(config);
let addr = SocketAddr::from(([0, 0, 0, 0], config.port));
let listener = TcpListener::bind(&addr).await?;
println!("HTTP server listening on {addr}");
@ -26,19 +30,13 @@ impl Client {
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.with_graceful_shutdown(shutdown(stop_rx))
.await?;
Ok(())
}
fn create_router(&self) -> Router {
let peers = self.peers.clone();
fn create_router(&self, config: &Config) -> Router {
let device = self.device.clone();
let state = Arc::new(JoecalState {
peers,
device: device.clone(),
sessions: self.sessions.clone(),
});
Router::new()
.route("/api/localsend/v2/register", post(register_device))
.route(
@ -55,7 +53,12 @@ impl Client {
.route("/api/localsend/v2/upload", post(register_upload))
.layer(DefaultBodyLimit::disable())
.layer(RequestBodyLimitLayer::new(1024 * 1024 * 1024))
.layer(Extension(self.download_dir.clone()))
.with_state(state)
.layer(Extension(config.download_dir.clone()))
.with_state(self.clone())
}
}
async fn shutdown(mut rx: mpsc::Receiver<()>) {
println!("shutting down");
rx.recv().await.unwrap_or_default()
}

View file

@ -1,120 +1,99 @@
pub mod discovery;
pub mod error;
pub mod http_server;
pub mod models;
pub mod server;
pub mod transfer;
use std::{
collections::HashMap,
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
sync::Arc,
time::Duration,
};
use tokio::{net::UdpSocket, sync::Mutex, task::JoinHandle};
use transfer::session::Session;
use models::Device;
use serde::{Deserialize, Serialize};
use tokio::{
net::UdpSocket,
sync::{Mutex, mpsc},
task::JoinHandle,
};
use transfer::Session;
use crate::models::device::DeviceInfo;
#[derive(Clone)]
pub struct Client {
pub device: DeviceInfo,
pub socket: Arc<UdpSocket>,
pub multicast_addr: SocketAddrV4,
pub port: u16,
pub peers: Arc<Mutex<HashMap<String, (SocketAddr, DeviceInfo)>>>,
pub sessions: Arc<Mutex<HashMap<String, Session>>>, // Session ID to Session
pub http_client: reqwest::Client,
pub download_dir: String,
}
pub const DEFAULT_PORT: u16 = 53317;
pub const MULTICAST_IP: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 167);
pub const LISTENING_SOCKET_ADDR: SocketAddrV4 =
SocketAddrV4::new(Ipv4Addr::from_bits(0), DEFAULT_PORT);
/// Contains the main network and application state for an application session.
#[derive(Clone)]
pub struct JoecalState {
pub device: DeviceInfo,
pub peers: Arc<Mutex<HashMap<String, (SocketAddr, DeviceInfo)>>>,
pub device: Device,
pub peers: Arc<Mutex<HashMap<String, (SocketAddr, Device)>>>,
pub sessions: Arc<Mutex<HashMap<String, Session>>>, // Session ID to Session
pub running_state: Arc<Mutex<RunningState>>,
pub socket: Arc<UdpSocket>,
pub client: reqwest::Client,
stop_tx: std::sync::OnceLock<mpsc::Sender<()>>,
}
impl Client {
pub async fn default() -> crate::error::Result<Self> {
let device = DeviceInfo::default();
let socket = UdpSocket::bind("0.0.0.0:53317").await?;
impl JoecalState {
pub async fn new(device: Device) -> crate::error::Result<Self> {
let socket = UdpSocket::bind(LISTENING_SOCKET_ADDR).await?;
socket.set_multicast_loop_v4(true)?;
socket.set_multicast_ttl_v4(255)?;
socket.join_multicast_v4(Ipv4Addr::new(224, 0, 0, 167), Ipv4Addr::new(0, 0, 0, 0))?;
let multicast_addr = SocketAddrV4::new(Ipv4Addr::new(224, 0, 0, 167), 53317);
let port = 53317;
let peers = Arc::new(Mutex::new(HashMap::new()));
let http_client = reqwest::Client::new();
let sessions = Arc::new(Mutex::new(HashMap::new()));
let download_dir = "/home/localsend".to_string();
socket.set_multicast_ttl_v4(2)?; // one hop out from localnet
socket.join_multicast_v4(MULTICAST_IP, Ipv4Addr::from_bits(0))?;
Ok(Self {
device,
peers: Default::default(),
sessions: Default::default(),
running_state: Default::default(),
socket: socket.into(),
multicast_addr,
port,
peers,
http_client,
sessions,
download_dir,
})
}
pub async fn with_config(
info: DeviceInfo,
port: u16,
download_dir: String,
) -> crate::error::Result<Self> {
let socket = UdpSocket::bind(format!("0.0.0.0:{}", port.clone())).await?;
socket.set_multicast_loop_v4(true)?;
socket.set_multicast_ttl_v4(255)?;
socket.join_multicast_v4(Ipv4Addr::new(224, 0, 0, 167), Ipv4Addr::new(0, 0, 0, 0))?;
let multicast_addr = SocketAddrV4::new(Ipv4Addr::new(224, 0, 0, 167), port);
let peers = Arc::new(Mutex::new(HashMap::new()));
let http_client = reqwest::Client::new();
let sessions = Arc::new(Mutex::new(HashMap::new()));
Ok(Self {
device: info,
socket: socket.into(),
multicast_addr,
port,
peers,
http_client,
sessions,
download_dir,
client: reqwest::Client::new(),
stop_tx: Default::default(),
})
}
pub async fn start(
&self,
config: &Config,
) -> crate::error::Result<(JoinHandle<()>, JoinHandle<()>, JoinHandle<()>)> {
let state = self.clone();
let konfig = config.clone();
let server_handle = {
let client = self.clone();
let (tx, rx) = mpsc::channel(1);
self.stop_tx.get_or_init(|| tx);
tokio::spawn(async move {
if let Err(e) = client.start_http_server().await {
if let Err(e) = state.start_http_server(rx, &konfig).await {
eprintln!("HTTP server error: {e}");
}
})
};
let state = self.clone();
let konfig = config.clone();
let udp_handle = {
let client = self.clone();
tokio::spawn(async move {
if let Err(e) = client.listen_multicast().await {
if let Err(e) = state.listen_multicast(&konfig).await {
eprintln!("UDP listener error: {e}");
}
})
};
let state = self.clone();
let config = config.clone();
let announcement_handle = {
let client = self.clone();
tokio::spawn(async move {
loop {
if let Err(e) = client.announce(None).await {
if let Err(e) = state.announce(None, &config).await {
eprintln!("Announcement error: {e}");
}
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
if let Ok(lock) = state.running_state.try_lock()
&& *lock == RunningState::Stopping
{
break;
}
}
})
};
@ -122,8 +101,59 @@ impl Client {
Ok((server_handle, udp_handle, announcement_handle))
}
pub async fn stop(&self) {
loop {
if let Ok(mut lock) = self.running_state.try_lock() {
*lock = RunningState::Stopping;
if self
.stop_tx
.get()
.expect("Could not get stop signal transmitter")
.send(())
.await
.is_ok()
{
break;
}
}
}
}
pub async fn refresh_peers(&self) {
let mut peers = self.peers.lock().await;
peers.clear();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RunningState {
Running,
Sending,
Receiving,
Stopping,
}
impl Default for RunningState {
fn default() -> Self {
Self::Running
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub multicast_addr: SocketAddrV4,
pub port: u16,
pub download_dir: String,
}
impl Default for Config {
fn default() -> Self {
let home = std::env::home_dir().unwrap_or("/tmp".into());
let dd = home.join("joecalsend-downloads");
Self {
multicast_addr: SocketAddrV4::new(MULTICAST_IP, DEFAULT_PORT),
port: DEFAULT_PORT,
download_dir: dd.to_string_lossy().into(),
}
}
}

View file

@ -1,6 +1,6 @@
use std::{io, sync::Arc};
use std::io;
use joecalsend::{Client, JoecalState, error, models::device::DeviceInfo};
use joecalsend::{Config, JoecalState, RunningState, error, models::Device};
use local_ip_address::local_ip;
use network_interface::{Addr, NetworkInterface, NetworkInterfaceConfig, V4IfAddr};
use ratatui::{
@ -16,7 +16,7 @@ use ratatui::{
#[tokio::main]
async fn main() -> error::Result<()> {
let device = DeviceInfo::default();
let device = Device::default();
dbg!(&device);
let std::net::IpAddr::V4(ip) = local_ip()? else {
@ -40,49 +40,39 @@ async fn main() -> error::Result<()> {
}
dbg!(network_ip);
let client = Client::with_config(device, 53317, "/home/ardent/joecalsend".into())
let state = JoecalState::new(device)
.await
.unwrap();
let (h1, h2, h3) = client.start().await.unwrap();
.expect("Could not create application session");
let config = Config::default();
let (h1, h2, h3) = state.start(&config).await.unwrap();
let mut app = App::new(Arc::new(client.clone()));
let mut app = App::new(state.clone());
let mut terminal = ratatui::init();
let result = app.run(&mut terminal);
let result = app.run(&mut terminal).await;
ratatui::restore();
//let _ = tokio::join!(h1, h2, h3);
let _ = tokio::join!(h1, h2, h3);
Ok(result?)
}
struct App {
client: Arc<Client>,
runstate: AppRunState,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AppRunState {
Running,
Stopping,
}
impl Default for AppRunState {
fn default() -> Self {
Self::Running
}
state: JoecalState,
}
impl App {
pub fn new(client: Arc<Client>) -> Self {
App {
client,
runstate: AppRunState::Running,
}
pub fn new(state: JoecalState) -> Self {
App { state }
}
pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
while AppRunState::Running == self.runstate {
pub async fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
loop {
terminal.draw(|frame| self.draw(frame))?;
self.handle_events()?;
self.handle_events().await?;
if let Ok(lock) = self.state.running_state.try_lock()
&& *lock == RunningState::Stopping
{
break;
}
}
Ok(())
}
@ -91,21 +81,21 @@ impl App {
frame.render_widget(self, frame.area());
}
fn handle_events(&mut self) -> io::Result<()> {
async fn handle_events(&mut self) -> io::Result<()> {
match event::read()? {
// it's important to check that the event is a key press event as
// crossterm also emits key release and repeat events on Windows.
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
self.handle_key_event(key_event)
self.handle_key_event(key_event).await
}
_ => {}
};
Ok(())
}
fn handle_key_event(&mut self, key_event: KeyEvent) {
async fn handle_key_event(&mut self, key_event: KeyEvent) {
match key_event.code {
KeyCode::Char('q') => self.exit(),
KeyCode::Char('q') => self.exit().await,
KeyCode::Char('s') => {}
KeyCode::Char('r') => {}
KeyCode::Char('d') => {}
@ -113,8 +103,8 @@ impl App {
}
}
fn exit(&mut self) {
self.runstate = AppRunState::Stopping
async fn exit(&self) {
self.state.stop().await;
}
}
@ -136,7 +126,12 @@ impl Widget for &App {
.title_bottom(instructions.centered())
.border_set(border::THICK);
let rs = format!("{:?}", self.runstate);
let rs = self
.state
.running_state
.try_lock()
.map(|s| format!("{s:?}"))
.unwrap_or("Just a moment...".into());
let state_text = Text::from(vec![Line::from(vec!["runstate: ".into(), rs.yellow()])]);
Paragraph::new(state_text)

View file

@ -1,6 +1,7 @@
use std::{path::Path, time::SystemTime};
use chrono::{DateTime, Utc};
use julid::Julid;
use serde::{Deserialize, Serialize};
use crate::error::LocalSendError;
@ -63,6 +64,64 @@ impl FileMetadata {
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "lowercase")]
pub enum DeviceType {
Mobile,
Desktop,
Web,
Headless,
Server,
Unknown,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Device {
pub alias: String,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub device_model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub device_type: Option<DeviceType>,
pub fingerprint: String,
pub port: u16,
pub protocol: String,
#[serde(default)]
pub download: bool,
#[serde(default)]
pub announce: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "lowercase")]
pub enum Protocol {
Http,
Https,
}
impl Default for Device {
fn default() -> Self {
Self {
alias: "RustSend".to_string(),
version: "2.1".to_string(),
device_model: None,
device_type: Some(DeviceType::Headless),
fingerprint: Julid::new().to_string(),
port: 53317,
protocol: "http".to_string(),
download: false,
announce: Some(true),
}
}
}
impl Device {
pub fn to_json(&self) -> crate::error::Result<String> {
Ok(serde_json::to_string(self)?)
}
}
fn format_datetime(system_time: SystemTime) -> String {
let datetime: DateTime<Utc> = system_time.into();
datetime.to_rfc3339()

View file

@ -1,60 +0,0 @@
use julid::Julid;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "lowercase")]
pub enum DeviceType {
Mobile,
Desktop,
Web,
Headless,
Server,
Unknown,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DeviceInfo {
pub alias: String,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub device_model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub device_type: Option<DeviceType>,
pub fingerprint: String,
pub port: u16,
pub protocol: String,
#[serde(default)]
pub download: bool,
#[serde(default)]
pub announce: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "lowercase")]
pub enum Protocol {
Http,
Https,
}
impl Default for DeviceInfo {
fn default() -> Self {
Self {
alias: "RustSend".to_string(),
version: "2.1".to_string(),
device_model: None,
device_type: Some(DeviceType::Headless),
fingerprint: Julid::new().to_string(),
port: 53317,
protocol: "http".to_string(),
download: false,
announce: Some(true),
}
}
}
impl DeviceInfo {
pub fn to_json(&self) -> crate::error::Result<String> {
Ok(serde_json::to_string(self)?)
}
}

View file

@ -1,3 +0,0 @@
pub mod device;
pub mod file;
pub mod session;

View file

@ -1 +0,0 @@

View file

@ -1 +0,0 @@
pub mod http;

View file

@ -1,4 +1,4 @@
use std::{collections::HashMap, net::SocketAddr, path::PathBuf, sync::Arc};
use std::{collections::HashMap, net::SocketAddr, path::PathBuf};
use axum::{
Extension, Json,
@ -12,12 +12,31 @@ use native_dialog::MessageDialogBuilder;
use serde::{Deserialize, Serialize};
use crate::{
Client, JoecalState,
JoecalState,
error::{LocalSendError, Result},
models::{device::DeviceInfo, file::FileMetadata},
transfer::session::{Session, SessionStatus},
models::{Device, FileMetadata},
};
#[derive(Deserialize, Serialize)]
pub struct Session {
pub session_id: String,
pub files: HashMap<String, FileMetadata>,
pub file_tokens: HashMap<String, String>,
pub receiver: Device,
pub sender: Device,
pub status: SessionStatus,
pub addr: SocketAddr,
}
#[derive(PartialEq, Deserialize, Serialize)]
pub enum SessionStatus {
Pending,
Active,
Completed,
Failed,
Cancelled,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PrepareUploadResponse {
@ -28,11 +47,11 @@ pub struct PrepareUploadResponse {
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PrepareUploadRequest {
pub info: DeviceInfo,
pub info: Device,
pub files: HashMap<String, FileMetadata>,
}
impl Client {
impl JoecalState {
pub async fn prepare_upload(
&self,
peer: String,
@ -46,8 +65,8 @@ impl Client {
println!("Peer: {peer:?}");
let response = self
.http_client
.post(&format!(
.client
.post(format!(
"{}://{}/api/localsend/v2/prepare-upload",
peer.1.protocol,
peer.0.clone()
@ -99,8 +118,7 @@ impl Client {
return Err(LocalSendError::InvalidToken);
}
let request = self
.http_client
let request = self.client
.post(format!(
"{}://{}/api/localsend/v2/upload?sessionId={session_id}&fileId={file_id}&token={token}",
session.receiver.protocol, session.addr))
@ -155,7 +173,7 @@ impl Client {
let session = sessions.get(&session_id).unwrap();
let request = self
.http_client
.client
.post(format!(
"{}://{}/api/localsend/v2/cancel?sessionId={}",
session.receiver.protocol, session.addr, session_id
@ -172,7 +190,7 @@ impl Client {
}
pub async fn register_prepare_upload(
State(state): State<Arc<JoecalState>>,
State(state): State<JoecalState>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
Json(req): Json<PrepareUploadRequest>,
) -> impl IntoResponse {
@ -225,7 +243,7 @@ pub async fn register_prepare_upload(
pub async fn register_upload(
Query(params): Query<UploadParams>,
State(state): State<Arc<JoecalState>>,
State(state): State<JoecalState>,
Extension(download_dir): Extension<String>,
body: Bytes,
) -> impl IntoResponse {

View file

@ -1 +0,0 @@

View file

@ -1,3 +0,0 @@
pub mod download;
pub mod session;
pub mod upload;

View file

@ -1,25 +0,0 @@
use std::{collections::HashMap, net::SocketAddr};
use serde::{Deserialize, Serialize};
use crate::models::{device::DeviceInfo, file::FileMetadata};
#[derive(Deserialize, Serialize)]
pub struct Session {
pub session_id: String,
pub files: HashMap<String, FileMetadata>,
pub file_tokens: HashMap<String, String>,
pub receiver: DeviceInfo,
pub sender: DeviceInfo,
pub status: SessionStatus,
pub addr: SocketAddr,
}
#[derive(PartialEq, Deserialize, Serialize)]
pub enum SessionStatus {
Pending,
Active,
Completed,
Failed,
Cancelled,
}