Compare commits

..
Author SHA1 Message Date
Glavo b88d7c4fd2 修复 saveETagIndex 可能写入不全的问题 (#2926) 2024-03-14 19:24:30 +08:00
Burning_TNTandGlavo c0838468b1 Close #2911: 下载失败时打印重定向链 (#2912)
* Support #2911.

* update

* update

* update

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-03-14 13:50:25 +08:00
Glavo 90cfe3d0b7 加载网络背景时设置超时 (#2856) 2024-03-13 16:25:55 +08:00
Burning_TNT baf1d22bc4 Fix bugs. (#2920)
* Always sort the results while searching in CurseForge.

* Fix bugs.
2024-03-12 22:14:32 +08:00
Glavo 7ca5744559 允许使用系统属性禁用系统代理 (#2921) 2024-03-12 21:44:08 +08:00
Glavo 57e728991b Fix #1938: 正确处理 map_to_resources (#2919)
* Fix #1938: 正确处理 map_to_resources

* fix checkstyle
2024-03-12 20:01:30 +08:00
Glavo e08ba4d421 不再尝试下载 .pack.xz 文件 (#2917)
* 不再尝试下载 .pack.xz 文件

* update
2024-03-11 20:21:17 +08:00
10 changed files with 72 additions and 99 deletions

No files matched your search

@@ -55,8 +55,8 @@ public final class Main {
}
public static void main(String[] args) {
System.setProperty("java.net.useSystemProxies", "true");
System.setProperty("javafx.autoproxy.disable", "true");
System.getProperties().putIfAbsent("java.net.useSystemProxies", "true");
System.getProperties().putIfAbsent("javafx.autoproxy.disable", "true");
System.getProperties().putIfAbsent("http.agent", "HMCL/" + Metadata.VERSION);
checkDirectoryPath();
@@ -50,6 +50,10 @@ import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -168,8 +172,23 @@ public class DecoratorController {
break;
case NETWORK:
String backgroundImageUrl = config().getBackgroundImageUrl();
if (backgroundImageUrl != null && NetworkUtils.isURL(backgroundImageUrl))
image = tryLoadImage(backgroundImageUrl).orElse(null);
if (backgroundImageUrl != null) {
try {
URLConnection connection = NetworkUtils.createConnection(new URL(backgroundImageUrl));
if (connection instanceof HttpURLConnection) {
connection = NetworkUtils.resolveConnection((HttpURLConnection) connection);
}
try (InputStream input = connection.getInputStream()) {
image = new Image(input);
if (image.isError()) {
throw image.getException();
}
}
} catch (Exception e) {
LOG.log(WARNING, "Couldn't load background image", e);
}
}
break;
case CLASSIC:
image = newBuiltinImage("/assets/img/background-classic.jpg");
@@ -164,7 +164,9 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
}
private void clearAssets() {
FileUtils.deleteDirectoryQuietly(new File(getProfile().getRepository().getBaseDirectory(), "assets"));
File baseDirectory = getProfile().getRepository().getBaseDirectory();
FileUtils.deleteDirectoryQuietly(new File(baseDirectory, "assets"));
FileUtils.deleteDirectoryQuietly(new File(baseDirectory, "resources"));
}
private void clearJunkFiles() {
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.download.game;
import org.jackhuang.hmcl.download.AbstractDependencyManager;
import org.jackhuang.hmcl.download.ArtifactMalformedException;
import org.jackhuang.hmcl.download.DefaultCacheRepository;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.task.DownloadException;
@@ -26,13 +25,12 @@ import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.Pack200Utils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.tukaani.xz.XZInputStream;
import java.io.*;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
@@ -41,7 +39,6 @@ import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Logging.LOG;
@@ -51,10 +48,8 @@ public class LibraryDownloadTask extends Task<Void> {
protected final File jar;
protected final DefaultCacheRepository cacheRepository;
protected final AbstractDependencyManager dependencyManager;
private final File xzFile;
protected final Library library;
protected final String url;
protected boolean xz;
private final Library originalLibrary;
private boolean cached = false;
@@ -72,8 +67,6 @@ public class LibraryDownloadTask extends Task<Void> {
url = library.getDownload().getUrl();
jar = file;
xzFile = new File(file.getAbsoluteFile().getParentFile(), file.getName() + ".pack.xz");
}
@Override
@@ -101,8 +94,6 @@ public class LibraryDownloadTask extends Task<Void> {
throw new CancellationException();
else
throw new LibraryDownloadException(library, t);
} else {
if (xz) unpackLibrary(jar, Files.readAllBytes(xzFile.toPath()));
}
}
@@ -126,36 +117,13 @@ public class LibraryDownloadTask extends Task<Void> {
}
}
if (Pack200Utils.isSupported() && testURLExistence(url)) {
List<URL> urls = dependencyManager.getDownloadProvider().injectURLWithCandidates(url + ".pack.xz");
task = new FileDownloadTask(urls, xzFile, null);
task.setCacheRepository(cacheRepository);
task.setCaching(true);
xz = true;
} else {
List<URL> urls = dependencyManager.getDownloadProvider().injectURLWithCandidates(url);
task = new FileDownloadTask(urls, jar,
library.getDownload().getSha1() != null ? new IntegrityCheck("SHA-1", library.getDownload().getSha1()) : null);
task.setCacheRepository(cacheRepository);
task.setCaching(true);
task.addIntegrityCheckHandler(FileDownloadTask.ZIP_INTEGRITY_CHECK_HANDLER);
xz = false;
}
}
private boolean testURLExistence(String rawUrl) {
List<URL> urls = dependencyManager.getDownloadProvider().injectURLWithCandidates(rawUrl);
for (URL url : urls) {
URL xzURL = NetworkUtils.toURL(url.toString() + ".pack.xz");
for (int retry = 0; retry < 3; retry++) {
try {
return NetworkUtils.urlExists(xzURL);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to test for url existence: " + url + ".pack.xz", e);
}
}
}
return false; // maybe some ugly implementation will give timeout for not existent url.
List<URL> urls = dependencyManager.getDownloadProvider().injectURLWithCandidates(url);
task = new FileDownloadTask(urls, jar,
library.getDownload().getSha1() != null ? new IntegrityCheck("SHA-1", library.getDownload().getSha1()) : null);
task.setCacheRepository(cacheRepository);
task.setCaching(true);
task.addIntegrityCheckHandler(FileDownloadTask.ZIP_INTEGRITY_CHECK_HANDLER);
}
@Override
@@ -167,7 +135,7 @@ public class LibraryDownloadTask extends Task<Void> {
public void postExecute() throws Exception {
if (!cached) {
try {
cacheRepository.cacheLibrary(library, jar.toPath(), xz);
cacheRepository.cacheLibrary(library, jar.toPath(), false);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to cache downloaded library " + library, e);
}
@@ -232,44 +200,4 @@ public class LibraryDownloadTask extends Task<Void> {
}
return false;
}
private static void unpackLibrary(File dest, byte[] src) throws IOException {
if (dest.exists())
if (!dest.delete())
throw new IOException("Unable to delete file " + dest);
byte[] decompressed;
try {
decompressed = IOUtils.readFullyAsByteArray(new XZInputStream(new ByteArrayInputStream(src)));
} catch (IOException e) {
throw new ArtifactMalformedException("Library " + dest + " is malformed");
}
String end = new String(decompressed, decompressed.length - 4, 4);
if (!end.equals("SIGN"))
throw new IOException("Unpacking failed, signature missing " + end);
int x = decompressed.length;
int len = decompressed[(x - 8)] & 0xFF | (decompressed[(x - 7)] & 0xFF) << 8 | (decompressed[(x - 6)] & 0xFF) << 16 | (decompressed[(x - 5)] & 0xFF) << 24;
Path temp = Files.createTempFile("minecraft", ".pack");
byte[] checksums = Arrays.copyOfRange(decompressed, decompressed.length - len - 8, decompressed.length - 8);
try (OutputStream out = Files.newOutputStream(temp)) {
out.write(decompressed, 0, decompressed.length - len - 8);
}
try (FileOutputStream jarBytes = new FileOutputStream(dest); JarOutputStream jos = new JarOutputStream(jarBytes)) {
Pack200Utils.unpack(temp.toFile(), jos);
JarEntry checksumsFile = new JarEntry("checksums.sha1");
checksumsFile.setTime(0L);
jos.putNextEntry(checksumsFile);
jos.write(checksums);
jos.closeEntry();
}
Files.delete(temp);
}
}
@@ -52,6 +52,10 @@ public final class AssetIndex {
return virtual || mapToResources;
}
public boolean needMapToResources() {
return mapToResources;
}
public Map<String, AssetObject> getObjects() {
return Collections.unmodifiableMap(objects);
}
@@ -459,6 +459,8 @@ public class DefaultGameRepository implements GameRepository {
return assetsDir;
if (index.isVirtual()) {
Path resourcesDir = getBaseDirectory().toPath().resolve("resources");
int cnt = 0;
int tot = index.getObjects().entrySet().size();
for (Map.Entry<String, AssetObject> entry : index.getObjects().entrySet()) {
@@ -468,6 +470,12 @@ public class DefaultGameRepository implements GameRepository {
cnt++;
if (!Files.isRegularFile(target))
FileUtils.copyFile(original, target);
if (index.needMapToResources()) {
target = resourcesDir.resolve(entry.getKey());
if (!Files.isRegularFile(target))
FileUtils.copyFile(original, target);
}
}
}
@@ -111,9 +111,8 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
.header("X-API-KEY", apiKey)
.getJson(new TypeToken<Response<List<CurseAddon>>>() {
}.getType());
Stream<RemoteMod> res = response.getData().stream().map(CurseAddon::toMod);
if (searchFilter.isEmpty()) {
return new SearchResult(res, (int)Math.ceil((double)response.pagination.totalCount / pageSize));
return new SearchResult(response.getData().stream().map(CurseAddon::toMod), (int)Math.ceil((double)response.pagination.totalCount / pageSize));
}
// https://github.com/HMCL-dev/HMCL/issues/1549
@@ -125,7 +124,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator();
return new SearchResult(res.map(remoteMod -> {
return new SearchResult(response.getData().stream().map(CurseAddon::toMod).map(remoteMod -> {
String lowerCaseResult = remoteMod.getTitle().toLowerCase();
int diff = levCalculator.calc(lowerCaseSearchFilter, lowerCaseResult);
@@ -136,7 +135,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
}
return pair(remoteMod, diff);
}).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), res, response.pagination.totalCount);
}).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), response.getData().stream().map(CurseAddon::toMod), response.pagination.totalCount);
}
@Override
@@ -94,6 +94,7 @@ public abstract class FetchTask<T> extends Task<T> {
break download;
}
List<String> redirects = null;
try {
beforeDownload(url);
@@ -103,7 +104,9 @@ public abstract class FetchTask<T> extends Task<T> {
if (checkETag) repository.injectConnection(conn);
if (conn instanceof HttpURLConnection) {
conn = NetworkUtils.resolveConnection((HttpURLConnection) conn);
redirects = new ArrayList<>();
conn = NetworkUtils.resolveConnection((HttpURLConnection) conn, redirects);
int responseCode = ((HttpURLConnection) conn).getResponseCode();
if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
@@ -164,13 +167,13 @@ public abstract class FetchTask<T> extends Task<T> {
} catch (FileNotFoundException ex) {
failedURL = url;
exception = ex;
Logging.LOG.log(Level.WARNING, "Failed to download " + url + ", not found", ex);
Logging.LOG.log(Level.WARNING, "Failed to download " + url + ", not found" + ((redirects == null || redirects.isEmpty()) ? "" : ", redirects: " + redirects), ex);
break; // we will not try this URL again
} catch (IOException ex) {
failedURL = url;
exception = ex;
Logging.LOG.log(Level.WARNING, "Failed to download " + url + ", repeat times: " + (++repeat), ex);
Logging.LOG.log(Level.WARNING, "Failed to download " + url + ", repeat times: " + (++repeat) + ((redirects == null || redirects.isEmpty()) ? "" : ", redirects: " + redirects), ex);
}
}
}
@@ -293,8 +293,12 @@ public class CacheRepository {
ETagIndex indexOnDisk = JsonUtils.fromMaybeMalformedJson(new String(IOUtils.readFullyWithoutClosing(Channels.newInputStream(channel)), UTF_8), ETagIndex.class);
Map<String, ETagItem> newIndex = joinETagIndexes(indexOnDisk == null ? null : indexOnDisk.eTag, index.values());
channel.truncate(0);
ETagIndex writeTo = new ETagIndex(newIndex.values());
channel.write(ByteBuffer.wrap(JsonUtils.GSON.toJson(writeTo).getBytes(UTF_8)));
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");
}
}
this.index = newIndex;
} finally {
lock.release();
@@ -29,7 +29,6 @@ import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.StringUtils.*;
/**
*
* @author huangyuhui
*/
public final class NetworkUtils {
@@ -131,16 +130,20 @@ public final class NetworkUtils {
return sb.toString();
}
public static HttpURLConnection resolveConnection(HttpURLConnection conn) throws IOException {
return resolveConnection(conn, null);
}
/**
* This method is a work-around that aims to solve problem when "Location" in
* stupid server's response is not encoded.
*
*
* @see <a href="https://github.com/curl/curl/issues/473">Issue with libcurl</a>
* @param conn the stupid http connection.
* @return manually redirected http connection.
* @throws IOException if an I/O error occurs.
*/
public static HttpURLConnection resolveConnection(HttpURLConnection conn) throws IOException {
public static HttpURLConnection resolveConnection(HttpURLConnection conn, List<String> redirects) throws IOException {
int redirect = 0;
while (true) {
conn.setUseCaches(false);
@@ -154,6 +157,9 @@ public final class NetworkUtils {
String newURL = conn.getHeaderField("Location");
conn.disconnect();
if (redirects != null) {
redirects.add(newURL);
}
if (redirect > 20) {
throw new IOException("Too much redirects");
}