feat(skin): WIP: download skin from CSL server.
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.jackhuang.hmcl.ui.account;
|
||||||
|
|
||||||
|
import com.jfoenix.controls.JFXDialogLayout;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||||
|
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
|
||||||
|
|
||||||
|
public class OfflineAccountSkinPane extends StackPane {
|
||||||
|
|
||||||
|
public OfflineAccountSkinPane(OfflineAccount account) {
|
||||||
|
|
||||||
|
JFXDialogLayout layout = new JFXDialogLayout();
|
||||||
|
getChildren().setAll(layout);
|
||||||
|
|
||||||
|
MultiFileItem<>
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,15 +51,13 @@ public class OfflineAccount extends Account {
|
|||||||
private final AuthlibInjectorArtifactProvider downloader;
|
private final AuthlibInjectorArtifactProvider downloader;
|
||||||
private final String username;
|
private final String username;
|
||||||
private final UUID uuid;
|
private final UUID uuid;
|
||||||
private final String skin;
|
private final Map<TextureType, Texture> textures;
|
||||||
private final String cape;
|
|
||||||
|
|
||||||
protected OfflineAccount(AuthlibInjectorArtifactProvider downloader, String username, UUID uuid, String skin, String cape) {
|
protected OfflineAccount(AuthlibInjectorArtifactProvider downloader, String username, UUID uuid, Map<TextureType, Texture> textures) {
|
||||||
this.downloader = requireNonNull(downloader);
|
this.downloader = requireNonNull(downloader);
|
||||||
this.username = requireNonNull(username);
|
this.username = requireNonNull(username);
|
||||||
this.uuid = requireNonNull(uuid);
|
this.uuid = requireNonNull(uuid);
|
||||||
this.skin = skin;
|
this.textures = textures;
|
||||||
this.cape = cape;
|
|
||||||
|
|
||||||
if (StringUtils.isBlank(username)) {
|
if (StringUtils.isBlank(username)) {
|
||||||
throw new IllegalArgumentException("Username cannot be blank");
|
throw new IllegalArgumentException("Username cannot be blank");
|
||||||
|
|||||||
202
HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/Skin.java
Normal file
202
HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/Skin.java
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
/*
|
||||||
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.jackhuang.hmcl.auth.offline;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
|
||||||
|
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
||||||
|
import org.jackhuang.hmcl.task.FetchTask;
|
||||||
|
import org.jackhuang.hmcl.task.GetTask;
|
||||||
|
import org.jackhuang.hmcl.task.Task;
|
||||||
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
|
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||||
|
|
||||||
|
public class Skin {
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
DEFAULT,
|
||||||
|
STEVE,
|
||||||
|
ALEX,
|
||||||
|
LOCAL_FILE,
|
||||||
|
CUSTOM_SKIN_LOADER_API,
|
||||||
|
YGGDRASIL_API
|
||||||
|
}
|
||||||
|
|
||||||
|
private Type type;
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
public Type getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Texture> toTexture(String username) {
|
||||||
|
switch (type) {
|
||||||
|
case DEFAULT:
|
||||||
|
return Task.supplyAsync(() -> null);
|
||||||
|
case STEVE:
|
||||||
|
return Task.supplyAsync(() -> Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/steve.png")));
|
||||||
|
case ALEX:
|
||||||
|
return Task.supplyAsync(() -> Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/alex.png")));
|
||||||
|
case LOCAL_FILE:
|
||||||
|
return Task.supplyAsync(() -> Texture.loadTexture(Files.newInputStream(Paths.get(value))));
|
||||||
|
case CUSTOM_SKIN_LOADER_API:
|
||||||
|
return Task.composeAsync(() -> new GetTask(new URL(String.format("%s/%s.json", value, username))))
|
||||||
|
.thenComposeAsync(json -> {
|
||||||
|
SkinJson result = JsonUtils.GSON.fromJson(json, SkinJson.class);
|
||||||
|
|
||||||
|
if (!result.hasSkin()) {
|
||||||
|
return Task.supplyAsync(() -> null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FetchBytesTask(new URL(String.format("%s/textures/%s", value, result.getHash())), 3);
|
||||||
|
}).thenApplyAsync(Texture::loadTexture);
|
||||||
|
default:
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class FetchBytesTask extends FetchTask<InputStream> {
|
||||||
|
|
||||||
|
public FetchBytesTask(URL url, int retry) {
|
||||||
|
super(Collections.singletonList(url), retry);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void useCachedResult(Path cachedFile) throws IOException {
|
||||||
|
setResult(Files.newInputStream(cachedFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected EnumCheckETag shouldCheckETag() {
|
||||||
|
return EnumCheckETag.CHECK_E_TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Context getContext(URLConnection conn, boolean checkETag) throws IOException {
|
||||||
|
return new Context() {
|
||||||
|
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(byte[] buffer, int offset, int len) {
|
||||||
|
baos.write(buffer, offset, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
if (!isSuccess()) return;
|
||||||
|
|
||||||
|
setResult(new ByteArrayInputStream(baos.toByteArray()));
|
||||||
|
|
||||||
|
if (checkETag) {
|
||||||
|
repository.cacheBytes(baos.toByteArray(), conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SkinJson {
|
||||||
|
private final String username;
|
||||||
|
private final String skin;
|
||||||
|
private final String cape;
|
||||||
|
private final String elytra;
|
||||||
|
|
||||||
|
@SerializedName(value = "textures", alternate = { "skins" })
|
||||||
|
private final TextureJson textures;
|
||||||
|
|
||||||
|
public SkinJson(String username, String skin, String cape, String elytra, TextureJson textures) {
|
||||||
|
this.username = username;
|
||||||
|
this.skin = skin;
|
||||||
|
this.cape = cape;
|
||||||
|
this.elytra = elytra;
|
||||||
|
this.textures = textures;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasSkin() {
|
||||||
|
return StringUtils.isNotBlank(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public TextureModel getModel() {
|
||||||
|
if (textures != null && textures.slim != null) {
|
||||||
|
return TextureModel.ALEX;
|
||||||
|
} else if (textures != null && textures.defaultSkin != null) {
|
||||||
|
return TextureModel.STEVE;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAlexModelHash() {
|
||||||
|
if (textures != null && textures.slim != null) {
|
||||||
|
return textures.slim;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSteveModelHash() {
|
||||||
|
if (textures != null && textures.defaultSkin != null) {
|
||||||
|
return textures.defaultSkin;
|
||||||
|
} else return skin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHash() {
|
||||||
|
TextureModel model = getModel();
|
||||||
|
if (model == TextureModel.ALEX)
|
||||||
|
return getAlexModelHash();
|
||||||
|
else if (model == TextureModel.STEVE)
|
||||||
|
return getSteveModelHash();
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TextureJson {
|
||||||
|
@SerializedName("default")
|
||||||
|
private final String defaultSkin;
|
||||||
|
|
||||||
|
private final String slim;
|
||||||
|
private final String cape;
|
||||||
|
private final String elytra;
|
||||||
|
|
||||||
|
public TextureJson(String defaultSkin, String slim, String cape, String elytra) {
|
||||||
|
this.defaultSkin = defaultSkin;
|
||||||
|
this.slim = slim;
|
||||||
|
this.cape = cape;
|
||||||
|
this.elytra = elytra;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
/*
|
||||||
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.jackhuang.hmcl.auth.offline;
|
||||||
|
|
||||||
|
import javax.imageio.IIOException;
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
|
public class Texture {
|
||||||
|
private final String hash;
|
||||||
|
private final byte[] data;
|
||||||
|
|
||||||
|
public Texture(String hash, byte[] data) {
|
||||||
|
this.hash = requireNonNull(hash);
|
||||||
|
this.data = requireNonNull(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHash() {
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream getInputStream() {
|
||||||
|
return new ByteArrayInputStream(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLength() {
|
||||||
|
return data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Map<String, Texture> textures = new HashMap<>();
|
||||||
|
|
||||||
|
public static boolean hasTexture(String hash) {
|
||||||
|
return textures.containsKey(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Texture getTexture(String hash) {
|
||||||
|
return textures.get(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String computeTextureHash(BufferedImage img) {
|
||||||
|
MessageDigest digest;
|
||||||
|
try {
|
||||||
|
digest = MessageDigest.getInstance("SHA-256");
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
int width = img.getWidth();
|
||||||
|
int height = img.getHeight();
|
||||||
|
byte[] buf = new byte[4096];
|
||||||
|
|
||||||
|
putInt(buf, 0, width);
|
||||||
|
putInt(buf, 4, height);
|
||||||
|
int pos = 8;
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
for (int y = 0; y < height; y++) {
|
||||||
|
putInt(buf, pos, img.getRGB(x, y));
|
||||||
|
if (buf[pos + 0] == 0) {
|
||||||
|
buf[pos + 1] = buf[pos + 2] = buf[pos + 3] = 0;
|
||||||
|
}
|
||||||
|
pos += 4;
|
||||||
|
if (pos == buf.length) {
|
||||||
|
pos = 0;
|
||||||
|
digest.update(buf, 0, buf.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pos > 0) {
|
||||||
|
digest.update(buf, 0, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] sha256 = digest.digest();
|
||||||
|
return String.format("%0" + (sha256.length << 1) + "x", new BigInteger(1, sha256));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void putInt(byte[] array, int offset, int x) {
|
||||||
|
array[offset + 0] = (byte) (x >> 24 & 0xff);
|
||||||
|
array[offset + 1] = (byte) (x >> 16 & 0xff);
|
||||||
|
array[offset + 2] = (byte) (x >> 8 & 0xff);
|
||||||
|
array[offset + 3] = (byte) (x >> 0 & 0xff);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Texture loadTexture(InputStream in) throws IOException {
|
||||||
|
if (in == null) return null;
|
||||||
|
BufferedImage img = ImageIO.read(in);
|
||||||
|
if (img == null) {
|
||||||
|
throw new IIOException("No image found");
|
||||||
|
}
|
||||||
|
|
||||||
|
String hash = computeTextureHash(img);
|
||||||
|
|
||||||
|
Texture existent = textures.get(hash);
|
||||||
|
if (existent != null) {
|
||||||
|
return existent;
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteArrayOutputStream buf = new ByteArrayOutputStream();
|
||||||
|
ImageIO.write(img, "png", buf);
|
||||||
|
Texture texture = new Texture(hash, buf.toByteArray());
|
||||||
|
|
||||||
|
existent = textures.putIfAbsent(hash, texture);
|
||||||
|
|
||||||
|
if (existent != null) {
|
||||||
|
return existent;
|
||||||
|
}
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Texture loadTexture(String url) throws IOException {
|
||||||
|
if (url == null) return null;
|
||||||
|
return loadTexture(new URL(url).openStream());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -28,15 +28,7 @@ import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
|||||||
import org.jackhuang.hmcl.util.io.HttpServer;
|
import org.jackhuang.hmcl.util.io.HttpServer;
|
||||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||||
|
|
||||||
import javax.imageio.IIOException;
|
|
||||||
import javax.imageio.ImageIO;
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.security.*;
|
import java.security.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@@ -50,7 +42,6 @@ import static org.jackhuang.hmcl.util.Pair.pair;
|
|||||||
|
|
||||||
public class YggdrasilServer extends HttpServer {
|
public class YggdrasilServer extends HttpServer {
|
||||||
|
|
||||||
private final Map<String, Texture> textures = new HashMap<>();
|
|
||||||
private final Map<UUID, Character> charactersByUuid = new HashMap<>();
|
private final Map<UUID, Character> charactersByUuid = new HashMap<>();
|
||||||
private final Map<String, Character> charactersByName = new HashMap<>();
|
private final Map<String, Character> charactersByName = new HashMap<>();
|
||||||
|
|
||||||
@@ -125,8 +116,8 @@ public class YggdrasilServer extends HttpServer {
|
|||||||
private Response texture(Request request) {
|
private Response texture(Request request) {
|
||||||
String hash = request.getPathVariables().group("hash");
|
String hash = request.getPathVariables().group("hash");
|
||||||
|
|
||||||
if (textures.containsKey(hash)) {
|
if (Texture.hasTexture(hash)) {
|
||||||
Texture texture = textures.get(hash);
|
Texture texture = Texture.getTexture(hash);
|
||||||
Response response = newFixedLengthResponse(Response.Status.OK, "image/png", texture.getInputStream(), texture.getLength());
|
Response response = newFixedLengthResponse(Response.Status.OK, "image/png", texture.getInputStream(), texture.getLength());
|
||||||
response.addHeader("Etag", String.format("\"%s\"", hash));
|
response.addHeader("Etag", String.format("\"%s\"", hash));
|
||||||
response.addHeader("Cache-Control", "max-age=2592000, public");
|
response.addHeader("Cache-Control", "max-age=2592000, public");
|
||||||
@@ -144,80 +135,6 @@ public class YggdrasilServer extends HttpServer {
|
|||||||
return Optional.ofNullable(charactersByName.get(uuid));
|
return Optional.ofNullable(charactersByName.get(uuid));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String computeTextureHash(BufferedImage img) {
|
|
||||||
MessageDigest digest;
|
|
||||||
try {
|
|
||||||
digest = MessageDigest.getInstance("SHA-256");
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
int width = img.getWidth();
|
|
||||||
int height = img.getHeight();
|
|
||||||
byte[] buf = new byte[4096];
|
|
||||||
|
|
||||||
putInt(buf, 0, width);
|
|
||||||
putInt(buf, 4, height);
|
|
||||||
int pos = 8;
|
|
||||||
for (int x = 0; x < width; x++) {
|
|
||||||
for (int y = 0; y < height; y++) {
|
|
||||||
putInt(buf, pos, img.getRGB(x, y));
|
|
||||||
if (buf[pos + 0] == 0) {
|
|
||||||
buf[pos + 1] = buf[pos + 2] = buf[pos + 3] = 0;
|
|
||||||
}
|
|
||||||
pos += 4;
|
|
||||||
if (pos == buf.length) {
|
|
||||||
pos = 0;
|
|
||||||
digest.update(buf, 0, buf.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (pos > 0) {
|
|
||||||
digest.update(buf, 0, pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] sha256 = digest.digest();
|
|
||||||
return String.format("%0" + (sha256.length << 1) + "x", new BigInteger(1, sha256));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void putInt(byte[] array, int offset, int x) {
|
|
||||||
array[offset + 0] = (byte) (x >> 24 & 0xff);
|
|
||||||
array[offset + 1] = (byte) (x >> 16 & 0xff);
|
|
||||||
array[offset + 2] = (byte) (x >> 8 & 0xff);
|
|
||||||
array[offset + 3] = (byte) (x >> 0 & 0xff);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Texture loadTexture(InputStream in) throws IOException {
|
|
||||||
if (in == null) return null;
|
|
||||||
BufferedImage img = ImageIO.read(in);
|
|
||||||
if (img == null) {
|
|
||||||
throw new IIOException("No image found");
|
|
||||||
}
|
|
||||||
|
|
||||||
String hash = computeTextureHash(img);
|
|
||||||
|
|
||||||
Texture existent = textures.get(hash);
|
|
||||||
if (existent != null) {
|
|
||||||
return existent;
|
|
||||||
}
|
|
||||||
|
|
||||||
String url = String.format("http://localhost:%d/textures/%s", getListeningPort(), hash);
|
|
||||||
ByteArrayOutputStream buf = new ByteArrayOutputStream();
|
|
||||||
ImageIO.write(img, "png", buf);
|
|
||||||
Texture texture = new Texture(hash, buf.toByteArray(), url);
|
|
||||||
|
|
||||||
existent = textures.putIfAbsent(hash, texture);
|
|
||||||
|
|
||||||
if (existent != null) {
|
|
||||||
return existent;
|
|
||||||
}
|
|
||||||
return texture;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Texture loadTexture(String url) throws IOException {
|
|
||||||
if (url == null) return null;
|
|
||||||
return loadTexture(new URL(url).openStream());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addCharacter(Character character) {
|
public void addCharacter(Character character) {
|
||||||
charactersByUuid.put(character.getUUID(), character);
|
charactersByUuid.put(character.getUUID(), character);
|
||||||
charactersByName.put(character.getName(), character);
|
charactersByName.put(character.getName(), character);
|
||||||
@@ -267,7 +184,7 @@ public class YggdrasilServer extends HttpServer {
|
|||||||
Map<String, Object> realTextures = new HashMap<>();
|
Map<String, Object> realTextures = new HashMap<>();
|
||||||
for (Map.Entry<TextureType, Texture> textureEntry : textures.entrySet()) {
|
for (Map.Entry<TextureType, Texture> textureEntry : textures.entrySet()) {
|
||||||
if (textureEntry.getValue() == null) continue;
|
if (textureEntry.getValue() == null) continue;
|
||||||
realTextures.put(textureEntry.getKey().name(), mapOf(pair("url", rootUrl + "/textures/" + textureEntry.getValue().hash)));
|
realTextures.put(textureEntry.getKey().name(), mapOf(pair("url", rootUrl + "/textures/" + textureEntry.getValue().getHash())));
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Object> textureResponse = mapOf(
|
Map<String, Object> textureResponse = mapOf(
|
||||||
@@ -289,30 +206,6 @@ public class YggdrasilServer extends HttpServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class Texture {
|
|
||||||
private final String hash;
|
|
||||||
private final byte[] data;
|
|
||||||
private final String url;
|
|
||||||
|
|
||||||
public Texture(String hash, byte[] data, String url) {
|
|
||||||
this.hash = requireNonNull(hash);
|
|
||||||
this.data = requireNonNull(data);
|
|
||||||
this.url = requireNonNull(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUrl() {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
public InputStream getInputStream() {
|
|
||||||
return new ByteArrayInputStream(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getLength() {
|
|
||||||
return data.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Signature ===
|
// === Signature ===
|
||||||
|
|
||||||
private static final KeyPair keyPair = KeyUtils.generateKey();
|
private static final KeyPair keyPair = KeyUtils.generateKey();
|
||||||
|
|||||||
@@ -17,10 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.util;
|
package org.jackhuang.hmcl.util;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.*;
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.RandomAccessFile;
|
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
import java.nio.channels.Channels;
|
import java.nio.channels.Channels;
|
||||||
import java.nio.channels.FileChannel;
|
import java.nio.channels.FileChannel;
|
||||||
@@ -35,6 +32,7 @@ import java.util.concurrent.locks.Lock;
|
|||||||
import java.util.concurrent.locks.ReadWriteLock;
|
import java.util.concurrent.locks.ReadWriteLock;
|
||||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
|
import java.util.function.Supplier;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
@@ -190,14 +188,34 @@ public class CacheRepository {
|
|||||||
// conn.setRequestProperty("If-Modified-Since", eTagItem.getRemoteLastModified());
|
// conn.setRequestProperty("If-Modified-Since", eTagItem.getRemoteLastModified());
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void cacheRemoteFile(Path downloaded, URLConnection conn) throws IOException {
|
public void cacheRemoteFile(Path downloaded, URLConnection conn) throws IOException {
|
||||||
|
cacheData(() -> {
|
||||||
|
String hash = Hex.encodeHex(DigestUtils.digest(SHA1, downloaded));
|
||||||
|
Path cached = cacheFile(downloaded, SHA1, hash);
|
||||||
|
return new CacheResult(hash, cached);
|
||||||
|
}, conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cacheText(String text, URLConnection conn) throws IOException {
|
||||||
|
cacheBytes(text.getBytes(UTF_8), conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cacheBytes(byte[] bytes, URLConnection conn) throws IOException {
|
||||||
|
cacheData(() -> {
|
||||||
|
String hash = Hex.encodeHex(DigestUtils.digest(SHA1, bytes));
|
||||||
|
Path cached = getFile(SHA1, hash);
|
||||||
|
FileUtils.writeBytes(cached.toFile(), bytes);
|
||||||
|
return new CacheResult(hash, cached);
|
||||||
|
}, conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void cacheData(ExceptionalSupplier<CacheResult, IOException> cacheSupplier, URLConnection conn) throws IOException {
|
||||||
String eTag = conn.getHeaderField("ETag");
|
String eTag = conn.getHeaderField("ETag");
|
||||||
if (eTag == null) return;
|
if (eTag == null) return;
|
||||||
String url = conn.getURL().toString();
|
String url = conn.getURL().toString();
|
||||||
String lastModified = conn.getHeaderField("Last-Modified");
|
String lastModified = conn.getHeaderField("Last-Modified");
|
||||||
String hash = Hex.encodeHex(DigestUtils.digest(SHA1, downloaded));
|
CacheResult cacheResult = cacheSupplier.get();
|
||||||
Path cached = cacheFile(downloaded, SHA1, hash);
|
ETagItem eTagItem = new ETagItem(url, eTag, cacheResult.hash, Files.getLastModifiedTime(cacheResult.cachedFile).toMillis(), lastModified);
|
||||||
ETagItem eTagItem = new ETagItem(url, eTag, hash, Files.getLastModifiedTime(cached).toMillis(), lastModified);
|
|
||||||
Lock writeLock = lock.writeLock();
|
Lock writeLock = lock.writeLock();
|
||||||
writeLock.lock();
|
writeLock.lock();
|
||||||
try {
|
try {
|
||||||
@@ -208,22 +226,13 @@ public class CacheRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void cacheText(String text, URLConnection conn) throws IOException {
|
private static class CacheResult {
|
||||||
String eTag = conn.getHeaderField("ETag");
|
public String hash;
|
||||||
if (eTag == null) return;
|
public Path cachedFile;
|
||||||
String url = conn.getURL().toString();
|
|
||||||
String lastModified = conn.getHeaderField("Last-Modified");
|
public CacheResult(String hash, Path cachedFile) {
|
||||||
String hash = Hex.encodeHex(DigestUtils.digest(SHA1, text));
|
this.hash = hash;
|
||||||
Path cached = getFile(SHA1, hash);
|
this.cachedFile = cachedFile;
|
||||||
FileUtils.writeText(cached.toFile(), text);
|
|
||||||
ETagItem eTagItem = new ETagItem(url, eTag, hash, Files.getLastModifiedTime(cached).toMillis(), lastModified);
|
|
||||||
Lock writeLock = lock.writeLock();
|
|
||||||
writeLock.lock();
|
|
||||||
try {
|
|
||||||
index.compute(eTagItem.url, updateEntity(eTagItem));
|
|
||||||
saveETagIndex();
|
|
||||||
} finally {
|
|
||||||
writeLock.unlock();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user