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
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "common"
version = "0.1.0"
edition = "2024"
[dependencies]
tokio = { workspace = true, features = ["io-util", "fs"] }
anyhow = { workspace = true }
h2 = { workspace = true }
bytes = { workspace = true }
serde = { workspace = true, features = ["derive"] }
+19
View File
@@ -0,0 +1,19 @@
use crate::updater::DownloadTask;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncResponse {
pub id: String,
pub tasks: Vec<DownloadTask>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DownloadRequest {
pub id: String,
pub task: DownloadTask,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CloseRequest {
pub id: String,
}
+3
View File
@@ -0,0 +1,3 @@
pub mod stream;
pub mod updater;
pub mod http;
+85
View File
@@ -0,0 +1,85 @@
use bytes::{BufMut, Bytes, BytesMut};
use h2::SendStream;
use std::path::Path;
use tokio::fs::File;
use tokio::io::{AsyncRead, AsyncReadExt};
pub async fn server_send_files<P: AsRef<Path>>(
mut send_stream: SendStream<Bytes>,
base: P,
files: &[P],
) -> Result<(), Box<dyn std::error::Error>> {
let mut header = BytesMut::with_capacity(4);
header.put_u32(files.len() as u32);
send_stream.send_data(header.freeze(), false)?;
for (i, path_ref) in files.iter().enumerate() {
let path = path_ref.as_ref();
let mut file = File::open(path).await?;
let metadata = file.metadata().await?;
let file_name = path
.strip_prefix(base.as_ref())
.map(|n| n.to_string_lossy().to_string())?;
let name_bytes = file_name.as_bytes();
let file_size = metadata.len();
let mut meta_buf = BytesMut::with_capacity(2 + name_bytes.len() + 8);
meta_buf.put_u16(name_bytes.len() as u16);
meta_buf.put_slice(name_bytes);
meta_buf.put_u64(file_size);
send_stream.send_data(meta_buf.freeze(), false)?;
let mut buffer = vec![0u8; 8192];
let mut sent_size = 0u64;
while sent_size < file_size {
let n = file.read(&mut buffer).await?;
if n == 0 {
break;
}
let chunk = Bytes::copy_from_slice(&buffer[..n]);
let is_last_chunk = (i == files.len() - 1) && (sent_size + n as u64 == file_size);
send_stream.send_data(chunk, is_last_chunk)?;
sent_size += n as u64;
}
}
Ok(())
}
pub async fn client_receive(
mut response_body_reader: impl AsyncRead + Unpin,
file_root: &Path,
) -> anyhow::Result<()> {
let file_count = response_body_reader.read_u32().await?;
for _ in 0..file_count {
let name_len = response_body_reader.read_u16().await?;
let mut name_buf = vec![0u8; name_len as usize];
response_body_reader.read_exact(&mut name_buf).await?;
let file_name = String::from_utf8(name_buf)?;
let data_len = response_body_reader.read_u64().await?;
let path = file_root.join(&file_name);
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
let mut file = File::create(&path).await?;
let mut take = response_body_reader.take(data_len);
tokio::io::copy(&mut take, &mut file).await?;
response_body_reader = take.into_inner();
}
Ok(())
}
+165
View File
@@ -0,0 +1,165 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncContext {
pub region: String,
#[serde(default)]
pub filters: RegionFiltersConfig,
#[serde(default)]
pub export: RegionExportConfig,
#[serde(default)]
pub asset_version: Option<String>,
#[serde(default)]
pub asset_hash: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct RegionFiltersConfig {
#[serde(default)]
pub start_app: Vec<String>,
#[serde(default)]
pub on_demand: Vec<String>,
#[serde(default)]
pub skip: Vec<String>,
#[serde(default)]
pub file_ext: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct RegionExportConfig {
#[serde(default)]
pub by_category: bool,
#[serde(default)]
pub usm: UsmExportConfig,
#[serde(default)]
pub acb: AcbExportConfig,
#[serde(default)]
pub hca: HcaExportConfig,
#[serde(default)]
pub images: ImageExportConfig,
#[serde(default)]
pub video: VideoExportConfig,
#[serde(default)]
pub audio: AudioExportConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct UsmExportConfig {
pub export: bool,
pub decode: bool,
}
impl Default for UsmExportConfig {
fn default() -> Self {
Self {
export: true,
decode: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct AcbExportConfig {
pub export: bool,
pub decode: bool,
}
impl Default for AcbExportConfig {
fn default() -> Self {
Self {
export: true,
decode: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct HcaExportConfig {
pub decode: bool,
}
impl Default for HcaExportConfig {
fn default() -> Self {
Self { decode: true }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct ImageExportConfig {
pub convert_to_webp: bool,
pub remove_png: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct VideoExportConfig {
pub convert_to_mp4: bool,
pub direct_usm_to_mp4_with_ffmpeg: bool,
pub remove_m2v: bool,
}
impl Default for VideoExportConfig {
fn default() -> Self {
Self {
convert_to_mp4: true,
direct_usm_to_mp4_with_ffmpeg: false,
remove_m2v: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct AudioExportConfig {
pub convert_to_mp3: bool,
pub convert_to_flac: bool,
pub remove_wav: bool,
}
impl Default for AudioExportConfig {
fn default() -> Self {
Self {
convert_to_mp3: true,
convert_to_flac: false,
remove_wav: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DownloadTask {
pub download_path: String,
pub bundle_path: String,
pub bundle_hash: String,
pub category: AssetCategory,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub enum AssetCategory {
StartApp,
OnDemand,
Other(String),
}
impl<'de> Deserialize<'de> for AssetCategory {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
// Treat nil/null as Other("") — matches Go's zero-value coercion.
let raw = Option::<String>::deserialize(deserializer)?.unwrap_or_default();
Ok(match raw.as_str() {
"StartApp" | "startApp" => Self::StartApp,
"OnDemand" | "onDemand" => Self::OnDemand,
other => Self::Other(other.to_string()),
})
}
}