diff --git a/Cargo.toml b/Cargo.toml index ab1f8b6..630deb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,10 +4,15 @@ version = "0.1.0" edition = "2021" [dependencies] +axum = { version = "0.7.7", features = ["multipart"] } crossbeam-channel = "0.5.13" rand = "0.8.5" rodio = "0.19.0" rusb = "0.9.4" serde = { version = "1.0.210", features = ["derive", "rc"] } serde_json = "1.0.132" +tokio = { version = "1.41.0", features = ["macros", "rt-multi-thread"] } +tower-http = { version = "0.6.1", features = ["fs"] } +tracing = { version = "0.1.40", features = ["async-await"] } +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } wifi-rs = "0.2.4" diff --git a/assets/index.html b/assets/index.html new file mode 100644 index 0000000..bf07b7d --- /dev/null +++ b/assets/index.html @@ -0,0 +1,93 @@ + + + + + + + + Marlinbox + + + + + + + +
+
+
+

Marlinbox

+

The Open Source Music Box

+
+
+
+ + +
+
+ + +
+
+
+ + + + \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index c79616e..cba2d42 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ pub mod card; pub mod card_reader; pub mod error; -pub mod library; -pub mod portal; +mod library; +pub mod manager; pub mod service; +pub use library::Library; diff --git a/src/library.rs b/src/library.rs index aab6148..a1b23fa 100644 --- a/src/library.rs +++ b/src/library.rs @@ -1,9 +1,10 @@ -use std::{collections::HashMap, sync::Arc}; +use std::{collections::HashMap, io::Write, sync::Arc}; use rand::seq::SliceRandom; use serde::{Deserialize, Serialize}; -use crate::card::Card; +use crate::{card::Card, error::Error}; +use std::fs::File; #[derive(Debug, Serialize, Deserialize)] pub struct Library { @@ -18,10 +19,44 @@ impl Library { } } + /// Saves the library to a file. + /// + /// # Arguments + /// + /// * `file_path` - The path to the file where the library will be saved. + /// + /// # Errors + /// + /// Returns an `Error` if there was an error writing the library to the file. + pub fn save_to_file>(&self, file_path: P) -> Result<(), Error> { + let serialized = serde_json::to_string(self)?; + let mut file = File::create(file_path.as_ref())?; + file.write_all(serialized.as_bytes())?; + Ok(()) + } + + /// Loads the library from a file. + /// + /// # Arguments + /// + /// * `file_path` - The path to the file from which the library will be loaded. + /// + /// # Errors + /// + /// Returns an `Error` if there was an error reading the library from the file. + pub fn from_file>(file_path: P) -> Result { + let file = File::open(file_path.as_ref())?; + Ok(serde_json::from_reader(file)?) + } + pub fn add(&mut self, card_id: &str) { self.music.insert(card_id.into(), None); } + pub fn remove(&mut self, card_id: &str) -> Option { + self.music.remove_entry(card_id).and_then(|(_, v)| v) + } + pub fn update(&mut self, card_id: &str, music_file: Option) { self.music .insert(card_id.into(), music_file.map(Into::into)); diff --git a/src/manager.rs b/src/manager.rs new file mode 100644 index 0000000..15fefa9 --- /dev/null +++ b/src/manager.rs @@ -0,0 +1,108 @@ +use std::{fs, path::PathBuf, sync::Arc}; + +use axum::{ + extract::{Multipart, State}, + response::IntoResponse, + routing::{get, post}, +}; +use crossbeam_channel::{Receiver, Sender}; +use tokio::runtime::Runtime; +use tower_http::services::{ServeDir, ServeFile}; +use tracing::{debug, error, info}; + +/// Starts the Manager and listens for incoming connections. +/// +/// # Arguments +/// +/// * `connection_string` - A string representing the connection address and port. +/// * `tx_pairing` - An Arc-wrapped Sender used for message passing. +/// * `rx_shutdown` - A Receiver used to receive shutdown signals. +/// +/// # Returns +/// +/// Returns `Ok(())` if the server started successfully, otherwise returns an error. +pub fn serve( + connection_string: &str, + tx_pairing: Arc>, + rx_shutdown: Arc>, +) -> Result<(), Box> { + let rt = Runtime::new()?; + let app = axum::Router::new() + .nest_service("/assets", ServeDir::new(PathBuf::from("assets"))) + .route_service("/", ServeFile::new(PathBuf::from("assets/index.html"))) + .route("/pair", get(handler)) + .route("/failed", get(|| async { "Failed to send message" })) + .route("/upload", post(upload_file)) + .with_state(tx_pairing); + + rt.block_on(async { + let listener = match tokio::net::TcpListener::bind(connection_string).await { + Ok(it) => { + info!("Manager listening on: {connection_string}"); + it + } + Err(err) => { + error!("Manager failed to bind to {connection_string}: {err}"); + return; + } + }; + match axum::serve(listener, app) + .with_graceful_shutdown(async move { + match rx_shutdown.recv() { + Ok(()) => info!("Manager shutting down"), + Err(err) => error!("Manager failed to shut down: {err}"), + } + }) + .await + { + Ok(()) => info!("Manager listening on {connection_string}"), + Err(err) => error!("Manager failed to start: {err}"), + } + }); + Ok(()) +} + +async fn handler(State(state): State>>) -> impl IntoResponse { + match state.send(()) { + Ok(()) => { + info!("Sent pairing request"); + "Pairing" + } + Err(err) => { + error!("Failed to send pairing request: {err}"); + "Failed to start pairing" + } + } +} + +async fn upload_file(mut multipart: Multipart) -> impl IntoResponse { + while let Ok(resp) = multipart.next_field().await { + if let Some(field) = resp { + let Some(filename) = field.file_name() else { + return "No file name provided"; + }; + let filepath = PathBuf::from(format!("./uploads/{filename}")); + + if let Err(err) = fs::create_dir_all("uploads") { + error!("Failed to create directory: {err}"); + return "Failed to create directory"; + } + + let data = match field.bytes().await { + Ok(bytes) => bytes, + Err(err) => { + error!("Failed to read file: {err}"); + return "Failed to read file"; + } + }; + if let Err(err) = fs::write(&filepath, data) { + error!("Failed to write file: {err}"); + return "Failed to write file"; + } + debug!("File uploaded: {}", filepath.to_string_lossy()); + } else { + break; + } + } + "File uploaded" +}