diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackExportTask.java index b282c02d67..a700f31c01 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackExportTask.java @@ -78,19 +78,27 @@ public class McbbsModpackExportTask extends Task { 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 (var zip = new Zipper(modpackFile)) { Path runDirectory = instance.getRunDirectory(); - List files = new ArrayList<>(); - zip.putDirectory(runDirectory, "overrides", path -> { + + List paths = new ArrayList<>(); + zip.putDirectoryProgressed(runDirectory, "overrides", path -> { if (Modpack.acceptFile(path, blackList, info.getWhitelist())) { Path file = runDirectory.resolve(path); if (Files.isRegularFile(file)) { - String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); - files.add(new McbbsModpackManifest.AddonFile(true, relativePath, DigestUtils.digestToString("SHA-1", file))); + paths.add(file); } return true; } else { return false; } - }); + }, progress -> updateProgress(0.5 * progress)); + + List files = new ArrayList<>(); + long current = 0; + for (Path file : paths) { + String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); + files.add(new McbbsModpackManifest.AddonFile(true, relativePath, DigestUtils.digestToString("SHA-1", file))); + updateProgress(0.5 + 0.5 * ++current / paths.size()); + } GameVersionNumber version = instance.getVersion(); if (version == GameVersionNumber.unknown()) { @@ -134,6 +142,7 @@ public class McbbsModpackExportTask extends Task { CurseManifest curseManifest = new CurseManifest(CurseManifest.MINECRAFT_MODPACK, 1, info.getName(), info.getVersion(), info.getAuthor(), "overrides", new CurseManifestMinecraft(gameVersion, modLoaders), Collections.emptyList()); zip.putTextFile(JsonUtils.GSON.toJson(curseManifest), "manifest.json"); } + updateProgress(1); } /// Export options supported by the MCBBS format. diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthModpackExportTask.java index 5cd0459ce3..e4d7593621 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthModpackExportTask.java @@ -2,14 +2,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL package org.jackhuang.hmcl.modpack.modrinth; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; - import org.jackhuang.hmcl.addon.LocalAddonManager; +import org.jackhuang.hmcl.addon.RemoteAddon; import org.jackhuang.hmcl.addon.mod.ModManager; +import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository; import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository; import org.jackhuang.hmcl.game.DefaultGameInstance; import org.jackhuang.hmcl.game.GameComponentAnalyzer; @@ -23,11 +19,16 @@ import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.Zipper; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; -import org.jackhuang.hmcl.addon.RemoteAddon; -import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository; import org.jetbrains.annotations.NotNullByDefault; import org.jetbrains.annotations.Nullable; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Stream; + import static org.jackhuang.hmcl.util.logging.Logger.LOG; /// Exports one registered game instance as a Modrinth modpack archive. @@ -114,10 +115,8 @@ public class ModrinthModpackExportTask extends Task { } List downloads = new ArrayList<>(); - if (modrinthVersion.isPresent()) - downloads.add(modrinthVersion.get().file().url()); - if (curseForgeVersion.isPresent()) - downloads.add(curseForgeVersion.get().file().url()); + modrinthVersion.ifPresent(version -> downloads.add(version.file().url())); + curseForgeVersion.ifPresent(version -> downloads.add(version.file().url())); long fileSize = Files.size(file); if (fileSize > Integer.MAX_VALUE) { @@ -145,39 +144,42 @@ public class ModrinthModpackExportTask extends Task { List files = new ArrayList<>(); Set filesInManifest = new HashSet<>(); - String[] resourceDirs = {"resourcepacks", "shaderpacks", "mods"}; - for (String dir : resourceDirs) { - Path dirPath = runDirectory.resolve(dir); - if (Files.exists(dirPath)) { - Files.walk(dirPath) - .filter(Files::isRegularFile) - .forEach(file -> { - try { - String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); + List resourceDirs = Stream.of("resourcepacks", "shaderpacks", "mods").map(runDirectory::resolve).filter(Files::exists).toList(); - if (!info.getWhitelist().contains(relativePath)) { - return; - } - - ModrinthManifest.File fileEntry = tryGetRemoteFile(file, relativePath); - if (fileEntry != null) { - files.add(fileEntry); - filesInManifest.add(relativePath); - } - } catch (IOException e) { - LOG.warning("Failed to process file: " + file, e); - } - }); + List paths = new ArrayList<>(); + for (Path dir : resourceDirs) { + try (var stream = Files.walk(dir)) { + paths.addAll(stream.filter(Files::isRegularFile).toList()); } } - zip.putDirectory(runDirectory, "client-overrides", path -> { + long current = 0; + for (var file : paths) { + updateProgress(0.5 * ++current / paths.size()); + String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); + + if (!info.getWhitelist().contains(relativePath)) { + continue; + } + + try { + ModrinthManifest.File fileEntry = tryGetRemoteFile(file, relativePath); + if (fileEntry != null) { + files.add(fileEntry); + filesInManifest.add(relativePath); + } + } catch (IOException e) { + LOG.warning("Failed to process file: " + file, e); + } + } + + zip.putDirectoryProgressed(runDirectory, "client-overrides", path -> { String relativePath = path.replace(File.separatorChar, '/'); if (filesInManifest.contains(relativePath)) { return false; } return Modpack.acceptFile(path, blackList, info.getWhitelist()); - }); + }, progress -> updateProgress(0.5 + 0.5 * progress)); GameVersionNumber version = instance.getVersion(); if (version == GameVersionNumber.unknown()) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackExportTask.java index 71e1046944..a9aa92765c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackExportTask.java @@ -70,7 +70,7 @@ public class ServerModpackExportTask extends Task { try (Zipper zip = new Zipper(modpackFile)) { Path runDirectory = instance.getRunDirectory(); List files = new ArrayList<>(); - zip.putDirectory(runDirectory, "overrides", path -> { + zip.putDirectoryProgressed(runDirectory, "overrides", path -> { if (Modpack.acceptFile(path, blackList, exportInfo.getWhitelist())) { Path file = runDirectory.resolve(path); if (Files.isRegularFile(file)) { @@ -81,7 +81,7 @@ public class ServerModpackExportTask extends Task { } else { return false; } - }); + }, this::updateProgress); GameVersionNumber version = instance.getVersion(); if (version == GameVersionNumber.unknown()) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/Zipper.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/Zipper.java index 138e108efb..07754a4a1e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/Zipper.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/Zipper.java @@ -3,7 +3,8 @@ package org.jackhuang.hmcl.util.io; import org.jackhuang.hmcl.util.function.ExceptionalPredicate; -import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.NotNullByDefault; +import org.jetbrains.annotations.Nullable; import java.io.*; import java.nio.charset.Charset; @@ -11,7 +12,10 @@ import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.Set; +import java.util.function.DoubleConsumer; import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipException; @@ -22,11 +26,12 @@ import java.util.zip.ZipOutputStream; * * @author huangyuhui */ +@NotNullByDefault public final class Zipper implements Closeable { private final ZipOutputStream zos; private final byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE]; - private final Set entryNames; + private final @Nullable Set entryNames; public Zipper(Path zipFile) throws IOException { this(zipFile, false); @@ -88,9 +93,9 @@ public final class Zipper implements Closeable { * @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, ExceptionalPredicate filter) throws IOException { + public void putDirectory(Path source, String targetDir, @Nullable ExceptionalPredicate filter) throws IOException { String root = normalize(targetDir); - Files.walkFileTree(source, new SimpleFileVisitor() { + Files.walkFileTree(source, new SimpleFileVisitor<>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (".DS_Store".equals(file.getFileName().toString())) { @@ -121,6 +126,52 @@ public final class Zipper implements Closeable { }); } + /// Compress all the files in source with progress reporting. + /// + /// @param source the directory to be compressed + /// @param targetDir the path of the directory in this zip file + /// @param filter returns false to skip a file or directory, or null to accept all + /// @param onProgressed receives byte-weighted progress from 0.0 to 1.0 + public void putDirectoryProgressed(Path source, String targetDir, @Nullable ExceptionalPredicate filter, DoubleConsumer onProgressed) throws IOException { + Map entries = new LinkedHashMap<>(); + 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 = normalize(source.relativize(file).normalize().toString()); + if (filter != null && !filter.test(relativePath)) { + return FileVisitResult.SKIP_SUBTREE; + } + entries.put(file, Math.max(1L, attrs.size())); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + String relativePath = normalize(source.relativize(dir).normalize().toString()); + if (filter != null && !filter.test(relativePath)) { + return FileVisitResult.SKIP_SUBTREE; + } + entries.put(dir, 0L); + return FileVisitResult.CONTINUE; + } + }); + + String root = normalize(targetDir); + long total = entries.values().stream().mapToLong(Long::longValue).sum(); + long completed = 0; + onProgressed.accept(0.0); + for (var entry : entries.entrySet()) { + String relativePath = normalize(source.relativize(entry.getKey()).normalize().toString()); + putFile(entry.getKey(), resolve(root, relativePath)); + if (entry.getValue() > 0) + onProgressed.accept((double) (completed += entry.getValue()) / total); + } + onProgressed.accept(1.0); + } + public void putFile(Path file, String path) throws IOException { path = normalize(path); @@ -160,11 +211,11 @@ public final class Zipper implements Closeable { zos.write(b); } - public void write(@NotNull byte[] b) throws IOException { + public void write(byte[] b) throws IOException { zos.write(b); } - public void write(@NotNull byte[] b, int off, int len) throws IOException { + public void write(byte[] b, int off, int len) throws IOException { zos.write(b, off, len); }