From 77fe1501e3d9be05f7f4d31e11faaa0dfc6e4e7a Mon Sep 17 00:00:00 2001 From: circlesabound Date: Sun, 14 Apr 2024 15:10:01 +1000 Subject: [PATCH] WIP savefile downloading --- Cargo.lock | 15 +++- Cargo.toml | 1 + openapi/mgmt-server-rest.yaml | 32 ++++++- src/agent/main.rs | 35 +++++++- src/agent/util/saves.rs | 21 ++++- src/mgmt-server/clients.rs | 13 +++ src/mgmt-server/error.rs | 4 + src/mgmt-server/link_download.rs | 36 ++++++++ src/mgmt-server/main.rs | 21 +++-- src/mgmt-server/routes/download.rs | 23 +++++ src/mgmt-server/routes/mod.rs | 29 +++++++ src/mgmt-server/routes/server.rs | 84 +++++++++++++++---- src/schema.rs | 76 +++++++++++++++-- src/ws-client/main.rs | 2 +- web/src/app/auth/bearer-auth-interceptor.ts | 2 - .../app/dashboard2/dashboard2.component.html | 1 + .../app/dashboard2/dashboard2.component.ts | 5 +- 17 files changed, 352 insertions(+), 48 deletions(-) create mode 100644 src/mgmt-server/link_download.rs create mode 100644 src/mgmt-server/routes/download.rs diff --git a/Cargo.lock b/Cargo.lock index d2c352b..6eece1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,6 +160,12 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" + [[package]] name = "binascii" version = "0.1.4" @@ -539,6 +545,7 @@ dependencies = [ name = "fctrl" version = "0.1.0" dependencies = [ + "base64 0.22.0", "bincode", "bytes", "chrono", @@ -1711,7 +1718,7 @@ version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ - "base64", + "base64 0.21.5", "bytes", "encoding_rs", "futures-core", @@ -1754,7 +1761,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e333b1eb9fe677f6893a9efcb0d277a2d3edd83f358a236b657c32301dc6e5f6" dependencies = [ - "base64", + "base64 0.21.5", "bytes", "encoding_rs", "futures-core", @@ -1939,7 +1946,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64", + "base64 0.21.5", ] [[package]] @@ -2089,7 +2096,7 @@ checksum = "385647faa24a889929028973650a4f158fb1b4272b2fcf94feb9fcc3c009e813" dependencies = [ "arrayvec", "async-trait", - "base64", + "base64 0.21.5", "bitflags 2.4.1", "bytes", "dashmap", diff --git a/Cargo.toml b/Cargo.toml index 7446640..3c2209f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ license = "Apache-2.0" publish = false [dependencies] +base64 = "0.22" bincode = "1.3" bytes = "1.0" chrono = { version = "0.4", features = [ "serde" ] } diff --git a/openapi/mgmt-server-rest.yaml b/openapi/mgmt-server-rest.yaml index ff82b23..38ec8f8 100644 --- a/openapi/mgmt-server-rest.yaml +++ b/openapi/mgmt-server-rest.yaml @@ -126,7 +126,7 @@ paths: $ref: '#/components/schemas/ServerSavefileGetResponse' /server/savefiles/{savefile_id}: get: - summary: Downloads the requested savefile as a zip + summary: Generate a link to download the requested savefile as a zip parameters: - name: savefile_id in: path @@ -136,7 +136,7 @@ paths: type: string responses: '200': - description: Savefile zip + description: Link ID to download the requested savefile as a zip content: application/octet-stream: schema: @@ -312,7 +312,7 @@ paths: description: Request accepted, check the Location header for a websocket address to connect and monitor progress of the operation. /server/mods/settings: get: - summary: Gets the mod-settings.dat file used by the Factorio server. + summary: Gets the mod-settings.dat file used by the Factorio server in JSON format responses: '200': description: The contents of the mod-settings.dat file, converted to a JSON format @@ -321,7 +321,7 @@ paths: schema: $ref: '#/components/schemas/ModSettingsObject' put: - summary: Pushes a mod-settings.dat file to the Factorio server for use + summary: Pushes contents of mod-settings.dat file in JSON format to the Factorio server for use requestBody: required: true description: A mod-settings.dat file, converted to a JSON format @@ -332,6 +332,30 @@ paths: responses: '200': description: Ok + /server/mods/settings-dat: + get: + summary: Gets the mod-settings.dat file used by the Factorio server. + responses: + '200': + description: The binary contents of the mod-settings.dat file + content: + application/octet-stream: + schema: + type: string + format: binary + put: + summary: Pushes a mod-settings.dat file to the Factorio server for use + requestBody: + required: true + description: A mod-settings.dat file in original binary format + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + '200': + description: Ok /server/rcon: post: summary: Send a command over RCON to the Factorio game instance. diff --git a/src/agent/main.rs b/src/agent/main.rs index fab6f83..34bc341 100644 --- a/src/agent/main.rs +++ b/src/agent/main.rs @@ -48,6 +48,8 @@ mod factorio; mod server; mod util; +const MAX_WS_PAYLOAD_BYTES: usize = 8000000; + #[tokio::main] async fn main() -> Result<(), Box> { env_logger::init(); @@ -309,7 +311,7 @@ impl AgentController { } AgentRequest::SaveGet(save_name) => { - todo!() + self.save_get(save_name, operation_id).await } AgentRequest::SaveList => { @@ -993,6 +995,31 @@ impl AgentController { } } + async fn save_get(&self, save_name: String, operation_id: OperationId) { + match util::saves::get_savefile(&save_name).await { + Ok(Some(savebytes)) => { + self.long_running_ack(&operation_id).await; + let chunks = savebytes.bytes.chunks(MAX_WS_PAYLOAD_BYTES); + let mut i = 0; + for chunk in chunks { + let msg = AgentOutMessage::SaveFile(SaveBytes { + multipart_seqnum: Some(i), + bytes: chunk.to_vec(), + }); + self.reply(msg, &operation_id).await; + i += 1; + } + self.reply_success(AgentOutMessage::SaveFile(SaveBytes::sentinel(i)), operation_id).await; + }, + Ok(None) => self.reply_failed( + AgentOutMessage::SaveNotFound, + operation_id).await, + Err(e) => self.reply_failed( + AgentOutMessage::Error(format!("Failed to get save: {:?}", e)), + operation_id).await, + } + } + async fn save_list(&self, operation_id: OperationId) { match util::saves::list_savefiles().await { Ok(saves) => { @@ -1090,7 +1117,7 @@ impl AgentController { match s.try_into() { Ok(bytes) => { self.reply_success( - AgentOutMessage::ModSettings(Some(ModSettingsBytes(bytes))), + AgentOutMessage::ModSettings(Some(ModSettingsBytes { bytes })), operation_id, ) .await; @@ -1122,11 +1149,11 @@ impl AgentController { } } - async fn mod_settings_set(&self, bytes: ModSettingsBytes, operation_id: OperationId) { + async fn mod_settings_set(&self, ms_bytes: ModSettingsBytes, operation_id: OperationId) { match ModManager::read_or_apply_default().await { Ok(mut m) => { // Validate by attempting to parse - match ModSettings::try_from(bytes.0.as_ref()) { + match ModSettings::try_from(ms_bytes.bytes.as_ref()) { Ok(ms) => { m.settings = Some(ms); if let Err(e) = m.apply_metadata_only().await { diff --git a/src/agent/util/saves.rs b/src/agent/util/saves.rs index 2b35448..a8832d3 100644 --- a/src/agent/util/saves.rs +++ b/src/agent/util/saves.rs @@ -1,13 +1,28 @@ use std::path::{Path, PathBuf}; -use fctrl::schema::Save; +use fctrl::schema::{Save, SaveBytes}; use log::warn; use tokio::fs; use crate::{consts::*, error::Result}; -pub fn get_savefile_path(save_name: &str) -> PathBuf { - SAVEFILE_DIR.join(format!("{}.zip", save_name)) +pub fn get_savefile_path(save_name: impl AsRef) -> PathBuf { + SAVEFILE_DIR.join(format!("{}.zip", save_name.as_ref())) +} + +pub async fn get_savefile(save_name: impl AsRef) -> Result> { + if !SAVEFILE_DIR.is_dir() { + return Ok(None); + } + + let savefiles = list_savefiles().await?; + match savefiles.into_iter().find(|s| s.name == save_name.as_ref()) { + Some(s) => { + let bytes = fs::read(get_savefile_path(s.name)).await?; + Ok(Some(SaveBytes::new(bytes))) + }, + None => Ok(None), + } } pub async fn list_savefiles() -> Result> { diff --git a/src/mgmt-server/clients.rs b/src/mgmt-server/clients.rs index 23ffc83..e031166 100644 --- a/src/mgmt-server/clients.rs +++ b/src/mgmt-server/clients.rs @@ -140,6 +140,17 @@ impl AgentApiClient { ack_or_timeout(sub, Duration::from_millis(500), id).await } + pub async fn save_get(&self, savefile_name: String) -> Result<(OperationId, impl Stream + Unpin)> { + if savefile_name.trim().is_empty() { + return Err(Error::BadRequest("Empty savefile name".to_owned())); + } + + let request = AgentRequest::SaveGet(savefile_name); + let (id, sub) = self.send_request_and_subscribe(request).await?; + + ack_or_timeout(sub, Duration::from_millis(500), id).await + } + pub async fn save_list(&self) -> Result> { let request = AgentRequest::SaveList; let (_id, sub) = self.send_request_and_subscribe(request).await?; @@ -402,6 +413,7 @@ fn default_message_handler(agent_message: AgentOutMessage) -> Error { | AgentOutMessage::ModsList(_) | AgentOutMessage::ModSettings(_) | AgentOutMessage::RconResponse(_) + | AgentOutMessage::SaveFile(_) | AgentOutMessage::SaveList(_) | AgentOutMessage::ServerStatus(_) | AgentOutMessage::Ok => Error::AgentCommunicationError, @@ -413,6 +425,7 @@ fn default_message_handler(agent_message: AgentOutMessage) -> Error { AgentOutMessage::NotInstalled => { Error::AgentInternalError("Factorio not installed".to_owned()) } + AgentOutMessage::SaveNotFound => Error::SaveNotFound, } } diff --git a/src/mgmt-server/error.rs b/src/mgmt-server/error.rs index 78bcd59..2bd00bf 100644 --- a/src/mgmt-server/error.rs +++ b/src/mgmt-server/error.rs @@ -28,8 +28,10 @@ pub enum Error { // Specific errors DiscordAlertingDisabled, + InvalidLink, ModSettingsNotInitialised, ModSettingsParseError(factorio_mod_settings_parser::Error), + SaveNotFound, SecretsNotInitialised, // Generic wrappers around external error types @@ -127,6 +129,8 @@ impl<'r> Responder<'r, 'static> for Error { | Error::AuthInvalid | Error::AuthRefreshUnavailable | Error::MetricInvalidKey(_) => Status::BadRequest, + Error::SaveNotFound + | Error::InvalidLink => Status::NotFound, Error::ModSettingsNotInitialised | Error::SecretsNotInitialised => Status::NoContent, }; diff --git a/src/mgmt-server/link_download.rs b/src/mgmt-server/link_download.rs new file mode 100644 index 0000000..6fb30ad --- /dev/null +++ b/src/mgmt-server/link_download.rs @@ -0,0 +1,36 @@ +use std::collections::HashMap; +use chrono::{DateTime, Utc}; +use log::info; +use tokio::sync::RwLock; +use uuid::Uuid; + +pub struct LinkDownloadManager { + links: RwLock)>>, +} + +#[derive(Clone, Debug)] +pub enum LinkDownloadTarget { + Savefile { id: String }, +} + +impl LinkDownloadManager { + pub fn new() -> LinkDownloadManager { + // TODO periodic job to expire out links + LinkDownloadManager { + links: RwLock::new(HashMap::new()), + } + } + + pub async fn create_link(&self, target: LinkDownloadTarget) -> String { + let mut w_guard = self.links.write().await; + let link = Uuid::new_v4().as_simple().to_string(); + info!("Generating download link: {} -> {:?}", link, target); + w_guard.insert(link.clone(), (target, Utc::now())); + link + } + + pub async fn get_link(&self, link: String) -> Option { + let r_guard = self.links.read().await; + r_guard.get(&link).map(|(target, _dt)| target.clone()) + } +} diff --git a/src/mgmt-server/main.rs b/src/mgmt-server/main.rs index 2248ed1..4b4023a 100644 --- a/src/mgmt-server/main.rs +++ b/src/mgmt-server/main.rs @@ -12,13 +12,7 @@ use log::{debug, error, info}; use rocket::{async_trait, catchers, fairing::Fairing, fs::FileServer, routes}; use crate::{ - auth::UserIdentity, - clients::AgentApiClient, - db::{Cf, Db, Record}, - discord::DiscordClient, - events::broker::EventBroker, - rpc::RpcHandler, - ws::WebSocketServer, + auth::UserIdentity, clients::AgentApiClient, db::{Cf, Db, Record}, discord::DiscordClient, events::broker::EventBroker, link_download::LinkDownloadManager, rpc::RpcHandler, ws::WebSocketServer }; mod auth; @@ -30,6 +24,7 @@ mod discord; mod error; mod events; mod guards; +mod link_download; mod metrics; mod routes; mod rpc; @@ -127,6 +122,9 @@ async fn main() -> std::result::Result<(), Box> { ) .await?; + info!("Creating link download manager"); + let link_download_manager = Arc::new(LinkDownloadManager::new()); + let ws_port = std::env::var("MGMT_SERVER_WS_PORT")?.parse()?; let ws_addr = std::env::var("MGMT_SERVER_WS_ADDRESS")?.parse()?; let ws_bind = SocketAddr::new(ws_addr, ws_port); @@ -144,6 +142,7 @@ async fn main() -> std::result::Result<(), Box> { .manage(event_broker) .manage(db) .manage(agent_client) + .manage(link_download_manager) .manage(ws) .mount("/", routes![routes::options::options,]) .mount( @@ -176,6 +175,8 @@ async fn main() -> std::result::Result<(), Box> { routes::server::apply_mods_list, routes::server::get_mod_settings, routes::server::put_mod_settings, + routes::server::get_mod_settings_dat, + routes::server::put_mod_settings_dat, routes::server::send_rcon_command, routes::logs::get, routes::logs::stream, @@ -190,6 +191,12 @@ async fn main() -> std::result::Result<(), Box> { routes::proxy::mod_portal_full_get, ], ) + .mount( + "/download", + routes![ + routes::download::download, + ] + ) .mount("/", FileServer::from(get_dist_path())) .register("/api/v0", catchers![catchers::not_found,]) .register("/", catchers![catchers::fallback_to_index_html,]) diff --git a/src/mgmt-server/routes/download.rs b/src/mgmt-server/routes/download.rs new file mode 100644 index 0000000..da2fad8 --- /dev/null +++ b/src/mgmt-server/routes/download.rs @@ -0,0 +1,23 @@ +use std::sync::Arc; + +use crate::{clients::AgentApiClient, error::{Error, Result}, link_download::{LinkDownloadManager, LinkDownloadTarget}}; + +use rocket::{get, response::stream::ByteStream, State}; + +#[get("/")] +pub async fn download( + agent_client: &State>, + link_download_manager: &State>, + link_id: String, +) -> Result]> { + match link_download_manager.get_link(link_id).await { + Some(target) => match target { + LinkDownloadTarget::Savefile { id } => crate::routes::server::get_savefile_real( + agent_client, + id, + ).await, + } + None => Err(Error::InvalidLink) + } +} + diff --git a/src/mgmt-server/routes/mod.rs b/src/mgmt-server/routes/mod.rs index dbcf637..fdf06ad 100644 --- a/src/mgmt-server/routes/mod.rs +++ b/src/mgmt-server/routes/mod.rs @@ -10,12 +10,41 @@ use rocket::{ use crate::{guards::HostHeader, ws::WebSocketServer}; pub mod auth; +pub mod download; pub mod logs; pub mod metrics; pub mod options; pub mod proxy; pub mod server; +pub struct LinkDownloadResponder { + pub link_id: String, + full_uri: String, +} + +impl LinkDownloadResponder { + fn new( + host: HostHeader, + link_id: String, + ) -> LinkDownloadResponder { + let path = format!("/download/{}", link_id); + let full_uri = format!("{}{}", host.hostname, path); + LinkDownloadResponder { + link_id, + full_uri, + } + } +} + +impl<'r> Responder<'r, 'static> for LinkDownloadResponder { + fn respond_to(self, _: &'r rocket::Request<'_>) -> rocket::response::Result<'static> { + Response::build() + .status(Status::Accepted) + .header(Header::new("Location", self.full_uri)) + .ok() + } +} + pub struct WsStreamingResponder { pub path: String, full_uri: String, diff --git a/src/mgmt-server/routes/server.rs b/src/mgmt-server/routes/server.rs index 60fbe7e..38b6dcc 100644 --- a/src/mgmt-server/routes/server.rs +++ b/src/mgmt-server/routes/server.rs @@ -6,18 +6,21 @@ use std::{ use factorio_mod_settings_parser::ModSettings; use fctrl::schema::{ - mgmt_server_rest::*, FactorioVersion, ModSettingsBytes, RconConfig, SecretsObject, - ServerStartSaveFile, ServerStatus, ServerSettingsConfig + mgmt_server_rest::*, AgentOutMessage, AgentResponseWithId, FactorioVersion, ModSettingsBytes, RconConfig, SecretsObject, ServerSettingsConfig, ServerStartSaveFile, ServerStatus }; -use rocket::serde::json::Json; +use log::{error, info}; +use rocket::{response::stream::ByteStream, serde::json::Json}; use rocket::{get, post, put}; use rocket::{http::Status, State}; +use tokio_stream::StreamExt; use crate::{ - auth::AuthorizedUser, clients::AgentApiClient, guards::HostHeader, ws::WebSocketServer, + auth::AuthorizedUser, clients::AgentApiClient, guards::HostHeader, link_download::{LinkDownloadManager, LinkDownloadTarget}, ws::WebSocketServer }; use crate::{error::Result, routes::WsStreamingResponder}; +use super::LinkDownloadResponder; + #[get("/server/control")] pub async fn status( _a: AuthorizedUser, @@ -134,13 +137,48 @@ pub async fn get_savefiles( } #[get("/server/savefiles/")] -pub async fn get_savefile( +pub async fn get_savefile<'a>( + host: HostHeader<'a>, _a: AuthorizedUser, + link_download_manager: &State>, + id: String, +) -> Result { + let link_id = link_download_manager.create_link(LinkDownloadTarget::Savefile { id }).await; + Ok(LinkDownloadResponder::new(host, link_id)) +} + +pub async fn get_savefile_real( agent_client: &State>, id: String, -) -> Result> { - // TODO not implemented - Err(crate::error::Error::NotImplemented) +) -> Result]> { + let (_operation_id, sub) = agent_client.save_get(id).await?; + // TODO figure out how to properly handle errors + let s = sub.filter_map(|event| { + match serde_json::from_str::(&event.content) { + Ok(m) => { + match m.content { + AgentOutMessage::SaveFile(sb) => { + if sb.bytes.len() == 0 { + info!("get_savefile completed with total multiparts = {:?}", sb.multipart_seqnum); + None + } else { + Some(sb.bytes) + } + } + c => { + error!("Expected AgentOutMessage::SaveFile during get_savefile, got something else: {:?}", c); + None + }, + } + } + Err(e) => { + error!("Error deserialising event content during get_savefile: {:?}", e); + None + } + } + }); + + Ok(ByteStream::from(s)) } #[get("/server/config/adminlist")] @@ -318,12 +356,12 @@ pub async fn apply_mods_list<'a>( pub async fn get_mod_settings( _a: AuthorizedUser, agent_client: &State>, -) -> Result> { - let bytes = agent_client.mod_settings_get().await?; - let ms = ModSettings::try_from(bytes.0.as_ref())?; - let json_str = serde_json::to_string(&ms)?; +) -> Result> { + let ms_bytes = agent_client.mod_settings_get().await?; + let ms = ModSettings::try_from(ms_bytes.bytes.as_ref())?; + // let json_str = serde_json::to_string(&ms)?; - Ok(Json(json_str)) + Ok(Json(ms)) } #[put("/server/mods/settings", data = "")] @@ -334,7 +372,25 @@ pub async fn put_mod_settings( ) -> Result<()> { let ms: ModSettings = serde_json::from_str(&body)?; let bytes = ms.try_into()?; - agent_client.mod_settings_set(ModSettingsBytes(bytes)).await + agent_client.mod_settings_set(ModSettingsBytes { bytes }).await +} + +#[get("/server/mods/settings-dat")] +pub async fn get_mod_settings_dat( + _a: AuthorizedUser, + agent_client: &State>, +) -> Result> { + let ms_bytes = agent_client.mod_settings_get().await?; + Ok(ms_bytes.bytes) +} + +#[put("/server/mods/settings-dat", data = "")] +pub async fn put_mod_settings_dat( + _a: AuthorizedUser, + agent_client: &State>, + body: Vec, +) -> Result<()> { + agent_client.mod_settings_set(ModSettingsBytes { bytes: body } ).await } #[post("/server/rcon", data = "")] diff --git a/src/schema.rs b/src/schema.rs index fda39d8..6843106 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -26,13 +26,13 @@ pub mod factorio_mod_portal_api { #[derive(Clone, Debug, Deserialize, derive_more::From, derive_more::Into, Serialize)] pub struct OperationId(pub String); -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct AgentRequestWithId { pub operation_id: OperationId, pub message: AgentRequest, } -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize)] pub enum AgentRequest { // ********************************* // * Installation management * @@ -135,7 +135,7 @@ pub enum AgentRequest { RconCommand(String), } -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct AgentResponseWithId { pub operation_id: OperationId, pub status: OperationStatus, @@ -161,7 +161,7 @@ pub enum OperationStatus { Failed, } -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize)] pub enum AgentOutMessage { // Generic responses Message(String), @@ -182,7 +182,9 @@ pub enum AgentOutMessage { MissingSecrets, NotInstalled, RconResponse(String), + SaveFile(SaveBytes), SaveList(Vec), + SaveNotFound, ServerStatus(ServerStatus), } @@ -209,11 +211,51 @@ pub struct Save { pub last_modified: DateTime, } -#[derive(Clone, Debug, Deserialize, derive_more::From, derive_more::Into, Serialize)] -pub struct SaveBytes(pub Vec); +#[derive(Deserialize, Serialize)] +pub struct SaveBytes { + pub multipart_seqnum: Option, + #[serde(with = "base64")] + pub bytes: Vec, +} -#[derive(Clone, Debug, Deserialize, derive_more::From, derive_more::Into, Serialize)] -pub struct ModSettingsBytes(pub Vec); +impl SaveBytes { + pub fn new(bytes: Vec) -> SaveBytes { + SaveBytes { + multipart_seqnum: None, + bytes, + } + } + + pub fn sentinel(total_num_parts: u32) -> SaveBytes { + SaveBytes { + multipart_seqnum: Some(total_num_parts), + bytes: vec![], + } + } +} + +impl std::fmt::Debug for SaveBytes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.bytes.len() > 16 { + let debug_bytes = format!("{:?}...", &self.bytes[..16]); + f.debug_struct("SaveBytes") + .field("multipart_seqnum", &self.multipart_seqnum) + .field("bytes", &debug_bytes) + .finish() + } else { + f.debug_struct("SaveBytes") + .field("multipart_seqnum", &self.multipart_seqnum) + .field("bytes", &self.bytes) + .finish() + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ModSettingsBytes { + #[serde(with = "base64")] + pub bytes: Vec, +} #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ModObject { @@ -295,3 +337,21 @@ pub enum AllowCommandsValue { #[serde(rename = "admins-only")] AdminsOnly, } + +mod base64 { + use base64::Engine; + use serde::{Deserialize, Serialize}; + use serde::{Deserializer, Serializer}; + + pub fn serialize(v: &Vec, s: S) -> Result { + let base64 = base64::engine::general_purpose::STANDARD_NO_PAD.encode(v); + String::serialize(&base64, s) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { + let base64 = String::deserialize(d)?; + base64::engine::general_purpose::STANDARD_NO_PAD + .decode(base64.as_bytes()) + .map_err(|e| serde::de::Error::custom(e)) + } +} diff --git a/src/ws-client/main.rs b/src/ws-client/main.rs index dd9d62e..8ad53e6 100644 --- a/src/ws-client/main.rs +++ b/src/ws-client/main.rs @@ -186,7 +186,7 @@ fn get_message_from_input(input: String) -> Option { .ok() .map(|bytes| AgentRequestWithId { operation_id, - message: AgentRequest::ModSettingsSet(bytes.into()), + message: AgentRequest::ModSettingsSet(ModSettingsBytes { bytes }), }) }) .flatten(), diff --git a/web/src/app/auth/bearer-auth-interceptor.ts b/web/src/app/auth/bearer-auth-interceptor.ts index 4aba3c0..fa1ec5b 100644 --- a/web/src/app/auth/bearer-auth-interceptor.ts +++ b/web/src/app/auth/bearer-auth-interceptor.ts @@ -8,7 +8,6 @@ export class BearerAuthInterceptor implements HttpInterceptor { constructor(private cfg: ApiRequestConfiguration) { } intercept(req: HttpRequest, next: HttpHandler): Observable> { - console.log('intercepting http request'); req = this.cfg.apply(req); return next.handle(req); } @@ -25,7 +24,6 @@ export class ApiRequestConfiguration { } useBearerAuth(accessToken: string): void { - console.log('setting bearer auth'); this.nextAuthHeader = Option.some('Authorization'); this.nextAuthValue = Option.some(`Bearer ${accessToken}`); } diff --git a/web/src/app/dashboard2/dashboard2.component.html b/web/src/app/dashboard2/dashboard2.component.html index 33fbd4e..bae2419 100644 --- a/web/src/app/dashboard2/dashboard2.component.html +++ b/web/src/app/dashboard2/dashboard2.component.html @@ -20,6 +20,7 @@

Saves:

.zip + diff --git a/web/src/app/dashboard2/dashboard2.component.ts b/web/src/app/dashboard2/dashboard2.component.ts index 3699741..cf817e4 100644 --- a/web/src/app/dashboard2/dashboard2.component.ts +++ b/web/src/app/dashboard2/dashboard2.component.ts @@ -66,7 +66,10 @@ export class Dashboard2Component implements OnInit { // update download link this.saveIsSelected = true; this.saveDownloadName = selectChangeEvent.value + ".zip"; - this.saveDownloadHref = "/server/savefiles/" + this.saveDownloadName; + this.saveDownloadHref = "api/v0/server/savefiles/" + selectChangeEvent.value; + // this.apiClient.serverSavefilesSavefileIdGet({ savefile_id: selectChangeEvent.value }).subscribe(s => { + // + // }) } startServer(): void {