feat(dtmm): Write deployment info to disk

Ref: #35.
Fixes #29.
This commit is contained in:
Lucas Schwiderski 2023-03-03 13:28:52 +01:00
parent f021e507b8
commit 61dbbcf2d9
Signed by: lucas
GPG key ID: AA12679AAA6DF4D8
3 changed files with 99 additions and 8 deletions

1
Cargo.lock generated
View file

@ -680,6 +680,7 @@ dependencies = [
"sdk",
"serde",
"serde_sjson",
"time",
"tokio",
"tokio-stream",
"tracing",

View file

@ -24,3 +24,4 @@ tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
zip = "0.6.4"
tokio-stream = { version = "0.1.12", features = ["fs"] }
path-slash = "0.2.1"
time = { version = "0.3.20", features = ["serde", "serde-well-known", "local-offset"] }

View file

@ -15,6 +15,8 @@ use sdk::murmur::Murmur64;
use sdk::{
Bundle, BundleDatabase, BundleFile, BundleFileType, BundleFileVariant, FromBinary, ToBinary,
};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tracing::Instrument;
@ -28,6 +30,13 @@ const BUNDLE_DATABASE_NAME: &str = "bundle_database.data";
const MOD_BOOT_SCRIPT: &str = "scripts/mod_main";
const MOD_DATA_SCRIPT: &str = "scripts/mods/mod_data";
const SETTINGS_FILE_PATH: &str = "application_settings/settings_common.ini";
const DEPLOYMENT_DATA_PATH: &str = "dtmm-deployment.sjson";
#[derive(Serialize, Deserialize)]
struct DeploymentData {
bundles: Vec<String>,
timestamp: OffsetDateTime,
}
#[tracing::instrument]
async fn read_file_with_backup<P>(path: P) -> Result<Vec<u8>>
@ -449,8 +458,11 @@ async fn patch_boot_bundle(state: Arc<State>) -> Result<Vec<Bundle>> {
Ok(bundles)
}
#[tracing::instrument(skip_all, fields(bundles = bundles.len()))]
async fn patch_bundle_database(state: Arc<State>, bundles: Vec<Bundle>) -> Result<()> {
#[tracing::instrument(skip_all, fields(bundles = bundles.as_ref().len()))]
async fn patch_bundle_database<B>(state: Arc<State>, bundles: B) -> Result<()>
where
B: AsRef<[Bundle]>,
{
let bundle_dir = Arc::new(state.game_dir.join("bundle"));
let database_path = bundle_dir.join(BUNDLE_DATABASE_NAME);
@ -464,9 +476,9 @@ async fn patch_bundle_database(state: Arc<State>, bundles: Vec<Bundle>) -> Resul
db
};
for bundle in bundles {
for bundle in bundles.as_ref() {
tracing::trace!("Adding '{}' to bundle database", bundle.name().display());
db.add_bundle(&bundle);
db.add_bundle(bundle);
}
{
@ -484,6 +496,29 @@ async fn patch_bundle_database(state: Arc<State>, bundles: Vec<Bundle>) -> Resul
Ok(())
}
#[tracing::instrument(skip_all, fields(bundles = bundles.as_ref().len()))]
async fn write_deployment_data<B>(state: Arc<State>, bundles: B) -> Result<()>
where
B: AsRef<[Bundle]>,
{
let info = DeploymentData {
timestamp: OffsetDateTime::now_utc(),
bundles: bundles
.as_ref()
.iter()
.map(|bundle| format!("{:x}", bundle.name().to_murmur64()))
.collect(),
};
let path = state.game_dir.join(DEPLOYMENT_DATA_PATH);
let data = serde_sjson::to_string(&info).wrap_err("failed to serizalie deployment data")?;
fs::write(&path, &data)
.await
.wrap_err_with(|| format!("failed to write deployment data to '{}'", path.display()))?;
Ok(())
}
#[tracing::instrument(skip_all, fields(
game_dir = %state.game_dir.display(),
mods = state.mods.len()
@ -522,10 +557,15 @@ pub(crate) async fn deploy_mods(state: State) -> Result<()> {
.wrap_err("failed to patch game settings")?;
tracing::info!("Patching bundle database");
patch_bundle_database(state.clone(), bundles)
patch_bundle_database(state.clone(), &bundles)
.await
.wrap_err("failed to patch bundle database")?;
tracing::info!("Writing deployment data");
write_deployment_data(state.clone(), &bundles)
.await
.wrap_err("failed to write deployment data")?;
tracing::info!("Finished deploying mods");
Ok(())
}
@ -538,6 +578,40 @@ pub(crate) async fn reset_mod_deployment(state: State) -> Result<()> {
tracing::info!("Resetting mod deployment in {}", bundle_dir.display());
tracing::debug!("Reading mod deployment");
let info: DeploymentData = {
let path = state.game_dir.join(DEPLOYMENT_DATA_PATH);
let data = match fs::read(&path).await {
Ok(data) => data,
Err(err) if err.kind() == ErrorKind::NotFound => {
tracing::info!("No deployment to reset");
return Ok(());
}
Err(err) => {
return Err(err).wrap_err_with(|| {
format!("failed to read deployment info at '{}'", path.display())
});
}
};
let data = String::from_utf8(data).wrap_err("invalid UTF8 in deployment data")?;
serde_sjson::from_str(&data).wrap_err("invalid SJSON in deployment data")?
};
for name in info.bundles {
let path = bundle_dir.join(name);
match fs::remove_file(&path).await {
Ok(_) => {}
Err(err) if err.kind() == ErrorKind::NotFound => {}
Err(err) => {
tracing::error!("Failed to remove '{}': {:?}", path.display(), err);
}
};
}
for p in paths {
let path = bundle_dir.join(p);
let backup = bundle_dir.join(&format!("{}.bak", p));
@ -555,9 +629,13 @@ pub(crate) async fn reset_mod_deployment(state: State) -> Result<()> {
tracing::debug!("Deleting backup: {}", backup.display());
fs::remove_file(&backup)
.await
.wrap_err_with(|| format!("failed to remove '{}'", backup.display()))
match fs::remove_file(&backup).await {
Ok(_) => Ok(()),
Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
Err(err) => {
Err(err).wrap_err_with(|| format!("failed to remove '{}'", backup.display()))
}
}
}
.await;
@ -570,6 +648,17 @@ pub(crate) async fn reset_mod_deployment(state: State) -> Result<()> {
}
}
{
let path = state.game_dir.join(DEPLOYMENT_DATA_PATH);
if let Err(err) = fs::remove_file(&path).await {
tracing::error!(
"Failed to remove deployment data '{}': {:?}",
path.display(),
err
);
}
}
tracing::info!("Reset finished");
Ok(())