From 325a73da5a2cb38295d3a3ec2f781e883243b807 Mon Sep 17 00:00:00 2001 From: shiroyashik Date: Wed, 18 Sep 2024 11:37:39 +0300 Subject: [PATCH] Assets seeker more effective, directories changed, added assets auto check/update. --- .gitignore | 2 + Cargo.lock | 312 +++++++++++++++++++++++++++++++++++-- Cargo.toml | 3 +- Config.example.toml | 3 + src/api/figura/assets.rs | 45 ++++-- src/api/figura/profile.rs | 13 +- src/api/v1/avatars.rs | 8 +- src/consts.rs | 12 +- src/main.rs | 71 ++++++--- src/state/config.rs | 3 +- src/utils/check_updates.rs | 138 +++++++++++++++- 11 files changed, 550 insertions(+), 60 deletions(-) diff --git a/.gitignore b/.gitignore index 5562fa5..7227fc1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,11 @@ /avatars /logs /assets +/data output.log docker-compose.yml Config.toml +config.toml .env perf.data* banned-players.json \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 10fc1a1..a4c4652 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,23 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -43,9 +60,18 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.88" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e1496f8fb1fbf272686b8d37f523dab3e4a7443300055e74cdaa449f3114356" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" + +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "async-trait" @@ -162,7 +188,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.4", "object", "rustc-demangle", ] @@ -219,11 +245,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] -name = "cc" -version = "1.1.18" +name = "bzip2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "cc" +version = "1.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d74707dde2ba56f86ae90effb3b43ddd369504387e718014de010cec7959800" +dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -254,6 +303,16 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "3.2.25" @@ -312,6 +371,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb58b6451e8c2a812ad979ed1d83378caa5e927eef2622017a45f251457c2c9d" +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "core-foundation" version = "0.9.4" @@ -337,6 +402,30 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "cross" version = "0.2.5" @@ -424,6 +513,12 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +[[package]] +name = "deflate64" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" + [[package]] name = "deranged" version = "0.3.11" @@ -433,6 +528,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "digest" version = "0.10.7" @@ -441,6 +547,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -463,6 +570,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -522,6 +640,16 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +[[package]] +name = "flate2" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +dependencies = [ + "crc32fast", + "miniz_oxide 0.8.0", +] + [[package]] name = "fnv" version = "1.0.7" @@ -559,6 +687,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -567,6 +696,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + [[package]] name = "futures-sink" version = "0.3.30" @@ -586,8 +721,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", + "futures-io", "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -678,6 +815,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "home" version = "0.5.9" @@ -809,9 +955,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -867,6 +1013,15 @@ dependencies = [ "serde", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "ipnet" version = "2.10.0" @@ -885,6 +1040,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.70" @@ -932,12 +1096,28 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + [[package]] name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + [[package]] name = "matchers" version = "0.1.0" @@ -974,6 +1154,15 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "1.0.2" @@ -1154,6 +1343,16 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1358,6 +1557,7 @@ dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", "h2", @@ -1540,6 +1740,7 @@ dependencies = [ "tracing-subscriber", "uuid", "walkdir", + "zip", ] [[package]] @@ -1690,6 +1891,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" version = "0.4.9" @@ -2012,9 +2219,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.20" +version = "0.22.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf" dependencies = [ "indexmap 2.5.0", "serde", @@ -2556,3 +2763,88 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "zip" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "hmac", + "indexmap 2.5.0", + "lzma-rs", + "memchr", + "pbkdf2", + "rand", + "sha1", + "thiserror", + "time", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 7efb173..845ad16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ dashmap = { version = "6.0.1", features = ["serde"] } hex = "0.4.3" uuid = { version = "1.8.0", features = ["serde"] } base64 = "0.22.1" -reqwest = { version = "0.12.6", features = ["json"] } +reqwest = { version = "0.12.7", features = ["blocking", "json"] } dotenvy = "0.15.7" semver = "1.0.23" walkdir = "2.5.0" @@ -39,6 +39,7 @@ axum = { version = "0.7.5", features = ["ws", "macros", "http2"] } tower-http = { version = "0.5.2", features = ["trace"] } tokio = { version = "1.37.0", features = ["full"] } indexmap = { version = "2.5.0", features = ["serde"] } +zip = "2.2.0" [dev-dependencies] cross = "0.2.5" diff --git a/Config.example.toml b/Config.example.toml index 9611bbf..1df7a32 100644 --- a/Config.example.toml +++ b/Config.example.toml @@ -15,6 +15,9 @@ listen = "0.0.0.0:6665" # { name = "ElyBy", url = "http://minecraft.ely.by/session/hasJoined" }, # ] +# FIXME: in dev +assetsUpdaterEnabled = true + ## Message of The Day ## It will be displayed to every player in the Figura menu who is connected to your server [motd] diff --git a/src/api/figura/assets.rs b/src/api/figura/assets.rs index eadc7f5..74df255 100644 --- a/src/api/figura/assets.rs +++ b/src/api/figura/assets.rs @@ -3,36 +3,47 @@ use std::path::PathBuf; use axum::{extract::Path, routing::get, Json, Router}; use indexmap::IndexMap; use ring::digest::{digest, SHA256}; -use serde_json::{json, Value}; +use serde_json::Value; use tokio::{fs, io::AsyncReadExt as _}; use walkdir::WalkDir; -use crate::{api::errors::internal_and_log, ApiError, ApiResult, AppState}; +use crate::{api::errors::internal_and_log, ApiError, ApiResult, AppState, ASSETS_ENV}; pub fn router() -> Router { Router::new() .route("/", get(versions)) - .route("/v1", get(v1)) - .route("/v2", get(v2)) - .route("/*path", get(download)) + .route("/:version", get(hashes)) + .route("/:version/*key", get(download)) } -async fn versions() -> Json { - Json(json!(["v1", "v2"])) +async fn versions() -> ApiResult> { + let dir_path = PathBuf::from(&std::env::var(ASSETS_ENV).unwrap()); + + let mut directories = Vec::new(); + + let mut entries = fs::read_dir(dir_path).await.map_err(|err| internal_and_log(err))?; + + while let Some(entry) = entries.next_entry().await.map_err(|err| internal_and_log(err))? { + if entry.metadata().await.map_err(|err| internal_and_log(err))?.is_dir() { + if let Some(name) = entry.file_name().to_str() { + let name = name.to_string(); + if !name.starts_with('.') { + directories.push(Value::String(name.to_string())); + } + } + } + } + + Ok(Json(serde_json::Value::Array(directories))) } -async fn v1() -> ApiResult>> { - let map = index_assets("v1").await.map_err(|err| internal_and_log(err))?; +async fn hashes(Path(version): Path) -> ApiResult>> { + let map = index_assets(&version).await.map_err(|err| internal_and_log(err))?; Ok(Json(map)) } -async fn v2() -> ApiResult>> { - let map = index_assets("v2").await.map_err(|err| internal_and_log(err))?; - Ok(Json(map)) -} - -async fn download(Path(path): Path) -> ApiResult> { - let mut file = if let Ok(file) = fs::File::open(format!("assets/{path}")).await { +async fn download(Path((version, path)): Path<(String, String)>) -> ApiResult> { + let mut file = if let Ok(file) = fs::File::open(format!("{}/{version}/{path}", std::env::var(ASSETS_ENV).unwrap())).await { file } else { return Err(ApiError::NotFound) @@ -46,7 +57,7 @@ async fn download(Path(path): Path) -> ApiResult> { async fn index_assets(version: &str) -> anyhow::Result> { let mut map = IndexMap::new(); - let version_path = PathBuf::from("assets/").join(version); + let version_path = PathBuf::from(std::env::var(ASSETS_ENV).unwrap()).join(version); for entry in WalkDir::new(version_path.clone()).into_iter().filter_map(|e| e.ok()) { let data = match fs::read(entry.path()).await { diff --git a/src/api/figura/profile.rs b/src/api/figura/profile.rs index 1c1148f..0783fe3 100644 --- a/src/api/figura/profile.rs +++ b/src/api/figura/profile.rs @@ -1,3 +1,5 @@ +use std::env::var; + use axum::{ body::Bytes, extract::{Path, State}, Json }; @@ -12,7 +14,7 @@ use uuid::Uuid; use crate::{ api::errors::internal_and_log, auth::Token, utils::{calculate_file_sha256, format_uuid}, - ApiError, ApiResult, AppState + ApiError, ApiResult, AppState, AVATARS_ENV }; use super::types::S2CMessage; @@ -24,7 +26,7 @@ pub async fn user_info( let formatted_uuid = format_uuid(&uuid); - let avatar_file = format!("avatars/{}.moon", formatted_uuid); + let avatar_file = format!("{}/{}.moon", var(AVATARS_ENV).unwrap(), formatted_uuid); let userinfo = if let Some(info) = state.user_manager.get_by_uuid(&uuid) { info } else { return Err(ApiError::BadRequest) // NOTE: Not Found (404) shows badge @@ -79,7 +81,7 @@ pub async fn user_info( pub async fn download_avatar(Path(uuid): Path) -> ApiResult> { let uuid = format_uuid(&uuid); tracing::info!("Requesting an avatar: {}", uuid); - let mut file = if let Ok(file) = fs::File::open(format!("avatars/{}.moon", uuid)).await { + let mut file = if let Ok(file) = fs::File::open(format!("{}/{}.moon", var(AVATARS_ENV).unwrap(), uuid)).await { file } else { return Err(ApiError::NotFound) @@ -102,7 +104,7 @@ pub async fn upload_avatar( user_info.uuid, user_info.username ); - let avatar_file = format!("avatars/{}.moon", user_info.uuid); + let avatar_file = format!("{}/{}.moon", var(AVATARS_ENV).unwrap(), user_info.uuid); let mut file = BufWriter::new(fs::File::create(&avatar_file).await.map_err(|err| internal_and_log(err))?); io::copy(&mut request_data.as_ref(), &mut file).await.map_err(|err| internal_and_log(err))?; } @@ -123,11 +125,10 @@ pub async fn delete_avatar(Token(token): Token, State(state): State) - user_info.uuid, user_info.username ); - let avatar_file = format!("avatars/{}.moon", user_info.uuid); + let avatar_file = format!("{}/{}.moon", var(AVATARS_ENV).unwrap(), user_info.uuid); fs::remove_file(avatar_file).await.map_err(|err| internal_and_log(err))?; send_event(&state, &user_info.uuid).await; } - // let avatar_file = format!("avatars/{}.moon",user_info.uuid); Ok("ok".to_string()) } diff --git a/src/api/v1/avatars.rs b/src/api/v1/avatars.rs index 5c434b9..3f17654 100644 --- a/src/api/v1/avatars.rs +++ b/src/api/v1/avatars.rs @@ -1,9 +1,11 @@ +use std::env::var; + use axum::{body::Bytes, extract::{Path, State}}; use tokio::{fs, io::{self, BufWriter}}; use tracing::warn; use uuid::Uuid; -use crate::{api::figura::profile::send_event, auth::Token, ApiResult, AppState}; +use crate::{api::figura::profile::send_event, auth::Token, ApiResult, AppState, AVATARS_ENV}; pub async fn upload_avatar( Path(uuid): Path, @@ -20,7 +22,7 @@ pub async fn upload_avatar( uuid, ); - let avatar_file = format!("avatars/{}.moon", &uuid); + let avatar_file = format!("{}/{}.moon", var(AVATARS_ENV).unwrap(), &uuid); let mut file = BufWriter::new(fs::File::create(&avatar_file).await.unwrap()); io::copy(&mut request_data.as_ref(), &mut file).await.unwrap(); send_event(&state, &uuid).await; @@ -40,7 +42,7 @@ pub async fn delete_avatar( uuid, ); - let avatar_file = format!("avatars/{}.moon", &uuid); + let avatar_file = format!("{}/{}.moon", var(AVATARS_ENV).unwrap(), &uuid); match fs::remove_file(avatar_file).await { Ok(_) => {}, Err(_) => { diff --git a/src/consts.rs b/src/consts.rs index 68da6e8..11d8967 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -1,12 +1,22 @@ +// Environment pub const LOGGER_ENV: &'static str = "RUST_LOG"; pub const CONFIG_ENV: &'static str = "RUST_CONFIG"; pub const LOGS_ENV: &'static str = "LOGS_FOLDER"; +pub const ASSETS_ENV: &'static str = "ASSETS_FOLDER"; +pub const AVATARS_ENV: &'static str = "AVATARS_FOLDER"; +// Sculptor update checker pub const SCULPTOR_VERSION: &'static str = env!("CARGO_PKG_VERSION"); pub const REPOSITORY: &'static str = "shiroyashik/sculptor"; +// reqwest parameters pub const USER_AGENT: &'static str = "reqwest"; pub const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); +// Figura update checker pub const FIGURA_RELEASES_URL: &'static str = "https://api.github.com/repos/figuramc/figura/releases"; -pub const FIGURA_DEFAULT_VERSION: &'static str = "0.1.4"; \ No newline at end of file +pub const FIGURA_DEFAULT_VERSION: &'static str = "0.1.4"; + +// Figura Assets +pub const FIGURA_ASSETS_ZIP_URL: &'static str = "https://github.com/FiguraMC/Assets/archive/refs/heads/main.zip"; +pub const FIGURA_ASSETS_COMMIT_URL: &'static str = "https://api.github.com/repos/FiguraMC/Assets/commits/main"; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 5bf69cd..b933592 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,10 +5,9 @@ use axum::{ use dashmap::DashMap; use tracing_panic::panic_hook; use tracing_subscriber::{fmt::{self, time::ChronoLocal}, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; -use std::{path::PathBuf, sync::Arc}; +use std::{path::PathBuf, sync::Arc, env::{set_var, var}}; use tokio::{fs, sync::{broadcast, mpsc, RwLock}, time::Instant}; use tower_http::trace::TraceLayer; -use tracing::info; use uuid::Uuid; // Consts @@ -35,7 +34,7 @@ use state::Config; // Utils mod utils; -use utils::{check_updates, get_log_file, update_advanced_users, update_bans_from_minecraft, FiguraVersions}; +use utils::{check_updates, download_assets, get_commit_sha, get_log_file, get_path_to_assets_hash, is_assets_outdated, remove_assets, update_advanced_users, update_bans_from_minecraft, write_sha_to_file, FiguraVersions}; #[derive(Debug, Clone)] pub struct AppState { @@ -49,19 +48,25 @@ pub struct AppState { broadcasts: Arc>>>, /// Current configuration config: Arc>, - /// Figura Versions + /// Caching Figura Versions figura_versions: Arc>>, } +fn apply_default_environment() { + if var(LOGGER_ENV).is_err() { set_var(LOGGER_ENV, "info") }; + if var(CONFIG_ENV).is_err() { set_var(CONFIG_ENV, "Config.toml") }; + if var(LOGS_ENV).is_err() { set_var(LOGS_ENV, "logs") }; + if var(ASSETS_ENV).is_err() { set_var(ASSETS_ENV, "data/assets") }; + if var(AVATARS_ENV).is_err() { set_var(ASSETS_ENV, "data/avatars") }; +} + #[tokio::main] async fn main() -> Result<()> { let _ = dotenvy::dotenv(); + apply_default_environment(); // "trace,axum=info,tower_http=info,tokio=info,tungstenite=info,tokio_tungstenite=info", - let logger_env = std::env::var(LOGGER_ENV).unwrap_or_else(|_| "info".into()); - let config_file = std::env::var(CONFIG_ENV).unwrap_or_else(|_| "Config.toml".into()); - let logs_folder = std::env::var(LOGS_ENV).unwrap_or_else(|_| "logs".into()); - let file_appender = tracing_appender::rolling::never(&logs_folder, get_log_file(&logs_folder)); + let file_appender = tracing_appender::rolling::never(&var(LOGS_ENV).unwrap(), get_log_file(&var(LOGS_ENV).unwrap())); let timer = ChronoLocal::new(String::from("%Y-%m-%dT%H:%M:%S%.3f%:z")); let file_layer = fmt::layer() @@ -79,7 +84,7 @@ async fn main() -> Result<()> { // Combine the layers and set the global subscriber tracing_subscriber::registry() - .with(EnvFilter::from(logger_env)) + .with(EnvFilter::from(var(LOGGER_ENV).unwrap())) .with(file_layer) .with(terminal_layer) .init(); @@ -91,20 +96,46 @@ async fn main() -> Result<()> { prev_hook(panic_info); })); - info!("The Sculptor v{}{}", SCULPTOR_VERSION, check_updates(REPOSITORY, &SCULPTOR_VERSION).await?); + tracing::info!("The Sculptor v{}{}", SCULPTOR_VERSION, check_updates(REPOSITORY, &SCULPTOR_VERSION).await?); + // Preparing for launch { - let path = PathBuf::from("avatars"); + let path = PathBuf::from(var(AVATARS_ENV).unwrap()); if !path.exists() { - fs::create_dir(path).await.expect("Can't create avatars folder!"); - info!("Created avatars directory"); + fs::create_dir_all(path).await.expect("Can't create avatars folder!"); + tracing::info!("Created avatars directory"); } } // Config - let config = Arc::new(RwLock::new(Config::parse(config_file.clone().into()))); + let config = Arc::new(RwLock::new(Config::parse(var(CONFIG_ENV).unwrap().into()))); let listen = config.read().await.listen.clone(); + if config.read().await.assets_updater_enabled { + // Force update assets if folder or hash file doesn't exists. + if !(PathBuf::from(var(ASSETS_ENV).unwrap()).is_dir() && get_path_to_assets_hash().is_file()) { + tracing::debug!("Removing broken assets..."); + remove_assets().await + } + match get_commit_sha(FIGURA_ASSETS_COMMIT_URL).await { + Ok(sha) => { + if is_assets_outdated(&sha).await.unwrap_or_else(|e| {tracing::error!("Can't check assets state due: {:?}", e); false}) { + remove_assets().await; + match tokio::task::spawn_blocking(|| { download_assets() }).await.unwrap() { + Err(e) => tracing::error!("Assets outdated! Can't download new version due: {:?}", e), + Ok(_) => { + match write_sha_to_file(&sha).await { + Ok(_) => tracing::info!("Assets successfully updated!"), + Err(e) => tracing::error!("Assets successfully updated! Can't create assets hash file due: {:?}", e), + } + } + }; + } else { tracing::info!("Assets are up to date!") } + }, + Err(e) => tracing::error!("Can't get assets last commit! Assets update check aborted due {:?}", e) + } + } + // State let state = AppState { uptime: Instant::now(), @@ -122,11 +153,11 @@ async fn main() -> Result<()> { tokio::spawn(async move { loop { tokio::time::sleep(std::time::Duration::from_secs(10)).await; - let new_config = Config::parse(config_file.clone().into()); + let new_config = Config::parse(var(CONFIG_ENV).unwrap().into()); let mut config = config_update.write().await; if new_config != *config { - info!("Server configuration modification detected!"); + tracing::info!("Server configuration modification detected!"); *config = new_config; update_advanced_users(&config.advanced_users.clone(), &user_manager); } @@ -161,11 +192,11 @@ async fn main() -> Result<()> { .route("/health", get(|| async { "ok" })); let listener = tokio::net::TcpListener::bind(listen).await?; - info!("Listening on {}", listener.local_addr()?); + tracing::info!("Listening on {}", listener.local_addr()?); axum::serve(listener, app) .with_graceful_shutdown(shutdown_signal()) .await?; - info!("Serve stopped. Closing..."); + tracing::info!("Serve stopped. Closing..."); Ok(()) } @@ -187,11 +218,11 @@ async fn shutdown_signal() { tokio::select! { () = ctrl_c => { println!(); - info!("Ctrl+C signal received"); + tracing::info!("Ctrl+C signal received"); }, () = terminate => { println!(); - info!("Terminate signal received"); + tracing::info!("Terminate signal received"); }, } } diff --git a/src/state/config.rs b/src/state/config.rs index 0f73880..1344c41 100644 --- a/src/state/config.rs +++ b/src/state/config.rs @@ -11,6 +11,7 @@ use crate::auth::{default_authproviders, AuthProviders, Userinfo}; pub struct Config { pub listen: String, pub token: Option, + pub assets_updater_enabled: bool, // FIXME: IN DEV BRANCH ONLY pub motd: CMotd, #[serde(default = "default_authproviders")] pub auth_providers: AuthProviders, @@ -78,7 +79,7 @@ impl Config { let mut data = String::new(); file.read_to_string(&mut data).unwrap(); - toml::from_str(&data).unwrap() + toml::from_str(&data).unwrap_or_else(|err| {tracing::error!("{err:#?}"); panic!("Panic occured! See log messages!")}) } pub fn verify_token(&self, suspicious: &str) -> crate::ApiResult<()> { diff --git a/src/utils/check_updates.rs b/src/utils/check_updates.rs index fcabf61..bb713ae 100644 --- a/src/utils/check_updates.rs +++ b/src/utils/check_updates.rs @@ -1,10 +1,13 @@ +use std::{env::{self, var}, path::{self, PathBuf}}; + use anyhow::anyhow; use reqwest::Client; use semver::Version; use serde::{Deserialize, Serialize}; +use tokio::{fs::{self, File}, io::{AsyncReadExt as _, AsyncWriteExt as _}}; use tracing::error; -use crate::{FIGURA_RELEASES_URL, TIMEOUT, USER_AGENT}; +use crate::{ASSETS_ENV, FIGURA_ASSETS_ZIP_URL, FIGURA_RELEASES_URL, TIMEOUT, USER_AGENT}; #[derive(Deserialize, Debug)] struct Tag { @@ -103,4 +106,137 @@ pub async fn get_figura_versions() -> anyhow::Result { pub struct FiguraVersions { pub release: String, pub prerelease: String +} + +// Assets + +#[derive(Deserialize, Debug)] +struct Commit { + sha: String +} + +pub fn get_path_to_assets_hash() -> PathBuf { + path::PathBuf::from(&env::var(ASSETS_ENV).unwrap()).join("..").join("assets_last_commit") +} + +pub async fn get_commit_sha(url: &str) -> anyhow::Result { + let client = Client::builder().timeout(TIMEOUT).user_agent(USER_AGENT).build().unwrap(); + let response: reqwest::Response = client.get(url).send().await?; + let commit: Commit = response.json().await?; + Ok(commit.sha) +} + +pub async fn is_assets_outdated(last_sha: &str) -> anyhow::Result { + let path = get_path_to_assets_hash(); + + match File::open(path.clone()).await { + Ok(mut file) => { + let mut contents = String::new(); + file.read_to_string(&mut contents).await?; + if contents.lines().count() != 1 { + // Lines count in file abnormal + Ok(true) + } else { + if contents == last_sha { + Ok(false) + } else { + // SHA in file mismatches with provided SHA + Ok(true) + } + } + }, + Err(err) => if err.kind() == tokio::io::ErrorKind::NotFound { + // Can't find file + Ok(true) + } else { + anyhow::bail!("{:?}", err); + } + } +} + +pub fn download_assets() -> anyhow::Result<()> { + use std::{fs::{File, self}, io::Write as _}; + + let assets_folder = var(ASSETS_ENV).unwrap(); + + // Path to save the downloaded ZIP file + let zip_file_path = path::PathBuf::from(&assets_folder).join("..").join("assets.zip"); + + // Download the ZIP file + + let response = reqwest::blocking::get(FIGURA_ASSETS_ZIP_URL)?; + let bytes = response.bytes()?; + + // Save the downloaded ZIP file to disk + let mut file = File::create(&zip_file_path)?; + file.write_all(&bytes)?; + + // Open the downloaded ZIP file + let file = File::open(&zip_file_path)?; + + let mut archive = zip::ZipArchive::new(file)?; + let mut extraction_info = String::from("Extraction complete! More info:\n"); + let mut first_folder = String::new(); + + for i in 0..archive.len() { + let mut file = archive.by_index(i)?; + let zipoutpath = match file.enclosed_name() { + Some(path) => path, + None => continue, + }; + + // Folder name spoofing + if i == 0 { + if file.is_dir() { + first_folder = zipoutpath.to_str().ok_or_else(|| anyhow::anyhow!("0 index doesn't contains path!"))?.to_string(); + } else { + anyhow::bail!("0 index is not a folder!") + } + } + let mut outpath = path::PathBuf::from(&assets_folder); + outpath.push(zipoutpath.strip_prefix(first_folder.clone())?); + // Spoof end + + { + let comment = file.comment(); + if !comment.is_empty() { + extraction_info.push_str(&format!("File {i} comment: {comment}\n")); + } + } + if file.is_dir() { + extraction_info.push_str(&format!("Dir {} extracted to \"{}\"\n", i, outpath.display())); + fs::create_dir_all(&outpath)?; + } else { + extraction_info.push_str(&format!( + "File {} extracted to \"{}\" ({} bytes)\n", + i, + outpath.display(), + file.size() + )); + if let Some(p) = outpath.parent() { + if !p.exists() { + fs::create_dir_all(p)?; + } + } + let mut outfile = fs::File::create(&outpath)?; + std::io::copy(&mut file, &mut outfile)?; + } + } + extraction_info.pop(); // Removes \n from end + tracing::debug!("{extraction_info}"); + Ok(()) +} + +pub async fn write_sha_to_file(sha: &str) -> anyhow::Result<()> { + let path = get_path_to_assets_hash(); + + let mut file = File::create(path).await?; + file.write_all(sha.as_bytes()).await?; + file.flush().await?; + Ok(()) +} + +pub async fn remove_assets() { + fs::remove_dir_all(&var(ASSETS_ENV).unwrap()).await.unwrap_or_else(|err| tracing::debug!("Assets dir remove failed due {err:?}")); + fs::remove_file(get_path_to_assets_hash()).await.unwrap_or_else(|err| tracing::debug!("Assets hash file remove failed due {err:?}")); } \ No newline at end of file