mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +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.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
public final class ModIngester {
|
public final class ModIngester {
|
||||||
private static final Set<String> RESERVED_NAMESPACES = Set.of("minecraft", "c", "iris");
|
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.");
|
throw new IOException("Source pack " + source + " was written by mod ingest; ingest from its original pack instead.");
|
||||||
}
|
}
|
||||||
Set<String> namespaces = selectNamespaces(request);
|
Set<String> namespaces = selectNamespaces(request);
|
||||||
prepareTarget(target, request.replaceTarget());
|
requireReplaceableTarget(target, request.replaceTarget());
|
||||||
String targetKey = target.getFileName().toString();
|
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 {
|
try {
|
||||||
return populate(request, target, targetKey, namespaces);
|
IrisProjectCopier.copyProject(source.toFile(), workspace.toFile(), request.dimensionKey(), targetKey);
|
||||||
} catch (IOException | RuntimeException e) {
|
ModIngestReport built = populate(request, workspace.resolve(targetKey), targetKey, namespaces);
|
||||||
try {
|
publish(workspace.resolve(targetKey), target, request.replaceTarget());
|
||||||
AtomicDirectoryPublisher.deleteTree(target);
|
return new ModIngestReport(target, built.items(), built.summaryLines());
|
||||||
} catch (IOException cleanupFailure) {
|
} finally {
|
||||||
e.addSuppressed(cleanupFailure);
|
AtomicDirectoryPublisher.deleteTree(workspace);
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,24 +134,53 @@ public final class ModIngester {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void requireSeparate(Path source, Path target) throws IOException {
|
private static void requireSeparate(Path source, Path target) throws IOException {
|
||||||
Path realSource = Files.exists(source) ? source.toRealPath() : source;
|
|
||||||
if (target.getParent() == null) {
|
if (target.getParent() == null) {
|
||||||
throw new IOException("Target pack needs a parent directory: " + target);
|
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)) {
|
for (Path candidate : List.of(source, realSource)) {
|
||||||
if (candidate.equals(target) || target.startsWith(candidate) || candidate.startsWith(target)) {
|
for (Path resolvedTarget : targets) {
|
||||||
throw new IOException("Target pack " + target + " overlaps the source pack " + candidate);
|
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 {
|
private static void requireReplaceableTarget(Path target, boolean replace) throws IOException {
|
||||||
if (Files.exists(target, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(target)) {
|
if (!Files.exists(target, LinkOption.NOFOLLOW_LINKS) && !Files.isSymbolicLink(target)) {
|
||||||
if (!replace) {
|
return;
|
||||||
throw new FileAlreadyExistsException(target.toString());
|
}
|
||||||
}
|
if (!replace) {
|
||||||
AtomicDirectoryPublisher.deleteTree(target);
|
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));
|
ModIngester.ingest(ModIngestFixture.request(source, target, Set.of(ModIngestFixture.MOD), true));
|
||||||
|
|
||||||
assertEquals(first, snapshot(target));
|
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
|
@Test
|
||||||
@@ -155,6 +203,12 @@ public class ModIngesterPackTest {
|
|||||||
assertTrue(report.items().stream().noneMatch(item -> item.namespace().equals("minecraft")));
|
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 {
|
private static Map<String, String> snapshot(Path root) throws IOException {
|
||||||
Map<String, String> files = new TreeMap<>();
|
Map<String, String> files = new TreeMap<>();
|
||||||
try (Stream<Path> walk = Files.walk(root)) {
|
try (Stream<Path> walk = Files.walk(root)) {
|
||||||
|
|||||||
Reference in new issue
Block a user