Initial Commit

This commit is contained in:
2026-04-10 15:23:22 +08:00
commit 7accb43048
40 changed files with 7950 additions and 0 deletions
+12
View File
@@ -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,
}
+115
View File
@@ -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
};
}
+46
View File
@@ -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(())
}
+91
View File
@@ -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(())
}
+18
View File
@@ -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
}
+55
View File
@@ -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(())
}
+31
View File
@@ -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
}
}