New downloader
This commit is contained in:
parent
1d3f4169c0
commit
36aa38a179
|
@ -9,7 +9,7 @@ buildscript {
|
|||
dependencies {
|
||||
classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '3.+', changing: true
|
||||
classpath 'org.spongepowered:mixingradle:0.7-SNAPSHOT'
|
||||
classpath 'com.github.IzzelAliz:arclight-gradle-plugin:1.3'
|
||||
classpath "com.github.IzzelAliz:arclight-gradle-plugin:$agpVersion"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ buildscript {
|
|||
dependencies {
|
||||
classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '3.+', changing: true
|
||||
classpath 'org.spongepowered:mixingradle:0.7-SNAPSHOT'
|
||||
classpath 'com.github.IzzelAliz:arclight-gradle-plugin:1.3'
|
||||
classpath "com.github.IzzelAliz:arclight-gradle-plugin:$agpVersion"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ buildscript {
|
|||
dependencies {
|
||||
classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '3.+', changing: true
|
||||
classpath 'org.spongepowered:mixingradle:0.7-SNAPSHOT'
|
||||
classpath 'com.github.IzzelAliz:arclight-gradle-plugin:1.3'
|
||||
classpath "com.github.IzzelAliz:arclight-gradle-plugin:$agpVersion"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,10 @@ allprojects {
|
|||
group 'io.izzel.arclight'
|
||||
version '1.0.0-SNAPSHOT'
|
||||
|
||||
ext {
|
||||
agpVersion = '1.5'
|
||||
}
|
||||
|
||||
task cleanBuild {
|
||||
doFirst {
|
||||
def f = project.file("build/libs")
|
||||
|
|
|
@ -4,9 +4,14 @@ plugins {
|
|||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
name = 'sponge'
|
||||
url = 'https://repo.spongepowered.org/maven'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile 'com.google.code.gson:gson:2.8.0'
|
||||
compile project(':arclight-api')
|
||||
compile project(':i18n-config')
|
||||
}
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
package io.izzel.arclight.forgeinstaller;
|
||||
|
||||
import io.izzel.arclight.api.Unsafe;
|
||||
import io.izzel.arclight.i18n.LocalizedException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class FileDownloader implements Supplier<Path> {
|
||||
|
||||
private final String url;
|
||||
private final String target;
|
||||
private final String hash;
|
||||
|
||||
public FileDownloader(String url, String target, String hash) {
|
||||
this.url = url;
|
||||
this.target = target;
|
||||
this.hash = hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path get() {
|
||||
try {
|
||||
Path path = new File(target).toPath();
|
||||
if (Files.exists(path) && Files.isDirectory(path)) {
|
||||
Files.delete(path);
|
||||
}
|
||||
if (Files.exists(path)) {
|
||||
if (Files.isDirectory(path)) {
|
||||
throw LocalizedException.checked("downloader.dir", target);
|
||||
} else {
|
||||
if (Util.hash(path).equals(hash)) return path;
|
||||
else Files.delete(path);
|
||||
}
|
||||
}
|
||||
if (!Files.exists(path) && path.getParent() != null) {
|
||||
Files.createDirectories(path.getParent());
|
||||
}
|
||||
URL url = new URL(this.url);
|
||||
try (InputStream stream = redirect(url)) {
|
||||
Files.copy(stream, path, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (SocketTimeoutException e) {
|
||||
throw LocalizedException.checked("downloader.timeout", e, url);
|
||||
}
|
||||
if (Files.exists(path)) {
|
||||
String hash = Util.hash(path);
|
||||
if (hash.equals(this.hash)) return path;
|
||||
else {
|
||||
Files.delete(path);
|
||||
throw LocalizedException.checked("downloader.hash-not-match", this.hash, hash, url);
|
||||
}
|
||||
} else {
|
||||
throw LocalizedException.checked("downloader.not-found", url);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Unsafe.throwException(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream redirect(URL url) throws IOException {
|
||||
return redirect(url, new HashSet<>());
|
||||
}
|
||||
|
||||
private InputStream redirect(URL url, Set<String> history) throws IOException {
|
||||
if (history.contains(url.toString())) {
|
||||
StringJoiner joiner = new StringJoiner("\n ");
|
||||
joiner.add("");
|
||||
history.forEach(joiner::add);
|
||||
throw LocalizedException.unchecked("downloader.redirect-error", joiner.toString());
|
||||
} else {
|
||||
history.add(url.toString());
|
||||
}
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setInstanceFollowRedirects(false);
|
||||
connection.setReadTimeout(15000);
|
||||
connection.setConnectTimeout(15000);
|
||||
int responseCode = connection.getResponseCode();
|
||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||
return connection.getInputStream();
|
||||
} else if (responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_MOVED_TEMP) {
|
||||
String location = URLDecoder.decode(connection.getHeaderField("Location"), "UTF-8");
|
||||
return redirect(new URL(url, location));
|
||||
} else if (responseCode == HttpURLConnection.HTTP_NOT_FOUND || responseCode == HttpURLConnection.HTTP_FORBIDDEN) {
|
||||
throw LocalizedException.unchecked("downloader.not-found", url);
|
||||
} else {
|
||||
throw LocalizedException.unchecked("downloader.http-error", responseCode, url);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +1,16 @@
|
|||
package io.izzel.arclight.forgeinstaller;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import io.izzel.arclight.api.Unsafe;
|
||||
import io.izzel.arclight.i18n.ArclightLocale;
|
||||
import io.izzel.arclight.i18n.LocalizedException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
|
@ -18,37 +22,145 @@ import java.nio.file.FileSystems;
|
|||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
public class ForgeInstaller {
|
||||
|
||||
private static final String INFO = "Download mirror service by BMCLAPI: https://bmclapidoc.bangbang93.com\n" +
|
||||
"Support MinecraftForge project at https://www.patreon.com/LexManos/";
|
||||
private static final String[] MAVEN_REPO = {
|
||||
"https://bmclapi2.bangbang93.com/maven/",
|
||||
"https://maven.aliyun.com/repository/public/",
|
||||
"https://repo.spongepowered.org/maven/",
|
||||
"https://oss.sonatype.org/content/repositories/snapshots/",
|
||||
"https://hub.spigotmc.org/nexus/content/repositories/snapshots/",
|
||||
"https://files.minecraftforge.net/maven/",
|
||||
"https://repo1.maven.org/maven2/"
|
||||
};
|
||||
private static final String INSTALLER_URL = "https://bmclapi2.bangbang93.com/maven/net/minecraftforge/forge/%s-%s/forge-%s-%s-installer.jar";
|
||||
private static final String BMCL_API = "https://bmclapi2.bangbang93.com/maven/";
|
||||
private static final String SERVER_URL = "https://bmclapi2.bangbang93.com/version/%s/server";
|
||||
private static final Map<String, String> VERSION_HASH = ImmutableMap.of(
|
||||
"1.14.4", "3dc3d84a581f14691199cf6831b71ed1296a9fdf",
|
||||
"1.15.2", "bb2b6b1aefcd70dfd1892149ac3a215f6c636b07"
|
||||
);
|
||||
|
||||
public static void install() throws Throwable {
|
||||
InputStream stream = ForgeInstaller.class.getResourceAsStream("/META-INF/installer.json");
|
||||
InstallInfo installInfo = new Gson().fromJson(new InputStreamReader(stream), InstallInfo.class);
|
||||
List<Supplier<Path>> suppliers = checkMaven(installInfo.libraries);
|
||||
Path path = Paths.get(String.format("forge-%s-%s.jar", installInfo.installer.minecraft, installInfo.installer.forge));
|
||||
if (!suppliers.isEmpty() || !Files.exists(path)) {
|
||||
ArclightLocale.info("downloader.info");
|
||||
ExecutorService pool = Executors.newFixedThreadPool(8);
|
||||
CompletableFuture<?>[] array = suppliers.stream().map(reportSupply(pool)).toArray(CompletableFuture[]::new);
|
||||
if (!Files.exists(path)) {
|
||||
System.out.println(INFO);
|
||||
Thread.sleep(5000);
|
||||
download(installInfo);
|
||||
CompletableFuture<?>[] futures = installForge(installInfo, pool);
|
||||
handleFutures(futures);
|
||||
ArclightLocale.info("downloader.forge-install");
|
||||
ProcessBuilder builder = new ProcessBuilder();
|
||||
builder.command("java", "-jar", String.format("forge-%s-%s-installer.jar", installInfo.installer.minecraft, installInfo.installer.forge), "--installServer", ".");
|
||||
builder.inheritIO();
|
||||
Process process = builder.start();
|
||||
process.waitFor();
|
||||
}
|
||||
handleFutures(array);
|
||||
pool.shutdownNow();
|
||||
}
|
||||
classpath(path, installInfo);
|
||||
}
|
||||
|
||||
private static Function<Supplier<Path>, CompletableFuture<Path>> reportSupply(ExecutorService service) {
|
||||
return it -> CompletableFuture.supplyAsync(it, service).thenApply(path -> {
|
||||
ArclightLocale.info("downloader.complete", path);
|
||||
return path;
|
||||
});
|
||||
}
|
||||
|
||||
private static CompletableFuture<?>[] installForge(InstallInfo info, ExecutorService pool) throws Exception {
|
||||
String format = String.format(INSTALLER_URL, info.installer.minecraft, info.installer.forge, info.installer.minecraft, info.installer.forge);
|
||||
String dist = String.format("forge-%s-%s-installer.jar", info.installer.minecraft, info.installer.forge);
|
||||
FileDownloader fd = new FileDownloader(format, dist, info.installer.hash);
|
||||
CompletableFuture<?> installerFuture = reportSupply(pool).apply(fd).thenAccept(path -> {
|
||||
try {
|
||||
FileSystem system = FileSystems.newFileSystem(path, null);
|
||||
Map<String, String> map = new HashMap<>();
|
||||
Path profile = system.getPath("install_profile.json");
|
||||
map.putAll(profileLibraries(profile));
|
||||
Path version = system.getPath("version.json");
|
||||
map.putAll(profileLibraries(version));
|
||||
List<Supplier<Path>> suppliers = checkMaven(map);
|
||||
CompletableFuture<?>[] array = suppliers.stream().map(reportSupply(pool)).toArray(CompletableFuture[]::new);
|
||||
handleFutures(array);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
CompletableFuture<?> serverFuture = reportSupply(pool).apply(
|
||||
new FileDownloader(String.format(SERVER_URL, info.installer.minecraft),
|
||||
String.format("minecraft_server.%s.jar", info.installer.minecraft), VERSION_HASH.get(info.installer.minecraft))
|
||||
);
|
||||
return new CompletableFuture<?>[]{installerFuture, serverFuture};
|
||||
}
|
||||
|
||||
private static void handleFutures(CompletableFuture<?>... futures) {
|
||||
for (CompletableFuture<?> future : futures) {
|
||||
try {
|
||||
future.join();
|
||||
} catch (CompletionException e) {
|
||||
Throwable cause = e.getCause();
|
||||
if (cause instanceof LocalizedException) {
|
||||
LocalizedException local = (LocalizedException) cause;
|
||||
ArclightLocale.error(local.node(), local.args());
|
||||
} else throw e;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, String> profileLibraries(Path path) throws IOException {
|
||||
Map<String, String> ret = new HashMap<>();
|
||||
JsonArray array = new JsonParser().parse(Files.newBufferedReader(path)).getAsJsonObject().getAsJsonArray("libraries");
|
||||
for (JsonElement element : array) {
|
||||
String name = element.getAsJsonObject().get("name").getAsString();
|
||||
JsonObject artifact = element.getAsJsonObject().getAsJsonObject("downloads").getAsJsonObject("artifact");
|
||||
String hash = artifact.get("sha1").getAsString();
|
||||
String url = artifact.get("url").getAsString();
|
||||
if (url == null || url.trim().isEmpty()) continue;
|
||||
ret.put(name, hash);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static List<Supplier<Path>> checkMaven(Map<String, String> map) {
|
||||
List<Supplier<Path>> incomplete = new ArrayList<>();
|
||||
for (Map.Entry<String, String> entry : map.entrySet()) {
|
||||
String maven = entry.getKey();
|
||||
String path = "libraries/" + Util.mavenToPath(maven);
|
||||
if (new File(path).exists()) {
|
||||
try {
|
||||
String hash = Util.hash(path);
|
||||
if (!hash.equals(entry.getValue())) {
|
||||
incomplete.add(new MavenDownloader(MAVEN_REPO, maven, path, entry.getValue()));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
incomplete.add(new MavenDownloader(MAVEN_REPO, maven, path, entry.getValue()));
|
||||
}
|
||||
} else {
|
||||
incomplete.add(new MavenDownloader(MAVEN_REPO, maven, path, entry.getValue()));
|
||||
}
|
||||
}
|
||||
return incomplete;
|
||||
}
|
||||
|
||||
private static void classpath(Path path, InstallInfo installInfo) throws Throwable {
|
||||
JarFile jarFile = new JarFile(path.toFile());
|
||||
Manifest manifest = jarFile.getManifest();
|
||||
|
@ -57,8 +169,8 @@ public class ForgeInstaller {
|
|||
if (s.contains("eventbus-1.0.0-service")) continue;
|
||||
addToPath(Paths.get(s));
|
||||
}
|
||||
for (String library : installInfo.libraries) {
|
||||
addToPath(Paths.get("libraries", mavenToPath(library)));
|
||||
for (String library : installInfo.libraries.keySet()) {
|
||||
addToPath(Paths.get("libraries", Util.mavenToPath(library)));
|
||||
}
|
||||
addToPath(path);
|
||||
}
|
||||
|
@ -71,68 +183,4 @@ public class ForgeInstaller {
|
|||
Method method = ucp.getClass().getDeclaredMethod("addURL", URL.class);
|
||||
Unsafe.lookup().unreflect(method).invoke(ucp, path.toUri().toURL());
|
||||
}
|
||||
|
||||
private static void download(InstallInfo info) throws Exception {
|
||||
SimpleDownloader downloader = new SimpleDownloader();
|
||||
String format = String.format(INSTALLER_URL, info.installer.minecraft, info.installer.forge, info.installer.minecraft, info.installer.forge);
|
||||
String dist = String.format("forge-%s-%s-installer.jar", info.installer.minecraft, info.installer.forge);
|
||||
downloader.download(format, dist, null,
|
||||
path -> processInstaller(path, downloader));
|
||||
for (String library : info.libraries) {
|
||||
String path = mavenToPath(library);
|
||||
downloader.downloadMaven(path);
|
||||
}
|
||||
downloader.download(String.format(SERVER_URL, info.installer.minecraft), String.format("minecraft_server.%s.jar", info.installer.minecraft), null);
|
||||
if (!downloader.awaitTermination()) {
|
||||
Files.deleteIfExists(Paths.get(dist));
|
||||
throw new Exception();
|
||||
}
|
||||
}
|
||||
|
||||
private static void processInstaller(Path path, SimpleDownloader downloader) {
|
||||
try {
|
||||
FileSystem system = FileSystems.newFileSystem(path, null);
|
||||
Set<String> set = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
Path profile = system.getPath("install_profile.json");
|
||||
downloadLibraries(downloader, profile, set);
|
||||
Path version = system.getPath("version.json");
|
||||
downloadLibraries(downloader, version, set);
|
||||
system.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private static void downloadLibraries(SimpleDownloader downloader, Path path, Set<String> set) throws IOException {
|
||||
JsonArray array = new JsonParser().parse(Files.newBufferedReader(path)).getAsJsonObject().getAsJsonArray("libraries");
|
||||
for (JsonElement element : array) {
|
||||
String name = element.getAsJsonObject().get("name").getAsString();
|
||||
if (!set.add(name)) continue;
|
||||
String libPath = mavenToPath(name);
|
||||
JsonObject artifact = element.getAsJsonObject().getAsJsonObject("downloads").getAsJsonObject("artifact");
|
||||
String url = artifact.get("url").getAsString();
|
||||
if (url == null || url.trim().isEmpty()) continue;
|
||||
String hash = artifact.get("sha1").getAsString();
|
||||
downloader.download(BMCL_API + libPath, "libraries/" + libPath, hash);
|
||||
}
|
||||
}
|
||||
|
||||
private static String mavenToPath(String maven) {
|
||||
String type;
|
||||
if (maven.matches(".*@\\w+$")) {
|
||||
int i = maven.lastIndexOf('@');
|
||||
type = maven.substring(i + 1);
|
||||
maven = maven.substring(0, i);
|
||||
} else {
|
||||
type = "jar";
|
||||
}
|
||||
String[] arr = maven.split(":");
|
||||
if (arr.length == 3) {
|
||||
String pkg = arr[0].replace('.', '/');
|
||||
return String.format("%s/%s/%s/%s-%s.%s", pkg, arr[1], arr[2], arr[1], arr[2], type);
|
||||
} else if (arr.length == 4) {
|
||||
String pkg = arr[0].replace('.', '/');
|
||||
return String.format("%s/%s/%s/%s-%s-%s.%s", pkg, arr[1], arr[2], arr[1], arr[2], arr[3], type);
|
||||
} else throw new RuntimeException("Wrong maven coordinate " + maven);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
package io.izzel.arclight.forgeinstaller;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class InstallInfo {
|
||||
|
||||
public Installer installer;
|
||||
public String[] libraries;
|
||||
public Map<String, String> libraries;
|
||||
|
||||
public static class Installer {
|
||||
|
||||
public String minecraft;
|
||||
public String forge;
|
||||
public String hash;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
package io.izzel.arclight.forgeinstaller;
|
||||
|
||||
import io.izzel.arclight.i18n.ArclightLocale;
|
||||
import io.izzel.arclight.i18n.LocalizedException;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class MavenDownloader implements Supplier<Path> {
|
||||
|
||||
private final String[] repos;
|
||||
private final String coord;
|
||||
private final String target;
|
||||
private final String hash;
|
||||
|
||||
public MavenDownloader(String[] repos, String coord, String target, String hash) {
|
||||
this.repos = repos;
|
||||
this.coord = coord;
|
||||
this.target = target;
|
||||
this.hash = hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path get() {
|
||||
String path = Util.mavenToPath(coord);
|
||||
List<Exception> exceptions = new ArrayList<>();
|
||||
for (String repo : repos) {
|
||||
try {
|
||||
return new FileDownloader(repo + path, target, hash).get();
|
||||
} catch (Exception e) {
|
||||
exceptions.add(e);
|
||||
}
|
||||
}
|
||||
StringJoiner joiner = new StringJoiner("\n ");
|
||||
joiner.add("");
|
||||
for (int i = 0; i < exceptions.size(); i++) {
|
||||
Exception exception = exceptions.get(i);
|
||||
if (exception instanceof LocalizedException) {
|
||||
LocalizedException local = (LocalizedException) exception;
|
||||
String format = ArclightLocale.getInstance().format(local.node(), local.args());
|
||||
joiner.add("(" + (i + 1) + ") " + format);
|
||||
} else {
|
||||
joiner.add("(" + (i + 1) + ") " + exception);
|
||||
}
|
||||
}
|
||||
throw LocalizedException.unchecked("downloader.maven-fail", coord, joiner.toString());
|
||||
}
|
||||
}
|
|
@ -1,152 +0,0 @@
|
|||
package io.izzel.arclight.forgeinstaller;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class SimpleDownloader {
|
||||
|
||||
private static final String[] MAVEN_REPO = {
|
||||
"https://repo.spongepowered.org/maven",
|
||||
"https://oss.sonatype.org/content/repositories/snapshots/",
|
||||
"https://hub.spigotmc.org/nexus/content/repositories/snapshots/",
|
||||
"https://bmclapi2.bangbang93.com/maven/",
|
||||
"https://maven.aliyun.com/repository/public/"
|
||||
};
|
||||
|
||||
private final CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
private final AtomicInteger integer = new AtomicInteger(0);
|
||||
private final ExecutorService service = Executors.newFixedThreadPool(8);
|
||||
private final AtomicBoolean error = new AtomicBoolean(false);
|
||||
|
||||
public void run(Callable<Boolean> callable) {
|
||||
integer.incrementAndGet();
|
||||
CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return callable.call();
|
||||
} catch (Throwable e) {
|
||||
System.err.println(e.toString());
|
||||
error.compareAndSet(false, true);
|
||||
return false;
|
||||
}
|
||||
}, service).thenAccept(b -> {
|
||||
int remain = integer.decrementAndGet();
|
||||
if (remain == 0) {
|
||||
if (error.get())
|
||||
future.completeExceptionally(new Exception());
|
||||
else
|
||||
future.complete(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void download(String url, String dist, String hash) {
|
||||
download(url, dist, hash, path -> {});
|
||||
}
|
||||
|
||||
public void download(String url, String dist, String hash, Consumer<Path> onComplete) {
|
||||
run(() -> downloadFile(url, dist, hash, 5, onComplete));
|
||||
}
|
||||
|
||||
public void downloadMaven(String path) {
|
||||
run(() -> {
|
||||
for (String s : MAVEN_REPO) {
|
||||
if (downloadFile(s + path, "libraries/" + path, null, 3, p -> {})) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public boolean awaitTermination() {
|
||||
try {
|
||||
future.join();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
} finally {
|
||||
service.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean downloadFile(String url, String dist, String hash, int retry, Consumer<Path> onComplete) throws Exception {
|
||||
if (retry <= 0) return false;
|
||||
try {
|
||||
Path path = Paths.get(dist);
|
||||
if (Files.exists(path) && hash != null) {
|
||||
String hash1 = hash(path);
|
||||
if (hash.equals(hash1)) {
|
||||
onComplete.accept(path);
|
||||
return true;
|
||||
} else {
|
||||
Files.delete(path);
|
||||
throw new Exception("Checksum failed: expect " + hash + " but found " + hash1);
|
||||
}
|
||||
}
|
||||
if (!Files.exists(path) && path.getParent() != null) {
|
||||
Files.createDirectories(path.getParent());
|
||||
}
|
||||
System.out.println("Downloading " + url);
|
||||
InputStream stream = redirect(new URL(url));
|
||||
Files.copy(stream, path, StandardCopyOption.REPLACE_EXISTING);
|
||||
stream.close();
|
||||
if (hash != null) {
|
||||
String hash1 = hash(path);
|
||||
if (!hash.equals(hash1)) {
|
||||
Files.delete(path);
|
||||
throw new Exception("Checksum failed: expect " + hash + " but found " + hash1);
|
||||
}
|
||||
}
|
||||
onComplete.accept(path);
|
||||
return true;
|
||||
} catch (FileNotFoundException | SocketTimeoutException e) {
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
run(() -> downloadFile(url, dist, hash, retry - 1, onComplete));
|
||||
System.err.println("Failed to download file " + dist);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private static InputStream redirect(URL url) throws Exception {
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setInstanceFollowRedirects(false);
|
||||
connection.setReadTimeout(15000);
|
||||
connection.setConnectTimeout(15000);
|
||||
switch (connection.getResponseCode()) {
|
||||
case HttpURLConnection.HTTP_MOVED_PERM:
|
||||
case HttpURLConnection.HTTP_MOVED_TEMP:
|
||||
String location = URLDecoder.decode(connection.getHeaderField("Location"), "UTF-8");
|
||||
return redirect(new URL(url, location));
|
||||
case HttpURLConnection.HTTP_FORBIDDEN:
|
||||
case HttpURLConnection.HTTP_NOT_FOUND:
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
return connection.getInputStream();
|
||||
}
|
||||
|
||||
private static String SHA_PAD = String.format("%040d", 0);
|
||||
|
||||
private static String hash(Path path) throws Exception {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||
String hash = new BigInteger(1, digest.digest(Files.readAllBytes(path))).toString(16);
|
||||
return (SHA_PAD + hash).substring(hash.length());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package io.izzel.arclight.forgeinstaller;
|
||||
|
||||
import java.io.File;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
class Util {
|
||||
|
||||
public static String mavenToPath(String maven) {
|
||||
String type;
|
||||
if (maven.matches(".*@\\w+$")) {
|
||||
int i = maven.lastIndexOf('@');
|
||||
type = maven.substring(i + 1);
|
||||
maven = maven.substring(0, i);
|
||||
} else {
|
||||
type = "jar";
|
||||
}
|
||||
String[] arr = maven.split(":");
|
||||
if (arr.length == 3) {
|
||||
String pkg = arr[0].replace('.', '/');
|
||||
return String.format("%s/%s/%s/%s-%s.%s", pkg, arr[1], arr[2], arr[1], arr[2], type);
|
||||
} else if (arr.length == 4) {
|
||||
String pkg = arr[0].replace('.', '/');
|
||||
return String.format("%s/%s/%s/%s-%s-%s.%s", pkg, arr[1], arr[2], arr[1], arr[2], arr[3], type);
|
||||
} else throw new RuntimeException("Wrong maven coordinate " + maven);
|
||||
}
|
||||
|
||||
private static final String SHA_PAD = String.format("%040d", 0);
|
||||
|
||||
public static String hash(String path) throws Exception {
|
||||
return hash(new File(path).toPath());
|
||||
}
|
||||
|
||||
public static String hash(File path) throws Exception {
|
||||
return hash(path.toPath());
|
||||
}
|
||||
|
||||
public static String hash(Path path) throws Exception {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||
String hash = new BigInteger(1, digest.digest(Files.readAllBytes(path))).toString(16);
|
||||
return (SHA_PAD + hash).substring(hash.length());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package io.izzel.arclight.i18n;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public interface LocalizedException {
|
||||
|
||||
String node();
|
||||
|
||||
Object[] args();
|
||||
|
||||
static <T extends Exception & LocalizedException> T checked(String node, Object... args) {
|
||||
class Checked extends Exception implements LocalizedException {
|
||||
|
||||
@Override
|
||||
public String node() {
|
||||
return node;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] args() {
|
||||
return args;
|
||||
}
|
||||
}
|
||||
return (T) new Checked();
|
||||
}
|
||||
|
||||
static <T extends RuntimeException & LocalizedException> T unchecked(String node, Object... args) {
|
||||
class Unchecked extends RuntimeException implements LocalizedException {
|
||||
|
||||
@Override
|
||||
public String node() {
|
||||
return node;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] args() {
|
||||
return args;
|
||||
}
|
||||
}
|
||||
return (T) new Unchecked();
|
||||
}
|
||||
}
|
|
@ -12,6 +12,22 @@ logo = [
|
|||
" §aBuild Date {}"
|
||||
""
|
||||
]
|
||||
downloader {
|
||||
info = [
|
||||
"Libraries are missing, downloading!"
|
||||
"Downloading service by BMCLAPI https://bmclapidoc.bangbang93.com"
|
||||
"Support MinecraftForge project https://www.patreon.com/LexManos/"
|
||||
]
|
||||
http-error = "HTTP Error {0} {1}"
|
||||
not-found = "Not found in {0}"
|
||||
redirect-error = "Redirect error {0}"
|
||||
dir = "Existing folder in destination {0}"
|
||||
timeout = "Timeout {0} {1}"
|
||||
hash-not-match = "File sha1 mismatch, expect {0} found {1}: {2}"
|
||||
maven-fail = "{0} failed to download: {1}"
|
||||
complete = "{0} complete"
|
||||
forge-install = "Forge installation is starting, please wait... "
|
||||
}
|
||||
|
||||
i18n {
|
||||
current-not-available = "Current locale {0} is not available"
|
||||
|
|
|
@ -12,6 +12,22 @@ logo = [
|
|||
" §a构建日期 {}"
|
||||
""
|
||||
]
|
||||
downloader {
|
||||
info = [
|
||||
"文件不完整,正在自动下载"
|
||||
"BMCLAPI 提供下载服务 https://bmclapidoc.bangbang93.com"
|
||||
"支持 MinecraftForge 项目 https://www.patreon.com/LexManos/"
|
||||
]
|
||||
http-error = "HTTP 错误 {0} {1}"
|
||||
not-found = "文件不存在 {0}"
|
||||
redirect-error = "重定向错误 {0}"
|
||||
dir = "下载地址已经有存在的文件夹 {0}"
|
||||
timeout = "下载超时 {0} {1}"
|
||||
hash-not-match = "文件 sha1 不符,期望 {0} 实际 {1}: {2}"
|
||||
maven-fail = "{0} 下载失败 {1}"
|
||||
complete = "{0} 下载完成"
|
||||
forge-install = "即将开始 Forge 安装,请等待一段时间"
|
||||
}
|
||||
|
||||
i18n {
|
||||
current-not-available = "选择的语言 {0} 不可用"
|
||||
|
|
Loading…
Reference in New Issue
Block a user