diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackExportTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackExportTask.java index 4abb4eb86..347861c4c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackExportTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackExportTask.java @@ -17,12 +17,11 @@ */ package org.jackhuang.hmcl.game; - import org.jackhuang.hmcl.mod.Modpack; -import org.jackhuang.hmcl.task.TaskResult; +import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.Constants; import org.jackhuang.hmcl.util.Logging; -import org.jackhuang.hmcl.util.ZipEngine; +import org.jackhuang.hmcl.util.Zipper; import java.io.File; import java.util.ArrayList; @@ -31,58 +30,44 @@ import java.util.List; /** * Export the game to a mod pack file. */ -public class HMCLModpackExportTask extends TaskResult { +public class HMCLModpackExportTask extends Task { private final DefaultGameRepository repository; private final String version; private final List whitelist; private final Modpack modpack; private final File output; - private final String id; - - public HMCLModpackExportTask(DefaultGameRepository repository, String version, List whitelist, Modpack modpack, File output) { - this(repository, version, whitelist, modpack, output, ID); - } /** * @param output mod pack file. * @param version to locate version.json */ - public HMCLModpackExportTask(DefaultGameRepository repository, String version, List whitelist, Modpack modpack, File output, String id) { + public HMCLModpackExportTask(DefaultGameRepository repository, String version, List whitelist, Modpack modpack, File output) { this.repository = repository; this.version = version; this.whitelist = whitelist; this.modpack = modpack; this.output = output; - this.id = id; onDone().register(event -> { if (event.isFailed()) output.delete(); }); } - @Override - public String getId() { - return id; - } - @Override public void execute() throws Exception { ArrayList blackList = new ArrayList<>(HMCLModpackManager.MODPACK_BLACK_LIST); blackList.add(version + ".jar"); blackList.add(version + ".json"); Logging.LOG.info("Compressing game files without some files in blacklist, including files or directories: usernamecache.json, asm, logs, backups, versions, assets, usercache.json, libraries, crash-reports, launcher_profiles.json, NVIDIA, TCNodeTracker"); - try (ZipEngine zip = new ZipEngine(output)) { - zip.putDirectory(repository.getRunDirectory(version), (String pathName, Boolean isDirectory) -> { + try (Zipper zip = new Zipper(output.toPath())) { + zip.putDirectory(repository.getRunDirectory(version).toPath(), "minecraft", path -> { for (String s : blackList) - if (isDirectory) { - if (pathName.startsWith(s + "/")) - return null; - } else if (pathName.equals(s)) - return null; + if (path.equals(s)) + return false; for (String s : whitelist) - if (pathName.equals(s + (isDirectory ? "/" : ""))) - return "minecraft/" + pathName; - return null; + if (path.equals(s)) + return true; + return false; }); Version mv = repository.getResolvedVersion(version); @@ -92,6 +77,4 @@ public class HMCLModpackExportTask extends TaskResult { zip.putTextFile(Constants.GSON.toJson(modpack.setGameVersion(gameVersion)), "modpack.json"); // Newer HMCL only reads 'gameVersion' field. } } - - public static final String ID = "zip_engine"; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java index 0fcb82976..b90162f04 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java @@ -74,7 +74,7 @@ public final class HMCLModpackInstallTask extends Task { } } catch (JsonParseException | IOException ignore) { } - dependents.add(new ModpackInstallTask<>(zipFile, run, "minecraft/", it -> !Objects.equals(it, "minecraft/pack.json"), config)); + dependents.add(new ModpackInstallTask<>(zipFile, run, "/minecraft", it -> !"pack.json".equals(it), config)); } @Override @@ -90,9 +90,9 @@ public final class HMCLModpackInstallTask extends Task { @Override public void execute() throws Exception { String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json"); - Version version = Constants.GSON.fromJson(json, Version.class).setJar(null); + Version version = Constants.GSON.fromJson(json, Version.class).setId(name).setJar(null); dependencies.add(new VersionJsonSaveTask(repository, version)); - dependencies.add(new MinecraftInstanceTask<>(zipFile, "minecraft/", modpack, MODPACK_TYPE, repository.getModpackConfiguration(name))); + dependencies.add(new MinecraftInstanceTask<>(zipFile, "/minecraft", modpack, MODPACK_TYPE, repository.getModpackConfiguration(name))); } public static final String MODPACK_TYPE = "HMCL"; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackManager.java index 89544be28..3aad7431e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackManager.java @@ -39,7 +39,7 @@ public final class HMCLModpackManager { "pack.json", "launcher.jar", "hmclmc.log", // HMCL "manifest.json", "minecraftinstance.json", ".curseclient", // Curse "minetweaker.log", // Mods - "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft + "jars", "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft "downloads", // Curse "asm", "backups", "TCNodeTracker", "CustomDISkins", "data" // Mods ); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java index 12fc3778f..e8d9618ae 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java @@ -86,11 +86,13 @@ public final class ModpackHelper { profile.getRepository().markVersionAsModpack(name); FinalizedCallback finalizeTask = (variables, isDependentsSucceeded) -> { - profile.getRepository().refreshVersions(); - VersionSetting vs = profile.specializeVersionSetting(name); - profile.getRepository().undoMark(name); - if (vs != null) - vs.setGameDirType(EnumGameDirectory.VERSION_FOLDER); + if (isDependentsSucceeded) { + profile.getRepository().refreshVersions(); + VersionSetting vs = profile.specializeVersionSetting(name); + profile.getRepository().undoMark(name); + if (vs != null) + vs.setGameDirType(EnumGameDirectory.VERSION_FOLDER); + } }; if (modpack.getManifest() instanceof CurseManifest) @@ -102,7 +104,7 @@ public final class ModpackHelper { else if (modpack.getManifest() instanceof MultiMCInstanceConfiguration) return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name) .finalized(finalizeTask) - .with(new MultiMCInstallVersionSettingTask(profile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name)); + .then(new MultiMCInstallVersionSettingTask(profile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name)); else throw new IllegalStateException("Unrecognized modpack: " + modpack); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackPage.java index 3c5b435d9..1716c2b64 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackPage.java @@ -98,6 +98,7 @@ public final class ModpackPage extends StackPane implements WizardPage { txtModpackName.setText(manifest.getName() + (StringUtils.isBlank(manifest.getVersion()) ? "" : "-" + manifest.getVersion())); } catch (UnsupportedModpackException e) { txtModpackName.setText(i18n("modpack.task.install.error")); + btnInstall.setDisable(true); } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ExportWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ExportWizardProvider.java index f495340a6..4a9ee699e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ExportWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ExportWizardProvider.java @@ -28,7 +28,7 @@ import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.wizard.WizardController; import org.jackhuang.hmcl.ui.wizard.WizardProvider; -import org.jackhuang.hmcl.util.ZipEngine; +import org.jackhuang.hmcl.util.Zipper; import java.io.File; import java.nio.file.Files; @@ -81,7 +81,7 @@ public final class ExportWizardProvider implements WizardProvider { dependency = dependency.then(Task.of(() -> { boolean flag = true; - try (ZipEngine zip = new ZipEngine(modpackFile)) { + try (Zipper zip = new Zipper(modpackFile.toPath())) { Config exported = new Config(); exported.setBackgroundImageType(config().getBackgroundImageType()); exported.setBackgroundImage(config().getBackgroundImage()); @@ -93,7 +93,7 @@ public final class ExportWizardProvider implements WizardProvider { File bg = new File("bg").getAbsoluteFile(); if (bg.isDirectory()) - zip.putDirectory(bg); + zip.putDirectory(bg.toPath(), "bg"); File background_png = new File("background.png").getAbsoluteFile(); if (background_png.isFile()) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibraryDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibraryDownloadTask.java index 9340a9db6..b01a6b508 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibraryDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibraryDownloadTask.java @@ -1,6 +1,5 @@ package org.jackhuang.hmcl.download.game; -import org.apache.commons.compress.compressors.xz.XZCompressorInputStream; import org.jackhuang.hmcl.download.AbstractDependencyManager; import org.jackhuang.hmcl.game.Library; import org.jackhuang.hmcl.task.FileDownloadTask; @@ -10,6 +9,7 @@ import org.jackhuang.hmcl.util.FileUtils; import org.jackhuang.hmcl.util.IOUtils; import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.NetworkUtils; +import org.tukaani.xz.XZInputStream; import java.io.*; import java.nio.charset.Charset; @@ -145,7 +145,7 @@ public final class LibraryDownloadTask extends Task { if (!dest.delete()) throw new IOException("Unable to delete file " + dest); - byte[] decompressed = IOUtils.readFullyAsByteArray(new XZCompressorInputStream(new ByteArrayInputStream(src))); + byte[] decompressed = IOUtils.readFullyAsByteArray(new XZInputStream(new ByteArrayInputStream(src))); String end = new String(decompressed, decompressed.length - 4, 4); if (!end.equals("SIGN")) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java index 67cb425fb..c6678f7c2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java @@ -148,10 +148,10 @@ public class DefaultGameRepository implements GameRepository { } public boolean removeVersionFromDisk(String id) { - if (!versions.containsKey(id)) - return true; if (EventBus.EVENT_BUS.fireEvent(new RemoveVersionEvent(this, id)) == Event.Result.DENY) return false; + if (!versions.containsKey(id)) + return FileUtils.deleteDirectoryQuietly(getVersionRoot(id)); File file = getVersionRoot(id); if (!file.exists()) return true; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameVersion.java index 801f6a400..ea73afbf5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameVersion.java @@ -17,8 +17,7 @@ */ package org.jackhuang.hmcl.game; -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; -import org.apache.commons.compress.archivers.zip.ZipFile; +import org.jackhuang.hmcl.util.CompressingUtils; import org.jenkinsci.constant_pool_scanner.ConstantPool; import org.jenkinsci.constant_pool_scanner.ConstantPoolScanner; import org.jenkinsci.constant_pool_scanner.ConstantType; @@ -26,6 +25,9 @@ import org.jenkinsci.constant_pool_scanner.StringConstant; import java.io.File; import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -35,8 +37,8 @@ import java.util.stream.StreamSupport; * @author huangyuhui */ public final class GameVersion { - private static Optional getVersionOfClassMinecraft(ZipFile file, ZipArchiveEntry entry) throws IOException { - ConstantPool pool = ConstantPoolScanner.parse(file.getInputStream(entry), ConstantType.STRING); + private static Optional getVersionOfClassMinecraft(byte[] bytecode) throws IOException { + ConstantPool pool = ConstantPoolScanner.parse(bytecode, ConstantType.STRING); return StreamSupport.stream(pool.list(StringConstant.class).spliterator(), false) .map(StringConstant::get) @@ -45,8 +47,8 @@ public final class GameVersion { .findFirst(); } - private static Optional getVersionFromClassMinecraftServer(ZipFile file, ZipArchiveEntry entry) throws IOException { - ConstantPool pool = ConstantPoolScanner.parse(file.getInputStream(entry), ConstantType.STRING); + private static Optional getVersionFromClassMinecraftServer(byte[] bytecode) throws IOException { + ConstantPool pool = ConstantPoolScanner.parse(bytecode, ConstantType.STRING); List list = StreamSupport.stream(pool.list(StringConstant.class).spliterator(), false) .map(StringConstant::get) @@ -72,16 +74,16 @@ public final class GameVersion { return Optional.empty(); try { - try (ZipFile gameJar = new ZipFile(file)) { - ZipArchiveEntry minecraft = gameJar.getEntry("net/minecraft/client/Minecraft.class"); - if (minecraft != null) { - Optional result = getVersionOfClassMinecraft(gameJar, minecraft); + try (FileSystem gameJar = CompressingUtils.createReadOnlyZipFileSystem(file.toPath())) { + Path minecraft = gameJar.getPath("net/minecraft/client/Minecraft.class"); + if (Files.exists(minecraft)) { + Optional result = getVersionOfClassMinecraft(Files.readAllBytes(minecraft)); if (result.isPresent()) return result; } - ZipArchiveEntry minecraftServer = gameJar.getEntry("net/minecraft/server/MinecraftServer.class"); - if (minecraftServer != null) - return getVersionFromClassMinecraftServer(gameJar, minecraftServer); + Path minecraftServer = gameJar.getPath("net/minecraft/server/MinecraftServer.class"); + if (Files.exists(minecraftServer)) + return getVersionFromClassMinecraftServer(Files.readAllBytes(minecraftServer)); return Optional.empty(); } } catch (IOException e) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java index 8d764f534..5f2817320 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java @@ -229,11 +229,9 @@ public class DefaultLauncher extends Launcher { try { for (Library library : version.getLibraries()) if (library.isNative()) - CompressingUtils.unzip(repository.getLibraryFile(version, library), - destination, - "", - library.getExtract()::shouldExtract, - false); + new Unzipper(repository.getLibraryFile(version, library), destination) + .setFilter((destFile, isDirectory, zipEntry, path) -> library.getExtract().shouldExtract(path)) + .setReplaceExistentFile(true).unzip(); } catch (IOException e) { throw new NotDecompressingNativesException(e); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/CurseInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/CurseInstallTask.java index c4116d2ad..3bcc53eea 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/CurseInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/CurseInstallTask.java @@ -76,6 +76,10 @@ public final class CurseInstallTask extends Task { builder.version("forge", modLoader.getId().substring("forge-".length())); dependents.add(builder.buildAsync()); + onDone().register(event -> { + if (event.isFailed()) repository.removeVersionFromDisk(name); + }); + ModpackConfiguration config = null; try { if (json.exists()) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ForgeModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ForgeModMetadata.java index 65487bfd5..1836fa68c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ForgeModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ForgeModMetadata.java @@ -20,15 +20,13 @@ package org.jackhuang.hmcl.mod; import com.google.gson.JsonParseException; import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; -import org.apache.commons.compress.archivers.zip.ZipFile; -import org.jackhuang.hmcl.util.Constants; -import org.jackhuang.hmcl.util.IOUtils; -import org.jackhuang.hmcl.util.Immutable; -import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.*; import java.io.File; import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; /** @@ -114,11 +112,11 @@ public final class ForgeModMetadata { } public static ModInfo fromFile(File modFile) throws IOException, JsonParseException { - try (ZipFile zipFile = new ZipFile(modFile)) { - ZipArchiveEntry entry = zipFile.getEntry("mcmod.info"); - if (entry == null) + try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.toPath())) { + Path mcmod = fs.getPath("mcmod.info"); + if (Files.notExists(mcmod)) throw new IOException("File " + modFile + " is not a Forge mod."); - List modList = Constants.GSON.fromJson(IOUtils.readFullyAsString(zipFile.getInputStream(entry)), + List modList = Constants.GSON.fromJson(IOUtils.readFullyAsString(Files.newInputStream(mcmod)), new TypeToken>() { }.getType()); if (modList == null || modList.isEmpty()) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MinecraftInstanceTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MinecraftInstanceTask.java index ec4cf1c68..3c7a4c923 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MinecraftInstanceTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MinecraftInstanceTask.java @@ -17,14 +17,15 @@ */ package org.jackhuang.hmcl.mod; -import org.apache.commons.compress.archivers.ArchiveEntry; -import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.CompressingUtils; import org.jackhuang.hmcl.util.Constants; import org.jackhuang.hmcl.util.FileUtils; import java.io.File; -import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; import java.util.LinkedList; import java.util.List; @@ -41,7 +42,7 @@ public final class MinecraftInstanceTask extends Task { public MinecraftInstanceTask(File zipFile, String subDirectory, T manifest, String type, File jsonFile) { this.zipFile = zipFile; - this.subDirectory = subDirectory; + this.subDirectory = FileUtils.normalizePath(subDirectory); this.manifest = manifest; this.jsonFile = jsonFile; this.type = type; @@ -54,18 +55,18 @@ public final class MinecraftInstanceTask extends Task { public void execute() throws Exception { List overrides = new LinkedList<>(); - try (ZipArchiveInputStream zip = new ZipArchiveInputStream(new FileInputStream(zipFile), null, true, true)) { - ArchiveEntry entry; - while ((entry = zip.getNextEntry()) != null) { - String path = entry.getName(); - if (!path.startsWith(subDirectory) || entry.isDirectory()) - continue; - path = path.substring(subDirectory.length()); - if (path.startsWith("/") || path.startsWith("\\")) - path = path.substring(1); + try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile.toPath())) { + Path root = fs.getPath(subDirectory); - overrides.add(new ModpackConfiguration.FileInformation(path, encodeHex(digest("SHA-1", zip)))); - } + if (Files.exists(root)) + Files.walkFileTree(fs.getPath(subDirectory), new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + String relativePath = root.relativize(file).normalize().toString(); + overrides.add(new ModpackConfiguration.FileInformation(relativePath, encodeHex(digest("SHA-1", Files.newInputStream(file))))); + return FileVisitResult.CONTINUE; + } + }); } FileUtils.writeText(jsonFile, Constants.GSON.toJson(new ModpackConfiguration<>(manifest, type, overrides))); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackInstallTask.java index a35e38892..8f95cc725 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackInstallTask.java @@ -17,17 +17,15 @@ */ package org.jackhuang.hmcl.mod; -import org.apache.commons.compress.archivers.ArchiveEntry; -import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.FileUtils; import org.jackhuang.hmcl.util.IOUtils; +import org.jackhuang.hmcl.util.Unzipper; -import java.io.*; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.*; import java.util.function.Predicate; import static org.jackhuang.hmcl.util.DigestUtils.digest; @@ -56,61 +54,36 @@ public class ModpackInstallTask extends Task { @Override public void execute() throws Exception { Set entries = new HashSet<>(); - byte[] buf = new byte[IOUtils.DEFAULT_BUFFER_SIZE]; if (!FileUtils.makeDirectory(dest)) throw new IOException("Unable to make directory " + dest); - HashSet files = new HashSet<>(); + HashMap files = new HashMap<>(); for (ModpackConfiguration.FileInformation file : overrides) - files.add(file.getPath()); + files.put(file.getPath(), file); - try (ZipArchiveInputStream zipStream = new ZipArchiveInputStream(new FileInputStream(modpackFile), null, true, true)) { - ArchiveEntry entry; - while ((entry = zipStream.getNextEntry()) != null) { - String path = entry.getName(); + new Unzipper(modpackFile, dest) + .setSubDirectory(subDirectory) + .setTerminateIfSubDirectoryNotExists() + .setReplaceExistentFile(true) + .setFilter((destPath, isDirectory, zipEntry, entryPath) -> { + if (isDirectory) return true; + entries.add(entryPath); - if (!path.startsWith(subDirectory)) - continue; - path = path.substring(subDirectory.length()); - if (path.startsWith("/") || path.startsWith("\\")) - path = path.substring(1); - File entryFile = new File(dest, path); - - if (callback != null) - if (!callback.test(path)) - continue; - - if (entry.isDirectory()) { - if (!FileUtils.makeDirectory(entryFile)) - throw new IOException("Unable to make directory: " + entryFile); - } else { - if (!FileUtils.makeDirectory(entryFile.getAbsoluteFile().getParentFile())) - throw new IOException("Unable to make parent directory for file " + entryFile); - - entries.add(path); - - ByteArrayOutputStream os = new ByteArrayOutputStream(IOUtils.DEFAULT_BUFFER_SIZE); - IOUtils.copyTo(zipStream, os, buf); - byte[] data = os.toByteArray(); - - if (files.contains(path) && entryFile.exists()) { - String oldHash = encodeHex(digest("SHA-1", new FileInputStream(entryFile))); - String newHash = encodeHex(digest("SHA-1", new ByteArrayInputStream(data))); - if (!oldHash.equals(newHash)) { - try (FileOutputStream fos = new FileOutputStream(entryFile)) { - IOUtils.copyTo(new ByteArrayInputStream(data), fos, buf); - } - } - } else if (!files.contains(path)) { - try (FileOutputStream fos = new FileOutputStream(entryFile)) { - IOUtils.copyTo(new ByteArrayInputStream(data), fos, buf); - } + if (!files.containsKey(entryPath)) { + // If old modpack does not have this entry, add this entry or override the file that user added. + return true; + } else if (!Files.exists(destPath)) { + // If both old and new modpacks have this entry, but the file is deleted by user, leave it missing. + return false; + } else { + // If user modified this entry file, we will not replace this file since this modified file is that user expects. + String fileHash = encodeHex(digest("SHA-1", Files.newInputStream(destPath))); + String oldHash = files.get(entryPath).getHash(); + return Objects.equals(oldHash, fileHash); } + }).unzip(); - } - } - } - + // If old modpack have this entry, and new modpack deleted it. Delete this file. for (ModpackConfiguration.FileInformation file : overrides) { File original = new File(dest, file.getPath()); if (original.exists() && !entries.contains(file.getPath())) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MultiMCInstanceConfiguration.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MultiMCInstanceConfiguration.java index bbdf22222..c93617b67 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MultiMCInstanceConfiguration.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MultiMCInstanceConfiguration.java @@ -17,14 +17,15 @@ */ package org.jackhuang.hmcl.mod; -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; -import org.apache.commons.compress.archivers.zip.ZipFile; +import org.jackhuang.hmcl.util.CompressingUtils; import org.jackhuang.hmcl.util.Lang; -import org.jackhuang.hmcl.util.StringUtils; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Optional; import java.util.Properties; @@ -262,15 +263,17 @@ public final class MultiMCInstanceConfiguration { return mmcPack; } - public static Modpack readMultiMCModpackManifest(File f) throws IOException { - try (ZipFile zipFile = new ZipFile(f)) { - ZipArchiveEntry firstEntry = zipFile.getEntries().nextElement(); - String name = StringUtils.substringBefore(firstEntry.getName(), '/'); - ZipArchiveEntry entry = zipFile.getEntry(name + "/instance.cfg"); - if (entry == null) - throw new IOException("`instance.cfg` not found, " + f + " is not a valid MultiMC modpack."); - MultiMCManifest manifest = MultiMCManifest.readMultiMCModpackManifest(f); - MultiMCInstanceConfiguration cfg = new MultiMCInstanceConfiguration(name, zipFile.getInputStream(entry), manifest); + public static Modpack readMultiMCModpackManifest(File modpackFile) throws IOException { + MultiMCManifest manifest = MultiMCManifest.readMultiMCModpackManifest(modpackFile); + try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modpackFile.toPath())) { + Path root = Files.list(fs.getPath("/")).filter(Files::isDirectory).findAny() + .orElseThrow(() -> new IOException("Not a valid MultiMC modpack")); + String name = root.normalize().getFileName().toString(); + + Path instancePath = root.resolve("instance.cfg"); + if (Files.notExists(instancePath)) + throw new IOException("`instance.cfg` not found, " + modpackFile + " is not a valid MultiMC modpack."); + MultiMCInstanceConfiguration cfg = new MultiMCInstanceConfiguration(name, Files.newInputStream(instancePath), manifest); return new Modpack(cfg.getName(), "", "", cfg.getGameVersion(), cfg.getNotes(), cfg); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MultiMCManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MultiMCManifest.java index 933d99695..7908027fd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MultiMCManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MultiMCManifest.java @@ -17,14 +17,17 @@ */ package org.jackhuang.hmcl.mod; -import com.google.gson.JsonParseException; import com.google.gson.annotations.SerializedName; -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; -import org.apache.commons.compress.archivers.zip.ZipFile; -import org.jackhuang.hmcl.util.*; +import org.jackhuang.hmcl.util.CompressingUtils; +import org.jackhuang.hmcl.util.IOUtils; +import org.jackhuang.hmcl.util.Immutable; +import org.jackhuang.hmcl.util.JsonUtils; import java.io.File; import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; @Immutable @@ -49,14 +52,14 @@ public final class MultiMCManifest { return components; } - public static MultiMCManifest readMultiMCModpackManifest(File f) throws IOException { - try (ZipFile zipFile = new ZipFile(f)) { - ZipArchiveEntry firstEntry = zipFile.getEntries().nextElement(); - String name = StringUtils.substringBefore(firstEntry.getName(), '/'); - ZipArchiveEntry entry = zipFile.getEntry(name + "/mmc-pack.json"); - if (entry == null) + public static MultiMCManifest readMultiMCModpackManifest(File zipFile) throws IOException { + try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile.toPath())) { + Path root = Files.list(fs.getPath("/")).filter(Files::isDirectory).findAny() + .orElseThrow(() -> new IOException("Not a valid MultiMC modpack")); + Path mmcPack = root.resolve("mmc-pack.json"); + if (Files.notExists(mmcPack)) return null; - String json = IOUtils.readFullyAsString(zipFile.getInputStream(entry)); + String json = IOUtils.readFullyAsString(Files.newInputStream(mmcPack)); MultiMCManifest manifest = JsonUtils.fromNonNullJson(json, MultiMCManifest.class); if (manifest != null && manifest.getComponents() == null) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MultiMCModpackInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MultiMCModpackInstallTask.java index eb7f291e8..ba0c91082 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MultiMCModpackInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MultiMCModpackInstallTask.java @@ -19,8 +19,6 @@ package org.jackhuang.hmcl.mod; import com.google.gson.JsonParseException; import com.google.gson.reflect.TypeToken; -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; -import org.apache.commons.compress.archivers.zip.ZipFile; import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.GameBuilder; import org.jackhuang.hmcl.download.game.VersionJsonSaveTask; @@ -28,13 +26,13 @@ import org.jackhuang.hmcl.game.Arguments; import org.jackhuang.hmcl.game.DefaultGameRepository; import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.util.Constants; -import org.jackhuang.hmcl.util.FileUtils; -import org.jackhuang.hmcl.util.IOUtils; -import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.*; import java.io.File; import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.LinkedList; import java.util.List; import java.util.Objects; @@ -98,7 +96,7 @@ public final class MultiMCModpackInstallTask extends Task { } catch (JsonParseException | IOException ignore) { } - dependents.add(new ModpackInstallTask<>(zipFile, run, manifest.getName() + "/minecraft/", Constants.truePredicate(), config)); + dependents.add(new ModpackInstallTask<>(zipFile, run, "/" + manifest.getName() + "/minecraft", Constants.truePredicate(), config)); } @Override @@ -115,26 +113,30 @@ public final class MultiMCModpackInstallTask extends Task { public void execute() throws Exception { Version version = Objects.requireNonNull(repository.readVersionJson(name)); - try (ZipFile zip = new ZipFile(zipFile)) { - for (ZipArchiveEntry entry : Lang.asIterable(zip.getEntries())) { - // ensure that this entry is in folder 'patches' and is a json file. - if (!entry.isDirectory() && entry.getName().startsWith(manifest.getName() + "/patches/") && entry.getName().endsWith(".json")) { - MultiMCInstancePatch patch = Constants.GSON.fromJson(IOUtils.readFullyAsString(zip.getInputStream(entry)), MultiMCInstancePatch.class); - List newArguments = new LinkedList<>(); - for (String arg : patch.getTweakers()) { - newArguments.add("--tweakClass"); - newArguments.add(arg); + try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile.toPath())) { + Path root = Files.list(fs.getPath("/")).filter(Files::isDirectory).findAny() + .orElseThrow(() -> new IOException("Not a valid MultiMC modpack")); + Path patches = root.resolve("patches"); + + if (Files.exists(patches)) + for (Path patchJson : Files.newDirectoryStream(patches)) { + if (patchJson.endsWith(".json")) { + MultiMCInstancePatch patch = Constants.GSON.fromJson(IOUtils.readFullyAsString(Files.newInputStream(patchJson)), MultiMCInstancePatch.class); + List newArguments = new LinkedList<>(); + for (String arg : patch.getTweakers()) { + newArguments.add("--tweakClass"); + newArguments.add(arg); + } + version = version + .setLibraries(Lang.merge(version.getLibraries(), patch.getLibraries())) + .setMainClass(patch.getMainClass()) + .setArguments(version.getArguments().orElseGet(Arguments::new).addGameArguments(newArguments)); } - version = version - .setLibraries(Lang.merge(version.getLibraries(), patch.getLibraries())) - .setMainClass(patch.getMainClass()) - .setArguments(version.getArguments().orElseGet(Arguments::new).addGameArguments(newArguments)); } - } } dependencies.add(new VersionJsonSaveTask(repository, version)); - dependencies.add(new MinecraftInstanceTask<>(zipFile, manifest.getName() + "/minecraft/", manifest, MODPACK_TYPE, repository.getModpackConfiguration(name))); + dependencies.add(new MinecraftInstanceTask<>(zipFile, "/" + manifest.getName() + "/minecraft", manifest, MODPACK_TYPE, repository.getModpackConfiguration(name))); } public static final String MODPACK_TYPE = "MultiMC"; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CompressingUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CompressingUtils.java index c3e1fc59b..6ba4eed65 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CompressingUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CompressingUtils.java @@ -17,16 +17,16 @@ */ package org.jackhuang.hmcl.util; -import org.apache.commons.compress.archivers.ArchiveEntry; -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; -import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; -import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; -import org.apache.commons.compress.archivers.zip.ZipFile; - -import java.io.*; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; -import java.util.function.BiFunction; -import java.util.function.Predicate; /** * Utilities of compressing @@ -38,176 +38,44 @@ public final class CompressingUtils { private CompressingUtils() { } - /** - * Compress the given directory to a zip file. - * - * @param sourceDir the source directory or a file. - * @param zipFile the location of dest zip file. - * @param pathNameCallback callback(pathName, isDirectory) returns your modified pathName - * @throws IOException if there is filesystem error. - */ - public static void zip(File sourceDir, File zipFile, BiFunction pathNameCallback) throws IOException { - try (ZipArchiveOutputStream zos = new ZipArchiveOutputStream(new FileOutputStream(zipFile))) { - String basePath; - if (sourceDir.isDirectory()) - basePath = sourceDir.getPath(); - else - basePath = sourceDir.getParent(); - zipFile(sourceDir, basePath, zos, pathNameCallback); - zos.closeArchiveEntry(); - } + public static FileSystem createReadOnlyZipFileSystem(Path zipFile) throws IOException { + return createReadOnlyZipFileSystem(zipFile, null); } - /** - * Zip file. - * - * @param src source directory to be compressed. - * @param basePath the file directory to be compressed, if [src] is a file, this is the parent directory of [src] - * @param zos the [ZipOutputStream] of dest zip file. - * @param pathNameCallback callback(pathName, isDirectory) returns your modified pathName, null if you dont want this file zipped - * @throws IOException if an I/O error occurs. - */ - private static void zipFile(File src, String basePath, - ZipArchiveOutputStream zos, BiFunction pathNameCallback) throws IOException { - File[] files = src.isDirectory() ? src.listFiles() : new File[] { src }; - String pathName;// the relative path (relative to the root directory to be compressed) - byte[] buf = new byte[IOUtils.DEFAULT_BUFFER_SIZE]; - - if (files == null) return; - for (File file : files) - if (file.isDirectory()) { - pathName = file.getPath().substring(basePath.length() + 1) + "/"; - if (pathNameCallback != null) - pathName = pathNameCallback.apply(pathName, true); - if (pathName == null) - continue; - zos.putArchiveEntry(new ZipArchiveEntry(pathName)); - zipFile(file, basePath, zos, pathNameCallback); - } else { - pathName = file.getPath().substring(basePath.length() + 1); - if (pathNameCallback != null) - pathName = pathNameCallback.apply(pathName, true); - if (pathName == null) - continue; - try (InputStream is = new FileInputStream(file)) { - zos.putArchiveEntry(new ZipArchiveEntry(pathName)); - IOUtils.copyTo(is, zos, buf); - } - } + public static FileSystem createReadOnlyZipFileSystem(Path zipFile, String encoding) throws IOException { + return createZipFileSystem(zipFile, false, false, encoding); } - /** - * Decompress the given zip file to a directory. - * - * @param src the input zip file. - * @param dest the dest directory. - * @throws IOException if an I/O error occurs. - */ - public static void unzip(File src, File dest) throws IOException { - unzip(src, dest, ""); + public static FileSystem createWritableZipFileSystem(Path zipFile) throws IOException { + return createWritableZipFileSystem(zipFile, null); } - /** - * Decompress the given zip file to a directory. - * - * @param src the input zip file. - * @param dest the dest directory. - * @param subDirectory the subdirectory of the zip file to be decompressed. - * @throws IOException if an I/O error occurs. - */ - public static void unzip(File src, File dest, String subDirectory) throws IOException { - unzip(src, dest, subDirectory, null); + public static FileSystem createWritableZipFileSystem(Path zipFile, String encoding) throws IOException { + return createZipFileSystem(zipFile, true, true, encoding); } - /** - * Decompress the given zip file to a directory. - * - * @param src the input zip file. - * @param dest the dest directory. - * @param subDirectory the subdirectory of the zip file to be decompressed. - * @param callback will be called for every entry in the zip file, returns false if you dont want this file being uncompressed. - * @throws IOException if an I/O error occurs. - */ - public static void unzip(File src, File dest, String subDirectory, Predicate callback) throws IOException { - unzip(src, dest, subDirectory, callback, true); - } - - /** - * Decompress the given zip file to a directory. - * - * @param src the input zip file. - * @param dest the dest directory. - * @param subDirectory the subdirectory of the zip file to be decompressed. - * @param callback will be called for every entry in the zip file, returns false if you dont want this file being uncompressed. - * @param ignoreExistentFile true if skip all existent files. - * @throws IOException if an I/O error occurs. - */ - public static void unzip(File src, File dest, String subDirectory, Predicate callback, boolean ignoreExistentFile) throws IOException { - unzip(src, dest, subDirectory, callback, ignoreExistentFile, false); - } - - /** - * Decompress the given zip file to a directory. - * - * @param src the input zip file. - * @param dest the dest directory. - * @param subDirectory the subdirectory of the zip file to be decompressed. - * @param callback will be called for every entry in the zip file, returns false if you dont want this file being uncompressed. - * @param ignoreExistentFile true if skip all existent files. - * @param allowStoredEntriesWithDataDescriptor whether the zip stream will try to read STORED entries that use a data descriptor - * @throws IOException if zip file is malformed or filesystem error. - */ - public static void unzip(File src, File dest, String subDirectory, Predicate callback, boolean ignoreExistentFile, boolean allowStoredEntriesWithDataDescriptor) throws IOException { - byte[] buf = new byte[IOUtils.DEFAULT_BUFFER_SIZE]; - if (!FileUtils.makeDirectory(dest)) - throw new IOException("Unable to make directory " + dest); - try (ZipArchiveInputStream zipStream = new ZipArchiveInputStream(new FileInputStream(src), null, true, allowStoredEntriesWithDataDescriptor)) { - ArchiveEntry entry; - while ((entry = zipStream.getNextEntry()) != null) { - String path = entry.getName(); - - if (!path.startsWith(subDirectory)) - continue; - path = path.substring(subDirectory.length()); - if (path.startsWith("/") || path.startsWith("\\")) - path = path.substring(1); - File entryFile = new File(dest, path); - - if (callback != null) - if (!callback.test(path)) - continue; - - if (entry.isDirectory()) { - if (!FileUtils.makeDirectory(entryFile)) - throw new IOException("Unable to make directory: " + entryFile); - } else { - if (!FileUtils.makeDirectory(entryFile.getAbsoluteFile().getParentFile())) - throw new IOException("Unable to make parent directory for file " + entryFile); - - if (ignoreExistentFile && entryFile.exists()) - continue; - try (FileOutputStream fos = new FileOutputStream(entryFile)) { - IOUtils.copyTo(zipStream, fos, buf); - } - } - } - } + public static FileSystem createZipFileSystem(Path zipFile, boolean create, boolean useTempFile, String encoding) throws IOException { + Map env = new HashMap<>(); + if (create) + env.put("create", "true"); + if (encoding != null) + env.put("encoding", encoding); + if (useTempFile) + env.put("useTempFile", true); + return FileSystems.newFileSystem(URI.create("jar:" + zipFile.toUri()), env); } /** * Read the text content of a file in zip. * - * @param file the zip file + * @param zipFile the zip file * @param name the location of the text in zip file, something like A/B/C/D.txt * @throws IOException if the file is not a valid zip file. - * @return the content of given file. + * @return the plain text content of given file. */ - public static String readTextZipEntry(File file, String name) throws IOException { - try (ZipFile zipFile = new ZipFile(file)) { - ZipArchiveEntry entry = zipFile.getEntry(name); - if (entry == null) - throw new IOException("ZipEntry `" + name + "` not found in " + file); - return IOUtils.readFullyAsString(zipFile.getInputStream(entry)); + public static String readTextZipEntry(File zipFile, String name) throws IOException { + try (FileSystem fs = createReadOnlyZipFileSystem(zipFile.toPath())) { + return new String(Files.readAllBytes(fs.getPath(name))); } } @@ -216,7 +84,7 @@ public final class CompressingUtils { * * @param file the zip file * @param name the location of the text in zip file, something like A/B/C/D.txt - * @return the content of given file. + * @return the plain text content of given file. */ public static Optional readTextZipEntryQuietly(File file, String name) { try { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/FileUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/FileUtils.java index 5c8620cc3..14e85afa0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/FileUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/FileUtils.java @@ -17,11 +17,11 @@ */ package org.jackhuang.hmcl.util; -import java.io.*; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; import java.nio.charset.Charset; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; +import java.nio.file.*; import java.util.LinkedList; import java.util.List; import java.util.Objects; @@ -45,6 +45,13 @@ public final class FileUtils { return StringUtils.substringAfterLast(file.getName(), '.'); } + /** + * This method is for normalizing ZipPath since Path.normalize of ZipFileSystem does not work properly. + */ + public static String normalizePath(String path) { + return StringUtils.addPrefix(StringUtils.removeSuffix(path, "/", "\\"), "/"); + } + public static String readText(File file) throws IOException { return readText(file, UTF_8); } @@ -54,11 +61,7 @@ public final class FileUtils { } public static byte[] readBytes(File file) throws IOException { - try (FileInputStream input = new FileInputStream(file)) { - if (file.length() > Integer.MAX_VALUE) - throw new OutOfMemoryError("File " + file + " is too big (" + file.length() + " bytes) to fit in memory."); - return IOUtils.readFullyAsByteArray(input); - } + return Files.readAllBytes(file.toPath()); } public static void writeText(File file, String text) throws IOException { @@ -123,16 +126,14 @@ public final class FileUtils { public static void forceDelete(File file) throws IOException { - if (file.isDirectory()) + if (file.isDirectory()) { deleteDirectory(file); - else { + } else { boolean filePresent = file.exists(); if (!file.delete()) { if (!filePresent) throw new FileNotFoundException("File does not exist: " + file); - String message = "Unable to delete file: " + file; - - throw new IOException(message); + throw new IOException("Unable to delete file: " + file); } } } @@ -153,10 +154,6 @@ public final class FileUtils { return !fileInCanonicalDir.getCanonicalFile().equals(fileInCanonicalDir.getAbsoluteFile()); } - public static void copyDirectory(Path src, Path dest) { - - } - public static void copyFile(File srcFile, File destFile) throws IOException { Objects.requireNonNull(srcFile, "Source must not be null"); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index 134321500..f2c69b36b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -137,18 +137,28 @@ public final class StringUtils { return str; } - public static String removePrefix(String str, String prefix) { + public static String addPrefix(String str, String prefix) { if (str.startsWith(prefix)) - return str.substring(prefix.length(), str.length()); - else return str; + else + return prefix + str; } - public static String removeSuffix(String str, String suffix) { - if (str.endsWith(suffix)) - return str.substring(0, str.length() - suffix.length()); - else - return str; + public static String removePrefix(String str, String... prefixes) { + for (String prefix : prefixes) + if (str.startsWith(prefix)) + return str.substring(prefix.length()); + return str; + } + + /** + * Remove one suffix of the suffixes of the string. + */ + public static String removeSuffix(String str, String... suffixes) { + for (String suffix : suffixes) + if (str.endsWith(suffix)) + return str.substring(0, str.length() - suffix.length()); + return str; } public static boolean containsOne(Collection patterns, String... targets) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Unzipper.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Unzipper.java new file mode 100644 index 000000000..34b602822 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Unzipper.java @@ -0,0 +1,142 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2018 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.util; + +import java.io.File; +import java.io.IOException; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; + +public class Unzipper { + private final Path zipFile, dest; + private boolean replaceExistentFile = false; + private boolean terminateIfSubDirectoryNotExists = false; + private String subDirectory = "/"; + private FileFilter filter = null; + private String encoding; + + /** + * Decompress the given zip file to a directory. + * + * @param zipFile the input zip file to be uncompressed + * @param destDir the dest directory to hold uncompressed files + */ + public Unzipper(Path zipFile, Path destDir) { + this.zipFile = zipFile; + this.dest = destDir; + } + + /** + * Decompress the given zip file to a directory. + * + * @param zipFile the input zip file to be uncompressed + * @param destDir the dest directory to hold uncompressed files + */ + public Unzipper(File zipFile, File destDir) { + this(zipFile.toPath(), destDir.toPath()); + } + + /** + * True if replace the existent files in destination directory, + * otherwise those conflict files will be ignored. + */ + public Unzipper setReplaceExistentFile(boolean replaceExistentFile) { + this.replaceExistentFile = replaceExistentFile; + return this; + } + + /** + * Will be called for every entry in the zip file. + * Callback returns false if you want leave the specific file uncompressed. + */ + public Unzipper setFilter(FileFilter filter) { + this.filter = filter; + return this; + } + + /** + * Will only uncompress files in the "subDirectory", their path will be also affected. + * + * For example, if you set subDirectory to /META-INF, files in /META-INF/ will be + * uncompressed to the destination directory without creating META-INF folder. + * + * Default value: "/" + */ + public Unzipper setSubDirectory(String subDirectory) { + this.subDirectory = FileUtils.normalizePath(subDirectory); + return this; + } + + public Unzipper setEncoding(String encoding) { + this.encoding = encoding; + return this; + } + + public Unzipper setTerminateIfSubDirectoryNotExists() { + this.terminateIfSubDirectoryNotExists = true; + return this; + } + + /** + * Decompress the given zip file to a directory. + * + * @throws IOException if zip file is malformed or filesystem error. + */ + public void unzip() throws IOException { + Files.createDirectories(dest); + try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile, encoding)) { + Path root = fs.getPath(subDirectory); + if (!root.isAbsolute() || (subDirectory.length() > 1 && subDirectory.endsWith("/"))) + throw new IllegalArgumentException("Subdirectory for unzipper must be absolute"); + + if (terminateIfSubDirectoryNotExists && Files.notExists(root)) + return; + + Files.walkFileTree(root, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, + BasicFileAttributes attrs) throws IOException { + String relativePath = root.relativize(file).toString(); + Path destFile = dest.resolve(relativePath); + if (filter != null && !filter.accept(file, false, destFile, relativePath)) + return FileVisitResult.CONTINUE; + if (replaceExistentFile || Files.notExists(destFile)) + Files.copy(file, destFile, StandardCopyOption.REPLACE_EXISTING); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, + BasicFileAttributes attrs) throws IOException { + String relativePath = root.relativize(dir).toString(); + Path dirToCreate = dest.resolve(relativePath); + if (filter != null && !filter.accept(dir, true, dirToCreate, relativePath)) + return FileVisitResult.CONTINUE; + if (Files.notExists(dirToCreate)) { + Files.createDirectory(dirToCreate); + } + return FileVisitResult.CONTINUE; + } + }); + } + } + + public interface FileFilter { + boolean accept(Path destPath, boolean isDirectory, Path zipEntry, String entryPath) throws IOException; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ZipEngine.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ZipEngine.java deleted file mode 100644 index c121b576c..000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ZipEngine.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2018 huangyuhui - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see {http://www.gnu.org/licenses/}. - */ -package org.jackhuang.hmcl.util; - - -import java.io.*; -import java.util.HashSet; -import java.util.function.BiFunction; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -/** - * Non thread-safe - * - * @author huangyuhui - */ -public final class ZipEngine implements Closeable { - - private final byte[] buf = new byte[IOUtils.DEFAULT_BUFFER_SIZE]; - ZipOutputStream zos; - - public ZipEngine(File f) throws IOException { - zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(f))); - } - - @Override - public void close() throws IOException { - zos.closeEntry(); - zos.close(); - } - - public void putDirectory(File sourceDir) throws IOException { - putDirectory(sourceDir, null); - } - - /** - * Compress all the files in sourceDir - * - * @param sourceDir the directory to be compressed. - * @param pathNameCallback callback(pathName, isDirectory) returns your - * modified pathName - * - * @throws java.io.IOException if unable to compress or read files. - */ - public void putDirectory(File sourceDir, BiFunction pathNameCallback) throws IOException { - putDirectoryImpl(sourceDir, sourceDir.isDirectory() ? sourceDir.getPath() : sourceDir.getParent(), pathNameCallback); - } - - /** - * Compress all the files in sourceDir - * - * @param source the file in basePath to be compressed - * @param basePath the root directory to be compressed. - * @param pathNameCallback callback(pathName, isDirectory) returns your - * modified pathName, null if you dont want this file zipped - */ - private void putDirectoryImpl(File source, String basePath, BiFunction pathNameCallback) throws IOException { - File[] files = null; - if (source.isDirectory()) - files = source.listFiles(); - else if (source.isFile()) - files = new File[] { source }; - - if (files == null) - return; - String pathName; // the relative path (relative to basePath) - for (File file : files) - if (file.isDirectory()) { - pathName = file.getPath().substring(basePath.length() + 1) - + "/"; - pathName = pathName.replace('\\', '/'); - if (pathNameCallback != null) - pathName = pathNameCallback.apply(pathName, true); - if (pathName == null) - continue; - put(new ZipEntry(pathName)); - putDirectoryImpl(file, basePath, pathNameCallback); - } else { - if (".DS_Store".equals(file.getName())) // For Mac computers. - continue; - pathName = file.getPath().substring(basePath.length() + 1); - pathName = pathName.replace('\\', '/'); - if (pathNameCallback != null) - pathName = pathNameCallback.apply(pathName, false); - if (pathName == null) - continue; - putFile(file, pathName); - } - } - - public void putFile(File file, String pathName) throws IOException { - try (FileInputStream fis = new FileInputStream(file)) { - putStream(fis, pathName); - } - } - - public void putStream(InputStream is, String pathName) throws IOException { - put(new ZipEntry(pathName)); - IOUtils.copyTo(is, zos, buf); - } - - public void putTextFile(String text, String pathName) throws IOException { - putTextFile(text, "UTF-8", pathName); - } - - public void putTextFile(String text, String encoding, String pathName) throws IOException { - putStream(new ByteArrayInputStream(text.getBytes(encoding)), pathName); - } - - private final HashSet names = new HashSet<>(); - - public void put(ZipEntry entry) throws IOException { - if (names.add(entry.getName())) - zos.putNextEntry(entry); - } - -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Zipper.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Zipper.java new file mode 100644 index 000000000..cfb57c817 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Zipper.java @@ -0,0 +1,123 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2018 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.util; + + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.function.Predicate; + +/** + * Non thread-safe + * + * @author huangyuhui + */ +public final class Zipper implements Closeable { + + private final FileSystem fs; + + public Zipper(Path zipFile) throws IOException { + this(zipFile, null); + } + + public Zipper(Path zipFile, String encoding) throws IOException { + Files.deleteIfExists(zipFile); + fs = CompressingUtils.createWritableZipFileSystem(zipFile, encoding); + } + + @Override + public void close() throws IOException { + fs.close(); + } + + /** + * Compress all the files in sourceDir + * + * @param source the file in basePath to be compressed + * @param rootDir the path of the directory in this zip file. + */ + public void putDirectory(Path source, String rootDir) throws IOException { + putDirectory(source, rootDir, null); + } + + /** + * Compress all the files in sourceDir + * + * @param source the file in basePath to be compressed + * @param targetDir the path of the directory in this zip file. + * @param filter returns false if you do not want that file or directory + */ + public void putDirectory(Path source, String targetDir, Predicate filter) throws IOException { + File[] files = null; + + Path root = fs.getPath(targetDir); + Files.createDirectories(root); + Files.walkFileTree(source, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (".DS_Store".equals(file.getFileName().toString())) { + return FileVisitResult.SKIP_SUBTREE; + } + String relativePath = source.relativize(file).normalize().toString(); + if (filter != null && !filter.test(relativePath)) { + return FileVisitResult.SKIP_SUBTREE; + } + Files.copy(file, root.resolve(relativePath)); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + String relativePath = source.relativize(dir).normalize().toString(); + if (filter != null && !filter.test(relativePath)) { + return FileVisitResult.SKIP_SUBTREE; + } + Path path = root.resolve(relativePath); + if (Files.notExists(path)) { + Files.createDirectory(path); + } + return FileVisitResult.CONTINUE; + } + }); + } + + public void putFile(File file, String path) throws IOException { + putFile(file.toPath(), path); + } + + public void putFile(Path file, String path) throws IOException { + Files.copy(file, fs.getPath(path)); + } + + public void putStream(InputStream in, String path) throws IOException { + Files.copy(in, fs.getPath(path)); + } + + public void putTextFile(String text, String path) throws IOException { + putTextFile(text, "UTF-8", path); + } + + public void putTextFile(String text, String encoding, String pathName) throws IOException { + Files.write(fs.getPath(pathName), text.getBytes(encoding)); + } + +} diff --git a/build.gradle b/build.gradle index 8a8ac1fc2..243ddb77f 100644 --- a/build.gradle +++ b/build.gradle @@ -39,7 +39,6 @@ allprojects { dependencies { compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5' - compile group: 'org.apache.commons', name: 'commons-compress', version: '1.17' compile group: 'org.tukaani', name: 'xz', version: '1.8' compile group: 'org.hildan.fxgson', name: 'fx-gson', version: '3.1.0' compile group: 'org.jenkins-ci', name: 'constant-pool-scanner', version: '1.2'