mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-08-03 03:16:35 -04:00
修复下载缓存不生效的问题 (#4167)
This commit is contained in:
parent
7d6f21dc3a
commit
bcd73e6d15
@ -49,6 +49,8 @@ import org.jackhuang.hmcl.download.quilt.QuiltAPIRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltRemoteVersion;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
@ -67,7 +69,6 @@ import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
@ -96,7 +97,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
private final StackPane center;
|
||||
|
||||
private final VersionList<?> versionList;
|
||||
private CompletableFuture<?> executor;
|
||||
private Task<?> executor;
|
||||
|
||||
private final HBox searchBar;
|
||||
private final StringProperty queryString = new SimpleStringProperty();
|
||||
@ -308,7 +309,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
public void refresh() {
|
||||
VersionList<?> currentVersionList = versionList;
|
||||
root.setContent(spinner, ContainerAnimations.FADE);
|
||||
executor = currentVersionList.refreshAsync(gameVersion).whenComplete((result, exception) -> {
|
||||
executor = currentVersionList.refreshAsync(gameVersion).whenComplete(Schedulers.defaultScheduler(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
List<RemoteVersion> items = loadVersions();
|
||||
|
||||
@ -338,6 +339,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
// https://github.com/HMCL-dev/HMCL/issues/938
|
||||
System.gc();
|
||||
});
|
||||
executor.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -348,8 +350,9 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
@Override
|
||||
public void cleanup(Map<String, Object> settings) {
|
||||
settings.remove(libraryId);
|
||||
if (executor != null)
|
||||
executor.cancel(true);
|
||||
// fixme
|
||||
// if (executor != null)
|
||||
// executor.cancel(true);
|
||||
}
|
||||
|
||||
private void onRefresh() {
|
||||
|
@ -29,13 +29,14 @@ import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@ -43,7 +44,6 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
public class DefaultCacheRepository extends CacheRepository {
|
||||
private Path librariesDir;
|
||||
private Path indexFile;
|
||||
private final ReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
private Index index = null;
|
||||
|
||||
public DefaultCacheRepository() {
|
||||
@ -64,10 +64,13 @@ public class DefaultCacheRepository extends CacheRepository {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
if (Files.isRegularFile(indexFile)) {
|
||||
index = JsonUtils.fromNonNullJson(Files.readString(indexFile), Index.class);
|
||||
}
|
||||
else
|
||||
index = JsonUtils.fromJsonFile(indexFile, Index.class);
|
||||
if (index == null) {
|
||||
throw new JsonParseException("Index file is empty or invalid");
|
||||
}
|
||||
} else {
|
||||
index = new Index();
|
||||
}
|
||||
} catch (IOException | JsonParseException e) {
|
||||
LOG.warning("Unable to read index file", e);
|
||||
index = new Index();
|
||||
@ -82,7 +85,7 @@ public class DefaultCacheRepository extends CacheRepository {
|
||||
* If cannot be verified, the library will not be cached.
|
||||
*
|
||||
* @param library the library being cached
|
||||
* @param jar the file of library
|
||||
* @param jar the file of library
|
||||
*/
|
||||
public void tryCacheLibrary(Library library, Path jar) {
|
||||
lock.readLock().lock();
|
||||
@ -171,8 +174,8 @@ public class DefaultCacheRepository extends CacheRepository {
|
||||
* 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
|
||||
* @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
|
||||
*/
|
||||
@ -201,25 +204,29 @@ public class DefaultCacheRepository extends CacheRepository {
|
||||
if (indexFile == null || index == null) return;
|
||||
try {
|
||||
Files.createDirectories(indexFile.getParent());
|
||||
JsonUtils.writeToJsonFile(indexFile, index);
|
||||
FileUtils.saveSafely(indexFile, outputStream -> {
|
||||
try (var writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8))) {
|
||||
JsonUtils.GSON.toJson(index, writer);
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
LOG.error("Unable to save index.json", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {
|
||||
* "libraries": {
|
||||
* // allow a library has multiple hash code.
|
||||
* [
|
||||
* "name": "net.minecraftforge:forge:1.11.2-13.20.0.2345",
|
||||
* "hash": "blablabla",
|
||||
* "type": "forge"
|
||||
* ]
|
||||
* }
|
||||
* // assets and versions will not be included in index.
|
||||
* }
|
||||
*/
|
||||
/// ```json
|
||||
/// {
|
||||
/// "libraries": {
|
||||
/// // allow a library has multiple hash code.
|
||||
/// [
|
||||
/// "name": "net.minecraftforge:forge:1.11.2-13.20.0.2345",
|
||||
/// "hash": "blablabla",
|
||||
/// "type": "forge"
|
||||
/// ]
|
||||
/// }
|
||||
/// }
|
||||
///```
|
||||
/// assets and versions will not be included in index.
|
||||
private static final class Index implements Validation {
|
||||
private final Set<LibraryIndex> libraries;
|
||||
|
||||
|
@ -148,7 +148,7 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
|
||||
if (baseVersion.isResolved()) throw new IllegalArgumentException("Version should not be resolved");
|
||||
|
||||
VersionList<?> versionList = getVersionList(libraryId);
|
||||
return Task.fromCompletableFuture(versionList.loadAsync(gameVersion))
|
||||
return versionList.loadAsync(gameVersion)
|
||||
.thenComposeAsync(() -> installLibraryAsync(baseVersion, versionList.getVersion(gameVersion, libraryVersion)
|
||||
.orElseThrow(() -> new IOException("Remote library " + libraryId + " has no version " + libraryVersion))))
|
||||
.withStage(String.format("hmcl.install.%s:%s", libraryId, libraryVersion));
|
||||
|
@ -17,8 +17,9 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.download;
|
||||
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@ -40,44 +41,40 @@ public class MultipleSourceVersionList extends VersionList<RemoteVersion> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> loadAsync() {
|
||||
public Task<?> loadAsync() {
|
||||
throw new UnsupportedOperationException("MultipleSourceVersionList does not support loading the entire remote version list.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> refreshAsync() {
|
||||
public Task<?> refreshAsync() {
|
||||
throw new UnsupportedOperationException("MultipleSourceVersionList does not support loading the entire remote version list.");
|
||||
}
|
||||
|
||||
private CompletableFuture<?> refreshAsync(String gameVersion, int sourceIndex) {
|
||||
private Task<?> refreshAsync(String gameVersion, int sourceIndex) {
|
||||
VersionList<?> versionList = backends[sourceIndex];
|
||||
CompletableFuture<Void> future = versionList.refreshAsync(gameVersion)
|
||||
.thenRunAsync(() -> {
|
||||
return versionList.refreshAsync(gameVersion)
|
||||
.thenComposeAsync(() -> {
|
||||
lock.writeLock().lock();
|
||||
|
||||
try {
|
||||
versions.putAll(gameVersion, versionList.getVersions(gameVersion));
|
||||
} catch (Exception e) {
|
||||
if (sourceIndex == backends.length - 1) {
|
||||
LOG.warning("Failed to fetch versions list from all sources", e);
|
||||
throw e;
|
||||
} else {
|
||||
LOG.warning("Failed to fetch versions list and try to fetch from other source", e);
|
||||
return refreshAsync(gameVersion, sourceIndex + 1);
|
||||
}
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
if (sourceIndex == backends.length - 1) {
|
||||
return future;
|
||||
} else {
|
||||
return future.<CompletableFuture<?>>handle((ignore, e) -> {
|
||||
if (e == null) {
|
||||
return future;
|
||||
}
|
||||
|
||||
LOG.warning("Failed to fetch versions list and try to fetch from other source", e);
|
||||
return refreshAsync(gameVersion, sourceIndex + 1);
|
||||
}).thenCompose(it -> it);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> refreshAsync(String gameVersion) {
|
||||
public Task<?> refreshAsync(String gameVersion) {
|
||||
versions.clear(gameVersion);
|
||||
return refreshAsync(gameVersion, 0);
|
||||
}
|
||||
|
@ -17,17 +17,16 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.download;
|
||||
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.SimpleMultimap;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
/**
|
||||
* The remote version list.
|
||||
*
|
||||
* @param <T> The subclass of {@code RemoteVersion}, the type of RemoteVersion.
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public abstract class VersionList<T extends RemoteVersion> {
|
||||
@ -48,6 +47,7 @@ public abstract class VersionList<T extends RemoteVersion> {
|
||||
|
||||
/**
|
||||
* True if the version list that contains the remote versions which depends on the specific game version has been loaded.
|
||||
*
|
||||
* @param gameVersion the remote version depends on
|
||||
*/
|
||||
public boolean isLoaded(String gameVersion) {
|
||||
@ -61,44 +61,36 @@ public abstract class VersionList<T extends RemoteVersion> {
|
||||
/**
|
||||
* @return the task to reload the remote version list.
|
||||
*/
|
||||
public abstract CompletableFuture<?> refreshAsync();
|
||||
public abstract Task<?> refreshAsync();
|
||||
|
||||
/**
|
||||
* @param gameVersion the remote version depends on
|
||||
* @return the task to reload the remote version list.
|
||||
*/
|
||||
public CompletableFuture<?> refreshAsync(String gameVersion) {
|
||||
public Task<?> refreshAsync(String gameVersion) {
|
||||
return refreshAsync();
|
||||
}
|
||||
|
||||
public CompletableFuture<?> loadAsync() {
|
||||
return CompletableFuture.completedFuture(null)
|
||||
.thenComposeAsync(unused -> {
|
||||
lock.readLock().lock();
|
||||
boolean loaded;
|
||||
|
||||
try {
|
||||
loaded = isLoaded();
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
return loaded ? CompletableFuture.completedFuture(null) : refreshAsync();
|
||||
});
|
||||
public Task<?> loadAsync() {
|
||||
return Task.composeAsync(() -> {
|
||||
lock.readLock().lock();
|
||||
try {
|
||||
return isLoaded() ? null : refreshAsync();
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<?> loadAsync(String gameVersion) {
|
||||
return CompletableFuture.completedFuture(null)
|
||||
.thenComposeAsync(unused -> {
|
||||
lock.readLock().lock();
|
||||
boolean loaded;
|
||||
|
||||
try {
|
||||
loaded = isLoaded(gameVersion);
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
return loaded ? CompletableFuture.completedFuture(null) : refreshAsync(gameVersion);
|
||||
});
|
||||
public Task<?> loadAsync(String gameVersion) {
|
||||
return Task.composeAsync(() -> {
|
||||
lock.readLock().lock();
|
||||
try {
|
||||
return isLoaded(gameVersion) ? null : refreshAsync(gameVersion);
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected Collection<T> getVersionsImpl(String gameVersion) {
|
||||
@ -123,7 +115,7 @@ public abstract class VersionList<T extends RemoteVersion> {
|
||||
/**
|
||||
* Get the specific remote version.
|
||||
*
|
||||
* @param gameVersion the Minecraft version that remote versions belong to
|
||||
* @param gameVersion the Minecraft version that remote versions belong to
|
||||
* @param remoteVersion the version of the remote version.
|
||||
* @return the specific remote version, null if it is not found.
|
||||
*/
|
||||
|
@ -21,12 +21,10 @@ import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.wrap;
|
||||
|
||||
public class FabricAPIVersionList extends VersionList<FabricAPIRemoteVersion> {
|
||||
|
||||
@ -42,14 +40,14 @@ public class FabricAPIVersionList extends VersionList<FabricAPIRemoteVersion> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> refreshAsync() {
|
||||
return CompletableFuture.runAsync(wrap(() -> {
|
||||
public Task<?> refreshAsync() {
|
||||
return Task.runAsync(() -> {
|
||||
for (RemoteMod.Version modVersion : Lang.toIterable(ModrinthRemoteModRepository.MODS.getRemoteVersionsById("P7dR8mSH"))) {
|
||||
for (String gameVersion : modVersion.getGameVersions()) {
|
||||
versions.put(gameVersion, new FabricAPIRemoteVersion(gameVersion, modVersion.getVersion(), modVersion.getName(), modVersion.getDatePublished(), modVersion,
|
||||
Collections.singletonList(modVersion.getFile().getUrl())));
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ package org.jackhuang.hmcl.download.fabric;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@ -26,10 +27,8 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.wrap;
|
||||
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
|
||||
|
||||
public final class FabricVersionList extends VersionList<FabricRemoteVersion> {
|
||||
@ -45,8 +44,8 @@ public final class FabricVersionList extends VersionList<FabricRemoteVersion> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> refreshAsync() {
|
||||
return CompletableFuture.runAsync(wrap(() -> {
|
||||
public Task<?> refreshAsync() {
|
||||
return Task.runAsync(() -> {
|
||||
List<String> gameVersions = getGameVersions(GAME_META_URL);
|
||||
List<String> loaderVersions = getGameVersions(LOADER_META_URL);
|
||||
|
||||
@ -60,7 +59,7 @@ public final class FabricVersionList extends VersionList<FabricRemoteVersion> {
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private static final String LOADER_META_URL = "https://meta.fabricmc.net/v2/versions/loader";
|
||||
|
@ -19,25 +19,25 @@ package org.jackhuang.hmcl.download.forge;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.wrap;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@ -58,12 +58,12 @@ public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion>
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> loadAsync() {
|
||||
public Task<?> loadAsync() {
|
||||
throw new UnsupportedOperationException("ForgeBMCLVersionList does not support loading the entire Forge remote version list.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> refreshAsync() {
|
||||
public Task<?> refreshAsync() {
|
||||
throw new UnsupportedOperationException("ForgeBMCLVersionList does not support loading the entire Forge remote version list.");
|
||||
}
|
||||
|
||||
@ -83,11 +83,10 @@ public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion>
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> refreshAsync(String gameVersion) {
|
||||
public Task<?> refreshAsync(String gameVersion) {
|
||||
String lookupVersion = toLookupVersion(gameVersion);
|
||||
|
||||
return CompletableFuture.completedFuture(null)
|
||||
.thenApplyAsync(wrap(unused -> HttpRequest.GET(apiRoot + "/forge/minecraft/" + lookupVersion).getJson(listTypeOf(ForgeVersion.class))))
|
||||
return new GetTask(URI.create(apiRoot + "/forge/minecraft/" + lookupVersion)).thenGetJsonAsync(listTypeOf(ForgeVersion.class))
|
||||
.thenAcceptAsync(forgeVersions -> {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
|
@ -19,14 +19,15 @@ package org.jackhuang.hmcl.download.forge;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
*
|
||||
@ -53,8 +54,8 @@ public final class ForgeVersionList extends VersionList<ForgeRemoteVersion> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> refreshAsync() {
|
||||
return HttpRequest.GET(FORGE_LIST).getJsonAsync(ForgeVersionRoot.class)
|
||||
public Task<?> refreshAsync() {
|
||||
return new GetTask(FORGE_LIST).thenGetJsonAsync(ForgeVersionRoot.class)
|
||||
.thenAcceptAsync(root -> {
|
||||
lock.writeLock().lock();
|
||||
|
||||
@ -95,5 +96,5 @@ public final class ForgeVersionList extends VersionList<ForgeRemoteVersion> {
|
||||
});
|
||||
}
|
||||
|
||||
public static final String FORGE_LIST = "https://hmcl-dev.github.io/metadata/forge/";
|
||||
public static final URI FORGE_LIST = URI.create("https://hmcl-dev.github.io/metadata/forge/");
|
||||
}
|
||||
|
@ -19,14 +19,15 @@ package org.jackhuang.hmcl.download.game;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.net.URI;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@ -52,8 +53,8 @@ public final class GameVersionList extends VersionList<GameRemoteVersion> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> refreshAsync() {
|
||||
return HttpRequest.GET(downloadProvider.getVersionListURL()).getJsonAsync(GameRemoteVersions.class)
|
||||
public Task<?> refreshAsync() {
|
||||
return new GetTask(URI.create(downloadProvider.getVersionListURL())).thenGetJsonAsync(GameRemoteVersions.class)
|
||||
.thenAcceptAsync(root -> {
|
||||
GameRemoteVersions unlistedVersions = null;
|
||||
|
||||
|
@ -44,7 +44,7 @@ public final class VersionJsonDownloadTask extends Task<String> {
|
||||
this.dependencyManager = dependencyManager;
|
||||
this.gameVersionList = dependencyManager.getVersionList("game");
|
||||
|
||||
dependents.add(Task.fromCompletableFuture(gameVersionList.loadAsync(gameVersion)));
|
||||
dependents.add(gameVersionList.loadAsync(gameVersion));
|
||||
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
@ -20,12 +20,12 @@ package org.jackhuang.hmcl.download.liteloader;
|
||||
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
@ -54,17 +54,15 @@ public final class LiteLoaderBMCLVersionList extends VersionList<LiteLoaderRemot
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> refreshAsync() {
|
||||
public Task<?> refreshAsync() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> refreshAsync(String gameVersion) {
|
||||
return HttpRequest.GET(
|
||||
downloadProvider.injectURL("https://bmclapi2.bangbang93.com/liteloader/list"), Pair.pair("mcversion", gameVersion)
|
||||
)
|
||||
.getJsonAsync(LiteLoaderBMCLVersion.class)
|
||||
.thenAccept(v -> {
|
||||
public Task<?> refreshAsync(String gameVersion) {
|
||||
return new GetTask(NetworkUtils.withQuery(downloadProvider.injectURLWithCandidates("https://bmclapi2.bangbang93.com/liteloader/list"), Map.of("mcversion", gameVersion)))
|
||||
.thenGetJsonAsync(LiteLoaderBMCLVersion.class)
|
||||
.thenAcceptAsync(v -> {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
versions.clear();
|
||||
|
@ -20,16 +20,18 @@ package org.jackhuang.hmcl.download.liteloader;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
@ -50,8 +52,9 @@ public final class LiteLoaderVersionList extends VersionList<LiteLoaderRemoteVer
|
||||
public static final String LITELOADER_LIST = "https://dl.liteloader.com/versions/versions.json";
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> refreshAsync(String gameVersion) {
|
||||
return HttpRequest.GET(downloadProvider.injectURL(LITELOADER_LIST)).getJsonAsync(LiteLoaderVersionsRoot.class)
|
||||
public Task<?> refreshAsync(String gameVersion) {
|
||||
return new GetTask(URI.create(downloadProvider.injectURL(LITELOADER_LIST)))
|
||||
.thenGetJsonAsync(LiteLoaderVersionsRoot.class)
|
||||
.thenAcceptAsync(root -> {
|
||||
LiteLoaderGameVersions versions = root.getVersions().get(gameVersion);
|
||||
if (versions == null) {
|
||||
@ -85,7 +88,7 @@ public final class LiteLoaderVersionList extends VersionList<LiteLoaderRemoteVer
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> refreshAsync() {
|
||||
public Task<?> refreshAsync() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
@ -20,15 +20,15 @@ package org.jackhuang.hmcl.download.neoforge;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.wrap;
|
||||
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
|
||||
|
||||
public final class NeoForgeBMCLVersionList extends VersionList<NeoForgeRemoteVersion> {
|
||||
@ -47,12 +47,12 @@ public final class NeoForgeBMCLVersionList extends VersionList<NeoForgeRemoteVer
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> loadAsync() {
|
||||
public Task<?> loadAsync() {
|
||||
throw new UnsupportedOperationException("NeoForgeBMCLVersionList does not support loading the entire NeoForge remote version list.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> refreshAsync() {
|
||||
public Task<?> refreshAsync() {
|
||||
throw new UnsupportedOperationException("NeoForgeBMCLVersionList does not support loading the entire NeoForge remote version list.");
|
||||
}
|
||||
|
||||
@ -65,9 +65,8 @@ public final class NeoForgeBMCLVersionList extends VersionList<NeoForgeRemoteVer
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> refreshAsync(String gameVersion) {
|
||||
return CompletableFuture.completedFuture((Void) null)
|
||||
.thenApplyAsync(wrap(unused -> HttpRequest.GET(apiRoot + "/neoforge/list/" + gameVersion).getJson(listTypeOf(NeoForgeVersion.class))))
|
||||
public Task<?> refreshAsync(String gameVersion) {
|
||||
return new GetTask(URI.create(apiRoot + "/neoforge/list/" + gameVersion)).thenGetJsonAsync(listTypeOf(NeoForgeVersion.class))
|
||||
.thenAcceptAsync(neoForgeVersions -> {
|
||||
lock.writeLock().lock();
|
||||
|
||||
|
@ -2,14 +2,14 @@ package org.jackhuang.hmcl.download.neoforge;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.wrap;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class NeoForgeOfficialVersionList extends VersionList<NeoForgeRemoteVersion> {
|
||||
@ -37,17 +37,17 @@ public final class NeoForgeOfficialVersionList extends VersionList<NeoForgeRemot
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> refreshAsync() {
|
||||
return CompletableFuture.supplyAsync(wrap(() -> new OfficialAPIResult[]{
|
||||
HttpRequest.GET(downloadProvider.injectURL(OLD_URL)).getJson(OfficialAPIResult.class),
|
||||
HttpRequest.GET(downloadProvider.injectURL(META_URL)).getJson(OfficialAPIResult.class)
|
||||
})).thenAccept(results -> {
|
||||
public Task<?> refreshAsync() {
|
||||
return Task.allOf(
|
||||
new GetTask(URI.create(downloadProvider.injectURL(OLD_URL))).thenGetJsonAsync(OfficialAPIResult.class),
|
||||
new GetTask(URI.create(downloadProvider.injectURL(META_URL))).thenGetJsonAsync(OfficialAPIResult.class)
|
||||
).thenAcceptAsync(results -> {
|
||||
lock.writeLock().lock();
|
||||
|
||||
try {
|
||||
versions.clear();
|
||||
|
||||
for (String version : results[0].versions) {
|
||||
for (String version : results.get(0).versions) {
|
||||
versions.put("1.20.1", new NeoForgeRemoteVersion(
|
||||
"1.20.1", NeoForgeRemoteVersion.normalize(version),
|
||||
Collections.singletonList(
|
||||
@ -56,7 +56,7 @@ public final class NeoForgeOfficialVersionList extends VersionList<NeoForgeRemot
|
||||
));
|
||||
}
|
||||
|
||||
for (String version : results[1].versions) {
|
||||
for (String version : results.get(1).versions) {
|
||||
String mcVersion;
|
||||
|
||||
try {
|
||||
|
@ -19,14 +19,15 @@ package org.jackhuang.hmcl.download.optifine;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
|
||||
|
||||
@ -71,8 +72,8 @@ public final class OptiFineBMCLVersionList extends VersionList<OptiFineRemoteVer
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> refreshAsync() {
|
||||
return HttpRequest.GET(apiRoot + "/optifine/versionlist").getJsonAsync(listTypeOf(OptiFineVersion.class)).thenAcceptAsync(root -> {
|
||||
public Task<?> refreshAsync() {
|
||||
return new GetTask(URI.create(apiRoot + "/optifine/versionlist")).thenGetJsonAsync(listTypeOf(OptiFineVersion.class)).thenAcceptAsync(root -> {
|
||||
lock.writeLock().lock();
|
||||
|
||||
try {
|
||||
|
@ -21,12 +21,10 @@ import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.wrap;
|
||||
|
||||
public class QuiltAPIVersionList extends VersionList<QuiltAPIRemoteVersion> {
|
||||
|
||||
@ -42,14 +40,14 @@ public class QuiltAPIVersionList extends VersionList<QuiltAPIRemoteVersion> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> refreshAsync() {
|
||||
return CompletableFuture.runAsync(wrap(() -> {
|
||||
public Task<?> refreshAsync() {
|
||||
return Task.runAsync(() -> {
|
||||
for (RemoteMod.Version modVersion : Lang.toIterable(ModrinthRemoteModRepository.MODS.getRemoteVersionsById("qsl"))) {
|
||||
for (String gameVersion : modVersion.getGameVersions()) {
|
||||
versions.put(gameVersion, new QuiltAPIRemoteVersion(gameVersion, modVersion.getVersion(), modVersion.getName(), modVersion.getDatePublished(), modVersion,
|
||||
Collections.singletonList(modVersion.getFile().getUrl())));
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ package org.jackhuang.hmcl.download.quilt;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@ -26,10 +27,8 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.wrap;
|
||||
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
|
||||
|
||||
public final class QuiltVersionList extends VersionList<QuiltRemoteVersion> {
|
||||
@ -45,8 +44,8 @@ public final class QuiltVersionList extends VersionList<QuiltRemoteVersion> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> refreshAsync() {
|
||||
return CompletableFuture.runAsync(wrap(() -> {
|
||||
public Task<?> refreshAsync() {
|
||||
return Task.runAsync(() -> {
|
||||
List<String> gameVersions = getGameVersions(GAME_META_URL);
|
||||
List<String> loaderVersions = getGameVersions(LOADER_META_URL);
|
||||
|
||||
@ -60,7 +59,7 @@ public final class QuiltVersionList extends VersionList<QuiltRemoteVersion> {
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private static final String LOADER_META_URL = "https://meta.quiltmc.org/v3/versions/loader";
|
||||
|
@ -84,13 +84,19 @@ public abstract class FetchTask<T> extends Task<T> {
|
||||
URI failedURI = null;
|
||||
boolean checkETag;
|
||||
switch (shouldCheckETag()) {
|
||||
case CHECK_E_TAG: checkETag = true; break;
|
||||
case NOT_CHECK_E_TAG: checkETag = false; break;
|
||||
default: return;
|
||||
case CHECK_E_TAG:
|
||||
checkETag = true;
|
||||
break;
|
||||
case NOT_CHECK_E_TAG:
|
||||
checkETag = false;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
int repeat = 0;
|
||||
download: for (URI uri : uris) {
|
||||
download:
|
||||
for (URI uri : uris) {
|
||||
for (int retryTime = 0; retryTime < retry; retryTime++) {
|
||||
if (isCancelled()) {
|
||||
break download;
|
||||
@ -116,9 +122,10 @@ public abstract class FetchTask<T> extends Task<T> {
|
||||
try {
|
||||
Path cache = repository.getCachedRemoteFile(conn.getURL().toURI());
|
||||
useCachedResult(cache);
|
||||
LOG.info("Using cached file for " + NetworkUtils.dropQuery(uri));
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Unable to use cached file, redownload " + uri, e);
|
||||
LOG.warning("Unable to use cached file, redownload " + NetworkUtils.dropQuery(uri), e);
|
||||
repository.removeRemoteEntry(conn.getURL().toURI());
|
||||
// Now we must reconnect the server since 304 may result in empty content,
|
||||
// if we want to redownload the file, we must reconnect the server without etag settings.
|
||||
|
@ -17,6 +17,9 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.task;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
@ -29,7 +32,6 @@ import java.util.List;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class GetTask extends FetchTask<String> {
|
||||
@ -95,4 +97,11 @@ public final class GetTask extends FetchTask<String> {
|
||||
};
|
||||
}
|
||||
|
||||
public <T> Task<T> thenGetJsonAsync(Class<T> type) {
|
||||
return thenGetJsonAsync(TypeToken.get(type));
|
||||
}
|
||||
|
||||
public <T> Task<T> thenGetJsonAsync(TypeToken<T> type) {
|
||||
return thenApplyAsync(jsonString -> JsonUtils.fromNonNullJson(jsonString, type));
|
||||
}
|
||||
}
|
||||
|
@ -917,7 +917,8 @@ public abstract class Task<T> {
|
||||
* @param tasks the Tasks
|
||||
* @return a new Task that is completed when all of the given Tasks complete
|
||||
*/
|
||||
public static Task<List<Object>> allOf(Task<?>... tasks) {
|
||||
@SafeVarargs
|
||||
public static <T> Task<List<T>> allOf(Task<? extends T>... tasks) {
|
||||
return allOf(Arrays.asList(tasks));
|
||||
}
|
||||
|
||||
@ -932,8 +933,8 @@ public abstract class Task<T> {
|
||||
* @param tasks the Tasks
|
||||
* @return a new Task that is completed when all of the given Tasks complete
|
||||
*/
|
||||
public static Task<List<Object>> allOf(Collection<Task<?>> tasks) {
|
||||
return new Task<List<Object>>() {
|
||||
public static <T> Task<List<T>> allOf(Collection<? extends Task<? extends T>> tasks) {
|
||||
return new Task<>() {
|
||||
{
|
||||
setSignificance(TaskSignificance.MINOR);
|
||||
}
|
||||
@ -944,7 +945,7 @@ public abstract class Task<T> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task<?>> getDependents() {
|
||||
public Collection<? extends Task<?>> getDependents() {
|
||||
return tasks;
|
||||
}
|
||||
};
|
||||
|
@ -18,45 +18,42 @@
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.*;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.jackhuang.hmcl.util.gson.JsonUtils.fromMaybeMalformedJson;
|
||||
import static org.jackhuang.hmcl.util.gson.JsonUtils.fromNonNullJson;
|
||||
import static org.jackhuang.hmcl.util.gson.JsonUtils.mapTypeOf;
|
||||
import static org.jackhuang.hmcl.util.gson.JsonUtils.*;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class CacheRepository {
|
||||
private Path commonDirectory;
|
||||
private Path cacheDirectory;
|
||||
private Path indexFile;
|
||||
private Map<String, ETagItem> index;
|
||||
private final Map<String, Storage> storages = new HashMap<>();
|
||||
private final ReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
private FileTime indexFileLastModified;
|
||||
private LinkedHashMap<URI, ETagItem> index;
|
||||
protected final ReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
|
||||
public void changeDirectory(Path commonDir) {
|
||||
commonDirectory = commonDir;
|
||||
@ -65,25 +62,25 @@ public class CacheRepository {
|
||||
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
for (Storage storage : storages.values()) {
|
||||
storage.changeDirectory(cacheDirectory);
|
||||
}
|
||||
|
||||
if (Files.isRegularFile(indexFile)) {
|
||||
ETagIndex raw = JsonUtils.fromJsonFile(indexFile, ETagIndex.class);
|
||||
if (raw == null)
|
||||
index = new HashMap<>();
|
||||
else
|
||||
index = joinETagIndexes(raw.eTag);
|
||||
} else
|
||||
index = new HashMap<>();
|
||||
try (FileChannel channel = FileChannel.open(indexFile, StandardOpenOption.READ);
|
||||
@SuppressWarnings("unused") FileLock lock = channel.tryLock(0, Long.MAX_VALUE, true)) {
|
||||
FileTime lastModified = Lang.ignoringException(() -> Files.getLastModifiedTime(indexFile));
|
||||
ETagIndex raw = JsonUtils.GSON.fromJson(new BufferedReader(Channels.newReader(channel, UTF_8)), ETagIndex.class);
|
||||
index = raw != null ? joinETagIndexes(raw.eTag) : new LinkedHashMap<>();
|
||||
indexFileLastModified = lastModified;
|
||||
}
|
||||
} else {
|
||||
index = new LinkedHashMap<>();
|
||||
indexFileLastModified = null;
|
||||
}
|
||||
} catch (IOException | JsonParseException e) {
|
||||
LOG.warning("Unable to read index file", e);
|
||||
index = new HashMap<>();
|
||||
index = new LinkedHashMap<>();
|
||||
indexFileLastModified = null;
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public Path getCommonDirectory() {
|
||||
@ -94,15 +91,6 @@ public class CacheRepository {
|
||||
return cacheDirectory;
|
||||
}
|
||||
|
||||
public Storage getStorage(String key) {
|
||||
lock.readLock().lock();
|
||||
try {
|
||||
return storages.computeIfAbsent(key, Storage::new);
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
protected Path getFile(String algorithm, String hash) {
|
||||
return getCacheDirectory().resolve(algorithm).resolve(hash.substring(0, 2)).resolve(hash);
|
||||
}
|
||||
@ -165,7 +153,7 @@ public class CacheRepository {
|
||||
lock.readLock().lock();
|
||||
ETagItem eTagItem;
|
||||
try {
|
||||
eTagItem = index.get(uri.toString());
|
||||
eTagItem = index.get(NetworkUtils.dropQuery(uri));
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
@ -181,20 +169,27 @@ public class CacheRepository {
|
||||
}
|
||||
|
||||
public void removeRemoteEntry(URI uri) {
|
||||
lock.readLock().lock();
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
index.remove(uri.toString());
|
||||
index.remove(NetworkUtils.dropQuery(uri));
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void injectConnection(URLConnection conn) {
|
||||
String url = conn.getURL().toString();
|
||||
lock.readLock().lock();
|
||||
ETagItem eTagItem;
|
||||
conn.setUseCaches(true);
|
||||
|
||||
URI uri;
|
||||
try {
|
||||
eTagItem = index.get(url);
|
||||
uri = NetworkUtils.dropQuery(conn.getURL().toURI());
|
||||
} catch (URISyntaxException e) {
|
||||
return;
|
||||
}
|
||||
ETagItem eTagItem;
|
||||
lock.readLock().lock();
|
||||
try {
|
||||
eTagItem = index.get(uri);
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
@ -228,22 +223,26 @@ public class CacheRepository {
|
||||
|
||||
private void cacheData(URLConnection connection, ExceptionalSupplier<CacheResult, IOException> cacheSupplier) throws IOException {
|
||||
String eTag = connection.getHeaderField("ETag");
|
||||
if (eTag == null || eTag.isEmpty()) return;
|
||||
String uri = connection.getURL().toString();
|
||||
if (StringUtils.isBlank(eTag)) return;
|
||||
URI uri;
|
||||
try {
|
||||
uri = NetworkUtils.dropQuery(connection.getURL().toURI());
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
String lastModified = connection.getHeaderField("Last-Modified");
|
||||
CacheResult cacheResult = cacheSupplier.get();
|
||||
ETagItem eTagItem = new ETagItem(uri, eTag, cacheResult.hash, Files.getLastModifiedTime(cacheResult.cachedFile).toMillis(), lastModified);
|
||||
Lock writeLock = lock.writeLock();
|
||||
writeLock.lock();
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
index.compute(eTagItem.url, updateEntity(eTagItem));
|
||||
index.compute(eTagItem.url, updateEntity(eTagItem, true));
|
||||
saveETagIndex();
|
||||
} finally {
|
||||
writeLock.unlock();
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private static class CacheResult {
|
||||
private static final class CacheResult {
|
||||
public String hash;
|
||||
public Path cachedFile;
|
||||
|
||||
@ -253,11 +252,11 @@ public class CacheRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private BiFunction<String, ETagItem, ETagItem> updateEntity(ETagItem newItem) {
|
||||
private BiFunction<URI, ETagItem, ETagItem> updateEntity(ETagItem newItem, boolean force) {
|
||||
return (key, oldItem) -> {
|
||||
if (oldItem == null) {
|
||||
return newItem;
|
||||
} else if (oldItem.compareTo(newItem) < 0) {
|
||||
} else if (force || oldItem.compareTo(newItem) < 0) {
|
||||
Path cached = getFile(SHA1, oldItem.hash);
|
||||
try {
|
||||
Files.deleteIfExists(cached);
|
||||
@ -272,36 +271,44 @@ public class CacheRepository {
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
private final Map<String, ETagItem> joinETagIndexes(Collection<ETagItem>... indexes) {
|
||||
Map<String, ETagItem> eTags = new ConcurrentHashMap<>();
|
||||
|
||||
Stream<ETagItem> stream = Arrays.stream(indexes).filter(Objects::nonNull).map(Collection::stream)
|
||||
.reduce(Stream.empty(), Stream::concat);
|
||||
|
||||
stream.forEach(eTag -> {
|
||||
eTags.compute(eTag.url, updateEntity(eTag));
|
||||
});
|
||||
|
||||
private LinkedHashMap<URI, ETagItem> joinETagIndexes(Collection<ETagItem>... indexes) {
|
||||
var eTags = new LinkedHashMap<URI, ETagItem>();
|
||||
for (Collection<ETagItem> eTagItems : indexes) {
|
||||
if (eTagItems != null) {
|
||||
for (ETagItem eTag : eTagItems) {
|
||||
eTags.compute(eTag.url, updateEntity(eTag, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
return eTags;
|
||||
}
|
||||
|
||||
public void saveETagIndex() throws IOException {
|
||||
try (FileChannel channel = FileChannel.open(indexFile, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
|
||||
FileLock lock = channel.lock();
|
||||
try {
|
||||
ETagIndex indexOnDisk = fromMaybeMalformedJson(new String(Channels.newInputStream(channel).readAllBytes(), UTF_8), ETagIndex.class);
|
||||
Map<String, ETagItem> newIndex = joinETagIndexes(indexOnDisk == null ? null : indexOnDisk.eTag, index.values());
|
||||
channel.truncate(0);
|
||||
ByteBuffer writeTo = ByteBuffer.wrap(JsonUtils.GSON.toJson(new ETagIndex(newIndex.values())).getBytes(UTF_8));
|
||||
while (writeTo.hasRemaining()) {
|
||||
if (channel.write(writeTo) == 0) {
|
||||
throw new IOException("No value is written");
|
||||
try (FileChannel channel = FileChannel.open(indexFile, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE);
|
||||
@SuppressWarnings("unused") FileLock lock = channel.lock()) {
|
||||
FileTime lastModified = Lang.ignoringException(() -> Files.getLastModifiedTime(indexFile));
|
||||
if (indexFileLastModified == null || lastModified == null || indexFileLastModified.compareTo(lastModified) < 0) {
|
||||
try {
|
||||
ETagIndex indexOnDisk = GSON.fromJson(
|
||||
// Should not be closed
|
||||
new BufferedReader(Channels.newReader(channel, UTF_8)),
|
||||
ETagIndex.class
|
||||
);
|
||||
if (indexOnDisk != null) {
|
||||
index = joinETagIndexes(index.values(), indexOnDisk.eTag);
|
||||
indexFileLastModified = lastModified;
|
||||
}
|
||||
} catch (JsonSyntaxException ignored) {
|
||||
}
|
||||
this.index = newIndex;
|
||||
} finally {
|
||||
lock.release();
|
||||
}
|
||||
|
||||
channel.truncate(0);
|
||||
BufferedWriter writer = new BufferedWriter(Channels.newWriter(channel, UTF_8));
|
||||
JsonUtils.GSON.toJson(new ETagIndex(index.values()), writer);
|
||||
writer.flush();
|
||||
channel.force(true);
|
||||
|
||||
this.indexFileLastModified = Lang.ignoringException(() -> Files.getLastModifiedTime(indexFile));
|
||||
}
|
||||
}
|
||||
|
||||
@ -318,7 +325,7 @@ public class CacheRepository {
|
||||
}
|
||||
|
||||
private static final class ETagItem {
|
||||
private final String url;
|
||||
private final URI url;
|
||||
private final String eTag;
|
||||
private final String hash;
|
||||
@SerializedName("local")
|
||||
@ -333,7 +340,7 @@ public class CacheRepository {
|
||||
this(null, null, null, 0, null);
|
||||
}
|
||||
|
||||
public ETagItem(String url, String eTag, String hash, long localLastModified, String remoteLastModified) {
|
||||
public ETagItem(URI url, String eTag, String hash, long localLastModified, String remoteLastModified) {
|
||||
this.url = url;
|
||||
this.eTag = eTag;
|
||||
this.hash = hash;
|
||||
@ -348,8 +355,8 @@ public class CacheRepository {
|
||||
ZonedDateTime thisTime = Lang.ignoringException(() -> ZonedDateTime.parse(remoteLastModified, DateTimeFormatter.RFC_1123_DATE_TIME), null);
|
||||
ZonedDateTime otherTime = Lang.ignoringException(() -> ZonedDateTime.parse(other.remoteLastModified, DateTimeFormatter.RFC_1123_DATE_TIME), null);
|
||||
if (thisTime == null && otherTime == null) return 0;
|
||||
else if (thisTime == null) return -1;
|
||||
else if (otherTime == null) return 1;
|
||||
else if (thisTime == null) return 1;
|
||||
else if (otherTime == null) return -1;
|
||||
else return thisTime.compareTo(otherTime);
|
||||
}
|
||||
|
||||
@ -369,80 +376,16 @@ public class CacheRepository {
|
||||
public int hashCode() {
|
||||
return Objects.hash(url, eTag, hash, localLastModified, remoteLastModified);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal cache
|
||||
*/
|
||||
public static final class Storage {
|
||||
private final String name;
|
||||
private Map<String, Object> storage;
|
||||
private final ReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
private Path indexFile;
|
||||
|
||||
public Storage(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Object getEntry(String key) {
|
||||
lock.readLock().lock();
|
||||
try {
|
||||
return storage.get(key);
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void putEntry(String key, Object value) {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
storage.put(key, value);
|
||||
saveToFile();
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void joinEntries(Map<String, Object> storage) {
|
||||
this.storage.putAll(storage);
|
||||
}
|
||||
|
||||
private void changeDirectory(Path cacheDirectory) {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
indexFile = cacheDirectory.resolve(name + ".json");
|
||||
if (Files.isRegularFile(indexFile)) {
|
||||
joinEntries(fromNonNullJson(Files.readString(indexFile), mapTypeOf(String.class, Object.class)));
|
||||
}
|
||||
} catch (IOException | JsonParseException e) {
|
||||
LOG.warning("Unable to read storage {" + name + "} file");
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void saveToFile() {
|
||||
try (FileChannel channel = FileChannel.open(indexFile, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
|
||||
FileLock lock = channel.lock();
|
||||
try {
|
||||
Map<String, Object> indexOnDisk = fromMaybeMalformedJson(new String(Channels.newInputStream(channel).readAllBytes(), UTF_8), mapTypeOf(String.class, Object.class));
|
||||
if (indexOnDisk == null) indexOnDisk = new HashMap<>();
|
||||
indexOnDisk.putAll(storage);
|
||||
channel.truncate(0);
|
||||
|
||||
ByteBuffer writeTo = ByteBuffer.wrap(JsonUtils.GSON.toJson(storage).getBytes(UTF_8));
|
||||
while (writeTo.hasRemaining()) {
|
||||
if (channel.write(writeTo) == 0) {
|
||||
throw new IOException("No value is written");
|
||||
}
|
||||
}
|
||||
this.storage = indexOnDisk;
|
||||
} finally {
|
||||
lock.release();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Unable to write storage {" + name + "} file");
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ETagItem[" +
|
||||
"url='" + url + '\'' +
|
||||
", eTag='" + eTag + '\'' +
|
||||
", hash='" + hash + '\'' +
|
||||
", localLastModified=" + localLastModified +
|
||||
", remoteLastModified='" + remoteLastModified + '\'' +
|
||||
']';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,7 @@ import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
@ -68,6 +69,10 @@ public final class NetworkUtils {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static List<URI> withQuery(List<URI> list, Map<String, String> params) {
|
||||
return list.stream().map(uri -> URI.create(withQuery(uri.toString(), params))).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static List<Pair<String, String>> parseQuery(URI uri) {
|
||||
return parseQuery(uri.getRawQuery());
|
||||
}
|
||||
@ -93,9 +98,20 @@ public final class NetworkUtils {
|
||||
return result;
|
||||
}
|
||||
|
||||
public static URI dropQuery(URI u) {
|
||||
if (u.getRawQuery() == null && u.getRawFragment() == null) {
|
||||
return u;
|
||||
}
|
||||
|
||||
try {
|
||||
return new URI(u.getScheme(), u.getUserInfo(), u.getHost(), u.getPort(), u.getPath(), null, null);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new AssertionError("Unreachable", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static URLConnection createConnection(URI uri) throws IOException {
|
||||
URLConnection connection = uri.toURL().openConnection();
|
||||
connection.setUseCaches(false);
|
||||
connection.setConnectTimeout(TIME_OUT);
|
||||
connection.setReadTimeout(TIME_OUT);
|
||||
connection.setRequestProperty("Accept-Language", Locale.getDefault().toLanguageTag());
|
||||
@ -152,9 +168,10 @@ public final class NetworkUtils {
|
||||
* @see <a href="https://github.com/curl/curl/issues/473">Issue with libcurl</a>
|
||||
*/
|
||||
public static HttpURLConnection resolveConnection(HttpURLConnection conn, List<String> redirects) throws IOException {
|
||||
final boolean useCache = conn.getUseCaches();
|
||||
int redirect = 0;
|
||||
while (true) {
|
||||
conn.setUseCaches(false);
|
||||
conn.setUseCaches(useCache);
|
||||
conn.setConnectTimeout(TIME_OUT);
|
||||
conn.setReadTimeout(TIME_OUT);
|
||||
conn.setInstanceFollowRedirects(false);
|
||||
|
Loading…
x
Reference in New Issue
Block a user