feat(modpack-export): 为整合包导出添加进度显示 (#7206)

fix #7205
This commit is contained in:
鱼前三百 authored and GitHub committed 2026-11-05 21:04:36 +08:00
1 parent a262a95408
commit b46a07cd45
4 files changed
+111 -49

No files matched your search

@@ -78,19 +78,27 @@ public class McbbsModpackExportTask extends Task<Void> {
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<McbbsModpackManifest.File> files = new ArrayList<>();
zip.putDirectory(runDirectory, "overrides", path -> {
List<Path> 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<McbbsModpackManifest.File> 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<Void> {
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.
@@ -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<Void> {
}
List<String> 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<Void> {
List<ModrinthManifest.File> files = new ArrayList<>();
Set<String> 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<Path> 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<Path> 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()) {
@@ -70,7 +70,7 @@ public class ServerModpackExportTask extends Task<Void> {
try (Zipper zip = new Zipper(modpackFile)) {
Path runDirectory = instance.getRunDirectory();
List<ModpackConfiguration.FileInformation> 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<Void> {
} else {
return false;
}
});
}, this::updateProgress);
GameVersionNumber version = instance.getVersion();
if (version == GameVersionNumber.unknown()) {
@@ -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<String> entryNames;
private final @Nullable Set<String> 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<String, IOException> filter) throws IOException {
public void putDirectory(Path source, String targetDir, @Nullable ExceptionalPredicate<String, IOException> filter) throws IOException {
String root = normalize(targetDir);
Files.walkFileTree(source, new SimpleFileVisitor<Path>() {
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<String, IOException> filter, DoubleConsumer onProgressed) throws IOException {
Map<Path, Long> 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);
}