mirror of
https://github.com/Bluemangoo/sekai-unpacker.git
synced 2026-09-20 00:06:52 +08:00
Initial Commit
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
use assets_updater::core::config::AppConfig;
|
||||
use communicator::ConnectConfig;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct ServerConfig {
|
||||
pub log_level: Option<String>,
|
||||
#[serde(flatten, default)]
|
||||
pub connect: ConnectConfig,
|
||||
#[serde(flatten, default)]
|
||||
pub updater_config: AppConfig,
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
mod config;
|
||||
mod router;
|
||||
mod session;
|
||||
|
||||
use crate::config::ServerConfig;
|
||||
use crate::router::build_routers;
|
||||
use crate::session::SessionStore;
|
||||
use assets_updater::core::asset_execution::AssetExecutionContext;
|
||||
use communicator::http::Server;
|
||||
use communicator::{Identity, TunnelEndpoint, TunnelListener, connect_tunnel};
|
||||
use lazy_static::lazy_static;
|
||||
use log::{LevelFilter, error, info};
|
||||
use moka::future::Cache;
|
||||
use simplelog::{ColorChoice, Config, TermLogger, TerminalMode};
|
||||
use std::fs;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let log_level = LevelFilter::from_str(
|
||||
CONFIG
|
||||
.log_level
|
||||
.clone()
|
||||
.unwrap_or("INFO".to_string())
|
||||
.as_str(),
|
||||
)
|
||||
.unwrap_or(LevelFilter::Info);
|
||||
|
||||
TermLogger::init(
|
||||
log_level,
|
||||
Config::default(),
|
||||
TerminalMode::Mixed,
|
||||
ColorChoice::Auto,
|
||||
)?;
|
||||
|
||||
let routers = Arc::new(build_routers());
|
||||
let http_server = Arc::new(Server::new(routers.clone()));
|
||||
|
||||
let mut tasks = vec![];
|
||||
|
||||
for server_conf in &CONFIG.connect.server {
|
||||
let server_conf = server_conf.clone().into_tunnel_config(Identity::Server)?;
|
||||
let url = server_conf.url.clone();
|
||||
let server = TunnelListener::bind(server_conf).await?;
|
||||
info!("tcp server started on {}", url);
|
||||
let http_server = http_server.clone();
|
||||
tasks.push(tokio::task::spawn(async move {
|
||||
loop {
|
||||
let endpoint = server
|
||||
.accept()
|
||||
.await
|
||||
.map_err(|e| error!("Failed to accept connection: {}", e));
|
||||
let endpoint = if let Ok(endpoint) = endpoint {
|
||||
endpoint
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
if let TunnelEndpoint::Server(connection) = endpoint {
|
||||
let result = http_server.on_conn(connection).await;
|
||||
if let Err(e) = result {
|
||||
error!("Failed to handle connection: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
for client_conf in &CONFIG.connect.client {
|
||||
let client_conf = client_conf.clone().into_tunnel_config(Identity::Server);
|
||||
let http_server = http_server.clone();
|
||||
info!("tcp client started for {}", client_conf.url);
|
||||
tasks.push(tokio::task::spawn(async move {
|
||||
loop {
|
||||
let endpoint = connect_tunnel(client_conf.clone())
|
||||
.await
|
||||
.map_err(|e| error!("Failed to accept connection: {}", e));
|
||||
let endpoint = if let Ok(endpoint) = endpoint {
|
||||
endpoint
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
if let TunnelEndpoint::Server(connection) = endpoint {
|
||||
let result = http_server.on_conn(connection).await;
|
||||
if let Err(e) = result {
|
||||
error!("Failed to handle connection: {}", e);
|
||||
}
|
||||
}
|
||||
sleep(Duration::from_secs(10)).await;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
for task in tasks {
|
||||
let _ = task.await.map_err(|e| error!("{}", e));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref SESSION_STORE: SessionStore<AssetExecutionContext> = SessionStore::new(
|
||||
Cache::builder()
|
||||
.time_to_idle(Duration::from_hours(3))
|
||||
.max_capacity(10000),
|
||||
);
|
||||
pub static ref CONFIG: ServerConfig = {
|
||||
let raw = fs::read_to_string("sekai-unpacker-server.yaml").unwrap();
|
||||
let config: ServerConfig = yaml_serde::from_str(raw.as_str()).unwrap();
|
||||
config.updater_config.validate().unwrap();
|
||||
config
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
use crate::SESSION_STORE;
|
||||
use bytes::Bytes;
|
||||
use common::http::CloseRequest;
|
||||
use communicator::http::{json_from_request, send, send_error};
|
||||
use h2::RecvStream;
|
||||
use h2::server::SendResponse;
|
||||
use http::Request;
|
||||
|
||||
pub async fn close(
|
||||
mut request: Request<RecvStream>,
|
||||
send_response: SendResponse<Bytes>,
|
||||
) -> Result<(), h2::Error> {
|
||||
let body = json_from_request(&mut request).await;
|
||||
if let Err(error) = body {
|
||||
send_error(send_response, error);
|
||||
return Ok(());
|
||||
}
|
||||
let req_body: CloseRequest = body.unwrap();
|
||||
let context = SESSION_STORE.remove(&req_body.id).await;
|
||||
match context {
|
||||
Some(_) => {
|
||||
send(
|
||||
send_response,
|
||||
200,
|
||||
"application/json",
|
||||
serde_json::json!({
|
||||
"msg": "OK"
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
None => {
|
||||
send(
|
||||
send_response,
|
||||
500,
|
||||
"application/json",
|
||||
serde_json::json!({
|
||||
"msg": "invalid session id"
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
use crate::{CONFIG, SESSION_STORE};
|
||||
use assets_updater::core::export_pipeline::{find_files, find_files_by_extensions};
|
||||
use bytes::Bytes;
|
||||
use common::http::DownloadRequest;
|
||||
use common::stream::server_send_files;
|
||||
use communicator::http::{json_from_request, send, send_error};
|
||||
use h2::RecvStream;
|
||||
use h2::server::SendResponse;
|
||||
use http::{Request, Response};
|
||||
|
||||
pub async fn download(
|
||||
mut request: Request<RecvStream>,
|
||||
mut send_response: SendResponse<Bytes>,
|
||||
) -> Result<(), h2::Error> {
|
||||
let body = json_from_request(&mut request).await;
|
||||
if let Err(error) = body {
|
||||
send_error(send_response, error);
|
||||
return Ok(());
|
||||
}
|
||||
let req_body: DownloadRequest = body.unwrap();
|
||||
|
||||
let id = req_body.id;
|
||||
let context = SESSION_STORE.get(&id).await;
|
||||
if context.is_none() {
|
||||
send(
|
||||
send_response,
|
||||
200,
|
||||
"application/json",
|
||||
serde_json::json!({
|
||||
"msg": "invalid session id"
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
let context = context.unwrap();
|
||||
let dir = context
|
||||
.download(&req_body.task, &CONFIG.updater_config)
|
||||
.await;
|
||||
if let Err(error) = dir {
|
||||
send_error(send_response, error.into());
|
||||
return Ok(());
|
||||
}
|
||||
let dir = dir.unwrap();
|
||||
|
||||
let files = if !dir.is_dir() {
|
||||
if context.sync_context.filters.file_ext.is_empty()
|
||||
|| dir.extension().is_some_and(|t| {
|
||||
context
|
||||
.sync_context
|
||||
.filters
|
||||
.file_ext
|
||||
.contains(&t.to_str().unwrap().to_lowercase())
|
||||
})
|
||||
{
|
||||
vec![dir.clone()]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
} else {
|
||||
let files = if context.sync_context.filters.file_ext.is_empty() {
|
||||
find_files(&dir)
|
||||
} else {
|
||||
find_files_by_extensions(&dir, &context.sync_context.filters.file_ext)
|
||||
};
|
||||
if let Err(error) = files {
|
||||
send_error(send_response, error.into());
|
||||
return Ok(());
|
||||
}
|
||||
files.unwrap()
|
||||
};
|
||||
|
||||
let response = Response::builder()
|
||||
.status(200)
|
||||
.header("content-type", "application/x-sekai-stream")
|
||||
.body(())
|
||||
.unwrap();
|
||||
|
||||
let dir_base = std::env::temp_dir()
|
||||
.join("sekai-updater")
|
||||
.join("extract")
|
||||
.join(&context.sync_context.region);
|
||||
|
||||
if let Ok(send_stream) = send_response.send_response(response, false) {
|
||||
let _ = server_send_files(send_stream, dir_base, &files).await;
|
||||
}
|
||||
|
||||
let _ = std::fs::remove_file(dir);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
mod close;
|
||||
mod download;
|
||||
mod sync;
|
||||
|
||||
use crate::router::close::close;
|
||||
use crate::router::download::download;
|
||||
use crate::router::sync::sync_route;
|
||||
use communicator::http::Router;
|
||||
|
||||
pub fn build_routers() -> Router {
|
||||
let mut router = Router::new();
|
||||
|
||||
router.add_route("/sync", sync_route);
|
||||
router.add_route("/download", download);
|
||||
router.add_route("/close", close);
|
||||
|
||||
router
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
use crate::{CONFIG, SESSION_STORE};
|
||||
use assets_updater::core::asset_execution::{
|
||||
should_download_bundle, AssetExecutionContext,
|
||||
};
|
||||
use assets_updater::core::regions::select_region;
|
||||
use bytes::Bytes;
|
||||
use common::http::SyncResponse;
|
||||
use common::updater::SyncContext;
|
||||
use communicator::http::{json_from_request, send, send_error};
|
||||
use h2::server::SendResponse;
|
||||
use h2::RecvStream;
|
||||
use http::Request;
|
||||
|
||||
pub async fn sync_route(
|
||||
mut request: Request<RecvStream>,
|
||||
send_response: SendResponse<Bytes>,
|
||||
) -> Result<(), h2::Error> {
|
||||
let body = json_from_request(&mut request).await;
|
||||
if let Err(error) = body {
|
||||
send_error(send_response, error);
|
||||
return Ok(());
|
||||
}
|
||||
let sync_context: SyncContext = body.unwrap();
|
||||
|
||||
let region = select_region(&CONFIG.updater_config, &sync_context.region);
|
||||
if let Err(error) = region {
|
||||
send_error(send_response, error.into());
|
||||
return Ok(());
|
||||
}
|
||||
let region = region.unwrap();
|
||||
let exec = AssetExecutionContext::new(&CONFIG.updater_config, &sync_context, region);
|
||||
if let Err(error) = exec {
|
||||
send_error(send_response, error.into());
|
||||
return Ok(());
|
||||
}
|
||||
let mut exec = exec.unwrap();
|
||||
let tasks = exec.fetch_tasks().await;
|
||||
if let Err(error) = tasks {
|
||||
send_error(send_response, error.into());
|
||||
return Ok(());
|
||||
}
|
||||
let tasks = tasks
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter(|task| should_download_bundle(&sync_context, &task.download_path, &task.category))
|
||||
.collect::<Vec<_>>();
|
||||
let id = SESSION_STORE.put(exec).await;
|
||||
|
||||
let resp = serde_json::to_string(&SyncResponse { id, tasks }).unwrap();
|
||||
|
||||
send(send_response, 200, "application/json", resp);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
use moka::future::{Cache, CacheBuilder};
|
||||
use std::hash::RandomState;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct SessionStore<T> {
|
||||
cache: Cache<String, T>,
|
||||
}
|
||||
|
||||
impl<T> SessionStore<T>
|
||||
where
|
||||
T: Clone + Send + Sync + 'static,
|
||||
{
|
||||
pub fn new(builder: CacheBuilder<String, T, Cache<String, T, RandomState>>) -> Self {
|
||||
let cache = builder.build();
|
||||
Self { cache }
|
||||
}
|
||||
|
||||
pub async fn put(&self, data: T) -> String {
|
||||
let id = Uuid::new_v4().to_string(); // 生成唯一的 UUID
|
||||
self.cache.insert(id.clone(), data).await;
|
||||
id
|
||||
}
|
||||
|
||||
pub async fn get(&self, id: &str) -> Option<T> {
|
||||
self.cache.get(id).await
|
||||
}
|
||||
|
||||
pub async fn remove(&self, id: &str) -> Option<T> {
|
||||
self.cache.remove(id).await
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user