Store model portraits on the server in client/server mode
This commit is contained in:
parent
f293f511f1
commit
39da801a61
|
@ -12,6 +12,9 @@ import ctbrec.event.Event;
|
||||||
import ctbrec.event.EventBusHolder;
|
import ctbrec.event.EventBusHolder;
|
||||||
import ctbrec.event.EventHandler;
|
import ctbrec.event.EventHandler;
|
||||||
import ctbrec.event.EventHandlerConfiguration;
|
import ctbrec.event.EventHandlerConfiguration;
|
||||||
|
import ctbrec.image.LocalPortraitStore;
|
||||||
|
import ctbrec.image.PortraitStore;
|
||||||
|
import ctbrec.image.RemotePortraitStore;
|
||||||
import ctbrec.io.BandwidthMeter;
|
import ctbrec.io.BandwidthMeter;
|
||||||
import ctbrec.io.ByteUnitFormatter;
|
import ctbrec.io.ByteUnitFormatter;
|
||||||
import ctbrec.io.HttpClient;
|
import ctbrec.io.HttpClient;
|
||||||
|
@ -94,6 +97,7 @@ public class CamrecApplication extends Application {
|
||||||
private final TabPane tabPane = new TabPane();
|
private final TabPane tabPane = new TabPane();
|
||||||
private final List<Site> sites = new ArrayList<>();
|
private final List<Site> sites = new ArrayList<>();
|
||||||
public static HttpClient httpClient;
|
public static HttpClient httpClient;
|
||||||
|
public static PortraitStore portraitStore;
|
||||||
public static String title;
|
public static String title;
|
||||||
private Stage primaryStage;
|
private Stage primaryStage;
|
||||||
private RecordingsTab recordingsTab;
|
private RecordingsTab recordingsTab;
|
||||||
|
@ -122,12 +126,21 @@ public class CamrecApplication extends Application {
|
||||||
createRecorder();
|
createRecorder();
|
||||||
initSites();
|
initSites();
|
||||||
startOnlineMonitor();
|
startOnlineMonitor();
|
||||||
|
createPortraitStore();
|
||||||
createGui(primaryStage);
|
createGui(primaryStage);
|
||||||
checkForUpdates();
|
checkForUpdates();
|
||||||
registerClipboardListener();
|
registerClipboardListener();
|
||||||
registerTrayIconListener();
|
registerTrayIconListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void createPortraitStore() {
|
||||||
|
if (config.getSettings().localRecording) {
|
||||||
|
portraitStore = new LocalPortraitStore(config);
|
||||||
|
} else {
|
||||||
|
portraitStore = new RemotePortraitStore(httpClient, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void registerTrayIconListener() {
|
private void registerTrayIconListener() {
|
||||||
EventBusHolder.BUS.register(new Object() {
|
EventBusHolder.BUS.register(new Object() {
|
||||||
@Subscribe
|
@Subscribe
|
||||||
|
|
|
@ -1,23 +1,19 @@
|
||||||
package ctbrec.ui.action;
|
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.Config;
|
||||||
import ctbrec.Model;
|
import ctbrec.Model;
|
||||||
import ctbrec.event.EventBusHolder;
|
import ctbrec.event.EventBusHolder;
|
||||||
|
import ctbrec.ui.CamrecApplication;
|
||||||
import javafx.scene.Node;
|
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 {
|
public abstract class AbstractPortraitAction {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractPortraitAction.class);
|
|
||||||
public static final String FORMAT = "jpg";
|
public static final String FORMAT = "jpg";
|
||||||
|
|
||||||
protected Node source;
|
protected Node source;
|
||||||
|
@ -32,11 +28,11 @@ public abstract class AbstractPortraitAction {
|
||||||
return bimage;
|
return bimage;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean copyToCacheAsJpg(String portraitId, BufferedImage portrait) throws IOException {
|
protected boolean store(String modelUrl, BufferedImage portrait) throws IOException {
|
||||||
File output = getPortraitFile(portraitId);
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
Files.createDirectories(output.getParentFile().toPath());
|
ImageIO.write(portrait, FORMAT, bytes);
|
||||||
LOG.debug("Writing scaled portrait to {}", output);
|
CamrecApplication.portraitStore.writePortrait(modelUrl, bytes.toByteArray());
|
||||||
return ImageIO.write(portrait, FORMAT, output);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected File getPortraitFile(String portraitId) {
|
protected File getPortraitFile(String portraitId) {
|
||||||
|
@ -63,7 +59,7 @@ public abstract class AbstractPortraitAction {
|
||||||
|
|
||||||
protected BufferedImage cropTopAndBottom(BufferedImage img) {
|
protected BufferedImage cropTopAndBottom(BufferedImage img) {
|
||||||
int overlap = img.getHeight() - img.getWidth();
|
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() {
|
protected void firePortraitChanged() {
|
||||||
|
@ -71,7 +67,7 @@ public abstract class AbstractPortraitAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class PortraitChangedEvent {
|
public static class PortraitChangedEvent {
|
||||||
private Model mdl;
|
private final Model mdl;
|
||||||
|
|
||||||
public PortraitChangedEvent(Model model) {
|
public PortraitChangedEvent(Model model) {
|
||||||
this.mdl = model;
|
this.mdl = model;
|
||||||
|
|
|
@ -1,22 +1,8 @@
|
||||||
package ctbrec.ui.action;
|
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.Model;
|
||||||
import ctbrec.StringUtil;
|
import ctbrec.StringUtil;
|
||||||
|
import ctbrec.ui.CamrecApplication;
|
||||||
import ctbrec.ui.controls.Dialogs;
|
import ctbrec.ui.controls.Dialogs;
|
||||||
import ctbrec.ui.controls.FileSelectionBox;
|
import ctbrec.ui.controls.FileSelectionBox;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
|
@ -24,11 +10,20 @@ import javafx.scene.Cursor;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.layout.GridPane;
|
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 {
|
public class SetPortraitAction extends AbstractPortraitAction {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(SetPortraitAction.class);
|
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) {
|
public SetPortraitAction(Node source, Model selectedModel, Consumer<Model> callback) {
|
||||||
this.source = source;
|
this.source = source;
|
||||||
|
@ -38,9 +33,6 @@ public class SetPortraitAction extends AbstractPortraitAction {
|
||||||
|
|
||||||
public void execute() {
|
public void execute() {
|
||||||
source.setCursor(Cursor.WAIT);
|
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();
|
GridPane pane = new GridPane();
|
||||||
Label l = new Label("Select a portrait image. Leave empty to remove a portrait again.");
|
Label l = new Label("Select a portrait image. Leave empty to remove a portrait again.");
|
||||||
pane.add(l, 0, 0);
|
pane.add(l, 0, 0);
|
||||||
|
@ -56,17 +48,15 @@ public class SetPortraitAction extends AbstractPortraitAction {
|
||||||
String selectedFile = portraitSelectionBox.fileProperty().getValue();
|
String selectedFile = portraitSelectionBox.fileProperty().getValue();
|
||||||
|
|
||||||
if (StringUtil.isBlank(selectedFile)) {
|
if (StringUtil.isBlank(selectedFile)) {
|
||||||
removePortrait(portraitId);
|
removePortrait(model.getUrl());
|
||||||
} else {
|
} else {
|
||||||
LOG.debug("User selected {}", selectedFile);
|
LOG.debug("User selected {}", selectedFile);
|
||||||
boolean success = processImageFile(portraitId, selectedFile);
|
boolean success = processImageFile(selectedFile);
|
||||||
if (success) {
|
if (success) {
|
||||||
Config.getInstance().getSettings().modelPortraits.put(model.getUrl(), portraitId);
|
|
||||||
try {
|
try {
|
||||||
Config.getInstance().save();
|
|
||||||
firePortraitChanged();
|
firePortraitChanged();
|
||||||
runCallback();
|
runCallback();
|
||||||
} catch (IOException e) {
|
} catch (Exception e) {
|
||||||
Dialogs.showError("Set Portrait", "Couldn't change portrait image: ", e);
|
Dialogs.showError("Set Portrait", "Couldn't change portrait image: ", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,14 +64,10 @@ public class SetPortraitAction extends AbstractPortraitAction {
|
||||||
source.setCursor(Cursor.DEFAULT);
|
source.setCursor(Cursor.DEFAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removePortrait(String portraitId) {
|
private void removePortrait(String modelUrl) {
|
||||||
File portraitFile = getPortraitFile(portraitId);
|
|
||||||
try {
|
try {
|
||||||
if (portraitFile.exists()) {
|
CamrecApplication.portraitStore.removePortrait(modelUrl);
|
||||||
Files.delete(portraitFile.toPath());
|
firePortraitChanged();
|
||||||
}
|
|
||||||
Config.getInstance().getSettings().modelPortraits.remove(model.getUrl());
|
|
||||||
Config.getInstance().save();
|
|
||||||
runCallback();
|
runCallback();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Dialogs.showError("Remove Portrait", "Couldn't remove portrait image: ", 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 {
|
try {
|
||||||
BufferedImage original = ImageIO.read(new File(selectedFile));
|
BufferedImage original = ImageIO.read(new File(selectedFile));
|
||||||
BufferedImage croppedImage = cropImage(original);
|
BufferedImage croppedImage = cropImage(original);
|
||||||
BufferedImage portrait = convertToScaledJpg(croppedImage);
|
BufferedImage portrait = convertToScaledJpg(croppedImage);
|
||||||
boolean success = copyToCacheAsJpg(portraitId, portrait);
|
boolean success = store(model.getUrl(), portrait);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
LOG.debug("Available formats: {}", Arrays.toString(ImageIO.getWriterFormatNames()));
|
LOG.debug("Available formats: {}", Arrays.toString(ImageIO.getWriterFormatNames()));
|
||||||
throw new IOException("No suitable writer found for image format " + FORMAT);
|
throw new IOException("No suitable writer found for image format " + FORMAT);
|
||||||
|
|
|
@ -1,25 +1,23 @@
|
||||||
package ctbrec.ui.action;
|
package ctbrec.ui.action;
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import ctbrec.GlobalThreadPool;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import ctbrec.Config;
|
|
||||||
import ctbrec.Model;
|
import ctbrec.Model;
|
||||||
import ctbrec.ui.controls.Dialogs;
|
import ctbrec.ui.controls.Dialogs;
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.embed.swing.SwingFXUtils;
|
import javafx.embed.swing.SwingFXUtils;
|
||||||
import javafx.scene.Cursor;
|
import javafx.scene.Cursor;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
|
||||||
public class SetThumbAsPortraitAction extends AbstractPortraitAction {
|
public class SetThumbAsPortraitAction extends AbstractPortraitAction {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(SetThumbAsPortraitAction.class);
|
private static final Logger LOG = LoggerFactory.getLogger(SetThumbAsPortraitAction.class);
|
||||||
|
|
||||||
private Image image;
|
private final Image image;
|
||||||
|
|
||||||
public SetThumbAsPortraitAction(Node source, Model model, Image image) {
|
public SetThumbAsPortraitAction(Node source, Model model, Image image) {
|
||||||
this.source = source;
|
this.source = source;
|
||||||
|
@ -29,20 +27,20 @@ public class SetThumbAsPortraitAction extends AbstractPortraitAction {
|
||||||
|
|
||||||
public void execute() {
|
public void execute() {
|
||||||
source.setCursor(Cursor.WAIT);
|
source.setCursor(Cursor.WAIT);
|
||||||
try {
|
GlobalThreadPool.submit(() -> {
|
||||||
BufferedImage bufferedImage = convertFxImageToAwt(image);
|
try {
|
||||||
BufferedImage croppedImage = cropImage(bufferedImage);
|
BufferedImage bufferedImage = convertFxImageToAwt(image);
|
||||||
BufferedImage portrait = convertToScaledJpg(croppedImage);
|
BufferedImage croppedImage = cropImage(bufferedImage);
|
||||||
String portraitId = UUID.nameUUIDFromBytes(model.getUrl().getBytes(StandardCharsets.UTF_8)).toString();
|
BufferedImage portrait = convertToScaledJpg(croppedImage);
|
||||||
copyToCacheAsJpg(portraitId, portrait);
|
store(model.getUrl(), portrait);
|
||||||
Config.getInstance().getSettings().modelPortraits.put(model.getUrl(), portraitId);
|
firePortraitChanged();
|
||||||
Config.getInstance().save();
|
} catch (Exception e) {
|
||||||
firePortraitChanged();
|
LOG.error("Error while changing portrait image", e);
|
||||||
} catch (Exception e) {
|
Platform.runLater(() -> Dialogs.showError("Set Portrait", "Couldn't change portrait image: ", e));
|
||||||
LOG.error("Error while changing portrait image", e);
|
} finally {
|
||||||
Dialogs.showError("Set Portrait", "Couldn't change portrait image: ", e);
|
Platform.runLater(() -> source.setCursor(Cursor.DEFAULT));
|
||||||
}
|
}
|
||||||
source.setCursor(Cursor.DEFAULT);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private BufferedImage convertFxImageToAwt(Image img) {
|
private BufferedImage convertFxImageToAwt(Image img) {
|
||||||
|
|
|
@ -4,11 +4,17 @@ import com.google.common.cache.CacheBuilder;
|
||||||
import com.google.common.cache.CacheLoader;
|
import com.google.common.cache.CacheLoader;
|
||||||
import com.google.common.cache.LoadingCache;
|
import com.google.common.cache.LoadingCache;
|
||||||
import com.google.common.eventbus.Subscribe;
|
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.event.EventBusHolder;
|
||||||
|
import ctbrec.image.PortraitStore;
|
||||||
|
import ctbrec.io.HttpException;
|
||||||
import ctbrec.recorder.Recorder;
|
import ctbrec.recorder.Recorder;
|
||||||
import ctbrec.sites.Site;
|
import ctbrec.sites.Site;
|
||||||
import ctbrec.ui.AutosizeAlert;
|
import ctbrec.ui.AutosizeAlert;
|
||||||
|
import ctbrec.ui.CamrecApplication;
|
||||||
import ctbrec.ui.JavaFxModel;
|
import ctbrec.ui.JavaFxModel;
|
||||||
import ctbrec.ui.PreviewPopupHandler;
|
import ctbrec.ui.PreviewPopupHandler;
|
||||||
import ctbrec.ui.action.AbstractPortraitAction.PortraitChangedEvent;
|
import ctbrec.ui.action.AbstractPortraitAction.PortraitChangedEvent;
|
||||||
|
@ -48,9 +54,9 @@ import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
import javafx.util.Callback;
|
import javafx.util.Callback;
|
||||||
import org.slf4j.Logger;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
@ -60,8 +66,8 @@ import java.util.Objects;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public abstract class AbstractRecordedModelsTab extends Tab implements TabSelectionListener {
|
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"));
|
private static final Image SILHOUETTE = new Image(AbstractRecordedModelsTab.class.getResourceAsStream("/silhouette_256.png"));
|
||||||
protected static final String STYLE_ALIGN_CENTER = "-fx-alignment: CENTER;";
|
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) {
|
AbstractRecordedModelsTab(String text, String stateStorePrefix) {
|
||||||
super(text);
|
super(text);
|
||||||
config = Config.getInstance();
|
config = Config.getInstance();
|
||||||
portraitStore = new PortraitStore(config);
|
portraitStore = CamrecApplication.portraitStore;
|
||||||
tableStateStore = new SettingTableViewStateStore(config, stateStorePrefix);
|
tableStateStore = new SettingTableViewStateStore(config, stateStorePrefix);
|
||||||
table = new StatePersistingTableView<>(tableStateStore);
|
table = new StatePersistingTableView<>(tableStateStore);
|
||||||
registerPortraitListener();
|
registerPortraitListener();
|
||||||
|
@ -112,6 +118,7 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void portraitChanged(PortraitChangedEvent e) {
|
public void portraitChanged(PortraitChangedEvent e) {
|
||||||
|
log.debug("Invalidate cache for {}", e.getModel());
|
||||||
portraitCache.invalidate(e.getModel());
|
portraitCache.invalidate(e.getModel());
|
||||||
if (table != null) {
|
if (table != null) {
|
||||||
table.refresh();
|
table.refresh();
|
||||||
|
@ -230,7 +237,7 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
String msg = "An error occurred while exporting the model list";
|
String msg = "An error occurred while exporting the model list";
|
||||||
Dialogs.showError(getTabPane().getScene(), "Export models", msg, e);
|
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 {
|
try {
|
||||||
recorder.addModel(model);
|
recorder.addModel(model);
|
||||||
} catch (Exception e) {
|
} 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;
|
return null;
|
||||||
|
@ -269,7 +276,7 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
String msg = "An error occurred while importing the model list";
|
String msg = "An error occurred while importing the model list";
|
||||||
Dialogs.showError(getTabPane().getScene(), "Import models", msg, e);
|
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();
|
ContextMenu menu = new CustomMouseBehaviorContextMenu();
|
||||||
|
|
||||||
ModelMenuContributor.newContributor(getTabPane(), Config.getInstance(), recorder) //
|
ModelMenuContributor.newContributor(getTabPane(), Config.getInstance(), recorder) //
|
||||||
.withStartStopCallback(m -> Platform.runLater(this::reload)) //
|
.withStartStopCallback(m -> Platform.runLater(this::reload)) //
|
||||||
.removeModelAfterIgnore(true) //
|
.removeModelAfterIgnore(true) //
|
||||||
.withPortraitCallback(m -> Platform.runLater(() -> {
|
// .withPortraitCallback(m -> Platform.runLater(() -> {
|
||||||
portraitCache.invalidate(m);
|
// portraitCache.invalidate(m);
|
||||||
table.refresh();
|
// table.refresh();
|
||||||
}))
|
// }))
|
||||||
.afterwards(() -> Platform.runLater(this::reload))
|
.afterwards(() -> Platform.runLater(this::reload))
|
||||||
.contributeToMenu(selectedModels, menu);
|
.contributeToMenu(selectedModels, menu);
|
||||||
|
|
||||||
return 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));
|
new MarkForLaterRecordingAction(modelInputField, List.of(newModel), true, recorder).execute(m -> Platform.runLater(this::reload));
|
||||||
} else {
|
} else {
|
||||||
new StartRecordingAction(modelInputField, List.of(newModel), recorder)
|
new StartRecordingAction(modelInputField, List.of(newModel), recorder)
|
||||||
.execute()
|
.execute()
|
||||||
.whenComplete((r, ex) -> Platform.runLater(this::reload));
|
.whenComplete((r, ex) -> Platform.runLater(this::reload));
|
||||||
}
|
}
|
||||||
return;
|
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));
|
new MarkForLaterRecordingAction(modelInputField, List.of(newModel), true, recorder).execute(m -> Platform.runLater(this::reload));
|
||||||
} else {
|
} else {
|
||||||
new StartRecordingAction(modelInputField, List.of(newModel), recorder)
|
new StartRecordingAction(modelInputField, List.of(newModel), recorder)
|
||||||
.execute()
|
.execute()
|
||||||
.whenComplete((r, ex) -> Platform.runLater(this::reload));
|
.whenComplete((r, ex) -> Platform.runLater(this::reload));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -558,6 +565,18 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Image loadModelPortrait(Model model) {
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,10 @@ import com.squareup.moshi.JsonReader.Token;
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
import ctbrec.Model;
|
import ctbrec.Model;
|
||||||
import ctbrec.ModelGroup;
|
import ctbrec.ModelGroup;
|
||||||
import ctbrec.io.FileJsonAdapter;
|
import ctbrec.image.LocalPortraitStore;
|
||||||
import ctbrec.io.LocalTimeJsonAdapter;
|
import ctbrec.image.PortraitStore;
|
||||||
import ctbrec.io.ModelJsonAdapter;
|
import ctbrec.image.RemotePortraitStore;
|
||||||
import ctbrec.io.UuidJSonAdapter;
|
import ctbrec.io.*;
|
||||||
import ctbrec.sites.Site;
|
import ctbrec.sites.Site;
|
||||||
import okio.Buffer;
|
import okio.Buffer;
|
||||||
import okio.Okio;
|
import okio.Okio;
|
||||||
|
@ -66,14 +66,25 @@ public class ModelImportExport {
|
||||||
}
|
}
|
||||||
if (exportOptions.includes().contains(ExportIncludes.PORTRAITS)) {
|
if (exportOptions.includes().contains(ExportIncludes.PORTRAITS)) {
|
||||||
var portraits = config.getSettings().modelPortraits;
|
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()) {
|
if (portraits != null && !portraits.isEmpty()) {
|
||||||
writer.name("portraits");
|
writer.name("portraits");
|
||||||
writer.beginArray();
|
writer.beginArray();
|
||||||
for (Map.Entry<String, String> entry : config.getSettings().modelPortraits.entrySet()) {
|
for (Map.Entry<String, String> entry : config.getSettings().modelPortraits.entrySet()) {
|
||||||
String modelUrl = entry.getKey();
|
String modelUrl = entry.getKey();
|
||||||
String portraitId = entry.getValue();
|
String portraitId = entry.getValue();
|
||||||
Optional<byte[]> portrait = portraitLoader.loadModelPortraitFile(modelUrl);
|
Optional<byte[]> portrait = portraitLoader.loadModelPortraitByModelUrl(modelUrl);
|
||||||
if (portrait.isPresent()) {
|
if (portrait.isPresent()) {
|
||||||
writer.beginObject();
|
writer.beginObject();
|
||||||
writer.name("url").value(modelUrl);
|
writer.name("url").value(modelUrl);
|
||||||
|
@ -122,7 +133,18 @@ public class ModelImportExport {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void importPortraits(JsonReader reader, Config config) throws IOException {
|
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();
|
reader.beginArray();
|
||||||
while (reader.hasNext()) {
|
while (reader.hasNext()) {
|
||||||
reader.beginObject();
|
reader.beginObject();
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 FORM_ENCODED = "application/x-www-form-urlencoded; charset=UTF-8";
|
||||||
public static final String KEEP_ALIVE = "keep-alive";
|
public static final String KEEP_ALIVE = "keep-alive";
|
||||||
public static final String MIMETYPE_APPLICATION_JSON = "application/json";
|
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 MIMETYPE_TEXT_HTML = "text/html";
|
||||||
public static final String NO_CACHE = "no-cache";
|
public static final String NO_CACHE = "no-cache";
|
||||||
public static final String ORIGIN = "Origin";
|
public static final String ORIGIN = "Origin";
|
||||||
|
|
|
@ -1,21 +1,20 @@
|
||||||
package ctbrec.recorder.server;
|
package ctbrec.recorder.server;
|
||||||
|
|
||||||
import static javax.servlet.http.HttpServletResponse.*;
|
import ctbrec.Config;
|
||||||
|
import ctbrec.Hmac;
|
||||||
import java.io.BufferedReader;
|
import org.slf4j.Logger;
|
||||||
import java.io.IOException;
|
import org.slf4j.LoggerFactory;
|
||||||
import java.security.InvalidKeyException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
|
|
||||||
import javax.servlet.http.HttpServlet;
|
import javax.servlet.http.HttpServlet;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
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 static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import ctbrec.Config;
|
|
||||||
import ctbrec.Hmac;
|
|
||||||
|
|
||||||
public abstract class AbstractCtbrecServlet extends HttpServlet {
|
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 checkAuthentication(HttpServletRequest req, String body) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||||
boolean authenticated = false;
|
boolean authenticated = false;
|
||||||
if(Config.getInstance().getSettings().key != null) {
|
if (Config.getInstance().getSettings().key != null) {
|
||||||
String reqParamHmac = req.getParameter("hmac");
|
String reqParamHmac = req.getParameter("hmac");
|
||||||
String httpHeaderHmac = req.getHeader("CTBREC-HMAC");
|
String httpHeaderHmac = req.getHeader("CTBREC-HMAC");
|
||||||
String hmac = null;
|
String hmac = null;
|
||||||
String url = req.getRequestURI();
|
String url = req.getRequestURI();
|
||||||
url = url.substring(getServletContext().getContextPath().length());
|
url = url.substring(getServletContext().getContextPath().length());
|
||||||
|
|
||||||
if(reqParamHmac != null) {
|
if (reqParamHmac != null) {
|
||||||
hmac = reqParamHmac;
|
hmac = reqParamHmac;
|
||||||
}
|
}
|
||||||
if(httpHeaderHmac != null) {
|
if (httpHeaderHmac != null) {
|
||||||
hmac = httpHeaderHmac;
|
hmac = httpHeaderHmac;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,13 +49,19 @@ public abstract class AbstractCtbrecServlet extends HttpServlet {
|
||||||
String body(HttpServletRequest req) throws IOException {
|
String body(HttpServletRequest req) throws IOException {
|
||||||
StringBuilder body = new StringBuilder();
|
StringBuilder body = new StringBuilder();
|
||||||
BufferedReader br = req.getReader();
|
BufferedReader br = req.getReader();
|
||||||
String line= null;
|
String line = null;
|
||||||
while( (line = br.readLine()) != null ) {
|
while ((line = br.readLine()) != null) {
|
||||||
body.append(line).append("\n");
|
body.append(line).append("\n");
|
||||||
}
|
}
|
||||||
return body.toString().trim();
|
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) {
|
void sendResponse(HttpServletResponse resp, int httpStatus, String message) {
|
||||||
try {
|
try {
|
||||||
resp.setStatus(httpStatus);
|
resp.setStatus(httpStatus);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import ctbrec.Version;
|
||||||
import ctbrec.event.EventBusHolder;
|
import ctbrec.event.EventBusHolder;
|
||||||
import ctbrec.event.EventHandler;
|
import ctbrec.event.EventHandler;
|
||||||
import ctbrec.event.EventHandlerConfiguration;
|
import ctbrec.event.EventHandlerConfiguration;
|
||||||
|
import ctbrec.image.LocalPortraitStore;
|
||||||
import ctbrec.recorder.NextGenLocalRecorder;
|
import ctbrec.recorder.NextGenLocalRecorder;
|
||||||
import ctbrec.recorder.OnlineMonitor;
|
import ctbrec.recorder.OnlineMonitor;
|
||||||
import ctbrec.recorder.Recorder;
|
import ctbrec.recorder.Recorder;
|
||||||
|
@ -59,6 +60,7 @@ import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
|
||||||
|
|
||||||
public class HttpServer {
|
public class HttpServer {
|
||||||
|
|
||||||
|
private static final int MiB = 1024 * 1024; // NOSONAR
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(HttpServer.class);
|
private static final Logger LOG = LoggerFactory.getLogger(HttpServer.class);
|
||||||
private final Recorder recorder;
|
private final Recorder recorder;
|
||||||
private final OnlineMonitor onlineMonitor;
|
private final OnlineMonitor onlineMonitor;
|
||||||
|
@ -188,7 +190,7 @@ public class HttpServer {
|
||||||
sslContextFactory.setTrustStorePassword(keyStorePassword);
|
sslContextFactory.setTrustStorePassword(keyStorePassword);
|
||||||
|
|
||||||
try (ServerConnector http = new ServerConnector(server, httpConnectionFactory);
|
try (ServerConnector http = new ServerConnector(server, httpConnectionFactory);
|
||||||
ServerConnector https = new ServerConnector(server, sslContextFactory, httpConnectionFactory)) {
|
ServerConnector https = new ServerConnector(server, sslContextFactory, httpConnectionFactory)) {
|
||||||
|
|
||||||
// connector for http
|
// connector for http
|
||||||
http.setPort(this.config.getSettings().httpPort);
|
http.setPort(this.config.getSettings().httpPort);
|
||||||
|
@ -219,6 +221,22 @@ public class HttpServer {
|
||||||
holder = new ServletHolder(hlsServlet);
|
holder = new ServletHolder(hlsServlet);
|
||||||
defaultContext.addServlet(holder, "/hls/*");
|
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) {
|
if (this.config.getSettings().webinterface) {
|
||||||
startWebInterface(defaultContext, basicAuthContext);
|
startWebInterface(defaultContext, basicAuthContext);
|
||||||
}
|
}
|
||||||
|
@ -227,9 +245,9 @@ public class HttpServer {
|
||||||
HandlerList handlers = new HandlerList();
|
HandlerList handlers = new HandlerList();
|
||||||
if (this.config.getSettings().transportLayerSecurity) {
|
if (this.config.getSettings().transportLayerSecurity) {
|
||||||
server.addConnector(https);
|
server.addConnector(https);
|
||||||
handlers.setHandlers(new Handler[] { new SecuredRedirectHandler(), basicAuthContext, defaultContext });
|
handlers.setHandlers(new Handler[]{new SecuredRedirectHandler(), basicAuthContext, defaultContext});
|
||||||
} else {
|
} else {
|
||||||
handlers.setHandlers(new Handler[] { basicAuthContext, defaultContext });
|
handlers.setHandlers(new Handler[]{basicAuthContext, defaultContext});
|
||||||
}
|
}
|
||||||
server.setHandler(handlers);
|
server.setHandler(handlers);
|
||||||
|
|
||||||
|
@ -255,7 +273,7 @@ public class HttpServer {
|
||||||
ServletHolder holder = new ServletHolder(staticFileServlet);
|
ServletHolder holder = new ServletHolder(staticFileServlet);
|
||||||
String staticFileContext = "/static/*";
|
String staticFileContext = "/static/*";
|
||||||
defaultContext.addServlet(holder, staticFileContext);
|
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)
|
// servlet to retrieve the HMAC (secured by basic auth if a hmac key is set in the config)
|
||||||
String username = this.config.getSettings().webinterfaceUsername;
|
String username = this.config.getSettings().webinterfaceUsername;
|
||||||
|
@ -298,7 +316,7 @@ public class HttpServer {
|
||||||
@Override
|
@Override
|
||||||
protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message) throws IOException {
|
protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message) throws IOException {
|
||||||
if (code == 404) {
|
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 {
|
} else {
|
||||||
super.handleErrorPage(request, writer, code, message);
|
super.handleErrorPage(request, writer, code, message);
|
||||||
}
|
}
|
||||||
|
@ -315,7 +333,7 @@ public class HttpServer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
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);
|
chain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -330,14 +348,14 @@ public class HttpServer {
|
||||||
private static SecurityHandler basicAuth(String username, String password) {
|
private static SecurityHandler basicAuth(String username, String password) {
|
||||||
String realm = "CTB Recorder";
|
String realm = "CTB Recorder";
|
||||||
UserStore userStore = new UserStore();
|
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();
|
HashLoginService l = new HashLoginService();
|
||||||
l.setUserStore(userStore);
|
l.setUserStore(userStore);
|
||||||
l.setName(realm);
|
l.setName(realm);
|
||||||
|
|
||||||
Constraint constraint = new Constraint();
|
Constraint constraint = new Constraint();
|
||||||
constraint.setName(Constraint.__BASIC_AUTH);
|
constraint.setName(Constraint.__BASIC_AUTH);
|
||||||
constraint.setRoles(new String[] { "user" });
|
constraint.setRoles(new String[]{"user"});
|
||||||
constraint.setAuthenticate(true);
|
constraint.setAuthenticate(true);
|
||||||
|
|
||||||
ConstraintMapping cm = new ConstraintMapping();
|
ConstraintMapping cm = new ConstraintMapping();
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue