diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java index db6c0eb3da1..5f9d72bbd08 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java @@ -498,7 +498,15 @@ public RemoteAddon.Version toVersion() { null, fileDate(), versionType, - new RemoteAddon.File(Collections.emptyMap(), downloadUrl(), fileName()), + new RemoteAddon.File(hashes == null ? Collections.emptyMap() : hashes.stream().collect(Collectors.toMap( + hash -> switch (hash.algo()) { + case 1 -> "sha1"; + case 2 -> "md5"; + default -> "algo" + hash.algo(); + }, + LatestFileHash::value, + (a, b) -> a + )), downloadUrl(), fileName()), dependencies.stream().map(dependency -> { if (!RELATION_TYPE.containsKey(dependency.relationType())) { throw new IllegalStateException("Broken datas."); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseCompletionTask.java index d48570f82a4..b26f282599e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseCompletionTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseCompletionTask.java @@ -121,16 +121,33 @@ public void execute() throws Exception { manifest.files().parallelStream() .map(file -> { updateProgress(finished.incrementAndGet(), manifest.files().size()); - if (StringUtils.isBlank(file.fileName()) || file.url() == null) { - try { - RemoteAddon.File remoteFile = CurseForgeRemoteAddonRepository.MODS.getAddonFile(Integer.toString(file.projectID()), Integer.toString(file.fileID())); - return file.withFileName(remoteFile.filename()).withURL(remoteFile.url()); - } catch (FileNotFoundException fof) { - LOG.warning("Could not query api.curseforge.com for deleted mods: " + file.projectID() + ", " + file.fileID(), fof); - notFound.set(true); - return file; - } catch (IOException | JsonParseException e) { - LOG.warning("Unable to fetch the file name projectID=" + file.projectID() + ", fileID=" + file.fileID(), e); + if (StringUtils.isBlank(file.fileName()) || file.url() == null || file.hashes() == null || file.hashes().isEmpty()) { + RemoteAddon.File remoteFile = null; + Exception lastException = null; + for (int attempt = 0; attempt < 3; attempt++) { + try { + remoteFile = CurseForgeRemoteAddonRepository.MODS.getAddonFile(Integer.toString(file.projectID()), Integer.toString(file.fileID())); + break; + } catch (FileNotFoundException fof) { + LOG.warning("Could not query api.curseforge.com for deleted mods: " + file.projectID() + ", " + file.fileID(), fof); + notFound.set(true); + return file; + } catch (IOException | JsonParseException e) { + lastException = e; + if (attempt < 2) { + try { + Thread.sleep(500L * (attempt + 1)); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + break; + } + } + } + } + if (remoteFile != null) { + return file.withFileName(remoteFile.filename()).withURL(remoteFile.url()).withHashes(remoteFile.hashes()); + } else { + LOG.warning("Unable to fetch the file name projectID=" + file.projectID() + ", fileID=" + file.fileID(), lastException); allNameKnown.set(false); return file; } @@ -150,12 +167,26 @@ public void execute() throws Exception { .filter(f -> f.fileName() != null) .flatMap(f -> { try { - Path path = guessFilePath(f, dependency.getDownloadProvider(), resourcePacksRoot, shaderPacksRoot); + Path path = null; + for (int attempt = 0; attempt < 3; attempt++) { + try { + path = guessFilePath(f, dependency.getDownloadProvider(), resourcePacksRoot, shaderPacksRoot); + break; + } catch (IOException e) { + if (attempt == 2) throw e; + try { + Thread.sleep(500L * (attempt + 1)); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + break; + } + } + } if (path == null) { return Stream.empty(); } - var task = new FileDownloadTask(f.url(), path); + var task = new FileDownloadTask(f.url(), path, f.getIntegrityCheck()); task.setCacheRepository(dependency.getCacheRepository()); task.setCaching(true); return Stream.of(task.withCounter("hmcl.modpack.download")); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseManifestFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseManifestFile.java index b8edc77f56f..bd86bdb91cc 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseManifestFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseManifestFile.java @@ -31,7 +31,12 @@ public record CurseManifestFile(@SerializedName("projectID") int projectID, @SerializedName("fileID") int fileID, @SerializedName("fileName") String fileName, @SerializedName("url") String url, - @SerializedName("required") boolean required) implements Validation { + @SerializedName("required") boolean required, + @SerializedName("hashes") java.util.Map hashes) implements Validation { + + public CurseManifestFile(int projectID, int fileID, String fileName, String url, boolean required) { + this(projectID, fileID, fileName, url, required, null); + } @Override public void validate() throws JsonParseException { @@ -51,12 +56,31 @@ public String url() { } } + @Nullable + public org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck getIntegrityCheck() { + if (hashes == null || hashes.isEmpty()) return null; + if (hashes.containsKey("sha1")) { + return new org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck("SHA-1", hashes.get("sha1")); + } else if (hashes.containsKey("md5")) { + return new org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck("MD5", hashes.get("md5")); + } else if (hashes.containsKey("sha256")) { + return new org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck("SHA-256", hashes.get("sha256")); + } else if (hashes.containsKey("sha512")) { + return new org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck("SHA-512", hashes.get("sha512")); + } + return null; + } + public CurseManifestFile withFileName(String fileName) { - return new CurseManifestFile(projectID, fileID, fileName, url, required); + return new CurseManifestFile(projectID, fileID, fileName, url, required, hashes); } public CurseManifestFile withURL(String url) { - return new CurseManifestFile(projectID, fileID, fileName, url, required); + return new CurseManifestFile(projectID, fileID, fileName, url, required, hashes); + } + + public CurseManifestFile withHashes(java.util.Map hashes) { + return new CurseManifestFile(projectID, fileID, fileName, url, required, hashes); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackCompletionTask.java index 2caca9ab70e..64f57a1082a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackCompletionTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackCompletionTask.java @@ -121,7 +121,7 @@ public void execute() throws Exception { dependencies.add(builder.buildAsync()); } - Path rootPath = repository.getInstanceRoot(instanceId).toAbsolutePath().normalize(); + Path rootPath = repository.getRunDirectory(instanceId).toAbsolutePath().normalize(); Map files = manifest.getManifest().getFiles().stream() .collect(Collectors.toMap(ModpackConfiguration.FileInformation::getPath, Function.identity()));