Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions packages/app-lib/src/api/pack/install_mrpack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1309,6 +1309,47 @@ fn modpack_source_kind(version_id: Option<&str>) -> ContentSourceKind {
}
}

/// Instance-relative paths installing or removing this pack can write: its
/// manifest files, its overrides and the instance icon.
pub(crate) async fn mrpack_instance_paths(
mrpack_file: &CreatePackFile,
) -> crate::Result<Vec<String>> {
let mut zip_reader = MrpackZipReader::new(mrpack_file).await?;
let Some(manifest_idx) = zip_reader.file().entries().iter().position(|f| {
matches!(f.filename().as_str(), Ok("modrinth.index.json"))
}) else {
return Err(crate::Error::from(crate::ErrorKind::InputError(
"No pack manifest found in mrpack".to_string(),
)));
};
let manifest = zip_reader.read_entry_to_string(manifest_idx).await?;
let pack: PackFormat = serde_json::from_str(&manifest)?;

let mut paths = vec!["icon.png".to_string()];
paths.extend(pack.files.iter().map(|file| file.path.to_string()));
for entry in zip_reader.file().entries() {
let Ok(filename) = entry.filename().as_str() else {
continue;
};
if filename.ends_with('/') {
continue;
}
let Some(relative) = filename
.strip_prefix("overrides/")
.or_else(|| filename.strip_prefix("client-overrides/"))
else {
continue;
};
if let Ok(relative) =
SafeRelativeUtf8UnixPathBuf::try_from(relative.to_string())
{
paths.push(relative.to_string());
}
}

Ok(paths)
}

#[tracing::instrument(skip(mrpack_file))]

pub async fn remove_all_related_files(
Expand Down
147 changes: 126 additions & 21 deletions packages/app-lib/src/install/recovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,16 @@ struct SharedInstanceUpdateRollback {
missing_file_ids: std::collections::HashSet<String>,
#[serde(default)]
copied_file_ids: std::collections::HashSet<String>,
#[serde(default)]
scoped_paths: Option<Vec<String>>,
}

/// With `scoped_paths`, only those instance-relative files and folders are backed
/// up. A path that does not exist yet is removed again on rollback.
pub(super) async fn prepare_instance_update_backup(
job_id: Uuid,
metadata: &InstanceMetadata,
scoped_paths: Option<Vec<String>>,
state: &State,
) -> crate::Result<PathBuf> {
let _lease = state.content_store.lease().await;
Expand Down Expand Up @@ -110,7 +115,7 @@ pub(super) async fn prepare_instance_update_backup(
copied_file_ids.insert(file.id.clone());
}
}
let skipped = files
let skipped: std::collections::HashSet<String> = files
.iter()
.filter(|file| {
bindings.iter().any(|binding| binding.file_id == file.id)
Expand All @@ -122,24 +127,44 @@ pub(super) async fn prepare_instance_update_backup(
.iter()
.map(|binding| binding.blob_sha512.clone())
.collect::<Vec<_>>();
let scoped_paths = scoped_paths.map(outermost_paths);
let instance_path = state
.directories
.instances_dir()
.join(&metadata.instance.path);
let backup_path =
staging_dir.join(SHARED_INSTANCE_ROLLBACK_INSTANCE_DIR);
match &scoped_paths {
Some(paths) => {
for path in paths {
let prefix = format!("{path}/");
let skipped = skipped
.iter()
.filter_map(|skipped| skipped.strip_prefix(&prefix))
.map(str::to_string)
.collect();
copy_path(
&instance_path.join(path),
&backup_path.join(path),
&skipped,
state,
)
.await?;
}
}
None => {
copy_directory(&instance_path, &backup_path, &skipped, state)
.await?;
}
}
let snapshot = SharedInstanceUpdateRollback {
files,
entries,
bindings,
missing_file_ids,
copied_file_ids,
scoped_paths,
};
let instance_path = state
.directories
.instances_dir()
.join(&metadata.instance.path);
copy_directory(
&instance_path,
&staging_dir.join(SHARED_INSTANCE_ROLLBACK_INSTANCE_DIR),
&skipped,
state,
)
.await?;
crate::util::io::write(
staging_dir.join(SHARED_INSTANCE_ROLLBACK_FILE),
serde_json::to_vec(&snapshot)?,
Expand Down Expand Up @@ -299,16 +324,33 @@ async fn restore_instance_update(
));
}
}
if tokio::fs::try_exists(&instance_path).await? {
crate::util::io::remove_dir_all(&instance_path).await?;
match &snapshot.scoped_paths {
Some(paths) => {
for path in paths {
let target = instance_path.join(path);
remove_path(&target).await?;
copy_path(
&backup_path.join(path),
&target,
&std::collections::HashSet::new(),
state,
)
.await?;
}
}
None => {
if tokio::fs::try_exists(&instance_path).await? {
crate::util::io::remove_dir_all(&instance_path).await?;
}
copy_directory(
&backup_path,
&instance_path,
&std::collections::HashSet::new(),
state,
)
.await?;
}
}
copy_directory(
&backup_path,
&instance_path,
&std::collections::HashSet::new(),
state,
)
.await?;
content_rows::restore_instance_content_snapshot(
&rollback.instance.instance.id,
&snapshot.files,
Expand Down Expand Up @@ -403,6 +445,69 @@ async fn restore_instance_metadata(
Ok(())
}

/// Drops duplicates and paths inside another listed folder.
fn outermost_paths(paths: Vec<String>) -> Vec<String> {
let all = paths
.iter()
.map(String::as_str)
.collect::<std::collections::HashSet<_>>();
let mut outermost = paths
.iter()
.filter(|path| {
!path
.match_indices('/')
.any(|(index, _)| all.contains(&path[..index]))
})
.cloned()
.collect::<Vec<_>>();
outermost.sort();
outermost.dedup();
outermost
}

async fn symlink_metadata(
path: &Path,
) -> crate::Result<Option<std::fs::Metadata>> {
match tokio::fs::symlink_metadata(path).await {
Ok(metadata) => Ok(Some(metadata)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(error.into()),
}
}

/// Removes a file, folder or symlink without following symlinks.
async fn remove_path(path: &Path) -> crate::Result<()> {
match symlink_metadata(path).await? {
Some(metadata) if metadata.is_file() => {
crate::util::io::remove_file(path).await?;
}
Some(_) => crate::util::io::remove_dir_all(path).await?,
None => {}
}
Ok(())
}

/// Copies a file, folder or symlink. Missing paths are skipped.
async fn copy_path(
source: &Path,
target: &Path,
skipped: &std::collections::HashSet<String>,
state: &State,
) -> crate::Result<()> {
match symlink_metadata(source).await? {
Some(metadata) if metadata.is_symlink() => {
copy_symlink(source, target).await
}
Some(metadata) if metadata.is_dir() => {
copy_directory(source, target, skipped, state).await
}
Some(_) => {
crate::util::fetch::copy(source, target, &state.io_semaphore).await
}
None => Ok(()),
}
}

async fn copy_directory(
source: &Path,
target: &Path,
Expand Down
Loading
Loading