diff --git a/src/main/java/org/mvplugins/multiverse/core/commands/DeleteCommand.java b/src/main/java/org/mvplugins/multiverse/core/commands/DeleteCommand.java index 4eea55104..fffc53af1 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commands/DeleteCommand.java +++ b/src/main/java/org/mvplugins/multiverse/core/commands/DeleteCommand.java @@ -29,6 +29,7 @@ import org.mvplugins.multiverse.core.world.MultiverseWorld; import org.mvplugins.multiverse.core.world.WorldManager; import org.mvplugins.multiverse.core.world.helpers.PlayerWorldTeleporter; +import org.mvplugins.multiverse.core.world.options.DeleteWorldOptions; @Service class DeleteCommand extends CoreCommand { @@ -94,7 +95,7 @@ private void runDeleteCommand(MVCommandIssuer issuer, MultiverseWorld world, Par } private void doWorldDeleting(MVCommandIssuer issuer, MultiverseWorld world) { - worldManager.deleteWorld(world) + worldManager.deleteWorld(DeleteWorldOptions.world(world)) .onSuccess(deletedWorldName -> { Logging.fine("World delete success: " + deletedWorldName); issuer.sendInfo(MVCorei18n.DELETE_SUCCESS, Replace.WORLD.with(deletedWorldName)); diff --git a/src/main/java/org/mvplugins/multiverse/core/commands/RegenCommand.java b/src/main/java/org/mvplugins/multiverse/core/commands/RegenCommand.java index 2258cba8d..745ffe93d 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commands/RegenCommand.java +++ b/src/main/java/org/mvplugins/multiverse/core/commands/RegenCommand.java @@ -19,7 +19,6 @@ import org.mvplugins.multiverse.core.command.LegacyAliasCommand; import org.mvplugins.multiverse.core.command.MVCommandIssuer; -import org.mvplugins.multiverse.core.command.MVCommandManager; import org.mvplugins.multiverse.core.command.flag.CommandFlag; import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager; import org.mvplugins.multiverse.core.command.flag.CommandValueFlag; @@ -30,6 +29,7 @@ import org.mvplugins.multiverse.core.locale.MVCorei18n; import org.mvplugins.multiverse.core.locale.message.Message; import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace; +import org.mvplugins.multiverse.core.utils.REPatterns; import org.mvplugins.multiverse.core.utils.WorldTickDeferrer; import org.mvplugins.multiverse.core.utils.result.AsyncAttemptsAggregate; import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld; @@ -112,7 +112,8 @@ private void doWorldRegening( .seed(parsedFlags.flagValue(flags.seed)) .keepWorldConfig(!parsedFlags.hasFlag(flags.resetWorldConfig)) .keepGameRule(!parsedFlags.hasFlag(flags.resetGamerules)) - .keepWorldBorder(!parsedFlags.hasFlag(flags.resetWorldBorder)); + .keepWorldBorder(!parsedFlags.hasFlag(flags.resetWorldBorder)) + .keepFiles(parsedFlags.flagValue(flags.keepFiles)); worldManager.regenWorld(regenWorldOptions).onSuccess(newWorld -> { Logging.fine("World regen success: " + newWorld); @@ -153,6 +154,13 @@ private Flags(@NotNull CommandFlagsManager flagsManager) { private final CommandFlag resetWorldBorder = flag(CommandFlag.builder("--reset-world-border") .addAlias("-wb") .build()); + + private final CommandValueFlag keepFiles = flag(CommandValueFlag.builder("--keep-files", List.class) + .addAlias("-f") + .completion(input -> List.of("paper-world.yml")) + .defaultValue(Collections.emptyList()) + .context(input -> List.of(REPatterns.COMMA.split(input))) + .build()); } @Service diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/FileUtils.java b/src/main/java/org/mvplugins/multiverse/core/utils/FileUtils.java index 40bd52b0d..64f6daf48 100644 --- a/src/main/java/org/mvplugins/multiverse/core/utils/FileUtils.java +++ b/src/main/java/org/mvplugins/multiverse/core/utils/FileUtils.java @@ -9,9 +9,12 @@ import java.io.File; import java.io.IOException; -import java.lang.management.ManagementFactory; -import java.nio.file.*; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -20,7 +23,6 @@ import com.dumptruckman.minecraft.util.Logging; import io.vavr.control.Try; import jakarta.inject.Inject; -import org.bukkit.Bukkit; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jvnet.hk2.annotations.Service; @@ -102,7 +104,11 @@ public File getServerFolder() { * the folder could not be deleted. */ public Try deleteFolder(File file) { - return deleteFolder(file.toPath()); + return deleteFolder(file, Collections.emptyList()); + } + + public Try deleteFolder(File file, Collection keepFiles) { + return deleteFolder(file.toPath(), keepFiles); } /** @@ -112,11 +118,19 @@ public Try deleteFolder(File file) { * @return A {@link Try} that will contain {@code null} if the folder was deleted successfully, or an exception if * the folder could not be deleted. */ - public Try deleteFolder(Path path) { + public Try deleteFolder(Path path, Collection keepFiles) { try (Stream files = Files.walk(path)) { files.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(file -> { + if (!isDirectoryEmpty(file)) { + Logging.finest("Cannot delete folder as it is not empty: " + file); + return; + } + if (file.isFile() && keepFiles.contains(file.getName())) { + Logging.finest("Keeping file: " + file); + return; + } if (!file.delete()) { - Logging.warning("Failed to delete file: " + file); + throw new IllegalStateException("Failed to delete file: " + file); } }); return Try.success(null); @@ -127,6 +141,17 @@ public Try deleteFolder(Path path) { } } + private boolean isDirectoryEmpty(File file) { + if (!file.isDirectory()) { + return true; + } + try (Stream entries = Files.list(file.toPath())) { + return entries.findFirst().isEmpty(); + } catch (IOException e) { + return true; + } + } + /** * Copies all the content of the given folder to the given target folder. * diff --git a/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java b/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java index 22b376746..53e400b50 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java @@ -55,6 +55,7 @@ import org.mvplugins.multiverse.core.world.helpers.WorldNameChecker; import org.mvplugins.multiverse.core.world.options.CloneWorldOptions; import org.mvplugins.multiverse.core.world.options.CreateWorldOptions; +import org.mvplugins.multiverse.core.world.options.DeleteWorldOptions; import org.mvplugins.multiverse.core.world.options.ImportWorldOptions; import org.mvplugins.multiverse.core.world.options.KeepWorldSettingsOptions; import org.mvplugins.multiverse.core.world.options.RegenWorldOptions; @@ -211,7 +212,7 @@ private Attempt validateCreateWorldOpti return worldActionResult(CreateFailureReason.WORLD_EXIST_LOADED, options.worldName()); } else if (getWorld(options.worldName()).isDefined()) { return worldActionResult(CreateFailureReason.WORLD_EXIST_UNLOADED, options.worldName()); - } else if (worldNameChecker.hasWorldFolder(options.worldName())) { + } else if (options.doFolderCheck() && worldNameChecker.hasWorldFolder(options.worldName())) { return worldActionResult(CreateFailureReason.WORLD_EXIST_FOLDER, options.worldName()); } return worldActionResult(options); @@ -506,28 +507,15 @@ private Attempt removeWorldFromConfig(@NotNull Mult * Deletes an existing multiverse world entirely. World will be loaded if it is not already loaded. * Warning: This will delete all world files. * - * @param worldName The name of the world to delete. + * @param options The options for customizing the deletion of a world. * @return The result of the delete action. */ - public Attempt deleteWorld(@NotNull String worldName) { - return getWorld(worldName) - .map(this::deleteWorld) - .getOrElse(() -> worldActionResult(DeleteFailureReason.WORLD_NON_EXISTENT, worldName)); - } - - /** - * Deletes an existing multiverse world entirely. World will be loaded if it is not already loaded. - * Warning: This will delete all world files. - * - * @param world The world to delete. - * @return The result of the delete action. - */ - public Attempt deleteWorld(@NotNull MultiverseWorld world) { - return getLoadedWorld(world).fold( - () -> loadWorld(world) + public Attempt deleteWorld(@NotNull DeleteWorldOptions options) { + return getLoadedWorld(options.world()).fold( + () -> loadWorld(options.world()) .transform(DeleteFailureReason.LOAD_FAILED) - .mapAttempt(this::deleteWorld), - this::deleteWorld); + .mapAttempt(world -> doDeleteWorld(world, options)), + world -> doDeleteWorld(world, options)); } /** @@ -536,7 +524,7 @@ public Attempt deleteWorld(@NotNull MultiverseWorld * @param world The multiverse world to delete. * @return The result of the delete action. */ - public Attempt deleteWorld(@NotNull LoadedMultiverseWorld world) { + private Attempt doDeleteWorld(@NotNull LoadedMultiverseWorld world, @NotNull DeleteWorldOptions options) { AtomicReference worldFolder = new AtomicReference<>(); return validateWorldToDelete(world) .peek(worldFolder::set) @@ -548,7 +536,7 @@ public Attempt deleteWorld(@NotNull LoadedMultivers : Attempt.success(null); }) .mapAttempt(() -> removeWorld(world).transform(DeleteFailureReason.REMOVE_FAILED)) - .mapAttempt(() -> fileUtils.deleteFolder(worldFolder.get()).fold( + .mapAttempt(() -> fileUtils.deleteFolder(worldFolder.get(), options.keepFiles()).fold( exception -> worldActionResult(DeleteFailureReason.FAILED_TO_DELETE_FOLDER, world.getName(), exception), success -> worldActionResult(world.getName()))); @@ -665,9 +653,10 @@ public Attempt regenWorld(@NotNull Re .generator(world.getGenerator()) .seed(options.seed()) .useSpawnAdjust(!shouldKeepSpawnLocation && world.getAdjustSpawn()) - .worldType(world.getWorldType().getOrElse(WorldType.NORMAL)); + .worldType(world.getWorldType().getOrElse(WorldType.NORMAL)) + .doFolderCheck(options.keepFiles().isEmpty()); - return deleteWorld(world) + return deleteWorld(DeleteWorldOptions.world(world).keepFiles(options.keepFiles())) .transform(RegenFailureReason.DELETE_FAILED) .mapAttempt(() -> createWorld(createWorldOptions).transform(RegenFailureReason.CREATE_FAILED)) .onSuccess(newWorld -> { diff --git a/src/main/java/org/mvplugins/multiverse/core/world/options/CreateWorldOptions.java b/src/main/java/org/mvplugins/multiverse/core/world/options/CreateWorldOptions.java index 6ed051aaa..9ea1d4ebf 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/options/CreateWorldOptions.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/options/CreateWorldOptions.java @@ -30,6 +30,7 @@ public final class CreateWorldOptions { private long seed; private boolean useSpawnAdjust = true; private WorldType worldType = WorldType.NORMAL; + private boolean doFolderCheck = true; /** * Creates a new {@link CreateWorldOptions} instance with the given world name. @@ -239,4 +240,24 @@ public boolean useSpawnAdjust() { public @NotNull WorldType worldType() { return worldType; } + + /** + * Sets whether to ensure folder does not exist before creating the world. + * + * @param doFolderCheckInput Whether to do the folder check + * @return This {@link CreateWorldOptions} instance + */ + public @NotNull CreateWorldOptions doFolderCheck(boolean doFolderCheckInput) { + this.doFolderCheck = doFolderCheckInput; + return this; + } + + /** + * Gets whether to ensure folder does not exist before creating the world. + * + * @return Whether to do the folder check + */ + public boolean doFolderCheck() { + return doFolderCheck; + } } diff --git a/src/main/java/org/mvplugins/multiverse/core/world/options/DeleteWorldOptions.java b/src/main/java/org/mvplugins/multiverse/core/world/options/DeleteWorldOptions.java new file mode 100644 index 000000000..d8023e606 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/world/options/DeleteWorldOptions.java @@ -0,0 +1,54 @@ +package org.mvplugins.multiverse.core.world.options; + +import org.jetbrains.annotations.NotNull; +import org.mvplugins.multiverse.core.world.MultiverseWorld; + +import java.util.Collections; +import java.util.List; + +/** + * Options for customizing the deletion of a world. + */ +public final class DeleteWorldOptions { + + /** + * Creates a new {@link DeleteWorldOptions} instance with the given world. + * + * @param world The world to delete. + * @return A new {@link DeleteWorldOptions} instance. + */ + public static @NotNull DeleteWorldOptions world(@NotNull MultiverseWorld world) { + return new DeleteWorldOptions(world); + } + + private final MultiverseWorld world; + private List keepFiles = Collections.emptyList(); + + DeleteWorldOptions(MultiverseWorld world) { + this.world = world; + } + + public MultiverseWorld world() { + return world; + } + + /** + * Sets the files to keep during deletion. + * + * @param keepFilesInput The files to keep during deletion. + * @return This {@link DeleteWorldOptions} instance. + */ + public @NotNull DeleteWorldOptions keepFiles(List keepFilesInput) { + this.keepFiles = keepFilesInput == null ? Collections.emptyList() : keepFilesInput.stream().toList(); + return this; + } + + /** + * Gets the files to keep during deletion. Note: The list is unmodifiable. + * + * @return The files to keep during deletion. + */ + public List keepFiles() { + return keepFiles; + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/world/options/RegenWorldOptions.java b/src/main/java/org/mvplugins/multiverse/core/world/options/RegenWorldOptions.java index a246d95e4..340ac08ed 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/options/RegenWorldOptions.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/options/RegenWorldOptions.java @@ -7,6 +7,9 @@ import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld; +import java.util.Collections; +import java.util.List; + /** * Options for customizing the regeneration of a world. */ @@ -31,6 +34,7 @@ public final class RegenWorldOptions implements KeepWorldSettingsOptions { private boolean keepWorldBorder = true; private boolean randomSeed = false; private long seed = UNINITIALIZED_SEED_VALUE; + private List keepFiles = Collections.emptyList(); RegenWorldOptions(@NotNull LoadedMultiverseWorld world) { this.world = world; @@ -211,4 +215,24 @@ public long seed() { } return world.getSeed(); } + + /** + * Sets the files to keep during regeneration. + * + * @param keepFilesInput The files to keep during regeneration. + * @return This {@link RegenWorldOptions} instance. + */ + public @NotNull RegenWorldOptions keepFiles(@Nullable List keepFilesInput) { + this.keepFiles = keepFilesInput == null ? Collections.emptyList() : keepFilesInput.stream().toList(); + return this; + } + + /** + * Gets the files to keep during regeneration. Note: The list is unmodifiable. + * + * @return The files to keep during regeneration. + */ + public @NotNull List keepFiles() { + return keepFiles; + } } diff --git a/src/test/java/org/mvplugins/multiverse/core/world/WorldManagerTest.kt b/src/test/java/org/mvplugins/multiverse/core/world/WorldManagerTest.kt index cb4929c8b..634f82aa5 100644 --- a/src/test/java/org/mvplugins/multiverse/core/world/WorldManagerTest.kt +++ b/src/test/java/org/mvplugins/multiverse/core/world/WorldManagerTest.kt @@ -9,6 +9,7 @@ import org.mvplugins.multiverse.core.TestWithMockBukkit import org.mvplugins.multiverse.core.event.world.* import org.mvplugins.multiverse.core.world.options.CloneWorldOptions import org.mvplugins.multiverse.core.world.options.CreateWorldOptions +import org.mvplugins.multiverse.core.world.options.DeleteWorldOptions import org.mvplugins.multiverse.core.world.options.RegenWorldOptions import org.mvplugins.multiverse.core.world.options.UnloadWorldOptions import org.mvplugins.multiverse.core.world.reasons.CloneFailureReason @@ -112,8 +113,10 @@ class WorldManagerTest : TestWithMockBukkit() { @Test fun `Delete world`() { - assertTrue(worldManager.deleteWorld(world).isSuccess) + assertTrue(File(Bukkit.getWorldContainer(), "world").isDirectory) + assertTrue(worldManager.deleteWorld(DeleteWorldOptions.world(world)).isSuccess) assertFalse(worldManager.getLoadedWorld("world").isDefined) + assertFalse(File(Bukkit.getWorldContainer(), "world").isDirectory) assertThat(server.pluginManager, hasFiredEventInstance(MVWorldDeleteEvent::class.java)) assertThat(server.pluginManager, hasFiredEventInstance(MVWorldUnloadedEvent::class.java))