diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLDependencyManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLDependencyManager.java index 815fd51e5..337401e61 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLDependencyManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLDependencyManager.java @@ -20,7 +20,6 @@ package org.jackhuang.hmcl.game; import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.download.GameBuilder; -import org.jackhuang.hmcl.download.game.GameAssetDownloadTask; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.task.ParallelTask; import org.jackhuang.hmcl.task.Task; @@ -45,7 +44,7 @@ public class HMCLDependencyManager extends DefaultDependencyManager { @Override public Task checkGameCompletionAsync(Version version) { return new ParallelTask( - new GameAssetDownloadTask(this, version), + new HMCLGameAssetDownloadTask(this, version), new HMCLGameLibrariesTask(this, version) ); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameAssetDownloadTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameAssetDownloadTask.java new file mode 100644 index 000000000..87cf5f0e1 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameAssetDownloadTask.java @@ -0,0 +1,94 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.game; + +import org.jackhuang.hmcl.download.AbstractDependencyManager; +import org.jackhuang.hmcl.task.FileDownloadTask; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.*; + +import java.io.File; +import java.nio.file.Path; +import java.util.*; + +public class HMCLGameAssetDownloadTask extends Task { + + private final AbstractDependencyManager dependencyManager; + private final Version version; + private final AssetIndexInfo assetIndexInfo; + private final File assetIndexFile; + private final List dependents = new LinkedList<>(); + private final List dependencies = new LinkedList<>(); + + /** + * Constructor. + * + * @param dependencyManager the dependency manager that can provides {@link GameRepository} + * @param version the resolved version + */ + public HMCLGameAssetDownloadTask(HMCLDependencyManager dependencyManager, Version version) { + this.dependencyManager = dependencyManager; + this.version = version; + this.assetIndexInfo = version.getAssetIndex(); + this.assetIndexFile = dependencyManager.getGameRepository().getIndexFile(version.getId(), assetIndexInfo.getId()); + + if (!assetIndexFile.exists()) + dependents.add(new HMCLGameAssetIndexDownloadTask(dependencyManager, version)); + } + + @Override + public Collection getDependents() { + return dependents; + } + + @Override + public Collection getDependencies() { + return dependencies; + } + + @Override + public void execute() throws Exception { + AssetIndex index = Constants.GSON.fromJson(FileUtils.readText(assetIndexFile), AssetIndex.class); + int progress = 0; + if (index != null) + for (AssetObject assetObject : index.getObjects().values()) { + if (Thread.interrupted()) + throw new InterruptedException(); + + File file = dependencyManager.getGameRepository().getAssetObject(version.getId(), assetIndexInfo.getId(), assetObject); + if (file.isFile()) + HMCLLocalRepository.REPOSITORY.tryCacheAssetObject(assetObject, file.toPath()); + else { + Optional path = HMCLLocalRepository.REPOSITORY.getAssetObject(assetObject); + if (path.isPresent()) { + FileUtils.copyFile(path.get().toFile(), file); + } else { + String url = dependencyManager.getDownloadProvider().getAssetBaseURL() + assetObject.getLocation(); + FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(url), file, new FileDownloadTask.IntegrityCheck("SHA-1", assetObject.getHash())); + task.setName(assetObject.getHash()); + dependencies.add(task.finalized((v, succ) -> { + if (succ) + HMCLLocalRepository.REPOSITORY.cacheAssetObject(assetObject, file.toPath()); + })); + } + } + + updateProgress(++progress, index.getObjects().size()); + } + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameAssetIndexDownloadTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameAssetIndexDownloadTask.java new file mode 100644 index 000000000..f42005549 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameAssetIndexDownloadTask.java @@ -0,0 +1,73 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.game; + +import org.jackhuang.hmcl.task.FileDownloadTask; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.FileUtils; +import org.jackhuang.hmcl.util.NetworkUtils; + +import java.io.File; +import java.net.URL; +import java.nio.file.Path; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; + +public class HMCLGameAssetIndexDownloadTask extends Task { + + private final HMCLDependencyManager dependencyManager; + private final Version version; + private final List dependencies = new LinkedList<>(); + + /** + * Constructor. + * + * @param dependencyManager the dependency manager that can provides {@link org.jackhuang.hmcl.game.GameRepository} + * @param version the resolved version + */ + public HMCLGameAssetIndexDownloadTask(HMCLDependencyManager dependencyManager, Version version) { + this.dependencyManager = dependencyManager; + this.version = version; + setSignificance(TaskSignificance.MODERATE); + } + + @Override + public List getDependencies() { + return dependencies; + } + + @Override + public void execute() throws Exception { + AssetIndexInfo assetIndexInfo = version.getAssetIndex(); + File assetIndexFile = dependencyManager.getGameRepository().getIndexFile(version.getId(), assetIndexInfo.getId()); + + Optional path = HMCLLocalRepository.REPOSITORY.getAssetIndex(assetIndexInfo); + if (path.isPresent()) { + FileUtils.copyFile(path.get().toFile(), assetIndexFile); + } else { + URL url = NetworkUtils.toURL(dependencyManager.getDownloadProvider().injectURL(assetIndexInfo.getUrl())); + dependencies.add(new FileDownloadTask(url, assetIndexFile) + .finalized((v, succ) -> { + if (succ) + HMCLLocalRepository.REPOSITORY.cacheAssetIndex(assetIndexInfo, assetIndexFile.toPath()); + })); + } + } + +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index 187b00ec9..e223ff037 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -50,18 +50,6 @@ public class HMCLGameRepository extends DefaultGameRepository { return profile; } - private boolean useSelf(String assetId) { - return new File(getBaseDirectory(), "assets/indexes/" + assetId + ".json").exists(); - } - - @Override - public File getAssetDirectory(String version, String assetId) { - if (useSelf(assetId)) - return super.getAssetDirectory(version, assetId); - else - return new File(Settings.instance().getCommonDirectory(), "assets"); - } - @Override public File getRunDirectory(String id) { if (beingModpackVersions.contains(id) || isModpack(id)) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLLibraryDownloadTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLLibraryDownloadTask.java index 2cd66ffea..8c21f336f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLLibraryDownloadTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLLibraryDownloadTask.java @@ -17,7 +17,6 @@ */ package org.jackhuang.hmcl.game; -import org.jackhuang.hmcl.download.game.LibraryDownloadException; import org.jackhuang.hmcl.download.game.LibraryDownloadTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.FileUtils; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLLocalRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLLocalRepository.java index a9b85fa7a..1a2aaf64f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLLocalRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLLocalRepository.java @@ -28,16 +28,17 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; -import java.util.function.Supplier; import java.util.logging.Level; import java.util.stream.Collectors; public class HMCLLocalRepository { private final StringProperty directory = new SimpleStringProperty(); + private Path commonDir; private Path cacheDir; private Path librariesDir; private Path jarsDir; + private Path assetObjectsDir; private Path indexFile; private Index index = null; @@ -59,9 +60,11 @@ public class HMCLLocalRepository { } private void changeDirectory(Path commonDir) { + this.commonDir = commonDir; cacheDir = commonDir.resolve("cache"); librariesDir = commonDir.resolve("libraries"); jarsDir = commonDir.resolve("jars"); + assetObjectsDir = commonDir.resolve("assets").resolve("objects"); indexFile = cacheDir.resolve("index.json"); try { @@ -81,6 +84,14 @@ public class HMCLLocalRepository { return Files.exists(getFile(algorithm, hash)); } + /** + * Try to cache the library given. + * This library will be cached only if it is verified. + * If cannot be verified, the library will not be cached. + * + * @param library the library being cached + * @param jar the file of library + */ public void tryCacheLibrary(Library library, Path jar) { if (index.getLibraries().stream().anyMatch(it -> library.getName().equals(it.getName()))) return; @@ -103,6 +114,12 @@ public class HMCLLocalRepository { } } + /** + * Get the path of cached library, empty if not cached + * + * @param library the library we check if cached. + * @return the cached path if exists, otherwise empty + */ public synchronized Optional getLibrary(Library library) { LibraryDownloadInfo info = library.getDownload(); String hash = info.getSha1(); @@ -146,6 +163,15 @@ public class HMCLLocalRepository { return Optional.empty(); } + /** + * Caches the library file to repository. + * + * @param library the library to cache + * @param path the file being cached, must be verified + * @param forge true if this library is provided by Forge + * @return cached file location + * @throws IOException if failed to calculate hash code of {@code path} or copy the file to cache + */ public synchronized Path cacheLibrary(Library library, Path path, boolean forge) throws IOException { String hash = library.getDownload().getSha1(); if (hash == null) @@ -161,6 +187,56 @@ public class HMCLLocalRepository { return cache; } + /** + * Get the path of cached asset index file, empty if not cached + * + * @param info the asset index info + * @return the cached path if exists, otherwise empty + */ + public synchronized Optional getAssetIndex(AssetIndexInfo info) { + String hash = info.getSha1(); + + if (fileExists(SHA1, hash)) + return Optional.of(getFile(SHA1, hash)); + + // check old common directory + Path file = commonDir.resolve("assets").resolve("indexes").resolve(info.getId() + ".json"); + if (Files.exists(file)) { + try { + if (hash != null) { + String checksum = Hex.encodeHex(DigestUtils.digest("SHA-1", file)); + if (hash.equalsIgnoreCase(checksum)) + return Optional.of(restore(file, () -> cacheAssetIndex(info, file))); + } else { + return Optional.of(file); + } + } catch (IOException e) { + // we cannot check the hashcode or unable to move file. + } + } + + return Optional.empty(); + } + + /** + * Caches the asset index file to repository. + * + * @param assetIndexInfo the asset index of + * @param path the file being cached, must be verified + * @return cached file location + * @throws IOException if failed to calculate hash code of {@code path} or copy the file to cache + */ + public synchronized Path cacheAssetIndex(AssetIndexInfo assetIndexInfo, Path path) throws IOException { + String hash = assetIndexInfo.getSha1(); + if (hash == null) + hash = Hex.encodeHex(DigestUtils.digest(SHA1, path)); + + Path cache = getFile(SHA1, hash); + FileUtils.copyFile(path.toFile(), cache.toFile()); + + return cache; + } + public synchronized Optional getVersion(String gameVersion, Version version) { DownloadInfo info = version.getDownloadInfo(); String hash = info.getSha1(); @@ -201,6 +277,48 @@ public class HMCLLocalRepository { return cache; } + public synchronized Optional getAssetObject(AssetObject assetObject) { + String hash = assetObject.getHash(); + + if (fileExists(SHA1, hash)) + return Optional.of(getFile(SHA1, hash)); + + // check old common directory, but we will no longer maintain it. + Path file = assetObjectsDir.resolve(assetObject.getLocation()); + if (Files.exists(file)) { + if (hash != null) { + try { + String checksum = Hex.encodeHex(DigestUtils.digest("SHA-1", file)); + if (!checksum.equalsIgnoreCase(hash)) { + // The file is not the one we want + return Optional.empty(); + } else { + return Optional.of(restore(file, () -> cacheAssetObject(assetObject, file))); + } + } catch (IOException e) { + // we cannot check the hashcode. + return Optional.empty(); + } + } else { + return Optional.of(file); + } + } + + return Optional.empty(); + } + + public synchronized Path cacheAssetObject(AssetObject assetObject, Path path) throws IOException { + Path cache = getFile(SHA1, assetObject.getHash()); + FileUtils.copyFile(path.toFile(), cache.toFile()); + return cache; + } + + public synchronized void tryCacheAssetObject(AssetObject assetObject, Path path) throws IOException { + Path cache = getFile(SHA1, assetObject.getHash()); + if (Files.exists(cache)) return; + FileUtils.copyFile(path.toFile(), cache.toFile()); + } + private Path restore(Path original, ExceptionalSupplier cacheSupplier) throws IOException { Path cache = cacheSupplier.get(); Files.delete(original); @@ -236,7 +354,7 @@ public class HMCLLocalRepository { * "hash": "..." * } * ] - * // we don't cache asset objects in our repository which are already stored in a cache repository. + * // assets and versions will not be included in index. * } */ private class Index { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java index 7ce551ff4..7760d20e8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java @@ -24,10 +24,9 @@ import javafx.scene.layout.BorderPane; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import org.jackhuang.hmcl.download.forge.ForgeInstallTask; -import org.jackhuang.hmcl.download.game.GameAssetDownloadTask; -import org.jackhuang.hmcl.download.game.GameAssetRefreshTask; import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask; import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask; +import org.jackhuang.hmcl.game.HMCLGameAssetDownloadTask; import org.jackhuang.hmcl.game.HMCLModpackExportTask; import org.jackhuang.hmcl.game.HMCLModpackInstallTask; import org.jackhuang.hmcl.mod.*; @@ -62,9 +61,7 @@ public final class TaskListPane extends StackPane { if (!task.getSignificance().shouldShow()) return; - if (task instanceof GameAssetRefreshTask) { - task.setName(i18n("assets.download")); - } else if (task instanceof GameAssetDownloadTask) { + if (task instanceof HMCLGameAssetDownloadTask) { task.setName(i18n("assets.download_all")); } else if (task instanceof ForgeInstallTask) { task.setName(i18n("install.installer.install", i18n("install.installer.forge"))); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetDownloadTask.java index f43b482ce..ff58389a6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetDownloadTask.java @@ -28,10 +28,7 @@ import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.NetworkUtils; import java.io.File; -import java.util.Collection; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.logging.Level; /** @@ -43,7 +40,6 @@ public final class GameAssetDownloadTask extends Task { private final AbstractDependencyManager dependencyManager; private final Version version; private final GameAssetRefreshTask refreshTask; - private final List dependents = new LinkedList<>(); private final List dependencies = new LinkedList<>(); /** @@ -56,16 +52,15 @@ public final class GameAssetDownloadTask extends Task { this.dependencyManager = dependencyManager; this.version = version; this.refreshTask = new GameAssetRefreshTask(dependencyManager, version); - this.dependents.add(refreshTask); } @Override public Collection getDependents() { - return dependents; + return Collections.singleton(refreshTask); } @Override - public List getDependencies() { + public Collection getDependencies() { return dependencies; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetIndexDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetIndexDownloadTask.java index 0e05560f9..d79f0fece 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetIndexDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetIndexDownloadTask.java @@ -65,6 +65,9 @@ public final class GameAssetIndexDownloadTask extends Task { if (!FileUtils.makeDirectory(assetDir)) throw new IOException("Cannot create directory: " + assetDir); File assetIndexFile = dependencyManager.getGameRepository().getIndexFile(version.getId(), assetIndexInfo.getId()); + + // We should not check the hash code of asset index file since this file is not consistent + // And Mojang will modify this file anytime. So assetIndex.hash might be outdated. dependencies.add(new FileDownloadTask( NetworkUtils.toURL(dependencyManager.getDownloadProvider().injectURL(assetIndexInfo.getUrl())), assetIndexFile diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/AssetObject.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/AssetObject.java index cd6b7c624..a09bd44ec 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/AssetObject.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/AssetObject.java @@ -53,7 +53,7 @@ public final class AssetObject implements Validation { @Override public void validate() throws JsonParseException { - if (StringUtils.isBlank(hash)) + if (StringUtils.isBlank(hash) || hash.length() < 2) throw new IllegalStateException("AssetObject hash cannot be blank."); } }