1
0
mirror of https://github.com/Bluemangoo/sekai-unpacker.git synced 2026-05-06 20:44:47 +08:00

Compare commits

...

2 Commits

Author SHA1 Message Date
2f1c7eb9d8
fix tons of bugs 2026-04-17 18:56:56 +08:00
d83d611ab8
dynamic config 2026-04-17 16:59:45 +08:00
15 changed files with 206 additions and 109 deletions

12
Cargo.lock generated
View File

@ -122,6 +122,7 @@ dependencies = [
"tempfile", "tempfile",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"twox-hash",
"yaml_serde", "yaml_serde",
] ]
@ -388,9 +389,11 @@ dependencies = [
"bytes", "bytes",
"common", "common",
"communicator", "communicator",
"futures-util",
"h2", "h2",
"lazy_static", "lazy_static",
"log", "log",
"reqwest",
"serde", "serde",
"serde_json", "serde_json",
"simplelog", "simplelog",
@ -2795,6 +2798,15 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "twox-hash"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c"
dependencies = [
"rand",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.19.0" version = "1.19.0"

View File

@ -34,3 +34,4 @@ lazy_static = "1.5.0"
structopt = "0.3.26" structopt = "0.3.26"
tokio-util = "0.7.18" tokio-util = "0.7.18"
futures-util = "0.3.32" futures-util = "0.3.32"
twox-hash = "2.1.2"

View File

@ -22,3 +22,4 @@ cridecoder = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
log = { workspace = true } log = { workspace = true }
cipher = { workspace = true, features = ["block-padding"] } cipher = { workspace = true, features = ["block-padding"] }
twox-hash = { workspace = true, features = ["xxhash3_128"] }

View File

@ -262,13 +262,17 @@ impl AssetExecutionContext {
} }
RegionProviderConfig::Nuverse { RegionProviderConfig::Nuverse {
asset_version_url, asset_version_url,
app_version,
asset_info_url_template, asset_info_url_template,
.. ..
} => { } => {
// For nuverse, always fetch the version from asset_version_url. // For nuverse, always fetch the version from asset_version_url.
// The incoming request.asset_version is intentionally ignored here // The incoming request.asset_version is intentionally ignored here
// to match Go reference behavior. // to match Go reference behavior.
let app_version = self.sync_context.app_version.as_ref().ok_or_else(|| {
AssetExecutionError::MissingAppVersion {
region: self.region_name.clone(),
}
})?;
let version_url = asset_version_url.replace("{app_version}", app_version); let version_url = asset_version_url.replace("{app_version}", app_version);
let resolved_version = let resolved_version =
String::from_utf8_lossy(&self.get_with_retry(&version_url).await?) String::from_utf8_lossy(&self.get_with_retry(&version_url).await?)
@ -319,9 +323,13 @@ impl AssetExecutionContext {
} }
RegionProviderConfig::Nuverse { RegionProviderConfig::Nuverse {
asset_bundle_url_template, asset_bundle_url_template,
app_version,
.. ..
} => { } => {
let app_version = self.sync_context.app_version.as_ref().ok_or_else(|| {
AssetExecutionError::MissingAppVersion {
region: self.region_name.clone(),
}
})?;
let asset_version = self let asset_version = self
.resolved_asset_version .resolved_asset_version
.as_deref() .as_deref()
@ -435,6 +443,7 @@ impl AssetExecutionContext {
&self.region, &self.region,
&temp_file, &temp_file,
&task.bundle_path, &task.bundle_path,
&task.download_path,
category, category,
) )
.await; .await;

View File

@ -150,7 +150,6 @@ pub enum RegionProviderConfig {
}, },
Nuverse { Nuverse {
asset_version_url: String, asset_version_url: String,
app_version: String,
asset_info_url_template: String, asset_info_url_template: String,
asset_bundle_url_template: String, asset_bundle_url_template: String,
#[serde(default)] #[serde(default)]

View File

@ -107,6 +107,8 @@ pub enum AssetExecutionError {
HttpStatus { url: String, status: u16 }, HttpStatus { url: String, status: u16 },
#[error("region `{region}` is missing asset_save_dir")] #[error("region `{region}` is missing asset_save_dir")]
MissingAssetSaveDir { region: String }, MissingAssetSaveDir { region: String },
#[error("nuverse region `{region}` requires asset_version and asset_hash")]
MissingAppVersion { region: String },
#[error("colorful_palette region `{region}` requires asset_version and asset_hash")] #[error("colorful_palette region `{region}` requires asset_version and asset_hash")]
MissingAssetVersionOrHash { region: String }, MissingAssetVersionOrHash { region: String },
#[error("colorful_palette region `{region}` is missing profile hash for `{profile}`")] #[error("colorful_palette region `{region}` is missing profile hash for `{profile}`")]

View File

@ -54,18 +54,26 @@ pub fn get_export_group(export_path: &str) -> &'static str {
"container" "container"
} }
pub fn get_hex_index(input: &str) -> String {
let hash_val = twox_hash::XxHash3_128::oneshot(input.as_bytes());
format!("{:032x}", hash_val)
}
pub async fn extract_unity_asset_bundle( pub async fn extract_unity_asset_bundle(
app_config: &AppConfig, app_config: &AppConfig,
sync_context: &SyncContext, sync_context: &SyncContext,
region: &RegionConfig, region: &RegionConfig,
asset_bundle_file: &Path, asset_bundle_file: &Path,
export_path: &str, export_path: &str,
download_path: &str,
category: &str, category: &str,
) -> Result<(PathBuf, bool), ExportPipelineError> { ) -> Result<(PathBuf, bool), ExportPipelineError> {
let hash = get_hex_index(export_path);
let output_dir = std::env::temp_dir() let output_dir = std::env::temp_dir()
.join("sekai-updater") .join("sekai-updater")
.join("extract") .join("extract")
.join(&sync_context.region); .join(&sync_context.region)
.join(hash);
let Some(asset_studio_cli_path) = app_config.tools.asset_studio_cli_path.as_deref() else { let Some(asset_studio_cli_path) = app_config.tools.asset_studio_cli_path.as_deref() else {
return Ok((asset_bundle_file.parent().unwrap().to_path_buf(), true)); return Ok((asset_bundle_file.parent().unwrap().to_path_buf(), true));
}; };
@ -82,7 +90,7 @@ pub async fn extract_unity_asset_bundle(
}; };
let actual_export_path = if sync_context.export.by_category { let actual_export_path = if sync_context.export.by_category {
output_dir.join(category.to_lowercase()).join(export_path) output_dir.join(category.to_lowercase())
} else { } else {
output_dir.join(export_path) output_dir.join(export_path)
}; };
@ -126,10 +134,23 @@ pub async fn extract_unity_asset_bundle(
.await?; .await?;
let mut count = 0; let mut count = 0;
walk(&actual_export_path, &mut |_| count += 1)?; let mut is_bundle_name_file = true;
let download_name = match download_path.rsplit_once("/") {
None => download_path,
Some((_, name)) => name,
};
walk(&output_dir, &mut |f| {
count += 1;
let file_name = f.file_name().unwrap().to_string_lossy();
is_bundle_name_file = is_bundle_name_file
&& match file_name.rsplit_once(".") {
Some((name, _)) => name.eq_ignore_ascii_case(download_name),
None => file_name.eq_ignore_ascii_case(download_name),
}
})?;
post_process_exported_files(app_config, sync_context, region, &actual_export_path).await?; post_process_exported_files(app_config, sync_context, region, &output_dir).await?;
Ok((actual_export_path, count <= 1)) Ok((output_dir, is_bundle_name_file && count <= 1))
} }
pub async fn post_process_exported_files( pub async fn post_process_exported_files(

View File

@ -19,3 +19,5 @@ anyhow = { workspace = true }
h2 = { workspace = true } h2 = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
tokio-util = { workspace = true } tokio-util = { workspace = true }
reqwest = { workspace = true }
futures-util = { workspace = true }

View File

@ -11,6 +11,12 @@ pub struct ClientConfig {
pub profiles: HashMap<String, Profile>, pub profiles: HashMap<String, Profile>,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DynamicLoad {
pub url: String,
pub map: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Profile { pub struct Profile {
#[serde(flatten)] #[serde(flatten)]
@ -18,4 +24,51 @@ pub struct Profile {
pub path: String, pub path: String,
pub interval: Option<u64>, pub interval: Option<u64>,
pub concurrent: Option<usize>, pub concurrent: Option<usize>,
dynamic_load: Option<DynamicLoad>,
}
impl Profile {
pub fn need_update(&self) -> bool {
self.dynamic_load.is_some()
}
pub async fn update(&mut self) -> anyhow::Result<()> {
let Some(load) = &self.dynamic_load else {
return Ok(());
};
let response = reqwest::get(&load.url).await?;
let body = response.text().await?;
let data: serde_json::Value = serde_json::from_str(&body)?;
if !data.is_object() {
return Err(anyhow::Error::msg("response is not an object"));
};
let obj = data.as_object().unwrap();
macro_rules! load_field {
(from $obj:expr; $($tail:tt)*) => {
load_field!(@munch $obj, $($tail)*);
};
(@munch $obj:expr, $field:ident$(.$f_more:ident)* = $key:ident as $t:ident $($rest:tt)*) => {
load_field!(@suffix $obj, ($field$(.$f_more)*), $key, $t, [], $($rest)*);
};
(@suffix $obj:expr, ($($f_full:tt)*), $key:ident, $t:ident, [$($acc:tt)*], ; $($rest:tt)*) => {
if let Some(k) = load.map.get(stringify!($key)) {
if let Some(serde_json::Value::$t(v)) = $obj.get(k) {
$($f_full)* = Some(v.clone() $($acc)*);
}
}
load_field!(@munch $obj, $($rest)*);
};
(@suffix $obj:expr, ($($f_full:tt)*), $key:ident, $t:ident, [$($acc:tt)*], $token:tt $($rest:tt)*) => {
load_field!(@suffix $obj, ($($f_full)*), $key, $t, [$($acc)* $token], $($rest)*);
};
(@munch $obj:expr, ) => {};
}
load_field! {
from obj;
self.sync_context.asset_version = asset_version as String;
self.sync_context.asset_hash = asset_hash as String;
self.concurrent = concurrent as Number.as_u64().ok_or(anyhow::Error::msg("concurrent is not usize"))? as usize;
}
Ok(())
}
} }

View File

@ -2,16 +2,17 @@ use crate::config::{ClientConfig, Profile};
use crate::task::run; use crate::task::run;
use common::strings::REGION_NOT_FOUND; use common::strings::REGION_NOT_FOUND;
use communicator::{ClientManager, Identity, TunnelEndpoint, TunnelListener, connect_tunnel}; use communicator::{ClientManager, Identity, TunnelEndpoint, TunnelListener, connect_tunnel};
use futures_util::future::join_all;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use log::{LevelFilter, error, info}; use log::{LevelFilter, error, info};
use simplelog::{ColorChoice, Config, TermLogger, TerminalMode}; use simplelog::{ColorChoice, Config, TermLogger, TerminalMode};
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
use std::fs; use std::fs;
use std::str::FromStr; use std::str::FromStr;
use std::sync::{Arc, RwLock}; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use structopt::StructOpt; use structopt::StructOpt;
use tokio::sync::{OwnedSemaphorePermit, Semaphore, mpsc}; use tokio::sync::{OwnedSemaphorePermit, RwLock, Semaphore, mpsc};
use tokio::task::JoinSet; use tokio::task::JoinSet;
use tokio::time::sleep; use tokio::time::sleep;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
@ -51,16 +52,22 @@ async fn main() -> anyhow::Result<()> {
std::process::exit(1); std::process::exit(1);
} }
let profiles = command_opts let profiles = join_all(command_opts.profile.iter().map(async |p| {
.profile
.iter()
.map(|p| {
let profile = CONFIG.profiles.get(p.as_str()); let profile = CONFIG.profiles.get(p.as_str());
match profile { match profile {
Some(profile) => Ok((p.to_string(), profile.clone())), Some(profile) => {
let mut profile = profile.clone();
if profile.need_update() {
info!("Loading async config of {}", p);
profile.update().await?;
}
Ok((p.to_string(), Arc::new(RwLock::new(profile))))
}
None => Err(anyhow::anyhow!("Profile `{}` not found in config", p)), None => Err(anyhow::anyhow!("Profile `{}` not found in config", p)),
} }
}) }))
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
let mut join_set = JoinSet::new(); let mut join_set = JoinSet::new();
@ -121,15 +128,25 @@ async fn main() -> anyhow::Result<()> {
let semaphore = Arc::new(Semaphore::new(1)); let semaphore = Arc::new(Semaphore::new(1));
let cancel_token = CancellationToken::new(); let cancel_token = CancellationToken::new();
let post_task = { let post_task = {
async |profile: Arc<(String, Profile)>, async |profile: Arc<(String, Arc<RwLock<Profile>>)>,
permit: OwnedSemaphorePermit, permit: OwnedSemaphorePermit,
cancel_token: CancellationToken| { cancel_token: CancellationToken| {
match profile.1.interval { let (interval, need_update) = {
let p = profile.1.read().await;
(p.interval, p.need_update())
};
match interval {
None => { None => {
cancel_token.cancel(); cancel_token.cancel();
} }
Some(interval) => { Some(interval) => {
sleep(Duration::from_secs(interval)).await; sleep(Duration::from_secs(interval)).await;
if need_update {
info!("Trying to update profile for {}", profile.0);
if let Err(e) = profile.1.write().await.update().await {
error!("Failed to update profile for {}: {}", profile.0, e);
}
}
} }
} }
drop(permit); drop(permit);
@ -263,13 +280,13 @@ async fn main() -> anyhow::Result<()> {
} }
struct Sender<T> { struct Sender<T> {
inner: RwLock<VecDeque<T>>, inner: std::sync::RwLock<VecDeque<T>>,
} }
impl<T> Sender<T> { impl<T> Sender<T> {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
inner: RwLock::new(VecDeque::new()), inner: std::sync::RwLock::new(VecDeque::new()),
} }
} }

View File

@ -6,27 +6,22 @@ use log::{error, info};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc};
use tokio::sync::{RwLock, Semaphore}; use tokio::sync::{RwLock, Semaphore};
use tokio::task::JoinSet; use tokio::task::JoinSet;
pub async fn run( pub async fn run(
client: Arc<ClientManager>, client: Arc<ClientManager>,
profile: Arc<(String, Profile)>, profile: Arc<(String, Arc<RwLock<Profile>>)>,
) -> anyhow::Result<bool> { ) -> anyhow::Result<bool> {
info!("[{}]: Starting sync", profile.0); info!("[{}]: Starting sync", profile.0);
tokio::fs::create_dir_all(&profile.1.path).await?; let p1 = Arc::new(profile.1.read().await.clone());
let sync_resp = sync(&mut client.get_client().await?, &profile.1).await?; tokio::fs::create_dir_all(&p1.path).await?;
let sync_resp = sync(&mut client.get_client().await?, &p1).await?;
let id = sync_resp.id; let id = sync_resp.id;
let local_manifest = Arc::new( let local_manifest = Arc::new(
AutoSaveManifest::new( AutoSaveManifest::new(5, Path::new(&p1.path).join("manifest.json").to_path_buf()).await?,
5,
Path::new(&profile.1.path)
.join("manifest.json")
.to_path_buf(),
)
.await?,
); );
let manifest_snapshot = { local_manifest.manifest.read().await.clone() }; let manifest_snapshot = { local_manifest.manifest.read().await.clone() };
let all_cnt = sync_resp.tasks.len(); let all_cnt = sync_resp.tasks.len();
@ -53,7 +48,7 @@ pub async fn run(
close(&mut client.get_client().await?, &req).await?; close(&mut client.get_client().await?, &req).await?;
return Ok(true); return Ok(true);
} }
let n = profile.1.concurrent.unwrap_or(5); let n = p1.concurrent.unwrap_or(5);
info!("[{}]: Start sync with {} thread", profile.0, n); info!("[{}]: Start sync with {} thread", profile.0, n);
let semaphore = Arc::new(Semaphore::new(n)); let semaphore = Arc::new(Semaphore::new(n));
let mut join_set = JoinSet::new(); let mut join_set = JoinSet::new();
@ -62,18 +57,18 @@ pub async fn run(
let client = client.clone(); let client = client.clone();
let id = id.clone(); let id = id.clone();
let local_manifest = local_manifest.clone(); let local_manifest = local_manifest.clone();
let profile = profile.clone(); let p1 = p1.clone();
join_set.spawn(async move { join_set.spawn(async move {
let req = DownloadRequest { let req = DownloadRequest {
id: id.clone(), id: id.clone(),
task: task.clone(), task: task.clone(),
}; };
let result = download(&mut client.get_client().await.unwrap(), &req, &profile.1).await; let result = download(&mut client.get_client().await.unwrap(), &req, &p1).await;
if let Err(e) = result if let Err(e) = result
&& let Some(_) = e.downcast_ref::<h2::Error>() && let Some(_) = e.downcast_ref::<h2::Error>()
{ {
download(&mut client.get_client().await.unwrap(), &req, &profile.1) download(&mut client.get_client().await.unwrap(), &req, &p1)
.await .await
.unwrap(); .unwrap();
} }

View File

@ -12,6 +12,8 @@ pub struct SyncContext {
#[serde(default)] #[serde(default)]
pub asset_hash: Option<String>, pub asset_hash: Option<String>,
#[serde(default)] #[serde(default)]
pub app_version: Option<String>,
#[serde(default)]
pub exact_single_file_bundle: bool, pub exact_single_file_bundle: bool,
} }

View File

@ -1,19 +1,20 @@
log_level: "DEBUG" log_level: "DEBUG"
#client: client:
# - url: "127.0.0.1:3333" - url: "127.0.0.1:3333"
# token: abc
server:
- url: 127.0.0.1:3333
token: abc token: abc
cert: "D:\\WorkDir\\Nginx\\cert\\_.bluemangoo.net\\_.bluemangoo.net-chain.pem" #server:
key: "D:\\WorkDir\\Nginx\\cert\\_.bluemangoo.net\\_.bluemangoo.net-key.pem" # - url: 127.0.0.1:3333
# token: abc
# cert: "D:\\WorkDir\\Nginx\\cert\\_.bluemangoo.net\\_.bluemangoo.net-chain.pem"
# key: "D:\\WorkDir\\Nginx\\cert\\_.bluemangoo.net\\_.bluemangoo.net-key.pem"
profiles: profiles:
cn: cn:
region: cn region: cn
# interval: 3 # seconds # interval: 3 # seconds
concurrent: 50 concurrent: 50
app_version: "6.0.0"
filters: filters:
start_app: start_app:
- "thumbnail" - "thumbnail"
@ -44,7 +45,7 @@ profiles:
convert_to_mp3: false convert_to_mp3: false
convert_to_flac: false convert_to_flac: false
remove_wav: false remove_wav: false
# exact_single_file_bundle: true exact_single_file_bundle: true
path: "./data/cn" path: "./data/cn"
jp: jp:
region: jp region: jp
@ -58,8 +59,11 @@ profiles:
- "stamp" - "stamp"
skip: [ ] skip: [ ]
file_ext: [ ] file_ext: [ ]
asset_version: 6.4.0.30 dynamic_load:
asset_hash: cce60d07-d60e-48dd-be22-12eac2e67950 url: "https://github.com/Team-Haruki/haruki-sekai-master/raw/refs/heads/main/versions/current_version.json"
map:
asset_version: assetVersion
asset_hash: assetHash
export: export:
by_category: false by_category: false
usm: usm:
@ -81,5 +85,5 @@ profiles:
convert_to_mp3: false convert_to_mp3: false
convert_to_flac: false convert_to_flac: false
remove_wav: false remove_wav: false
# exact_single_file_bundle: true exact_single_file_bundle: true
path: "./data/jp" path: "./data/jp"

View File

@ -1,12 +1,12 @@
log_level: "DEBUG" log_level: "DEBUG"
client: #client:
- url: "127.0.0.1:3333" # - url: "127.0.0.1:3333"
token: abc
host: "local.bluemangoo.net"
#server:
# - url: 127.0.0.1:3333
# token: abc # token: abc
# host: "local.bluemangoo.net"
server:
- url: 127.0.0.1:3333
token: abc
execution: execution:
proxy: "" proxy: ""
@ -49,7 +49,6 @@ regions:
provider: provider:
kind: nuverse kind: nuverse
asset_version_url: "https://lf3-mkcncdn-tos.dailygn.com/obj/rt-game-lf/gdl_app_5236/Mainland/{app_version}/Release/cn_online/ios/version" asset_version_url: "https://lf3-mkcncdn-tos.dailygn.com/obj/rt-game-lf/gdl_app_5236/Mainland/{app_version}/Release/cn_online/ios/version"
app_version: "5.2.0"
asset_info_url_template: "https://lf3-mkcncdn-tos.dailygn.com/obj/sf-game-lf/gdl_app_5236/AssetBundle/{app_version}/Release/cn_online/ios{asset_version}/AssetBundleInfoNew.json" asset_info_url_template: "https://lf3-mkcncdn-tos.dailygn.com/obj/sf-game-lf/gdl_app_5236/AssetBundle/{app_version}/Release/cn_online/ios{asset_version}/AssetBundleInfoNew.json"
asset_bundle_url_template: "https://lf3-mkcncdn-tos.dailygn.com/obj/sf-game-lf/gdl_app_5236/AssetBundle/{app_version}/Release/cn_online/{bundle_path}" asset_bundle_url_template: "https://lf3-mkcncdn-tos.dailygn.com/obj/sf-game-lf/gdl_app_5236/AssetBundle/{app_version}/Release/cn_online/{bundle_path}"
required_cookies: false required_cookies: false

View File

@ -7,6 +7,7 @@ use communicator::http::{json_from_request, send, send_error};
use h2::RecvStream; use h2::RecvStream;
use h2::server::SendResponse; use h2::server::SendResponse;
use http::{Request, Response}; use http::{Request, Response};
use log::debug;
pub async fn download( pub async fn download(
mut request: Request<RecvStream>, mut request: Request<RecvStream>,
@ -43,32 +44,6 @@ pub async fn download(
} }
let (dir, single_file) = dir.unwrap(); let (dir, single_file) = dir.unwrap();
let dir_base = std::env::temp_dir()
.join("sekai-updater")
.join("extract")
.join(&context.sync_context.region);
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(),
dir.strip_prefix(&dir_base)
.unwrap()
.to_string_lossy()
.to_string(),
)]
} else {
vec![]
}
} else {
let files = if context.sync_context.filters.file_ext.is_empty() { let files = if context.sync_context.filters.file_ext.is_empty() {
find_files(&dir) find_files(&dir)
} else { } else {
@ -78,23 +53,28 @@ pub async fn download(
send_error(send_response, error.into()); send_error(send_response, error.into());
return Ok(()); return Ok(());
} }
let base = dir.strip_prefix(&dir_base).unwrap(); let files = files
let base_str = if single_file {
base.parent().unwrap().to_str().unwrap()
} else {
base.to_str().unwrap()
};
files
.unwrap() .unwrap()
.iter() .iter()
.map(|p| { .map(|p| {
let raw = p.clone(); let raw = p.clone();
let name = p.strip_prefix(&dir).unwrap(); let name = p.strip_prefix(&dir).unwrap();
let path = format!("{}/{}", base_str, name.to_string_lossy()); debug!("{} {}", p.to_string_lossy(), single_file);
let path = if context.sync_context.exact_single_file_bundle
&& single_file
{
format!(
"{}/{}",
name.parent().unwrap().parent().unwrap().to_string_lossy(),
p.file_name().unwrap().to_string_lossy()
)
} else {
name.to_string_lossy().to_string()
};
(raw, path) (raw, path)
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>();
};
let response = Response::builder() let response = Response::builder()
.status(200) .status(200)