Use ZipFileSystem instead of commons-compress

This commit is contained in:
huanghongxun 2018-08-07 11:52:01 +08:00
parent cadafe13e1
commit 51afcf2dee
24 changed files with 487 additions and 510 deletions

View File

@ -17,12 +17,11 @@
*/ */
package org.jackhuang.hmcl.game; package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.mod.Modpack; 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.Constants;
import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.ZipEngine; import org.jackhuang.hmcl.util.Zipper;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
@ -31,58 +30,44 @@ import java.util.List;
/** /**
* Export the game to a mod pack file. * Export the game to a mod pack file.
*/ */
public class HMCLModpackExportTask extends TaskResult<ZipEngine> { public class HMCLModpackExportTask extends Task {
private final DefaultGameRepository repository; private final DefaultGameRepository repository;
private final String version; private final String version;
private final List<String> whitelist; private final List<String> whitelist;
private final Modpack modpack; private final Modpack modpack;
private final File output; private final File output;
private final String id;
public HMCLModpackExportTask(DefaultGameRepository repository, String version, List<String> whitelist, Modpack modpack, File output) {
this(repository, version, whitelist, modpack, output, ID);
}
/** /**
* @param output mod pack file. * @param output mod pack file.
* @param version to locate version.json * @param version to locate version.json
*/ */
public HMCLModpackExportTask(DefaultGameRepository repository, String version, List<String> whitelist, Modpack modpack, File output, String id) { public HMCLModpackExportTask(DefaultGameRepository repository, String version, List<String> whitelist, Modpack modpack, File output) {
this.repository = repository; this.repository = repository;
this.version = version; this.version = version;
this.whitelist = whitelist; this.whitelist = whitelist;
this.modpack = modpack; this.modpack = modpack;
this.output = output; this.output = output;
this.id = id;
onDone().register(event -> { onDone().register(event -> {
if (event.isFailed()) output.delete(); if (event.isFailed()) output.delete();
}); });
} }
@Override
public String getId() {
return id;
}
@Override @Override
public void execute() throws Exception { public void execute() throws Exception {
ArrayList<String> blackList = new ArrayList<>(HMCLModpackManager.MODPACK_BLACK_LIST); ArrayList<String> blackList = new ArrayList<>(HMCLModpackManager.MODPACK_BLACK_LIST);
blackList.add(version + ".jar"); blackList.add(version + ".jar");
blackList.add(version + ".json"); 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"); 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)) { try (Zipper zip = new Zipper(output.toPath())) {
zip.putDirectory(repository.getRunDirectory(version), (String pathName, Boolean isDirectory) -> { zip.putDirectory(repository.getRunDirectory(version).toPath(), "minecraft", path -> {
for (String s : blackList) for (String s : blackList)
if (isDirectory) { if (path.equals(s))
if (pathName.startsWith(s + "/")) return false;
return null;
} else if (pathName.equals(s))
return null;
for (String s : whitelist) for (String s : whitelist)
if (pathName.equals(s + (isDirectory ? "/" : ""))) if (path.equals(s))
return "minecraft/" + pathName; return true;
return null; return false;
}); });
Version mv = repository.getResolvedVersion(version); Version mv = repository.getResolvedVersion(version);
@ -92,6 +77,4 @@ public class HMCLModpackExportTask extends TaskResult<ZipEngine> {
zip.putTextFile(Constants.GSON.toJson(modpack.setGameVersion(gameVersion)), "modpack.json"); // Newer HMCL only reads 'gameVersion' field. zip.putTextFile(Constants.GSON.toJson(modpack.setGameVersion(gameVersion)), "modpack.json"); // Newer HMCL only reads 'gameVersion' field.
} }
} }
public static final String ID = "zip_engine";
} }

View File

@ -74,7 +74,7 @@ public final class HMCLModpackInstallTask extends Task {
} }
} catch (JsonParseException | IOException ignore) { } 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 @Override
@ -90,9 +90,9 @@ public final class HMCLModpackInstallTask extends Task {
@Override @Override
public void execute() throws Exception { public void execute() throws Exception {
String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json"); 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 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"; public static final String MODPACK_TYPE = "HMCL";

View File

@ -39,7 +39,7 @@ public final class HMCLModpackManager {
"pack.json", "launcher.jar", "hmclmc.log", // HMCL "pack.json", "launcher.jar", "hmclmc.log", // HMCL
"manifest.json", "minecraftinstance.json", ".curseclient", // Curse "manifest.json", "minecraftinstance.json", ".curseclient", // Curse
"minetweaker.log", // Mods "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 "downloads", // Curse
"asm", "backups", "TCNodeTracker", "CustomDISkins", "data" // Mods "asm", "backups", "TCNodeTracker", "CustomDISkins", "data" // Mods
); );

View File

@ -86,11 +86,13 @@ public final class ModpackHelper {
profile.getRepository().markVersionAsModpack(name); profile.getRepository().markVersionAsModpack(name);
FinalizedCallback finalizeTask = (variables, isDependentsSucceeded) -> { FinalizedCallback finalizeTask = (variables, isDependentsSucceeded) -> {
if (isDependentsSucceeded) {
profile.getRepository().refreshVersions(); profile.getRepository().refreshVersions();
VersionSetting vs = profile.specializeVersionSetting(name); VersionSetting vs = profile.specializeVersionSetting(name);
profile.getRepository().undoMark(name); profile.getRepository().undoMark(name);
if (vs != null) if (vs != null)
vs.setGameDirType(EnumGameDirectory.VERSION_FOLDER); vs.setGameDirType(EnumGameDirectory.VERSION_FOLDER);
}
}; };
if (modpack.getManifest() instanceof CurseManifest) if (modpack.getManifest() instanceof CurseManifest)
@ -102,7 +104,7 @@ public final class ModpackHelper {
else if (modpack.getManifest() instanceof MultiMCInstanceConfiguration) else if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name) return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name)
.finalized(finalizeTask) .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); else throw new IllegalStateException("Unrecognized modpack: " + modpack);
} }

View File

@ -98,6 +98,7 @@ public final class ModpackPage extends StackPane implements WizardPage {
txtModpackName.setText(manifest.getName() + (StringUtils.isBlank(manifest.getVersion()) ? "" : "-" + manifest.getVersion())); txtModpackName.setText(manifest.getName() + (StringUtils.isBlank(manifest.getVersion()) ? "" : "-" + manifest.getVersion()));
} catch (UnsupportedModpackException e) { } catch (UnsupportedModpackException e) {
txtModpackName.setText(i18n("modpack.task.install.error")); txtModpackName.setText(i18n("modpack.task.install.error"));
btnInstall.setDisable(true);
} }
} }
} }

View File

@ -28,7 +28,7 @@ import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.wizard.WizardController; import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider; 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.io.File;
import java.nio.file.Files; import java.nio.file.Files;
@ -81,7 +81,7 @@ public final class ExportWizardProvider implements WizardProvider {
dependency = dependency.then(Task.of(() -> { dependency = dependency.then(Task.of(() -> {
boolean flag = true; boolean flag = true;
try (ZipEngine zip = new ZipEngine(modpackFile)) { try (Zipper zip = new Zipper(modpackFile.toPath())) {
Config exported = new Config(); Config exported = new Config();
exported.setBackgroundImageType(config().getBackgroundImageType()); exported.setBackgroundImageType(config().getBackgroundImageType());
exported.setBackgroundImage(config().getBackgroundImage()); exported.setBackgroundImage(config().getBackgroundImage());
@ -93,7 +93,7 @@ public final class ExportWizardProvider implements WizardProvider {
File bg = new File("bg").getAbsoluteFile(); File bg = new File("bg").getAbsoluteFile();
if (bg.isDirectory()) if (bg.isDirectory())
zip.putDirectory(bg); zip.putDirectory(bg.toPath(), "bg");
File background_png = new File("background.png").getAbsoluteFile(); File background_png = new File("background.png").getAbsoluteFile();
if (background_png.isFile()) if (background_png.isFile())

View File

@ -1,6 +1,5 @@
package org.jackhuang.hmcl.download.game; package org.jackhuang.hmcl.download.game;
import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
import org.jackhuang.hmcl.download.AbstractDependencyManager; import org.jackhuang.hmcl.download.AbstractDependencyManager;
import org.jackhuang.hmcl.game.Library; import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.task.FileDownloadTask; 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.IOUtils;
import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.NetworkUtils; import org.jackhuang.hmcl.util.NetworkUtils;
import org.tukaani.xz.XZInputStream;
import java.io.*; import java.io.*;
import java.nio.charset.Charset; import java.nio.charset.Charset;
@ -145,7 +145,7 @@ public final class LibraryDownloadTask extends Task {
if (!dest.delete()) if (!dest.delete())
throw new IOException("Unable to delete file " + dest); 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); String end = new String(decompressed, decompressed.length - 4, 4);
if (!end.equals("SIGN")) if (!end.equals("SIGN"))

View File

@ -148,10 +148,10 @@ public class DefaultGameRepository implements GameRepository {
} }
public boolean removeVersionFromDisk(String id) { public boolean removeVersionFromDisk(String id) {
if (!versions.containsKey(id))
return true;
if (EventBus.EVENT_BUS.fireEvent(new RemoveVersionEvent(this, id)) == Event.Result.DENY) if (EventBus.EVENT_BUS.fireEvent(new RemoveVersionEvent(this, id)) == Event.Result.DENY)
return false; return false;
if (!versions.containsKey(id))
return FileUtils.deleteDirectoryQuietly(getVersionRoot(id));
File file = getVersionRoot(id); File file = getVersionRoot(id);
if (!file.exists()) if (!file.exists())
return true; return true;

View File

@ -17,8 +17,7 @@
*/ */
package org.jackhuang.hmcl.game; package org.jackhuang.hmcl.game;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.jackhuang.hmcl.util.CompressingUtils;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jenkinsci.constant_pool_scanner.ConstantPool; import org.jenkinsci.constant_pool_scanner.ConstantPool;
import org.jenkinsci.constant_pool_scanner.ConstantPoolScanner; import org.jenkinsci.constant_pool_scanner.ConstantPoolScanner;
import org.jenkinsci.constant_pool_scanner.ConstantType; 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.File;
import java.io.IOException; 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.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -35,8 +37,8 @@ import java.util.stream.StreamSupport;
* @author huangyuhui * @author huangyuhui
*/ */
public final class GameVersion { public final class GameVersion {
private static Optional<String> getVersionOfClassMinecraft(ZipFile file, ZipArchiveEntry entry) throws IOException { private static Optional<String> getVersionOfClassMinecraft(byte[] bytecode) throws IOException {
ConstantPool pool = ConstantPoolScanner.parse(file.getInputStream(entry), ConstantType.STRING); ConstantPool pool = ConstantPoolScanner.parse(bytecode, ConstantType.STRING);
return StreamSupport.stream(pool.list(StringConstant.class).spliterator(), false) return StreamSupport.stream(pool.list(StringConstant.class).spliterator(), false)
.map(StringConstant::get) .map(StringConstant::get)
@ -45,8 +47,8 @@ public final class GameVersion {
.findFirst(); .findFirst();
} }
private static Optional<String> getVersionFromClassMinecraftServer(ZipFile file, ZipArchiveEntry entry) throws IOException { private static Optional<String> getVersionFromClassMinecraftServer(byte[] bytecode) throws IOException {
ConstantPool pool = ConstantPoolScanner.parse(file.getInputStream(entry), ConstantType.STRING); ConstantPool pool = ConstantPoolScanner.parse(bytecode, ConstantType.STRING);
List<String> list = StreamSupport.stream(pool.list(StringConstant.class).spliterator(), false) List<String> list = StreamSupport.stream(pool.list(StringConstant.class).spliterator(), false)
.map(StringConstant::get) .map(StringConstant::get)
@ -72,16 +74,16 @@ public final class GameVersion {
return Optional.empty(); return Optional.empty();
try { try {
try (ZipFile gameJar = new ZipFile(file)) { try (FileSystem gameJar = CompressingUtils.createReadOnlyZipFileSystem(file.toPath())) {
ZipArchiveEntry minecraft = gameJar.getEntry("net/minecraft/client/Minecraft.class"); Path minecraft = gameJar.getPath("net/minecraft/client/Minecraft.class");
if (minecraft != null) { if (Files.exists(minecraft)) {
Optional<String> result = getVersionOfClassMinecraft(gameJar, minecraft); Optional<String> result = getVersionOfClassMinecraft(Files.readAllBytes(minecraft));
if (result.isPresent()) if (result.isPresent())
return result; return result;
} }
ZipArchiveEntry minecraftServer = gameJar.getEntry("net/minecraft/server/MinecraftServer.class"); Path minecraftServer = gameJar.getPath("net/minecraft/server/MinecraftServer.class");
if (minecraftServer != null) if (Files.exists(minecraftServer))
return getVersionFromClassMinecraftServer(gameJar, minecraftServer); return getVersionFromClassMinecraftServer(Files.readAllBytes(minecraftServer));
return Optional.empty(); return Optional.empty();
} }
} catch (IOException e) { } catch (IOException e) {

View File

@ -229,11 +229,9 @@ public class DefaultLauncher extends Launcher {
try { try {
for (Library library : version.getLibraries()) for (Library library : version.getLibraries())
if (library.isNative()) if (library.isNative())
CompressingUtils.unzip(repository.getLibraryFile(version, library), new Unzipper(repository.getLibraryFile(version, library), destination)
destination, .setFilter((destFile, isDirectory, zipEntry, path) -> library.getExtract().shouldExtract(path))
"", .setReplaceExistentFile(true).unzip();
library.getExtract()::shouldExtract,
false);
} catch (IOException e) { } catch (IOException e) {
throw new NotDecompressingNativesException(e); throw new NotDecompressingNativesException(e);
} }

View File

@ -76,6 +76,10 @@ public final class CurseInstallTask extends Task {
builder.version("forge", modLoader.getId().substring("forge-".length())); builder.version("forge", modLoader.getId().substring("forge-".length()));
dependents.add(builder.buildAsync()); dependents.add(builder.buildAsync());
onDone().register(event -> {
if (event.isFailed()) repository.removeVersionFromDisk(name);
});
ModpackConfiguration<CurseManifest> config = null; ModpackConfiguration<CurseManifest> config = null;
try { try {
if (json.exists()) { if (json.exists()) {

View File

@ -20,15 +20,13 @@ package org.jackhuang.hmcl.mod;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.jackhuang.hmcl.util.*;
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 java.io.File; import java.io.File;
import java.io.IOException; 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.List;
/** /**
@ -114,11 +112,11 @@ public final class ForgeModMetadata {
} }
public static ModInfo fromFile(File modFile) throws IOException, JsonParseException { public static ModInfo fromFile(File modFile) throws IOException, JsonParseException {
try (ZipFile zipFile = new ZipFile(modFile)) { try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.toPath())) {
ZipArchiveEntry entry = zipFile.getEntry("mcmod.info"); Path mcmod = fs.getPath("mcmod.info");
if (entry == null) if (Files.notExists(mcmod))
throw new IOException("File " + modFile + " is not a Forge mod."); throw new IOException("File " + modFile + " is not a Forge mod.");
List<ForgeModMetadata> modList = Constants.GSON.fromJson(IOUtils.readFullyAsString(zipFile.getInputStream(entry)), List<ForgeModMetadata> modList = Constants.GSON.fromJson(IOUtils.readFullyAsString(Files.newInputStream(mcmod)),
new TypeToken<List<ForgeModMetadata>>() { new TypeToken<List<ForgeModMetadata>>() {
}.getType()); }.getType());
if (modList == null || modList.isEmpty()) if (modList == null || modList.isEmpty())

View File

@ -17,14 +17,15 @@
*/ */
package org.jackhuang.hmcl.mod; 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.task.Task;
import org.jackhuang.hmcl.util.CompressingUtils;
import org.jackhuang.hmcl.util.Constants; import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.FileUtils; import org.jackhuang.hmcl.util.FileUtils;
import java.io.File; 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.LinkedList;
import java.util.List; import java.util.List;
@ -41,7 +42,7 @@ public final class MinecraftInstanceTask<T> extends Task {
public MinecraftInstanceTask(File zipFile, String subDirectory, T manifest, String type, File jsonFile) { public MinecraftInstanceTask(File zipFile, String subDirectory, T manifest, String type, File jsonFile) {
this.zipFile = zipFile; this.zipFile = zipFile;
this.subDirectory = subDirectory; this.subDirectory = FileUtils.normalizePath(subDirectory);
this.manifest = manifest; this.manifest = manifest;
this.jsonFile = jsonFile; this.jsonFile = jsonFile;
this.type = type; this.type = type;
@ -54,18 +55,18 @@ public final class MinecraftInstanceTask<T> extends Task {
public void execute() throws Exception { public void execute() throws Exception {
List<ModpackConfiguration.FileInformation> overrides = new LinkedList<>(); List<ModpackConfiguration.FileInformation> overrides = new LinkedList<>();
try (ZipArchiveInputStream zip = new ZipArchiveInputStream(new FileInputStream(zipFile), null, true, true)) { try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile.toPath())) {
ArchiveEntry entry; Path root = fs.getPath(subDirectory);
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);
overrides.add(new ModpackConfiguration.FileInformation(path, encodeHex(digest("SHA-1", zip)))); if (Files.exists(root))
Files.walkFileTree(fs.getPath(subDirectory), new SimpleFileVisitor<Path>() {
@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))); FileUtils.writeText(jsonFile, Constants.GSON.toJson(new ModpackConfiguration<>(manifest, type, overrides)));

View File

@ -17,17 +17,15 @@
*/ */
package org.jackhuang.hmcl.mod; 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.task.Task;
import org.jackhuang.hmcl.util.FileUtils; import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.IOUtils; import org.jackhuang.hmcl.util.IOUtils;
import org.jackhuang.hmcl.util.Unzipper;
import java.io.*; import java.io.File;
import java.util.Collections; import java.io.IOException;
import java.util.HashSet; import java.nio.file.Files;
import java.util.List; import java.util.*;
import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
import static org.jackhuang.hmcl.util.DigestUtils.digest; import static org.jackhuang.hmcl.util.DigestUtils.digest;
@ -56,61 +54,36 @@ public class ModpackInstallTask<T> extends Task {
@Override @Override
public void execute() throws Exception { public void execute() throws Exception {
Set<String> entries = new HashSet<>(); Set<String> entries = new HashSet<>();
byte[] buf = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
if (!FileUtils.makeDirectory(dest)) if (!FileUtils.makeDirectory(dest))
throw new IOException("Unable to make directory " + dest); throw new IOException("Unable to make directory " + dest);
HashSet<String> files = new HashSet<>(); HashMap<String, ModpackConfiguration.FileInformation> files = new HashMap<>();
for (ModpackConfiguration.FileInformation file : overrides) 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)) { new Unzipper(modpackFile, dest)
ArchiveEntry entry; .setSubDirectory(subDirectory)
while ((entry = zipStream.getNextEntry()) != null) { .setTerminateIfSubDirectoryNotExists()
String path = entry.getName(); .setReplaceExistentFile(true)
.setFilter((destPath, isDirectory, zipEntry, entryPath) -> {
if (isDirectory) return true;
entries.add(entryPath);
if (!path.startsWith(subDirectory)) if (!files.containsKey(entryPath)) {
continue; // If old modpack does not have this entry, add this entry or override the file that user added.
path = path.substring(subDirectory.length()); return true;
if (path.startsWith("/") || path.startsWith("\\")) } else if (!Files.exists(destPath)) {
path = path.substring(1); // If both old and new modpacks have this entry, but the file is deleted by user, leave it missing.
File entryFile = new File(dest, path); return false;
if (callback != null)
if (!callback.test(path))
continue;
if (entry.isDirectory()) {
if (!FileUtils.makeDirectory(entryFile))
throw new IOException("Unable to make directory: " + entryFile);
} else { } else {
if (!FileUtils.makeDirectory(entryFile.getAbsoluteFile().getParentFile())) // If user modified this entry file, we will not replace this file since this modified file is that user expects.
throw new IOException("Unable to make parent directory for file " + entryFile); String fileHash = encodeHex(digest("SHA-1", Files.newInputStream(destPath)));
String oldHash = files.get(entryPath).getHash();
entries.add(path); return Objects.equals(oldHash, fileHash);
}
ByteArrayOutputStream os = new ByteArrayOutputStream(IOUtils.DEFAULT_BUFFER_SIZE); }).unzip();
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 old modpack have this entry, and new modpack deleted it. Delete this file.
for (ModpackConfiguration.FileInformation file : overrides) { for (ModpackConfiguration.FileInformation file : overrides) {
File original = new File(dest, file.getPath()); File original = new File(dest, file.getPath());
if (original.exists() && !entries.contains(file.getPath())) if (original.exists() && !entries.contains(file.getPath()))

View File

@ -17,14 +17,15 @@
*/ */
package org.jackhuang.hmcl.mod; package org.jackhuang.hmcl.mod;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.jackhuang.hmcl.util.CompressingUtils;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; 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.Optional;
import java.util.Properties; import java.util.Properties;
@ -262,15 +263,17 @@ public final class MultiMCInstanceConfiguration {
return mmcPack; return mmcPack;
} }
public static Modpack readMultiMCModpackManifest(File f) throws IOException { public static Modpack readMultiMCModpackManifest(File modpackFile) throws IOException {
try (ZipFile zipFile = new ZipFile(f)) { MultiMCManifest manifest = MultiMCManifest.readMultiMCModpackManifest(modpackFile);
ZipArchiveEntry firstEntry = zipFile.getEntries().nextElement(); try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modpackFile.toPath())) {
String name = StringUtils.substringBefore(firstEntry.getName(), '/'); Path root = Files.list(fs.getPath("/")).filter(Files::isDirectory).findAny()
ZipArchiveEntry entry = zipFile.getEntry(name + "/instance.cfg"); .orElseThrow(() -> new IOException("Not a valid MultiMC modpack"));
if (entry == null) String name = root.normalize().getFileName().toString();
throw new IOException("`instance.cfg` not found, " + f + " is not a valid MultiMC modpack.");
MultiMCManifest manifest = MultiMCManifest.readMultiMCModpackManifest(f); Path instancePath = root.resolve("instance.cfg");
MultiMCInstanceConfiguration cfg = new MultiMCInstanceConfiguration(name, zipFile.getInputStream(entry), manifest); 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); return new Modpack(cfg.getName(), "", "", cfg.getGameVersion(), cfg.getNotes(), cfg);
} }
} }

View File

@ -17,14 +17,17 @@
*/ */
package org.jackhuang.hmcl.mod; package org.jackhuang.hmcl.mod;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.jackhuang.hmcl.util.CompressingUtils;
import org.apache.commons.compress.archivers.zip.ZipFile; import org.jackhuang.hmcl.util.IOUtils;
import org.jackhuang.hmcl.util.*; import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.JsonUtils;
import java.io.File; import java.io.File;
import java.io.IOException; 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.List;
@Immutable @Immutable
@ -49,14 +52,14 @@ public final class MultiMCManifest {
return components; return components;
} }
public static MultiMCManifest readMultiMCModpackManifest(File f) throws IOException { public static MultiMCManifest readMultiMCModpackManifest(File zipFile) throws IOException {
try (ZipFile zipFile = new ZipFile(f)) { try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile.toPath())) {
ZipArchiveEntry firstEntry = zipFile.getEntries().nextElement(); Path root = Files.list(fs.getPath("/")).filter(Files::isDirectory).findAny()
String name = StringUtils.substringBefore(firstEntry.getName(), '/'); .orElseThrow(() -> new IOException("Not a valid MultiMC modpack"));
ZipArchiveEntry entry = zipFile.getEntry(name + "/mmc-pack.json"); Path mmcPack = root.resolve("mmc-pack.json");
if (entry == null) if (Files.notExists(mmcPack))
return null; return null;
String json = IOUtils.readFullyAsString(zipFile.getInputStream(entry)); String json = IOUtils.readFullyAsString(Files.newInputStream(mmcPack));
MultiMCManifest manifest = JsonUtils.fromNonNullJson(json, MultiMCManifest.class); MultiMCManifest manifest = JsonUtils.fromNonNullJson(json, MultiMCManifest.class);
if (manifest != null && manifest.getComponents() == null) if (manifest != null && manifest.getComponents() == null)

View File

@ -19,8 +19,6 @@ package org.jackhuang.hmcl.mod;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken; 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.DefaultDependencyManager;
import org.jackhuang.hmcl.download.GameBuilder; import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.download.game.VersionJsonSaveTask; 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.DefaultGameRepository;
import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Constants; import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.IOUtils;
import org.jackhuang.hmcl.util.Lang;
import java.io.File; import java.io.File;
import java.io.IOException; 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.LinkedList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -98,7 +96,7 @@ public final class MultiMCModpackInstallTask extends Task {
} catch (JsonParseException | IOException ignore) { } 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 @Override
@ -115,11 +113,15 @@ public final class MultiMCModpackInstallTask extends Task {
public void execute() throws Exception { public void execute() throws Exception {
Version version = Objects.requireNonNull(repository.readVersionJson(name)); Version version = Objects.requireNonNull(repository.readVersionJson(name));
try (ZipFile zip = new ZipFile(zipFile)) { try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile.toPath())) {
for (ZipArchiveEntry entry : Lang.asIterable(zip.getEntries())) { Path root = Files.list(fs.getPath("/")).filter(Files::isDirectory).findAny()
// ensure that this entry is in folder 'patches' and is a json file. .orElseThrow(() -> new IOException("Not a valid MultiMC modpack"));
if (!entry.isDirectory() && entry.getName().startsWith(manifest.getName() + "/patches/") && entry.getName().endsWith(".json")) { Path patches = root.resolve("patches");
MultiMCInstancePatch patch = Constants.GSON.fromJson(IOUtils.readFullyAsString(zip.getInputStream(entry)), MultiMCInstancePatch.class);
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<String> newArguments = new LinkedList<>(); List<String> newArguments = new LinkedList<>();
for (String arg : patch.getTweakers()) { for (String arg : patch.getTweakers()) {
newArguments.add("--tweakClass"); newArguments.add("--tweakClass");
@ -134,7 +136,7 @@ public final class MultiMCModpackInstallTask extends Task {
} }
dependencies.add(new VersionJsonSaveTask(repository, version)); 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"; public static final String MODPACK_TYPE = "MultiMC";

View File

@ -17,16 +17,16 @@
*/ */
package org.jackhuang.hmcl.util; package org.jackhuang.hmcl.util;
import org.apache.commons.compress.archivers.ArchiveEntry; import java.io.File;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import java.io.IOException;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; import java.net.URI;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import java.nio.file.FileSystem;
import org.apache.commons.compress.archivers.zip.ZipFile; import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.io.*; import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Predicate;
/** /**
* Utilities of compressing * Utilities of compressing
@ -38,176 +38,44 @@ public final class CompressingUtils {
private CompressingUtils() { private CompressingUtils() {
} }
/** public static FileSystem createReadOnlyZipFileSystem(Path zipFile) throws IOException {
* Compress the given directory to a zip file. return createReadOnlyZipFileSystem(zipFile, null);
*
* @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<String, Boolean, String> 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, String encoding) throws IOException {
* Zip file. return createZipFileSystem(zipFile, false, false, encoding);
*
* @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<String, Boolean, String> 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 createWritableZipFileSystem(Path zipFile) throws IOException {
* Decompress the given zip file to a directory. return createWritableZipFileSystem(zipFile, null);
*
* @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, String encoding) throws IOException {
* Decompress the given zip file to a directory. return createZipFileSystem(zipFile, true, true, encoding);
*
* @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 createZipFileSystem(Path zipFile, boolean create, boolean useTempFile, String encoding) throws IOException {
* Decompress the given zip file to a directory. Map<String, Object> env = new HashMap<>();
* if (create)
* @param src the input zip file. env.put("create", "true");
* @param dest the dest directory. if (encoding != null)
* @param subDirectory the subdirectory of the zip file to be decompressed. env.put("encoding", encoding);
* @param callback will be called for every entry in the zip file, returns false if you dont want this file being uncompressed. if (useTempFile)
* @throws IOException if an I/O error occurs. env.put("useTempFile", true);
*/ return FileSystems.newFileSystem(URI.create("jar:" + zipFile.toUri()), env);
public static void unzip(File src, File dest, String subDirectory, Predicate<String> 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<String> 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<String> 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);
}
}
}
}
} }
/** /**
* Read the text content of a file in zip. * 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 * @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. * @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 { public static String readTextZipEntry(File zipFile, String name) throws IOException {
try (ZipFile zipFile = new ZipFile(file)) { try (FileSystem fs = createReadOnlyZipFileSystem(zipFile.toPath())) {
ZipArchiveEntry entry = zipFile.getEntry(name); return new String(Files.readAllBytes(fs.getPath(name)));
if (entry == null)
throw new IOException("ZipEntry `" + name + "` not found in " + file);
return IOUtils.readFullyAsString(zipFile.getInputStream(entry));
} }
} }
@ -216,7 +84,7 @@ public final class CompressingUtils {
* *
* @param file the zip file * @param file the zip file
* @param name the location of the text in zip file, something like A/B/C/D.txt * @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<String> readTextZipEntryQuietly(File file, String name) { public static Optional<String> readTextZipEntryQuietly(File file, String name) {
try { try {

View File

@ -17,11 +17,11 @@
*/ */
package org.jackhuang.hmcl.util; 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.charset.Charset;
import java.nio.file.Files; import java.nio.file.*;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -45,6 +45,13 @@ public final class FileUtils {
return StringUtils.substringAfterLast(file.getName(), '.'); 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 { public static String readText(File file) throws IOException {
return readText(file, UTF_8); return readText(file, UTF_8);
} }
@ -54,11 +61,7 @@ public final class FileUtils {
} }
public static byte[] readBytes(File file) throws IOException { public static byte[] readBytes(File file) throws IOException {
try (FileInputStream input = new FileInputStream(file)) { return Files.readAllBytes(file.toPath());
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);
}
} }
public static void writeText(File file, String text) throws IOException { public static void writeText(File file, String text) throws IOException {
@ -123,16 +126,14 @@ public final class FileUtils {
public static void forceDelete(File file) public static void forceDelete(File file)
throws IOException { throws IOException {
if (file.isDirectory()) if (file.isDirectory()) {
deleteDirectory(file); deleteDirectory(file);
else { } else {
boolean filePresent = file.exists(); boolean filePresent = file.exists();
if (!file.delete()) { if (!file.delete()) {
if (!filePresent) if (!filePresent)
throw new FileNotFoundException("File does not exist: " + file); throw new FileNotFoundException("File does not exist: " + file);
String message = "Unable to delete file: " + file; throw new IOException("Unable to delete file: " + file);
throw new IOException(message);
} }
} }
} }
@ -153,10 +154,6 @@ public final class FileUtils {
return !fileInCanonicalDir.getCanonicalFile().equals(fileInCanonicalDir.getAbsoluteFile()); return !fileInCanonicalDir.getCanonicalFile().equals(fileInCanonicalDir.getAbsoluteFile());
} }
public static void copyDirectory(Path src, Path dest) {
}
public static void copyFile(File srcFile, File destFile) public static void copyFile(File srcFile, File destFile)
throws IOException { throws IOException {
Objects.requireNonNull(srcFile, "Source must not be null"); Objects.requireNonNull(srcFile, "Source must not be null");

View File

@ -137,17 +137,27 @@ public final class StringUtils {
return str; return str;
} }
public static String removePrefix(String str, String prefix) { public static String addPrefix(String str, String prefix) {
if (str.startsWith(prefix)) if (str.startsWith(prefix))
return str.substring(prefix.length(), str.length()); return str;
else else
return prefix + str;
}
public static String removePrefix(String str, String... prefixes) {
for (String prefix : prefixes)
if (str.startsWith(prefix))
return str.substring(prefix.length());
return str; return str;
} }
public static String removeSuffix(String str, String suffix) { /**
* 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)) if (str.endsWith(suffix))
return str.substring(0, str.length() - suffix.length()); return str.substring(0, str.length() - suffix.length());
else
return str; return str;
} }

View File

@ -0,0 +1,142 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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<Path>() {
@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;
}
}

View File

@ -1,132 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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<String, Boolean, String> 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<String, Boolean, String> 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<String> names = new HashSet<>();
public void put(ZipEntry entry) throws IOException {
if (names.add(entry.getName()))
zos.putNextEntry(entry);
}
}

View File

@ -0,0 +1,123 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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<String> filter) throws IOException {
File[] files = null;
Path root = fs.getPath(targetDir);
Files.createDirectories(root);
Files.walkFileTree(source, new SimpleFileVisitor<Path>() {
@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));
}
}

View File

@ -39,7 +39,6 @@ allprojects {
dependencies { dependencies {
compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5' 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.tukaani', name: 'xz', version: '1.8'
compile group: 'org.hildan.fxgson', name: 'fx-gson', version: '3.1.0' compile group: 'org.hildan.fxgson', name: 'fx-gson', version: '3.1.0'
compile group: 'org.jenkins-ci', name: 'constant-pool-scanner', version: '1.2' compile group: 'org.jenkins-ci', name: 'constant-pool-scanner', version: '1.2'