Store model portraits on the server in client/server mode

This commit is contained in:
0xb00bface 2023-04-19 19:48:29 +02:00
parent f293f511f1
commit 39da801a61
14 changed files with 463 additions and 174 deletions

View File

@ -12,6 +12,9 @@ import ctbrec.event.Event;
import ctbrec.event.EventBusHolder;
import ctbrec.event.EventHandler;
import ctbrec.event.EventHandlerConfiguration;
import ctbrec.image.LocalPortraitStore;
import ctbrec.image.PortraitStore;
import ctbrec.image.RemotePortraitStore;
import ctbrec.io.BandwidthMeter;
import ctbrec.io.ByteUnitFormatter;
import ctbrec.io.HttpClient;
@ -94,6 +97,7 @@ public class CamrecApplication extends Application {
private final TabPane tabPane = new TabPane();
private final List<Site> sites = new ArrayList<>();
public static HttpClient httpClient;
public static PortraitStore portraitStore;
public static String title;
private Stage primaryStage;
private RecordingsTab recordingsTab;
@ -122,12 +126,21 @@ public class CamrecApplication extends Application {
createRecorder();
initSites();
startOnlineMonitor();
createPortraitStore();
createGui(primaryStage);
checkForUpdates();
registerClipboardListener();
registerTrayIconListener();
}
private void createPortraitStore() {
if (config.getSettings().localRecording) {
portraitStore = new LocalPortraitStore(config);
} else {
portraitStore = new RemotePortraitStore(httpClient, config);
}
}
private void registerTrayIconListener() {
EventBusHolder.BUS.register(new Object() {
@Subscribe

View File

@ -1,23 +1,19 @@
package ctbrec.ui.action;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import javax.imageio.ImageIO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.event.EventBusHolder;
import ctbrec.ui.CamrecApplication;
import javafx.scene.Node;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
public abstract class AbstractPortraitAction {
private static final Logger LOG = LoggerFactory.getLogger(AbstractPortraitAction.class);
public static final String FORMAT = "jpg";
protected Node source;
@ -32,11 +28,11 @@ public abstract class AbstractPortraitAction {
return bimage;
}
protected boolean copyToCacheAsJpg(String portraitId, BufferedImage portrait) throws IOException {
File output = getPortraitFile(portraitId);
Files.createDirectories(output.getParentFile().toPath());
LOG.debug("Writing scaled portrait to {}", output);
return ImageIO.write(portrait, FORMAT, output);
protected boolean store(String modelUrl, BufferedImage portrait) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
ImageIO.write(portrait, FORMAT, bytes);
CamrecApplication.portraitStore.writePortrait(modelUrl, bytes.toByteArray());
return true;
}
protected File getPortraitFile(String portraitId) {
@ -63,7 +59,7 @@ public abstract class AbstractPortraitAction {
protected BufferedImage cropTopAndBottom(BufferedImage img) {
int overlap = img.getHeight() - img.getWidth();
return img.getSubimage(0, overlap/2, img.getWidth(), img.getWidth());
return img.getSubimage(0, overlap / 2, img.getWidth(), img.getWidth());
}
protected void firePortraitChanged() {
@ -71,7 +67,7 @@ public abstract class AbstractPortraitAction {
}
public static class PortraitChangedEvent {
private Model mdl;
private final Model mdl;
public PortraitChangedEvent(Model model) {
this.mdl = model;

View File

@ -1,22 +1,8 @@
package ctbrec.ui.action;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.UUID;
import java.util.function.Consumer;
import javax.imageio.ImageIO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.StringUtil;
import ctbrec.ui.CamrecApplication;
import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.controls.FileSelectionBox;
import javafx.geometry.Insets;
@ -24,11 +10,20 @@ import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.function.Consumer;
public class SetPortraitAction extends AbstractPortraitAction {
private static final Logger LOG = LoggerFactory.getLogger(SetPortraitAction.class);
private Consumer<Model> callback;
private final Consumer<Model> callback;
public SetPortraitAction(Node source, Model selectedModel, Consumer<Model> callback) {
this.source = source;
@ -38,9 +33,6 @@ public class SetPortraitAction extends AbstractPortraitAction {
public void execute() {
source.setCursor(Cursor.WAIT);
String portraitId = Config.getInstance().getSettings().modelPortraits.getOrDefault(model.getUrl(),
UUID.nameUUIDFromBytes(model.getUrl().getBytes(StandardCharsets.UTF_8)).toString());
GridPane pane = new GridPane();
Label l = new Label("Select a portrait image. Leave empty to remove a portrait again.");
pane.add(l, 0, 0);
@ -56,17 +48,15 @@ public class SetPortraitAction extends AbstractPortraitAction {
String selectedFile = portraitSelectionBox.fileProperty().getValue();
if (StringUtil.isBlank(selectedFile)) {
removePortrait(portraitId);
removePortrait(model.getUrl());
} else {
LOG.debug("User selected {}", selectedFile);
boolean success = processImageFile(portraitId, selectedFile);
boolean success = processImageFile(selectedFile);
if (success) {
Config.getInstance().getSettings().modelPortraits.put(model.getUrl(), portraitId);
try {
Config.getInstance().save();
firePortraitChanged();
runCallback();
} catch (IOException e) {
} catch (Exception e) {
Dialogs.showError("Set Portrait", "Couldn't change portrait image: ", e);
}
}
@ -74,14 +64,10 @@ public class SetPortraitAction extends AbstractPortraitAction {
source.setCursor(Cursor.DEFAULT);
}
private void removePortrait(String portraitId) {
File portraitFile = getPortraitFile(portraitId);
private void removePortrait(String modelUrl) {
try {
if (portraitFile.exists()) {
Files.delete(portraitFile.toPath());
}
Config.getInstance().getSettings().modelPortraits.remove(model.getUrl());
Config.getInstance().save();
CamrecApplication.portraitStore.removePortrait(modelUrl);
firePortraitChanged();
runCallback();
} catch (IOException e) {
Dialogs.showError("Remove Portrait", "Couldn't remove portrait image: ", e);
@ -98,12 +84,12 @@ public class SetPortraitAction extends AbstractPortraitAction {
}
}
private boolean processImageFile(String portraitId, String selectedFile) {
private boolean processImageFile(String selectedFile) {
try {
BufferedImage original = ImageIO.read(new File(selectedFile));
BufferedImage croppedImage = cropImage(original);
BufferedImage portrait = convertToScaledJpg(croppedImage);
boolean success = copyToCacheAsJpg(portraitId, portrait);
boolean success = store(model.getUrl(), portrait);
if (!success) {
LOG.debug("Available formats: {}", Arrays.toString(ImageIO.getWriterFormatNames()));
throw new IOException("No suitable writer found for image format " + FORMAT);

View File

@ -1,25 +1,23 @@
package ctbrec.ui.action;
import java.awt.image.BufferedImage;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.ui.controls.Dialogs;
import javafx.application.Platform;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.image.Image;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.image.BufferedImage;
public class SetThumbAsPortraitAction extends AbstractPortraitAction {
private static final Logger LOG = LoggerFactory.getLogger(SetThumbAsPortraitAction.class);
private Image image;
private final Image image;
public SetThumbAsPortraitAction(Node source, Model model, Image image) {
this.source = source;
@ -29,20 +27,20 @@ public class SetThumbAsPortraitAction extends AbstractPortraitAction {
public void execute() {
source.setCursor(Cursor.WAIT);
try {
BufferedImage bufferedImage = convertFxImageToAwt(image);
BufferedImage croppedImage = cropImage(bufferedImage);
BufferedImage portrait = convertToScaledJpg(croppedImage);
String portraitId = UUID.nameUUIDFromBytes(model.getUrl().getBytes(StandardCharsets.UTF_8)).toString();
copyToCacheAsJpg(portraitId, portrait);
Config.getInstance().getSettings().modelPortraits.put(model.getUrl(), portraitId);
Config.getInstance().save();
firePortraitChanged();
} catch (Exception e) {
LOG.error("Error while changing portrait image", e);
Dialogs.showError("Set Portrait", "Couldn't change portrait image: ", e);
}
source.setCursor(Cursor.DEFAULT);
GlobalThreadPool.submit(() -> {
try {
BufferedImage bufferedImage = convertFxImageToAwt(image);
BufferedImage croppedImage = cropImage(bufferedImage);
BufferedImage portrait = convertToScaledJpg(croppedImage);
store(model.getUrl(), portrait);
firePortraitChanged();
} catch (Exception e) {
LOG.error("Error while changing portrait image", e);
Platform.runLater(() -> Dialogs.showError("Set Portrait", "Couldn't change portrait image: ", e));
} finally {
Platform.runLater(() -> source.setCursor(Cursor.DEFAULT));
}
});
}
private BufferedImage convertFxImageToAwt(Image img) {

View File

@ -4,11 +4,17 @@ import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.eventbus.Subscribe;
import ctbrec.*;
import ctbrec.Config;
import ctbrec.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.StringUtil;
import ctbrec.event.EventBusHolder;
import ctbrec.image.PortraitStore;
import ctbrec.io.HttpException;
import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
import ctbrec.ui.AutosizeAlert;
import ctbrec.ui.CamrecApplication;
import ctbrec.ui.JavaFxModel;
import ctbrec.ui.PreviewPopupHandler;
import ctbrec.ui.action.AbstractPortraitAction.PortraitChangedEvent;
@ -48,9 +54,9 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.stage.FileChooser;
import javafx.util.Callback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.time.Instant;
@ -60,8 +66,8 @@ import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public abstract class AbstractRecordedModelsTab extends Tab implements TabSelectionListener {
private static final Logger LOG = LoggerFactory.getLogger(AbstractRecordedModelsTab.class);
private static final Image SILHOUETTE = new Image(AbstractRecordedModelsTab.class.getResourceAsStream("/silhouette_256.png"));
protected static final String STYLE_ALIGN_CENTER = "-fx-alignment: CENTER;";
@ -100,7 +106,7 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect
AbstractRecordedModelsTab(String text, String stateStorePrefix) {
super(text);
config = Config.getInstance();
portraitStore = new PortraitStore(config);
portraitStore = CamrecApplication.portraitStore;
tableStateStore = new SettingTableViewStateStore(config, stateStorePrefix);
table = new StatePersistingTableView<>(tableStateStore);
registerPortraitListener();
@ -112,6 +118,7 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect
@Subscribe
public void portraitChanged(PortraitChangedEvent e) {
log.debug("Invalidate cache for {}", e.getModel());
portraitCache.invalidate(e.getModel());
if (table != null) {
table.refresh();
@ -230,7 +237,7 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect
} catch (IOException e) {
String msg = "An error occurred while exporting the model list";
Dialogs.showError(getTabPane().getScene(), "Export models", msg, e);
LOG.error(msg, e);
log.error(msg, e);
}
}
}
@ -244,7 +251,7 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect
try {
recorder.addModel(model);
} catch (Exception e) {
LOG.error("Couldn't add model to recording list", e);
log.error("Couldn't add model to recording list", e);
}
}
return null;
@ -269,7 +276,7 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect
} catch (IOException e) {
String msg = "An error occurred while importing the model list";
Dialogs.showError(getTabPane().getScene(), "Import models", msg, e);
LOG.error(msg, e);
log.error(msg, e);
}
}
}
@ -370,14 +377,14 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect
ContextMenu menu = new CustomMouseBehaviorContextMenu();
ModelMenuContributor.newContributor(getTabPane(), Config.getInstance(), recorder) //
.withStartStopCallback(m -> Platform.runLater(this::reload)) //
.removeModelAfterIgnore(true) //
.withPortraitCallback(m -> Platform.runLater(() -> {
portraitCache.invalidate(m);
table.refresh();
}))
.afterwards(() -> Platform.runLater(this::reload))
.contributeToMenu(selectedModels, menu);
.withStartStopCallback(m -> Platform.runLater(this::reload)) //
.removeModelAfterIgnore(true) //
// .withPortraitCallback(m -> Platform.runLater(() -> {
// portraitCache.invalidate(m);
// table.refresh();
// }))
.afterwards(() -> Platform.runLater(this::reload))
.contributeToMenu(selectedModels, menu);
return menu;
}
@ -408,8 +415,8 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect
new MarkForLaterRecordingAction(modelInputField, List.of(newModel), true, recorder).execute(m -> Platform.runLater(this::reload));
} else {
new StartRecordingAction(modelInputField, List.of(newModel), recorder)
.execute()
.whenComplete((r, ex) -> Platform.runLater(this::reload));
.execute()
.whenComplete((r, ex) -> Platform.runLater(this::reload));
}
return;
}
@ -437,8 +444,8 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect
new MarkForLaterRecordingAction(modelInputField, List.of(newModel), true, recorder).execute(m -> Platform.runLater(this::reload));
} else {
new StartRecordingAction(modelInputField, List.of(newModel), recorder)
.execute()
.whenComplete((r, ex) -> Platform.runLater(this::reload));
.execute()
.whenComplete((r, ex) -> Platform.runLater(this::reload));
}
return;
}
@ -558,6 +565,18 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect
}
protected Image loadModelPortrait(Model model) {
return portraitStore.loadModelPortrait(model.getUrl()).orElse(SILHOUETTE);
try {
return portraitStore
.loadModelPortraitByModelUrl(model.getUrl())
.map(bytes -> new Image(new ByteArrayInputStream(bytes)))
.orElse(SILHOUETTE);
} catch (HttpException e) {
if (e.getResponseCode() != 404) {
log.debug("Could not load portrait from server", e);
}
} catch (IOException e) {
log.debug("Could not load portrait from server", e);
}
return SILHOUETTE;
}
}

View File

@ -5,10 +5,10 @@ import com.squareup.moshi.JsonReader.Token;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.io.FileJsonAdapter;
import ctbrec.io.LocalTimeJsonAdapter;
import ctbrec.io.ModelJsonAdapter;
import ctbrec.io.UuidJSonAdapter;
import ctbrec.image.LocalPortraitStore;
import ctbrec.image.PortraitStore;
import ctbrec.image.RemotePortraitStore;
import ctbrec.io.*;
import ctbrec.sites.Site;
import okio.Buffer;
import okio.Okio;
@ -66,14 +66,25 @@ public class ModelImportExport {
}
if (exportOptions.includes().contains(ExportIncludes.PORTRAITS)) {
var portraits = config.getSettings().modelPortraits;
var portraitLoader = new PortraitStore(config);
PortraitStore portraitLoader;
if (config.getSettings().localRecording) {
portraitLoader = new LocalPortraitStore(config);
} else {
var httpClient = new HttpClient("camrec", config) {
@Override
public boolean login() {
return false;
}
};
portraitLoader = new RemotePortraitStore(httpClient, config);
}
if (portraits != null && !portraits.isEmpty()) {
writer.name("portraits");
writer.beginArray();
for (Map.Entry<String, String> entry : config.getSettings().modelPortraits.entrySet()) {
String modelUrl = entry.getKey();
String portraitId = entry.getValue();
Optional<byte[]> portrait = portraitLoader.loadModelPortraitFile(modelUrl);
Optional<byte[]> portrait = portraitLoader.loadModelPortraitByModelUrl(modelUrl);
if (portrait.isPresent()) {
writer.beginObject();
writer.name("url").value(modelUrl);
@ -122,7 +133,18 @@ public class ModelImportExport {
}
private static void importPortraits(JsonReader reader, Config config) throws IOException {
PortraitStore portraitStore = new PortraitStore(config);
PortraitStore portraitStore;
if (config.getSettings().localRecording) {
portraitStore = new LocalPortraitStore(config);
} else {
var httpClient = new HttpClient("camrec", config) {
@Override
public boolean login() {
return false;
}
};
portraitStore = new RemotePortraitStore(httpClient, config);
}
reader.beginArray();
while (reader.hasNext()) {
reader.beginObject();

View File

@ -1,47 +0,0 @@
package ctbrec.ui.tabs.recorded;
import ctbrec.Config;
import ctbrec.StringUtil;
import javafx.scene.image.Image;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Optional;
import static ctbrec.ui.action.AbstractPortraitAction.FORMAT;
public record PortraitStore(Config config) {
private static final Logger LOG = LoggerFactory.getLogger(PortraitStore.class);
public Optional<Image> loadModelPortrait(String modelUrl) {
return loadModelPortraitFile(modelUrl).map(bytes -> new Image(new ByteArrayInputStream(bytes)));
}
public Optional<byte[]> loadModelPortraitFile(String modelUrl) {
String portraitId = config.getSettings().modelPortraits.get(modelUrl);
if (StringUtil.isNotBlank(portraitId)) {
File configDir = config.getConfigDir();
File portraitDir = new File(configDir, "portraits");
File portraitFile = new File(portraitDir, portraitId + '.' + FORMAT);
try {
return Optional.of(Files.readAllBytes(portraitFile.toPath()));
} catch (IOException e) {
LOG.error("Couldn't load portrait file {}", portraitFile, e);
}
}
return Optional.empty();
}
public void writePortrait(String id, byte[] data) throws IOException {
File configDir = config.getConfigDir();
File portraitDir = new File(configDir, "portraits");
File portraitFile = new File(portraitDir, id + '.' + FORMAT);
Files.createDirectories(portraitDir.toPath());
Files.write(portraitFile.toPath(), data);
}
}

View File

@ -0,0 +1,63 @@
package ctbrec.image;
import ctbrec.Config;
import ctbrec.StringUtil;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Optional;
import java.util.UUID;
import static java.nio.charset.StandardCharsets.UTF_8;
@Slf4j
public record LocalPortraitStore(Config config) implements PortraitStore {
@Override
public Optional<byte[]> loadModelPortraitByModelUrl(String modelUrl) {
String portraitId = config.getSettings().modelPortraits.get(modelUrl);
return loadModelPortraitById(portraitId);
}
@Override
public Optional<byte[]> loadModelPortraitById(String portraitId) {
if (StringUtil.isNotBlank(portraitId)) {
File configDir = config.getConfigDir();
File portraitDir = new File(configDir, "portraits");
File portraitFile = new File(portraitDir, portraitId + '.' + FORMAT);
try {
return Optional.of(Files.readAllBytes(portraitFile.toPath()));
} catch (IOException e) {
log.error("Couldn't load portrait file {}", portraitFile, e);
}
}
return Optional.empty();
}
@Override
public void writePortrait(String modelUrl, byte[] data) throws IOException {
String portraitId = config.getSettings().modelPortraits.getOrDefault(modelUrl, UUID.nameUUIDFromBytes(modelUrl.getBytes(UTF_8)).toString());
File portraitFile = getPortraitFile(portraitId);
Files.write(portraitFile.toPath(), data);
config.getSettings().modelPortraits.put(modelUrl, portraitId);
config.save();
}
@Override
public void removePortrait(String modelUrl) throws IOException {
String portraitId = config.getSettings().modelPortraits.get(modelUrl);
File portraitFile = getPortraitFile(portraitId);
Files.delete(portraitFile.toPath());
config.getSettings().modelPortraits.remove(modelUrl);
config.save();
}
private File getPortraitFile(String portraitId) throws IOException {
File configDir = config.getConfigDir();
File portraitDir = new File(configDir, "portraits");
Files.createDirectories(portraitDir.toPath());
return new File(portraitDir, portraitId + '.' + FORMAT);
}
}

View File

@ -0,0 +1,18 @@
package ctbrec.image;
import java.io.IOException;
import java.util.Optional;
public interface PortraitStore {
String FORMAT = "jpg";
Optional<byte[]> loadModelPortraitById(String id) throws IOException;
Optional<byte[]> loadModelPortraitByModelUrl(String modelUrl) throws IOException;
//void writePortrait(String id, byte[] data) throws IOException;
void writePortrait(String modelUrl, byte[] data) throws IOException;
void removePortrait(String modelUrl) throws IOException;
}

View File

@ -0,0 +1,84 @@
package ctbrec.image;
import ctbrec.Config;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.Optional;
import static ctbrec.io.HttpConstants.MIMETYPE_IMAGE_JPG;
import static java.nio.charset.StandardCharsets.UTF_8;
@Slf4j
@RequiredArgsConstructor
public class RemotePortraitStore implements PortraitStore {
private final HttpClient httpClient;
private final Config config;
private String getEndpoint() {
return config.getServerUrl() + "/image/portrait";
}
private String getModelUrlEndpoint() {
return getEndpoint() + "/url/";
}
@Override
public Optional<byte[]> loadModelPortraitById(String id) throws IOException {
return load(getEndpoint() + '/' + id);
}
@Override
public Optional<byte[]> loadModelPortraitByModelUrl(String modelUrl) throws IOException {
return load(getModelUrlEndpoint() + URLEncoder.encode(modelUrl, UTF_8));
}
private Optional<byte[]> load(String url) throws IOException {
Request req = new Request.Builder()
.url(url)
.build();
try (Response resp = httpClient.execute(req)) {
if (resp.isSuccessful()) {
return Optional.of(resp.body().bytes());
} else {
throw new HttpException(resp.code(), resp.message());
}
}
}
@Override
public void writePortrait(String modelUrl, byte[] data) throws IOException {
RequestBody body = RequestBody.create(data, MediaType.parse(MIMETYPE_IMAGE_JPG));
Request req = new Request.Builder()
.url(getModelUrlEndpoint() + URLEncoder.encode(modelUrl, UTF_8))
.post(body)
.build();
try (Response resp = httpClient.execute(req)) {
if (!resp.isSuccessful()) {
throw new HttpException(resp.code(), resp.message());
}
}
}
@Override
public void removePortrait(String modelUrl) throws IOException {
Request req = new Request.Builder()
.url(getModelUrlEndpoint() + URLEncoder.encode(modelUrl, UTF_8))
.delete()
.build();
try (Response resp = httpClient.execute(req)) {
if (!resp.isSuccessful()) {
throw new HttpException(resp.code(), resp.message());
}
}
}
}

View File

@ -16,6 +16,7 @@ public class HttpConstants {
public static final String FORM_ENCODED = "application/x-www-form-urlencoded; charset=UTF-8";
public static final String KEEP_ALIVE = "keep-alive";
public static final String MIMETYPE_APPLICATION_JSON = "application/json";
public static final String MIMETYPE_IMAGE_JPG = "image/jpeg";
public static final String MIMETYPE_TEXT_HTML = "text/html";
public static final String NO_CACHE = "no-cache";
public static final String ORIGIN = "Origin";

View File

@ -1,21 +1,20 @@
package ctbrec.recorder.server;
import static javax.servlet.http.HttpServletResponse.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import ctbrec.Config;
import ctbrec.Hmac;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Hmac;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
public abstract class AbstractCtbrecServlet extends HttpServlet {
@ -23,17 +22,17 @@ public abstract class AbstractCtbrecServlet extends HttpServlet {
boolean checkAuthentication(HttpServletRequest req, String body) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
boolean authenticated = false;
if(Config.getInstance().getSettings().key != null) {
if (Config.getInstance().getSettings().key != null) {
String reqParamHmac = req.getParameter("hmac");
String httpHeaderHmac = req.getHeader("CTBREC-HMAC");
String hmac = null;
String url = req.getRequestURI();
url = url.substring(getServletContext().getContextPath().length());
if(reqParamHmac != null) {
if (reqParamHmac != null) {
hmac = reqParamHmac;
}
if(httpHeaderHmac != null) {
if (httpHeaderHmac != null) {
hmac = httpHeaderHmac;
}
@ -50,13 +49,19 @@ public abstract class AbstractCtbrecServlet extends HttpServlet {
String body(HttpServletRequest req) throws IOException {
StringBuilder body = new StringBuilder();
BufferedReader br = req.getReader();
String line= null;
while( (line = br.readLine()) != null ) {
String line = null;
while ((line = br.readLine()) != null) {
body.append(line).append("\n");
}
return body.toString().trim();
}
byte[] bodyAsByteArray(HttpServletRequest req) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream(req.getContentLength());
req.getInputStream().transferTo(bos);
return bos.toByteArray();
}
void sendResponse(HttpServletResponse resp, int httpStatus, String message) {
try {
resp.setStatus(httpStatus);

View File

@ -7,6 +7,7 @@ import ctbrec.Version;
import ctbrec.event.EventBusHolder;
import ctbrec.event.EventHandler;
import ctbrec.event.EventHandlerConfiguration;
import ctbrec.image.LocalPortraitStore;
import ctbrec.recorder.NextGenLocalRecorder;
import ctbrec.recorder.OnlineMonitor;
import ctbrec.recorder.Recorder;
@ -59,6 +60,7 @@ import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
public class HttpServer {
private static final int MiB = 1024 * 1024; // NOSONAR
private static final Logger LOG = LoggerFactory.getLogger(HttpServer.class);
private final Recorder recorder;
private final OnlineMonitor onlineMonitor;
@ -188,7 +190,7 @@ public class HttpServer {
sslContextFactory.setTrustStorePassword(keyStorePassword);
try (ServerConnector http = new ServerConnector(server, httpConnectionFactory);
ServerConnector https = new ServerConnector(server, sslContextFactory, httpConnectionFactory)) {
ServerConnector https = new ServerConnector(server, sslContextFactory, httpConnectionFactory)) {
// connector for http
http.setPort(this.config.getSettings().httpPort);
@ -219,6 +221,22 @@ public class HttpServer {
holder = new ServletHolder(hlsServlet);
defaultContext.addServlet(holder, "/hls/*");
LocalPortraitStore portraitStore = new LocalPortraitStore(config);
ImageServlet imageServlet = new ImageServlet(portraitStore, config);
holder = new ServletHolder(imageServlet);
String location;
try {
location = File.createTempFile("upload", "").getParentFile().toString();
} catch (IOException e) {
location = ".";
}
long maxFileSize = 10L * MiB;
long maxRequestSize = 10L * MiB;
int fileSizeThreshold = MiB;
MultipartConfigElement multipartConfig = new MultipartConfigElement(location, maxFileSize, maxRequestSize, fileSizeThreshold);
holder.getRegistration().setMultipartConfig(multipartConfig);
defaultContext.addServlet(holder, ImageServlet.BASE_URL + "/*");
if (this.config.getSettings().webinterface) {
startWebInterface(defaultContext, basicAuthContext);
}
@ -227,9 +245,9 @@ public class HttpServer {
HandlerList handlers = new HandlerList();
if (this.config.getSettings().transportLayerSecurity) {
server.addConnector(https);
handlers.setHandlers(new Handler[] { new SecuredRedirectHandler(), basicAuthContext, defaultContext });
handlers.setHandlers(new Handler[]{new SecuredRedirectHandler(), basicAuthContext, defaultContext});
} else {
handlers.setHandlers(new Handler[] { basicAuthContext, defaultContext });
handlers.setHandlers(new Handler[]{basicAuthContext, defaultContext});
}
server.setHandler(handlers);
@ -255,7 +273,7 @@ public class HttpServer {
ServletHolder holder = new ServletHolder(staticFileServlet);
String staticFileContext = "/static/*";
defaultContext.addServlet(holder, staticFileContext);
LOG.info("Register static file servlet under {}", defaultContext.getContextPath()+staticFileContext);
LOG.info("Register static file servlet under {}", defaultContext.getContextPath() + staticFileContext);
// servlet to retrieve the HMAC (secured by basic auth if a hmac key is set in the config)
String username = this.config.getSettings().webinterfaceUsername;
@ -298,7 +316,7 @@ public class HttpServer {
@Override
protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message) throws IOException {
if (code == 404) {
writer.write("<html><head><title>404</title><style>* {font-family: sans-serif}</style></head><body><h1>404</h1><p>Looking for <a href=\""+contextPath+"/static/index.html\">CTB Recorder</a>?</p></body>");
writer.write("<html><head><title>404</title><style>* {font-family: sans-serif}</style></head><body><h1>404</h1><p>Looking for <a href=\"" + contextPath + "/static/index.html\">CTB Recorder</a>?</p></body>");
} else {
super.handleErrorPage(request, writer, code, message);
}
@ -315,7 +333,7 @@ public class HttpServer {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
((HttpServletResponse)response).addHeader("Server", "CTB Recorder/" + getVersion());
((HttpServletResponse) response).addHeader("Server", "CTB Recorder/" + getVersion());
chain.doFilter(request, response);
}
@ -330,14 +348,14 @@ public class HttpServer {
private static SecurityHandler basicAuth(String username, String password) {
String realm = "CTB Recorder";
UserStore userStore = new UserStore();
userStore.addUser(username, Credential.getCredential(password), new String[] { "user" });
userStore.addUser(username, Credential.getCredential(password), new String[]{"user"});
HashLoginService l = new HashLoginService();
l.setUserStore(userStore);
l.setName(realm);
Constraint constraint = new Constraint();
constraint.setName(Constraint.__BASIC_AUTH);
constraint.setRoles(new String[] { "user" });
constraint.setRoles(new String[]{"user"});
constraint.setAuthenticate(true);
ConstraintMapping cm = new ConstraintMapping();

View File

@ -0,0 +1,113 @@
package ctbrec.recorder.server;
import ctbrec.Config;
import ctbrec.image.PortraitStore;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLDecoder;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static ctbrec.io.HttpConstants.MIMETYPE_IMAGE_JPG;
import static java.nio.charset.StandardCharsets.UTF_8;
import static javax.servlet.http.HttpServletResponse.*;
@Slf4j
@RequiredArgsConstructor
public class ImageServlet extends AbstractCtbrecServlet {
public static final String BASE_URL = "/image";
public static final String INTERNAL_SERVER_ERROR = "Internal Server Error";
private static final Pattern URL_PATTERN_PORTRAIT_BY_ID = Pattern.compile(BASE_URL + "/portrait/([0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12})");
private static final Pattern URL_PATTERN_PORTRAIT_BY_URL = Pattern.compile(BASE_URL + "/portrait/url/(.*)");
private final PortraitStore portraitStore;
private final Config config;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
String requestURI = req.getRequestURI();
try {
boolean authenticated = checkAuthentication(req, body(req));
if (!authenticated) {
sendResponse(resp, SC_UNAUTHORIZED, "HMAC does not match");
return;
}
Matcher m;
if ((m = URL_PATTERN_PORTRAIT_BY_ID.matcher(requestURI)).matches()) {
String portraitId = m.group(1);
servePortrait(resp, portraitId);
} else if ((m = URL_PATTERN_PORTRAIT_BY_URL.matcher(requestURI)).matches()) {
String modelUrl = URLDecoder.decode(m.group(1), UTF_8);
String portraitId = config.getSettings().modelPortraits.get(modelUrl);
servePortrait(resp, portraitId);
}
} catch (Exception e) {
log.error(INTERNAL_SERVER_ERROR, e);
sendResponse(resp, SC_INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR);
}
}
private void servePortrait(HttpServletResponse resp, String portraitId) throws IOException {
log.debug("serving portrait {}", portraitId);
Optional<byte[]> imageData = portraitStore.loadModelPortraitById(portraitId);
if (imageData.isPresent()) {
resp.setStatus(SC_OK);
resp.setContentType(MIMETYPE_IMAGE_JPG);
byte[] b = imageData.get();
resp.setContentLength(b.length);
resp.getOutputStream().write(b, 0, b.length);
resp.getOutputStream().flush();
} else {
sendResponse(resp, SC_NOT_FOUND, "Portrait with ID " + portraitId + " not found");
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
String requestURI = req.getRequestURI();
try {
// boolean authenticated = checkAuthentication(req, body(req));
// if (!authenticated) {
// sendResponse(resp, SC_UNAUTHORIZED, "HMAC does not match");
// return;
// }
Matcher m;
if ((m = URL_PATTERN_PORTRAIT_BY_URL.matcher(requestURI)).matches()) {
String modelUrl = URLDecoder.decode(m.group(1), UTF_8);
byte[] data = bodyAsByteArray(req);
portraitStore.writePortrait(modelUrl, data);
}
} catch (Exception e) {
log.error(INTERNAL_SERVER_ERROR, e);
sendResponse(resp, SC_INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR);
}
}
@Override
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) {
String requestURI = req.getRequestURI();
try {
// boolean authenticated = checkAuthentication(req, body(req));
// if (!authenticated) {
// sendResponse(resp, SC_UNAUTHORIZED, "HMAC does not match");
// return;
// }
Matcher m;
if ((m = URL_PATTERN_PORTRAIT_BY_URL.matcher(requestURI)).matches()) {
String modelUrl = URLDecoder.decode(m.group(1), UTF_8);
portraitStore.removePortrait(modelUrl);
}
} catch (Exception e) {
log.error(INTERNAL_SERVER_ERROR, e);
sendResponse(resp, SC_INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR);
}
}
}