mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-20 23:36:52 +00:00
Guarded ingest pack replacement
- Mod ingest compares source and target by real path and file identity, so a differently cased target on a case-insensitive filesystem can no longer resolve to and delete the source pack - replace=true only replaces a target that contains ingest/manifest.json - Ingest builds into a hidden staging directory and swaps it in atomically, deleting the previous output only after the new pack is complete
This commit is contained in:
1 parent
3ba6d5b9c1
commit
191d9e88e6
2 files changed
+103
-20
No files matched your search
@@ -42,6 +42,7 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class ModIngester {
|
||||
private static final Set<String> RESERVED_NAMESPACES = Set.of("minecraft", "c", "iris");
|
||||
@@ -58,18 +59,17 @@ public final class ModIngester {
|
||||
throw new IOException("Source pack " + source + " was written by mod ingest; ingest from its original pack instead.");
|
||||
}
|
||||
Set<String> namespaces = selectNamespaces(request);
|
||||
prepareTarget(target, request.replaceTarget());
|
||||
requireReplaceableTarget(target, request.replaceTarget());
|
||||
String targetKey = target.getFileName().toString();
|
||||
IrisProjectCopier.copyProject(source.toFile(), target.getParent().toFile(), request.dimensionKey(), targetKey);
|
||||
Files.createDirectories(target.getParent());
|
||||
Path workspace = Files.createTempDirectory(target.getParent(), "." + targetKey + ".ingest-");
|
||||
try {
|
||||
return populate(request, target, targetKey, namespaces);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
try {
|
||||
AtomicDirectoryPublisher.deleteTree(target);
|
||||
} catch (IOException cleanupFailure) {
|
||||
e.addSuppressed(cleanupFailure);
|
||||
}
|
||||
throw e;
|
||||
IrisProjectCopier.copyProject(source.toFile(), workspace.toFile(), request.dimensionKey(), targetKey);
|
||||
ModIngestReport built = populate(request, workspace.resolve(targetKey), targetKey, namespaces);
|
||||
publish(workspace.resolve(targetKey), target, request.replaceTarget());
|
||||
return new ModIngestReport(target, built.items(), built.summaryLines());
|
||||
} finally {
|
||||
AtomicDirectoryPublisher.deleteTree(workspace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,24 +134,53 @@ public final class ModIngester {
|
||||
}
|
||||
|
||||
private static void requireSeparate(Path source, Path target) throws IOException {
|
||||
Path realSource = Files.exists(source) ? source.toRealPath() : source;
|
||||
if (target.getParent() == null) {
|
||||
throw new IOException("Target pack needs a parent directory: " + target);
|
||||
}
|
||||
Path realSource = Files.exists(source) ? source.toRealPath() : source;
|
||||
List<Path> targets = new ArrayList<>(List.of(target));
|
||||
if (Files.exists(target, LinkOption.NOFOLLOW_LINKS)) {
|
||||
targets.add(target.toRealPath());
|
||||
if (Files.exists(source) && Files.isSameFile(source, target)) {
|
||||
throw new IOException("Target pack " + target + " is the source pack " + realSource);
|
||||
}
|
||||
} else if (Files.exists(target.getParent())) {
|
||||
targets.add(target.getParent().toRealPath().resolve(target.getFileName()));
|
||||
}
|
||||
for (Path candidate : List.of(source, realSource)) {
|
||||
if (candidate.equals(target) || target.startsWith(candidate) || candidate.startsWith(target)) {
|
||||
throw new IOException("Target pack " + target + " overlaps the source pack " + candidate);
|
||||
for (Path resolvedTarget : targets) {
|
||||
if (candidate.equals(resolvedTarget) || resolvedTarget.startsWith(candidate) || candidate.startsWith(resolvedTarget)) {
|
||||
throw new IOException("Target pack " + target + " overlaps the source pack " + candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void prepareTarget(Path target, boolean replace) throws IOException {
|
||||
if (Files.exists(target, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(target)) {
|
||||
if (!replace) {
|
||||
throw new FileAlreadyExistsException(target.toString());
|
||||
}
|
||||
AtomicDirectoryPublisher.deleteTree(target);
|
||||
private static void requireReplaceableTarget(Path target, boolean replace) throws IOException {
|
||||
if (!Files.exists(target, LinkOption.NOFOLLOW_LINKS) && !Files.isSymbolicLink(target)) {
|
||||
return;
|
||||
}
|
||||
if (!replace) {
|
||||
throw new FileAlreadyExistsException(target.toString());
|
||||
}
|
||||
if (Files.isSymbolicLink(target)
|
||||
|| !Files.isDirectory(target, LinkOption.NOFOLLOW_LINKS)
|
||||
|| !Files.isRegularFile(target.resolve(IngestManifest.PATH), LinkOption.NOFOLLOW_LINKS)) {
|
||||
throw new IOException("Iris only replaces packs written by mod ingest, and " + target + " has no "
|
||||
+ IngestManifest.PATH + "; choose another target pack name.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void publish(Path built, Path target, boolean replace) throws IOException {
|
||||
Path staged = target.resolveSibling("." + target.getFileName() + ".staged-" + UUID.randomUUID());
|
||||
Files.move(built, staged);
|
||||
try (AtomicDirectoryPublisher.Publication publication = replace
|
||||
? AtomicDirectoryPublisher.publish(staged, target)
|
||||
: AtomicDirectoryPublisher.publishAbsent(staged, target)) {
|
||||
publication.commit();
|
||||
publication.cleanupBackup();
|
||||
} finally {
|
||||
AtomicDirectoryPublisher.deleteTree(staged);
|
||||
}
|
||||
Files.createDirectories(target.getParent());
|
||||
}
|
||||
}
|
||||
@@ -111,6 +111,54 @@ public class ModIngesterPackTest {
|
||||
ModIngester.ingest(ModIngestFixture.request(source, target, Set.of(ModIngestFixture.MOD), true));
|
||||
|
||||
assertEquals(first, snapshot(target));
|
||||
assertEquals(List.of("modded"), directoryNames(root.resolve("packs")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void replaceRefusesAPackThatWasNotWrittenByIngest() throws Exception {
|
||||
Path root = folder.getRoot().toPath();
|
||||
Path source = ModIngestFixture.writeSourcePack(root);
|
||||
Path handmade = root.resolve("packs").resolve("handmade");
|
||||
ModIngestFixture.write(handmade, "dimensions/handmade.json", "{\"name\": \"Handmade\"}");
|
||||
|
||||
IOException refused = assertThrows(IOException.class, () -> ModIngester.ingest(
|
||||
ModIngestFixture.request(source, handmade, Set.of(ModIngestFixture.MOD), true)));
|
||||
|
||||
assertTrue(refused.getMessage(), refused.getMessage().contains("ingest/manifest.json"));
|
||||
assertEquals(Map.of("dimensions/handmade.json", "{\"name\": \"Handmade\"}"), snapshot(handmade));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void replaceNeverDeletesTheSourcePackThroughAnotherSpelling() throws Exception {
|
||||
Path root = folder.getRoot().toPath();
|
||||
Path source = ModIngestFixture.writeSourcePack(root);
|
||||
Map<String, String> before = snapshot(source);
|
||||
Path respelled = root.resolve("SOURCE");
|
||||
boolean caseInsensitive = Files.isDirectory(respelled);
|
||||
|
||||
if (caseInsensitive) {
|
||||
assertThrows(IOException.class, () -> ModIngester.ingest(
|
||||
ModIngestFixture.request(source, respelled, Set.of(ModIngestFixture.MOD), true)));
|
||||
} else {
|
||||
ModIngester.ingest(ModIngestFixture.request(source, respelled, Set.of(ModIngestFixture.MOD), true));
|
||||
}
|
||||
|
||||
assertEquals(before, snapshot(source));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedReplaceKeepsThePreviousOutput() throws Exception {
|
||||
Path root = folder.getRoot().toPath();
|
||||
Path source = ModIngestFixture.writeSourcePack(root);
|
||||
Path target = root.resolve("packs").resolve("modded");
|
||||
ModIngester.ingest(ModIngestFixture.request(source, target, Set.of(ModIngestFixture.MOD), false));
|
||||
Map<String, String> first = snapshot(target);
|
||||
|
||||
assertThrows(IOException.class, () -> ModIngester.ingest(new ModIngestRequest(
|
||||
source, target, "missing", ModIngestFixture.catalog(), Set.of(ModIngestFixture.MOD), true)));
|
||||
|
||||
assertEquals(first, snapshot(target));
|
||||
assertEquals(List.of("modded"), directoryNames(root.resolve("packs")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -155,6 +203,12 @@ public class ModIngesterPackTest {
|
||||
assertTrue(report.items().stream().noneMatch(item -> item.namespace().equals("minecraft")));
|
||||
}
|
||||
|
||||
private static List<String> directoryNames(Path root) throws IOException {
|
||||
try (Stream<Path> entries = Files.list(root)) {
|
||||
return entries.map((Path entry) -> entry.getFileName().toString()).sorted().toList();
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, String> snapshot(Path root) throws IOException {
|
||||
Map<String, String> files = new TreeMap<>();
|
||||
try (Stream<Path> walk = Files.walk(root)) {
|
||||
|
||||
Reference in new issue
Block a user