Config transfer (#951)

* fixed config dir issue

* jackson's sync write
This commit is contained in:
Wyatt Verchere 2024-01-05 11:00:48 -08:00 committed by GitHub
parent 9702dae19d
commit 0d3f007dd4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 116 additions and 48 deletions

View File

@ -301,7 +301,7 @@ pub async fn copy_dotminecraft(
#[theseus_macros::debug_pin] #[theseus_macros::debug_pin]
#[async_recursion::async_recursion] #[async_recursion::async_recursion]
#[tracing::instrument] #[tracing::instrument]
async fn get_all_subfiles(src: &Path) -> crate::Result<Vec<PathBuf>> { pub async fn get_all_subfiles(src: &Path) -> crate::Result<Vec<PathBuf>> {
if !src.is_dir() { if !src.is_dir() {
return Ok(vec![src.to_path_buf()]); return Ok(vec![src.to_path_buf()]);
} }

View File

@ -1,6 +1,6 @@
//! Theseus profile management interface //! Theseus profile management interface
use std::path::PathBuf; use std::path::{PathBuf, Path};
use tokio::fs; use tokio::fs;
use io::IOError; use io::IOError;
@ -10,7 +10,7 @@ use crate::{
event::emit::{emit_loading, init_loading}, event::emit::{emit_loading, init_loading},
prelude::DirectoryInfo, prelude::DirectoryInfo,
state::{self, Profiles}, state::{self, Profiles},
util::io, util::{io, fetch},
}; };
pub use crate::{ pub use crate::{
state::{ state::{
@ -77,6 +77,7 @@ pub async fn set(settings: Settings) -> crate::Result<()> {
/// Sets the new config dir, the location of all Theseus data except for the settings.json and caches /// Sets the new config dir, the location of all Theseus data except for the settings.json and caches
/// Takes control of the entire state and blocks until completion /// Takes control of the entire state and blocks until completion
pub async fn set_config_dir(new_config_dir: PathBuf) -> crate::Result<()> { pub async fn set_config_dir(new_config_dir: PathBuf) -> crate::Result<()> {
tracing::trace!("Changing config dir to: {}", new_config_dir.display());
if !new_config_dir.is_dir() { if !new_config_dir.is_dir() {
return Err(crate::ErrorKind::FSError(format!( return Err(crate::ErrorKind::FSError(format!(
"New config dir is not a folder: {}", "New config dir is not a folder: {}",
@ -85,6 +86,14 @@ pub async fn set_config_dir(new_config_dir: PathBuf) -> crate::Result<()> {
.as_error()); .as_error());
} }
if !is_dir_writeable(new_config_dir.clone()).await? {
return Err(crate::ErrorKind::FSError(format!(
"New config dir is not writeable: {}",
new_config_dir.display()
))
.as_error());
}
let loading_bar = init_loading( let loading_bar = init_loading(
crate::LoadingBarType::ConfigChange { crate::LoadingBarType::ConfigChange {
new_path: new_config_dir.clone(), new_path: new_config_dir.clone(),
@ -100,6 +109,52 @@ pub async fn set_config_dir(new_config_dir: PathBuf) -> crate::Result<()> {
let old_config_dir = let old_config_dir =
state_write.directories.config_dir.read().await.clone(); state_write.directories.config_dir.read().await.clone();
// Reset file watcher
tracing::trace!("Reset file watcher");
let file_watcher = state::init_watcher().await?;
state_write.file_watcher = RwLock::new(file_watcher);
// Getting files to be moved
let mut config_entries = io::read_dir(&old_config_dir).await?;
let across_drives = is_different_drive(&old_config_dir, &new_config_dir);
let mut entries = vec![];
let mut deletable_entries = vec![];
while let Some(entry) = config_entries
.next_entry()
.await
.map_err(|e| IOError::with_path(e, &old_config_dir))?
{
let entry_path = entry.path();
if let Some(file_name) = entry_path.file_name() {
// We are only moving the profiles and metadata folders
if file_name == state::PROFILES_FOLDER_NAME || file_name == state::METADATA_FOLDER_NAME {
if across_drives {
entries.extend(crate::pack::import::get_all_subfiles(&entry_path).await?);
deletable_entries.push(entry_path.clone());
} else {
entries.push(entry_path.clone());
}
}
}
}
tracing::trace!("Moving files");
let semaphore = &state_write.io_semaphore;
let num_entries = entries.len() as f64;
for entry_path in entries {
let relative_path = entry_path.strip_prefix(&old_config_dir)?;
let new_path = new_config_dir.join(relative_path);
if across_drives {
fetch::copy(&entry_path, &new_path, semaphore).await?;
} else {
io::rename(entry_path.clone(), new_path.clone()).await?;
}
emit_loading(&loading_bar, 80.0 * (1.0 / num_entries), None)
.await?;
}
tracing::trace!("Setting configuration setting"); tracing::trace!("Setting configuration setting");
// Set load config dir setting // Set load config dir setting
let settings = { let settings = {
@ -132,41 +187,19 @@ pub async fn set_config_dir(new_config_dir: PathBuf) -> crate::Result<()> {
tracing::trace!("Reinitializing directory"); tracing::trace!("Reinitializing directory");
// Set new state information // Set new state information
state_write.directories = DirectoryInfo::init(&settings)?; state_write.directories = DirectoryInfo::init(&settings)?;
let total_entries = std::fs::read_dir(&old_config_dir)
.map_err(|e| IOError::with_path(e, &old_config_dir))?
.count() as f64;
// Move all files over from state_write.directories.config_dir to new_config_dir // Delete entries that were from a different drive
tracing::trace!("Renaming folder structure"); let deletable_entries_len = deletable_entries.len();
let mut i = 0.0; if deletable_entries_len > 0 {
let mut entries = io::read_dir(&old_config_dir).await?; tracing::trace!("Deleting old files");
while let Some(entry) = entries }
.next_entry() for entry in deletable_entries {
.await io::remove_dir_all(entry).await?;
.map_err(|e| IOError::with_path(e, &old_config_dir))? emit_loading(
{ &loading_bar,
let entry_path = entry.path(); 10.0 * (1.0 / deletable_entries_len as f64),
if let Some(file_name) = entry_path.file_name() { None,
// Ignore settings.json ).await?;
if file_name == state::SETTINGS_FILE_NAME {
continue;
}
// Ignore caches folder
if file_name == state::CACHES_FOLDER_NAME {
continue;
}
// Ignore modrinth_logs folder
if file_name == state::LAUNCHER_LOGS_FOLDER_NAME {
continue;
}
let new_path = new_config_dir.join(file_name);
io::rename(entry_path, new_path).await?;
i += 1.0;
emit_loading(&loading_bar, 90.0 * (i / total_entries), None)
.await?;
}
} }
// Reset file watcher // Reset file watcher
@ -181,15 +214,21 @@ pub async fn set_config_dir(new_config_dir: PathBuf) -> crate::Result<()> {
emit_loading(&loading_bar, 10.0, None).await?; emit_loading(&loading_bar, 10.0, None).await?;
// TODO: need to be able to safely error out of this function, reverting the changes
tracing::info!( tracing::info!(
"Successfully switched config folder to: {}", "Successfully switched config folder to: {}",
new_config_dir.display() new_config_dir.display()
); );
Ok(()) Ok(())
} }
// Function to check if two paths are on different drives/roots
fn is_different_drive(path1: &Path, path2: &Path) -> bool {
let root1 = path1.components().next();
let root2 = path2.components().next();
root1 != root2
}
pub async fn is_dir_writeable(new_config_dir: PathBuf) -> crate::Result<bool> { pub async fn is_dir_writeable(new_config_dir: PathBuf) -> crate::Result<bool> {
let temp_path = new_config_dir.join(".tmp"); let temp_path = new_config_dir.join(".tmp");
match fs::write(temp_path.clone(), "test").await { match fs::write(temp_path.clone(), "test").await {

View File

@ -9,6 +9,8 @@ use super::{ProfilePathId, Settings};
pub const SETTINGS_FILE_NAME: &str = "settings.json"; pub const SETTINGS_FILE_NAME: &str = "settings.json";
pub const CACHES_FOLDER_NAME: &str = "caches"; pub const CACHES_FOLDER_NAME: &str = "caches";
pub const LAUNCHER_LOGS_FOLDER_NAME: &str = "launcher_logs"; pub const LAUNCHER_LOGS_FOLDER_NAME: &str = "launcher_logs";
pub const PROFILES_FOLDER_NAME: &str = "profiles";
pub const METADATA_FOLDER_NAME: &str = "meta";
#[derive(Debug)] #[derive(Debug)]
pub struct DirectoryInfo { pub struct DirectoryInfo {
@ -75,7 +77,7 @@ impl DirectoryInfo {
/// Get the Minecraft instance metadata directory /// Get the Minecraft instance metadata directory
#[inline] #[inline]
pub async fn metadata_dir(&self) -> PathBuf { pub async fn metadata_dir(&self) -> PathBuf {
self.config_dir.read().await.join("meta") self.config_dir.read().await.join(METADATA_FOLDER_NAME)
} }
/// Get the Minecraft java versions metadata directory /// Get the Minecraft java versions metadata directory
@ -153,7 +155,7 @@ impl DirectoryInfo {
/// Get the profiles directory for created profiles /// Get the profiles directory for created profiles
#[inline] #[inline]
pub async fn profiles_dir(&self) -> PathBuf { pub async fn profiles_dir(&self) -> PathBuf {
self.config_dir.read().await.join("profiles") self.config_dir.read().await.join(PROFILES_FOLDER_NAME)
} }
/// Gets the logs dir for a given profile /// Gets the logs dir for a given profile

View File

@ -1,7 +1,10 @@
// IO error // IO error
// A wrapper around the tokio IO functions that adds the path to the error message, instead of the uninformative std::io::Error. // A wrapper around the tokio IO functions that adds the path to the error message, instead of the uninformative std::io::Error.
use std::path::Path; use std::{path::Path, io::Write};
use tempfile::NamedTempFile;
use tauri::async_runtime::spawn_blocking;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum IOError { pub enum IOError {
@ -113,15 +116,39 @@ pub async fn write(
path: impl AsRef<std::path::Path>, path: impl AsRef<std::path::Path>,
data: impl AsRef<[u8]>, data: impl AsRef<[u8]>,
) -> Result<(), IOError> { ) -> Result<(), IOError> {
let path = path.as_ref(); let path = path.as_ref().to_owned();
tokio::fs::write(path, data) let data = data.as_ref().to_owned();
.await spawn_blocking(move || {
.map_err(|e| IOError::IOPathError { let cloned_path = path.clone();
sync_write(data, path).map_err(|e| IOError::IOPathError {
source: e, source: e,
path: path.to_string_lossy().to_string(), path: cloned_path.to_string_lossy().to_string(),
}) })
})
.await
.map_err(|_| {
std::io::Error::new(std::io::ErrorKind::Other, "background task failed")
})??;
Ok(())
} }
fn sync_write(
data: impl AsRef<[u8]>,
path: impl AsRef<Path>,
) -> Result<(), std::io::Error> {
let mut tempfile = NamedTempFile::new_in(path.as_ref().parent().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::Other,
"could not get parent directory for temporary file",
)
})?)?;
tempfile.write_all(data.as_ref())?;
let tmp_path = tempfile.into_temp_path();
let path = path.as_ref();
tmp_path.persist(path)?;
std::io::Result::Ok(())
}
// rename // rename
pub async fn rename( pub async fn rename(
from: impl AsRef<std::path::Path>, from: impl AsRef<std::path::Path>,

View File

@ -139,7 +139,7 @@ async function findLauncherDir() {
} }
async function refreshDir() { async function refreshDir() {
await change_config_dir(settingsDir.value) await change_config_dir(settingsDir.value).catch(handleError)
settings.value = await accessSettings().catch(handleError) settings.value = await accessSettings().catch(handleError)
settingsDir.value = settings.value.loaded_config_dir settingsDir.value = settings.value.loaded_config_dir
} }