From 36aa38a179a3792dddf1890cb6bced401500286f Mon Sep 17 00:00:00 2001 From: IzzelAliz Date: Tue, 16 Jun 2020 16:18:26 +0800 Subject: [PATCH] New downloader --- arclight-common/build.gradle | 2 +- arclight-forge-1.14/build.gradle | 2 +- arclight-forge-1.15/build.gradle | 2 +- build.gradle | 4 + forge-installer/build.gradle | 5 + .../forgeinstaller/FileDownloader.java | 102 +++++++++ .../forgeinstaller/ForgeInstaller.java | 210 +++++++++++------- .../arclight/forgeinstaller/InstallInfo.java | 5 +- .../forgeinstaller/MavenDownloader.java | 51 +++++ .../forgeinstaller/SimpleDownloader.java | 152 ------------- .../izzel/arclight/forgeinstaller/Util.java | 46 ++++ .../arclight/i18n/LocalizedException.java | 41 ++++ .../main/resources/META-INF/i18n/en_us.conf | 16 ++ .../main/resources/META-INF/i18n/zh_cn.conf | 16 ++ 14 files changed, 417 insertions(+), 237 deletions(-) create mode 100644 forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/FileDownloader.java create mode 100644 forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/MavenDownloader.java delete mode 100644 forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/SimpleDownloader.java create mode 100644 forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/Util.java create mode 100644 i18n-config/src/main/java/io/izzel/arclight/i18n/LocalizedException.java diff --git a/arclight-common/build.gradle b/arclight-common/build.gradle index 8656057e..083948c3 100644 --- a/arclight-common/build.gradle +++ b/arclight-common/build.gradle @@ -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" } } diff --git a/arclight-forge-1.14/build.gradle b/arclight-forge-1.14/build.gradle index 4a19892a..e3114704 100644 --- a/arclight-forge-1.14/build.gradle +++ b/arclight-forge-1.14/build.gradle @@ -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" } } diff --git a/arclight-forge-1.15/build.gradle b/arclight-forge-1.15/build.gradle index 26e44d35..c58ca3c9 100644 --- a/arclight-forge-1.15/build.gradle +++ b/arclight-forge-1.15/build.gradle @@ -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" } } diff --git a/build.gradle b/build.gradle index af986954..5686fd6f 100644 --- a/build.gradle +++ b/build.gradle @@ -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") diff --git a/forge-installer/build.gradle b/forge-installer/build.gradle index 38df83fb..782257b3 100644 --- a/forge-installer/build.gradle +++ b/forge-installer/build.gradle @@ -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') } diff --git a/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/FileDownloader.java b/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/FileDownloader.java new file mode 100644 index 00000000..edebd7af --- /dev/null +++ b/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/FileDownloader.java @@ -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 { + + 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 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); + } + } +} diff --git a/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/ForgeInstaller.java b/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/ForgeInstaller.java index d989589d..8570f872 100644 --- a/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/ForgeInstaller.java +++ b/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/ForgeInstaller.java @@ -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 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> suppliers = checkMaven(installInfo.libraries); Path path = Paths.get(String.format("forge-%s-%s.jar", installInfo.installer.minecraft, installInfo.installer.forge)); - if (!Files.exists(path)) { - System.out.println(INFO); - Thread.sleep(5000); - download(installInfo); - 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(); + 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)) { + 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", "."); + Process process = builder.start(); + process.waitFor(); + } + handleFutures(array); + pool.shutdownNow(); } classpath(path, installInfo); } + private static Function, CompletableFuture> 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 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> 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 profileLibraries(Path path) throws IOException { + Map 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> checkMaven(Map map) { + List> incomplete = new ArrayList<>(); + for (Map.Entry 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 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 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); - } } diff --git a/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/InstallInfo.java b/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/InstallInfo.java index e7061b12..7846e754 100644 --- a/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/InstallInfo.java +++ b/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/InstallInfo.java @@ -1,13 +1,16 @@ package io.izzel.arclight.forgeinstaller; +import java.util.Map; + public class InstallInfo { public Installer installer; - public String[] libraries; + public Map libraries; public static class Installer { public String minecraft; public String forge; + public String hash; } } diff --git a/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/MavenDownloader.java b/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/MavenDownloader.java new file mode 100644 index 00000000..b7309c27 --- /dev/null +++ b/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/MavenDownloader.java @@ -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 { + + 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 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()); + } +} diff --git a/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/SimpleDownloader.java b/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/SimpleDownloader.java deleted file mode 100644 index 989f9398..00000000 --- a/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/SimpleDownloader.java +++ /dev/null @@ -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 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 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 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 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()); - } -} diff --git a/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/Util.java b/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/Util.java new file mode 100644 index 00000000..c577fe26 --- /dev/null +++ b/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/Util.java @@ -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()); + } + +} diff --git a/i18n-config/src/main/java/io/izzel/arclight/i18n/LocalizedException.java b/i18n-config/src/main/java/io/izzel/arclight/i18n/LocalizedException.java new file mode 100644 index 00000000..0d2a04a3 --- /dev/null +++ b/i18n-config/src/main/java/io/izzel/arclight/i18n/LocalizedException.java @@ -0,0 +1,41 @@ +package io.izzel.arclight.i18n; + +@SuppressWarnings("unchecked") +public interface LocalizedException { + + String node(); + + Object[] args(); + + static 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 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(); + } +} diff --git a/i18n-config/src/main/resources/META-INF/i18n/en_us.conf b/i18n-config/src/main/resources/META-INF/i18n/en_us.conf index 3063c8a8..9d9c9525 100644 --- a/i18n-config/src/main/resources/META-INF/i18n/en_us.conf +++ b/i18n-config/src/main/resources/META-INF/i18n/en_us.conf @@ -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" diff --git a/i18n-config/src/main/resources/META-INF/i18n/zh_cn.conf b/i18n-config/src/main/resources/META-INF/i18n/zh_cn.conf index 447f6aa8..b1e25112 100644 --- a/i18n-config/src/main/resources/META-INF/i18n/zh_cn.conf +++ b/i18n-config/src/main/resources/META-INF/i18n/zh_cn.conf @@ -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} 不可用"