feat(dtmm): Implement importing Nexus downloads

For now, this merely parses and retains the API information encoded in
the archive's file name.
This commit is contained in:
Lucas Schwiderski 2023-03-14 10:19:23 +01:00
parent 2fb0d8fb72
commit c7203127bb
Signed by: lucas
GPG key ID: AA12679AAA6DF4D8
4 changed files with 51 additions and 3 deletions

1
Cargo.lock generated
View file

@ -756,6 +756,7 @@ dependencies = [
"dtmt-shared", "dtmt-shared",
"futures", "futures",
"lazy_static", "lazy_static",
"nexusmods",
"oodle-sys", "oodle-sys",
"path-slash", "path-slash",
"sdk", "sdk",

View file

@ -15,6 +15,7 @@ dtmt-shared = { path = "../../lib/dtmt-shared", version = "*" }
futures = "0.3.25" futures = "0.3.25"
oodle-sys = { path = "../../lib/oodle-sys", version = "*" } oodle-sys = { path = "../../lib/oodle-sys", version = "*" }
sdk = { path = "../../lib/sdk", version = "*" } sdk = { path = "../../lib/sdk", version = "*" }
nexusmods = { path = "../../lib/nexusmods", version = "*" }
serde_sjson = { path = "../../lib/serde_sjson", version = "*" } serde_sjson = { path = "../../lib/serde_sjson", version = "*" }
serde = { version = "1.0.152", features = ["derive", "rc"] } serde = { version = "1.0.152", features = ["derive", "rc"] }
tokio = { version = "1.23.0", features = ["rt", "fs", "tracing", "sync"] } tokio = { version = "1.23.0", features = ["rt", "fs", "tracing", "sync"] }

View file

@ -8,13 +8,14 @@ use color_eyre::{Help, Report, Result};
use druid::im::Vector; use druid::im::Vector;
use druid::{FileInfo, ImageBuf}; use druid::{FileInfo, ImageBuf};
use dtmt_shared::ModConfig; use dtmt_shared::ModConfig;
use nexusmods::Api as NexusApi;
use tokio::fs::{self, DirEntry}; use tokio::fs::{self, DirEntry};
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use tokio_stream::wrappers::ReadDirStream; use tokio_stream::wrappers::ReadDirStream;
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
use zip::ZipArchive; use zip::ZipArchive;
use crate::state::{ActionState, ModInfo, ModOrder, PackageInfo}; use crate::state::{ActionState, ModInfo, ModOrder, NexusInfo, PackageInfo};
use crate::util::config::{ConfigSerialize, LoadOrderEntry}; use crate::util::config::{ConfigSerialize, LoadOrderEntry};
use super::read_sjson_file; use super::read_sjson_file;
@ -26,6 +27,17 @@ pub(crate) async fn import_mod(state: ActionState, info: FileInfo) -> Result<Mod
.wrap_err_with(|| format!("Failed to read file {}", info.path.display()))?; .wrap_err_with(|| format!("Failed to read file {}", info.path.display()))?;
let data = Cursor::new(data); let data = Cursor::new(data);
let nexus = info
.path
.file_name()
.and_then(|s| s.to_str())
.and_then(NexusApi::parse_file_name)
.map(|(_, id, version, updated)| NexusInfo {
id,
version: Some(version),
updated,
});
let mut archive = ZipArchive::new(data).wrap_err("Failed to open ZIP archive")?; let mut archive = ZipArchive::new(data).wrap_err("Failed to open ZIP archive")?;
if tracing::enabled!(tracing::Level::DEBUG) { if tracing::enabled!(tracing::Level::DEBUG) {
@ -137,11 +149,19 @@ pub(crate) async fn import_mod(state: ActionState, info: FileInfo) -> Result<Mod
.extract(Arc::as_ref(&mod_dir)) .extract(Arc::as_ref(&mod_dir))
.wrap_err_with(|| format!("Failed to extract archive to {}", mod_dir.display()))?; .wrap_err_with(|| format!("Failed to extract archive to {}", mod_dir.display()))?;
if let Some(nexus) = &nexus {
let data = serde_sjson::to_string(nexus).wrap_err("Failed to serialize Nexus info")?;
let path = mod_dir.join(&mod_cfg.id).join("nexus.sjson");
fs::write(&path, data.as_bytes())
.await
.wrap_err_with(|| format!("Failed to write Nexus info to '{}'", path.display()))?;
}
let packages = files let packages = files
.into_iter() .into_iter()
.map(|(name, files)| Arc::new(PackageInfo::new(name, files.into_iter().collect()))) .map(|(name, files)| Arc::new(PackageInfo::new(name, files.into_iter().collect())))
.collect(); .collect();
let info = ModInfo::new(mod_cfg, packages, image); let info = ModInfo::new(mod_cfg, packages, image, nexus);
Ok(info) Ok(info)
} }
@ -181,12 +201,25 @@ pub(crate) async fn save_settings(state: ActionState) -> Result<()> {
async fn read_mod_dir_entry(res: Result<DirEntry>) -> Result<ModInfo> { async fn read_mod_dir_entry(res: Result<DirEntry>) -> Result<ModInfo> {
let entry = res?; let entry = res?;
let config_path = entry.path().join("dtmt.cfg"); let config_path = entry.path().join("dtmt.cfg");
let nexus_path = entry.path().join("nexus.sjson");
let index_path = entry.path().join("files.sjson"); let index_path = entry.path().join("files.sjson");
let cfg: ModConfig = read_sjson_file(&config_path) let cfg: ModConfig = read_sjson_file(&config_path)
.await .await
.wrap_err_with(|| format!("Failed to read mod config '{}'", config_path.display()))?; .wrap_err_with(|| format!("Failed to read mod config '{}'", config_path.display()))?;
let nexus: Option<NexusInfo> = match read_sjson_file(&nexus_path)
.await
.wrap_err_with(|| format!("Failed to read Nexus info '{}'", nexus_path.display()))
{
Ok(nexus) => Some(nexus),
Err(err) if err.is::<std::io::Error>() => match err.downcast_ref::<std::io::Error>() {
Some(err) if err.kind() == std::io::ErrorKind::NotFound => None,
_ => return Err(err),
},
Err(err) => return Err(err),
};
let files: HashMap<String, Vec<String>> = read_sjson_file(&index_path) let files: HashMap<String, Vec<String>> = read_sjson_file(&index_path)
.await .await
.wrap_err_with(|| format!("Failed to read file index '{}'", index_path.display()))?; .wrap_err_with(|| format!("Failed to read file index '{}'", index_path.display()))?;
@ -222,7 +255,7 @@ async fn read_mod_dir_entry(res: Result<DirEntry>) -> Result<ModInfo> {
.into_iter() .into_iter()
.map(|(name, files)| Arc::new(PackageInfo::new(name, files.into_iter().collect()))) .map(|(name, files)| Arc::new(PackageInfo::new(name, files.into_iter().collect())))
.collect(); .collect();
let info = ModInfo::new(cfg, packages, image); let info = ModInfo::new(cfg, packages, image, nexus);
Ok(info) Ok(info)
} }

View file

@ -5,6 +5,7 @@ use druid::{
Data, ImageBuf, Lens, WindowHandle, WindowId, Data, ImageBuf, Lens, WindowHandle, WindowId,
}; };
use dtmt_shared::ModConfig; use dtmt_shared::ModConfig;
use time::OffsetDateTime;
use super::SelectedModLens; use super::SelectedModLens;
@ -69,6 +70,15 @@ impl From<dtmt_shared::ModDependency> for ModDependency {
} }
} }
#[derive(Clone, Data, Debug, Lens, serde::Serialize, serde::Deserialize)]
pub(crate) struct NexusInfo {
pub id: u64,
pub version: Option<String>,
#[data(ignore)]
#[serde(with = "time::serde::timestamp")]
pub updated: OffsetDateTime,
}
#[derive(Clone, Data, Debug, Lens)] #[derive(Clone, Data, Debug, Lens)]
pub(crate) struct ModInfo { pub(crate) struct ModInfo {
pub id: String, pub id: String,
@ -87,6 +97,7 @@ pub(crate) struct ModInfo {
#[data(ignore)] #[data(ignore)]
pub resources: ModResourceInfo, pub resources: ModResourceInfo,
pub depends: Vector<ModDependency>, pub depends: Vector<ModDependency>,
pub nexus: Option<NexusInfo>,
} }
impl ModInfo { impl ModInfo {
@ -94,6 +105,7 @@ impl ModInfo {
cfg: ModConfig, cfg: ModConfig,
packages: Vector<Arc<PackageInfo>>, packages: Vector<Arc<PackageInfo>>,
image: Option<ImageBuf>, image: Option<ImageBuf>,
nexus: Option<NexusInfo>,
) -> Self { ) -> Self {
Self { Self {
id: cfg.id, id: cfg.id,
@ -112,6 +124,7 @@ impl ModInfo {
localization: cfg.resources.localization, localization: cfg.resources.localization,
}, },
depends: cfg.depends.into_iter().map(ModDependency::from).collect(), depends: cfg.depends.into_iter().map(ModDependency::from).collect(),
nexus,
} }
} }
} }