mirror of
https://github.com/MarginaliaSearch/MarginaliaSearch.git
synced 2025-02-23 21:18:58 +00:00
Merge pull request 'Memex refactored' (#32) from master into release
Reviewed-on: https://git.marginalia.nu/marginalia/marginalia.nu/pulls/32
This commit is contained in:
commit
e219bd83f3
@ -43,6 +43,22 @@ public abstract class E2ETestBase {
|
|||||||
.withReadTimeout(Duration.ofSeconds(15)))
|
.withReadTimeout(Duration.ofSeconds(15)))
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
public static GenericContainer<?> forService(ServiceDescriptor service, GenericContainer<?> mariaDB, String setupScript) {
|
||||||
|
return new GenericContainer<>("openjdk:17-alpine")
|
||||||
|
.dependsOn(mariaDB)
|
||||||
|
.withCopyFileToContainer(jarFile(), "/WMSA.jar")
|
||||||
|
.withCopyFileToContainer(MountableFile.forClasspathResource(setupScript), "/" + setupScript)
|
||||||
|
.withExposedPorts(service.port)
|
||||||
|
.withFileSystemBind(modelsPath(), "/wmsa/model", BindMode.READ_ONLY)
|
||||||
|
.withNetwork(network)
|
||||||
|
.withNetworkAliases(service.name)
|
||||||
|
.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(service.name)))
|
||||||
|
.withCommand("sh", setupScript, service.name)
|
||||||
|
.waitingFor(Wait.forHttp("/internal/ping")
|
||||||
|
.forPort(service.port)
|
||||||
|
.withReadTimeout(Duration.ofSeconds(15)))
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
public static MountableFile jarFile() {
|
public static MountableFile jarFile() {
|
||||||
Path cwd = Path.of(System.getProperty("user.dir"));
|
Path cwd = Path.of(System.getProperty("user.dir"));
|
||||||
|
@ -0,0 +1,95 @@
|
|||||||
|
package nu.marginalia.wmsa.edge;
|
||||||
|
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import org.junit.jupiter.api.Tag;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mariadb.jdbc.Driver;
|
||||||
|
import org.openqa.selenium.OutputType;
|
||||||
|
import org.openqa.selenium.chrome.ChromeOptions;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.testcontainers.containers.*;
|
||||||
|
import org.testcontainers.containers.output.Slf4jLogConsumer;
|
||||||
|
import org.testcontainers.junit.jupiter.Container;
|
||||||
|
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||||
|
import org.testcontainers.utility.MountableFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static nu.marginalia.wmsa.configuration.ServiceDescriptor.AUTH;
|
||||||
|
import static nu.marginalia.wmsa.configuration.ServiceDescriptor.MEMEX;
|
||||||
|
|
||||||
|
@Tag("e2e")
|
||||||
|
@Testcontainers
|
||||||
|
public class MemexE2ETest extends E2ETestBase {
|
||||||
|
@Container
|
||||||
|
public MariaDBContainer<?> mariaDB = getMariaDBContainer();
|
||||||
|
|
||||||
|
@Container
|
||||||
|
public GenericContainer<?> auth = forService(AUTH, mariaDB);
|
||||||
|
|
||||||
|
@Container
|
||||||
|
public GenericContainer<?> memexContainer = forService(MEMEX, mariaDB, "memex.sh")
|
||||||
|
.withClasspathResourceMapping("/memex", "/memex", BindMode.READ_ONLY);
|
||||||
|
|
||||||
|
@Container
|
||||||
|
public NginxContainer<?> proxyNginx = new NginxContainer<>("nginx:stable")
|
||||||
|
.dependsOn(auth)
|
||||||
|
.dependsOn(memexContainer)
|
||||||
|
.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("nginx")))
|
||||||
|
.withCopyFileToContainer(MountableFile.forClasspathResource("nginx/memex.conf"), "/etc/nginx/conf.d/default.conf")
|
||||||
|
.withNetwork(network)
|
||||||
|
.withNetworkAliases("proxyNginx");
|
||||||
|
|
||||||
|
@Container
|
||||||
|
public BrowserWebDriverContainer<?> chrome = new BrowserWebDriverContainer<>()
|
||||||
|
.withNetwork(network)
|
||||||
|
.withCapabilities(new ChromeOptions());
|
||||||
|
|
||||||
|
private Gson gson = new GsonBuilder().create();
|
||||||
|
private OkHttpClient httpClient = new OkHttpClient.Builder()
|
||||||
|
.connectTimeout(100, TimeUnit.MILLISECONDS)
|
||||||
|
.readTimeout(6000, TimeUnit.SECONDS)
|
||||||
|
.retryOnConnectionFailure(true)
|
||||||
|
.followRedirects(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void run() throws IOException, InterruptedException {
|
||||||
|
Thread.sleep(10_000);
|
||||||
|
new Driver();
|
||||||
|
|
||||||
|
var driver = chrome.getWebDriver();
|
||||||
|
|
||||||
|
driver.get("http://proxyNginx/");
|
||||||
|
Files.move(driver.getScreenshotAs(OutputType.FILE).toPath(), screenshotFilename("frontpage"));
|
||||||
|
|
||||||
|
driver.get("http://proxyNginx/log/");
|
||||||
|
Files.move(driver.getScreenshotAs(OutputType.FILE).toPath(), screenshotFilename("log"));
|
||||||
|
|
||||||
|
driver.get("http://proxyNginx/log/a.gmi");
|
||||||
|
Files.move(driver.getScreenshotAs(OutputType.FILE).toPath(), screenshotFilename("log-a.gmi"));
|
||||||
|
|
||||||
|
driver.get("http://proxyNginx/log/b.gmi");
|
||||||
|
Files.move(driver.getScreenshotAs(OutputType.FILE).toPath(), screenshotFilename("log-b.gmi"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path screenshotFilename(String operation) throws IOException {
|
||||||
|
var path = Path.of(System.getProperty("user.dir")).resolve("build/test/e2e/");
|
||||||
|
Files.createDirectories(path);
|
||||||
|
|
||||||
|
String name = String.format("test-%s-%s.png", operation, LocalDateTime.now());
|
||||||
|
path = path.resolve(name);
|
||||||
|
|
||||||
|
System.out.println("Screenshot in " + path);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -69,4 +69,5 @@ memex memex
|
|||||||
dating dating
|
dating dating
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
echo "*** Starting $1"
|
||||||
WMSA_HOME=${HOME} java -Dsmall-ram=TRUE -Dservice-host=0.0.0.0 -jar /WMSA.jar start $1
|
WMSA_HOME=${HOME} java -Dsmall-ram=TRUE -Dservice-host=0.0.0.0 -jar /WMSA.jar start $1
|
39
marginalia_nu/src/e2e/resources/memex.sh
Normal file
39
marginalia_nu/src/e2e/resources/memex.sh
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
HOME=/wmsa
|
||||||
|
|
||||||
|
mkdir -p ${HOME}/conf
|
||||||
|
|
||||||
|
cat > ${HOME}/conf/db.properties <<EOF
|
||||||
|
db.user=wmsa
|
||||||
|
db.pass=wmsa
|
||||||
|
db.conn=jdbc:mariadb://mariadb:3306/WMSA_prod?rewriteBatchedStatements=true
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > ${HOME}/conf/hosts <<EOF
|
||||||
|
# service-name host-name
|
||||||
|
resource-store resource-store
|
||||||
|
renderer renderer
|
||||||
|
auth auth
|
||||||
|
api api
|
||||||
|
smhi-scraper smhi-scraper
|
||||||
|
podcast-scraper podcast-scraper
|
||||||
|
edge-index edge-index
|
||||||
|
edge-search edge-search
|
||||||
|
encyclopedia encyclopedia
|
||||||
|
edge-assistant edge-assistant
|
||||||
|
memex memex
|
||||||
|
dating dating
|
||||||
|
EOF
|
||||||
|
|
||||||
|
mkdir -p /memex /gmi /html
|
||||||
|
|
||||||
|
echo "*** Starting $1"
|
||||||
|
WMSA_HOME=${HOME} java \
|
||||||
|
-Dmemex-root=/memex\
|
||||||
|
-Dmemex-html-resources=/html\
|
||||||
|
-Dmemex-gmi-resources=/gmi\
|
||||||
|
-Dmemex-disable-git=TRUE\
|
||||||
|
-Dmemex-disable-gemini=TRUE\
|
||||||
|
-Dservice-host=0.0.0.0\
|
||||||
|
-jar /WMSA.jar start $1
|
6
marginalia_nu/src/e2e/resources/memex/index.gmi
Normal file
6
marginalia_nu/src/e2e/resources/memex/index.gmi
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# Memex Index
|
||||||
|
|
||||||
|
Nemo enim ipsam voluptatem, quia voluptas sit, aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos,
|
||||||
|
qui ratione voluptatem sequi nesciunt, neque porro quisquam est, qui dolorem ipsum, quia dolor sit amet consectetur
|
||||||
|
adipiscing velit, sed quia non numquam do eius modi tempora incididunt, ut labore et dolore magnam aliquam quaerat
|
||||||
|
voluptatem.
|
7
marginalia_nu/src/e2e/resources/memex/log/a.gmi
Normal file
7
marginalia_nu/src/e2e/resources/memex/log/a.gmi
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# A
|
||||||
|
|
||||||
|
AAAAAAAAAAA
|
||||||
|
AAAA
|
||||||
|
|
||||||
|
|
||||||
|
AAAAA
|
6
marginalia_nu/src/e2e/resources/memex/log/b.gmi
Normal file
6
marginalia_nu/src/e2e/resources/memex/log/b.gmi
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# B
|
||||||
|
|
||||||
|
BBBB
|
||||||
|
BBBB
|
||||||
|
BBBB
|
||||||
|
BBBB
|
7
marginalia_nu/src/e2e/resources/memex/log/index.gmi
Normal file
7
marginalia_nu/src/e2e/resources/memex/log/index.gmi
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Log
|
||||||
|
|
||||||
|
Log of stuff
|
||||||
|
|
||||||
|
%%%FEED
|
||||||
|
%%%LISTING
|
||||||
|
|
27
marginalia_nu/src/e2e/resources/nginx/memex.conf
Normal file
27
marginalia_nu/src/e2e/resources/nginx/memex.conf
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name nginx;
|
||||||
|
|
||||||
|
location ~* \.(gmi|png)$ {
|
||||||
|
|
||||||
|
types {
|
||||||
|
text/html gmi;
|
||||||
|
text/html png;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy_pass http://memex:5030$uri;
|
||||||
|
}
|
||||||
|
location ~* feed\.xml {
|
||||||
|
types {
|
||||||
|
application/xml+atom xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy_pass http://memex:5030$uri;
|
||||||
|
}
|
||||||
|
location / {
|
||||||
|
proxy_pass http://memex:5030;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -1,164 +1,7 @@
|
|||||||
package nu.marginalia.gemini;
|
package nu.marginalia.gemini;
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
public interface GeminiService {
|
||||||
import com.google.inject.Singleton;
|
String DEFAULT_FILENAME = "index.gmi";
|
||||||
import com.google.inject.name.Named;
|
|
||||||
import nu.marginalia.gemini.io.GeminiConnection;
|
|
||||||
import nu.marginalia.gemini.io.GeminiSSLSetUp;
|
|
||||||
import nu.marginalia.gemini.io.GeminiStatusCode;
|
|
||||||
import nu.marginalia.gemini.io.GeminiUserException;
|
|
||||||
import nu.marginalia.gemini.plugins.BareStaticPagePlugin;
|
|
||||||
import nu.marginalia.gemini.plugins.Plugin;
|
|
||||||
import nu.marginalia.gemini.plugins.SearchPlugin;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.net.ssl.SSLException;
|
|
||||||
import javax.net.ssl.SSLServerSocket;
|
|
||||||
import javax.net.ssl.SSLServerSocketFactory;
|
|
||||||
import javax.net.ssl.SSLSocket;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
public class GeminiService {
|
|
||||||
|
|
||||||
public static final String DEFAULT_FILENAME = "index.gmi";
|
|
||||||
public final Path serverRoot;
|
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger("GeminiServer");
|
|
||||||
private final Executor pool = Executors.newFixedThreadPool(32);
|
|
||||||
private final SSLServerSocket serverSocket;
|
|
||||||
|
|
||||||
private final Plugin[] plugins;
|
|
||||||
private final BadBotList badBotList = BadBotList.INSTANCE;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public GeminiService(@Named("gemini-server-root") Path serverRoot,
|
|
||||||
@Named("gemini-server-port") Integer port,
|
|
||||||
GeminiSSLSetUp sslSetUp,
|
|
||||||
BareStaticPagePlugin pagePlugin,
|
|
||||||
SearchPlugin searchPlugin) throws Exception {
|
|
||||||
this.serverRoot = serverRoot;
|
|
||||||
logger.info("Setting up crypto");
|
|
||||||
final SSLServerSocketFactory socketFactory = sslSetUp.getServerSocketFactory();
|
|
||||||
|
|
||||||
serverSocket = (SSLServerSocket) socketFactory.createServerSocket(port /* 1965 */);
|
|
||||||
serverSocket.setEnabledCipherSuites(socketFactory.getSupportedCipherSuites());
|
|
||||||
serverSocket.setEnabledProtocols(new String[] {"TLSv1.3", "TLSv1.2"});
|
|
||||||
|
|
||||||
logger.info("Verifying setup");
|
|
||||||
if (!Files.exists(this.serverRoot)) {
|
|
||||||
logger.error("Could not find SERVER_ROOT {}", this.serverRoot);
|
|
||||||
System.exit(255);
|
|
||||||
}
|
|
||||||
|
|
||||||
plugins = new Plugin[] {
|
|
||||||
pagePlugin,
|
|
||||||
searchPlugin
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public void run() {
|
|
||||||
logger.info("Awaiting connections");
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (; ; ) {
|
|
||||||
SSLSocket connection = (SSLSocket) serverSocket.accept();
|
|
||||||
connection.setSoTimeout(10_000);
|
|
||||||
|
|
||||||
if (!badBotList.isAllowed(connection.getInetAddress())) {
|
|
||||||
connection.close();
|
|
||||||
} else {
|
|
||||||
pool.execute(() -> serve(connection));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (IOException ex) {
|
|
||||||
logger.error("IO Exception in gemini server", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void serve(SSLSocket socket) {
|
|
||||||
final GeminiConnection connection;
|
|
||||||
try {
|
|
||||||
connection = new GeminiConnection(socket);
|
|
||||||
}
|
|
||||||
catch (IOException ex) {
|
|
||||||
logger.error("Failed to create connection object", ex);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
handleRequest(connection);
|
|
||||||
}
|
|
||||||
catch (GeminiUserException ex) {
|
|
||||||
errorResponse(connection, ex.getMessage());
|
|
||||||
}
|
|
||||||
catch (SSLException ex) {
|
|
||||||
logger.error(connection.getAddress() + " SSL error");
|
|
||||||
connection.close();
|
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
errorResponse(connection, "Error");
|
|
||||||
logger.error(connection.getAddress(), ex);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
connection.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void errorResponse(GeminiConnection connection, String message) {
|
|
||||||
if (connection.isConnected()) {
|
|
||||||
try {
|
|
||||||
logger.error("=> " + connection.getAddress(), message);
|
|
||||||
connection.writeStatusLine(GeminiStatusCode.ERROR_PERMANENT, message);
|
|
||||||
}
|
|
||||||
catch (IOException ex) {
|
|
||||||
logger.error("Exception while sending error", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleRequest(GeminiConnection connection) throws Exception {
|
|
||||||
|
|
||||||
final String address = connection.getAddress();
|
|
||||||
logger.info("Connect: " + address);
|
|
||||||
|
|
||||||
final Optional<URI> maybeUri = connection.readUrl();
|
|
||||||
if (maybeUri.isEmpty()) {
|
|
||||||
logger.info("Done: {}", address);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final URI uri = maybeUri.get();
|
|
||||||
logger.info("Request {}", uri);
|
|
||||||
|
|
||||||
if (!uri.getScheme().equals("gemini")) {
|
|
||||||
throw new GeminiUserException("Unsupported protocol");
|
|
||||||
}
|
|
||||||
|
|
||||||
servePage(connection, uri);
|
|
||||||
logger.info("Done: {}", address);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void servePage(GeminiConnection connection, URI url) throws IOException {
|
|
||||||
String path = url.getPath();
|
|
||||||
|
|
||||||
for (Plugin p : plugins) {
|
|
||||||
if (p.serve(url, connection)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error("FileNotFound {}", path);
|
|
||||||
connection.writeStatusLine(GeminiStatusCode.ERROR_TEMPORARY, "No such file");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
void run();
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
package nu.marginalia.gemini;
|
||||||
|
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class GeminiServiceDummy implements GeminiService {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,164 @@
|
|||||||
|
package nu.marginalia.gemini;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import com.google.inject.name.Named;
|
||||||
|
import nu.marginalia.gemini.io.GeminiConnection;
|
||||||
|
import nu.marginalia.gemini.io.GeminiSSLSetUp;
|
||||||
|
import nu.marginalia.gemini.io.GeminiStatusCode;
|
||||||
|
import nu.marginalia.gemini.io.GeminiUserException;
|
||||||
|
import nu.marginalia.gemini.plugins.BareStaticPagePlugin;
|
||||||
|
import nu.marginalia.gemini.plugins.Plugin;
|
||||||
|
import nu.marginalia.gemini.plugins.SearchPlugin;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLException;
|
||||||
|
import javax.net.ssl.SSLServerSocket;
|
||||||
|
import javax.net.ssl.SSLServerSocketFactory;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class GeminiServiceImpl implements GeminiService {
|
||||||
|
|
||||||
|
public final Path serverRoot;
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
|
||||||
|
private final Executor pool = Executors.newFixedThreadPool(32);
|
||||||
|
private final SSLServerSocket serverSocket;
|
||||||
|
|
||||||
|
private final Plugin[] plugins;
|
||||||
|
private final BadBotList badBotList = BadBotList.INSTANCE;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public GeminiServiceImpl(@Named("gemini-server-root") Path serverRoot,
|
||||||
|
@Named("gemini-server-port") Integer port,
|
||||||
|
GeminiSSLSetUp sslSetUp,
|
||||||
|
BareStaticPagePlugin pagePlugin,
|
||||||
|
SearchPlugin searchPlugin) throws Exception {
|
||||||
|
this.serverRoot = serverRoot;
|
||||||
|
logger.info("Setting up crypto");
|
||||||
|
final SSLServerSocketFactory socketFactory = sslSetUp.getServerSocketFactory();
|
||||||
|
|
||||||
|
serverSocket = (SSLServerSocket) socketFactory.createServerSocket(port /* 1965 */);
|
||||||
|
serverSocket.setEnabledCipherSuites(socketFactory.getSupportedCipherSuites());
|
||||||
|
serverSocket.setEnabledProtocols(new String[] {"TLSv1.3", "TLSv1.2"});
|
||||||
|
|
||||||
|
logger.info("Verifying setup");
|
||||||
|
if (!Files.exists(this.serverRoot)) {
|
||||||
|
logger.error("Could not find SERVER_ROOT {}", this.serverRoot);
|
||||||
|
System.exit(255);
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins = new Plugin[] {
|
||||||
|
pagePlugin,
|
||||||
|
searchPlugin
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
logger.info("Awaiting connections");
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (;;) {
|
||||||
|
SSLSocket connection = (SSLSocket) serverSocket.accept();
|
||||||
|
connection.setSoTimeout(10_000);
|
||||||
|
|
||||||
|
if (!badBotList.isAllowed(connection.getInetAddress())) {
|
||||||
|
connection.close();
|
||||||
|
} else {
|
||||||
|
pool.execute(() -> serve(connection));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
logger.error("IO Exception in gemini server", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void serve(SSLSocket socket) {
|
||||||
|
final GeminiConnection connection;
|
||||||
|
try {
|
||||||
|
connection = new GeminiConnection(socket);
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
logger.error("Failed to create connection object", ex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
handleRequest(connection);
|
||||||
|
}
|
||||||
|
catch (GeminiUserException ex) {
|
||||||
|
errorResponse(connection, ex.getMessage());
|
||||||
|
}
|
||||||
|
catch (SSLException ex) {
|
||||||
|
logger.error(connection.getAddress() + " SSL error");
|
||||||
|
connection.close();
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
errorResponse(connection, "Error");
|
||||||
|
logger.error(connection.getAddress(), ex);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
connection.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void errorResponse(GeminiConnection connection, String message) {
|
||||||
|
if (connection.isConnected()) {
|
||||||
|
try {
|
||||||
|
logger.error("=> " + connection.getAddress(), message);
|
||||||
|
connection.writeStatusLine(GeminiStatusCode.ERROR_PERMANENT, message);
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
logger.error("Exception while sending error", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleRequest(GeminiConnection connection) throws Exception {
|
||||||
|
|
||||||
|
final String address = connection.getAddress();
|
||||||
|
logger.info("Connect: " + address);
|
||||||
|
|
||||||
|
final Optional<URI> maybeUri = connection.readUrl();
|
||||||
|
if (maybeUri.isEmpty()) {
|
||||||
|
logger.info("Done: {}", address);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final URI uri = maybeUri.get();
|
||||||
|
logger.info("Request {}", uri);
|
||||||
|
|
||||||
|
if (!uri.getScheme().equals("gemini")) {
|
||||||
|
throw new GeminiUserException("Unsupported protocol");
|
||||||
|
}
|
||||||
|
|
||||||
|
servePage(connection, uri);
|
||||||
|
logger.info("Done: {}", address);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void servePage(GeminiConnection connection, URI url) throws IOException {
|
||||||
|
String path = url.getPath();
|
||||||
|
|
||||||
|
for (Plugin p : plugins) {
|
||||||
|
if (p.serve(url, connection)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error("FileNotFound {}", path);
|
||||||
|
connection.writeStatusLine(GeminiStatusCode.ERROR_TEMPORARY, "No such file");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -2,6 +2,7 @@ package nu.marginalia.gemini.plugins;
|
|||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.name.Named;
|
import com.google.inject.name.Named;
|
||||||
|
import nu.marginalia.gemini.GeminiService;
|
||||||
import nu.marginalia.gemini.io.GeminiConnection;
|
import nu.marginalia.gemini.io.GeminiConnection;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@ -11,8 +12,6 @@ import java.net.URI;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
import static nu.marginalia.gemini.GeminiService.DEFAULT_FILENAME;
|
|
||||||
|
|
||||||
public class BareStaticPagePlugin implements Plugin {
|
public class BareStaticPagePlugin implements Plugin {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||||
@ -43,8 +42,8 @@ public class BareStaticPagePlugin implements Plugin {
|
|||||||
private Path getServerPath(String requestPath) {
|
private Path getServerPath(String requestPath) {
|
||||||
final Path serverPath = Path.of(geminiServerRoot + requestPath);
|
final Path serverPath = Path.of(geminiServerRoot + requestPath);
|
||||||
|
|
||||||
if (Files.isDirectory(serverPath) && Files.isRegularFile(serverPath.resolve(DEFAULT_FILENAME))) {
|
if (Files.isDirectory(serverPath) && Files.isRegularFile(serverPath.resolve(GeminiService.DEFAULT_FILENAME))) {
|
||||||
return serverPath.resolve(DEFAULT_FILENAME);
|
return serverPath.resolve(GeminiService.DEFAULT_FILENAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
return serverPath;
|
return serverPath;
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package nu.marginalia.wmsa.auth;
|
package nu.marginalia.wmsa.auth;
|
||||||
|
|
||||||
import com.github.jknack.handlebars.internal.Files;
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.name.Named;
|
import com.google.inject.name.Named;
|
||||||
import nu.marginalia.wmsa.auth.model.LoginFormModel;
|
import nu.marginalia.wmsa.auth.model.LoginFormModel;
|
||||||
@ -14,11 +13,12 @@ import spark.Request;
|
|||||||
import spark.Response;
|
import spark.Response;
|
||||||
import spark.Spark;
|
import spark.Spark;
|
||||||
|
|
||||||
import java.io.FileReader;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import static spark.Spark.*;
|
import static spark.Spark.*;
|
||||||
|
|
||||||
@ -40,11 +40,8 @@ public class AuthService extends Service {
|
|||||||
|
|
||||||
super(ip, port, initialization, metricsServer);
|
super(ip, port, initialization, metricsServer);
|
||||||
|
|
||||||
try (var is = new FileReader(topSecretPasswordFile.toFile())) {
|
password = initPassword(topSecretPasswordFile);
|
||||||
password = Files.read(is);
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.error("Could not read password from file " + topSecretPasswordFile, e);
|
|
||||||
}
|
|
||||||
loginFormRenderer = rendererFactory.renderer("auth/login");
|
loginFormRenderer = rendererFactory.renderer("auth/login");
|
||||||
|
|
||||||
Spark.path("public/api", () -> {
|
Spark.path("public/api", () -> {
|
||||||
@ -60,6 +57,18 @@ public class AuthService extends Service {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String initPassword(Path topSecretPasswordFile) {
|
||||||
|
if (Files.exists(topSecretPasswordFile)) {
|
||||||
|
try {
|
||||||
|
return Files.readString(topSecretPasswordFile);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Could not read password from file " + topSecretPasswordFile, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.error("Setting random password");
|
||||||
|
return UUID.randomUUID().toString();
|
||||||
|
}
|
||||||
|
|
||||||
private Object loginForm(Request request, Response response) {
|
private Object loginForm(Request request, Response response) {
|
||||||
String redir = Objects.requireNonNull(request.queryParams("redirect"));
|
String redir = Objects.requireNonNull(request.queryParams("redirect"));
|
||||||
String service = Objects.requireNonNull(request.queryParams("service"));
|
String service = Objects.requireNonNull(request.queryParams("service"));
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package nu.marginalia.wmsa.configuration;
|
package nu.marginalia.wmsa.configuration;
|
||||||
|
|
||||||
import nu.marginalia.wmsa.auth.AuthMain;
|
|
||||||
import nu.marginalia.wmsa.api.ApiMain;
|
import nu.marginalia.wmsa.api.ApiMain;
|
||||||
|
import nu.marginalia.wmsa.auth.AuthMain;
|
||||||
import nu.marginalia.wmsa.configuration.command.Command;
|
import nu.marginalia.wmsa.configuration.command.Command;
|
||||||
import nu.marginalia.wmsa.configuration.command.ListCommand;
|
import nu.marginalia.wmsa.configuration.command.ListCommand;
|
||||||
import nu.marginalia.wmsa.configuration.command.StartCommand;
|
import nu.marginalia.wmsa.configuration.command.StartCommand;
|
||||||
@ -35,7 +35,7 @@ public enum ServiceDescriptor {
|
|||||||
EDGE_SEARCH("edge-search", 5023, EdgeSearchMain.class),
|
EDGE_SEARCH("edge-search", 5023, EdgeSearchMain.class),
|
||||||
EDGE_ASSISTANT("edge-assistant", 5025, EdgeAssistantMain.class),
|
EDGE_ASSISTANT("edge-assistant", 5025, EdgeAssistantMain.class),
|
||||||
|
|
||||||
EDGE_MEMEX("memex", 5030, MemexMain.class),
|
MEMEX("memex", 5030, MemexMain.class),
|
||||||
|
|
||||||
ENCYCLOPEDIA("encyclopedia", 5040, EncyclopediaMain.class),
|
ENCYCLOPEDIA("encyclopedia", 5040, EncyclopediaMain.class),
|
||||||
|
|
||||||
@ -79,7 +79,6 @@ public enum ServiceDescriptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String... args) {
|
public static void main(String... args) {
|
||||||
|
|
||||||
MainMapLookup.setMainArguments(args);
|
MainMapLookup.setMainArguments(args);
|
||||||
Map<String, Command> functions = Stream.of(new ListCommand(),
|
Map<String, Command> functions = Stream.of(new ListCommand(),
|
||||||
new StartCommand(),
|
new StartCommand(),
|
||||||
|
@ -16,7 +16,6 @@ public class StartCommand extends Command {
|
|||||||
System.err.println("Usage: start service-descriptor");
|
System.err.println("Usage: start service-descriptor");
|
||||||
System.exit(255);
|
System.exit(255);
|
||||||
}
|
}
|
||||||
|
|
||||||
var mainMethod = getKind(args[1]).mainClass.getMethod("main", String[].class);
|
var mainMethod = getKind(args[1]).mainClass.getMethod("main", String[].class);
|
||||||
String[] args2 = Arrays.copyOfRange(args, 2, args.length);
|
String[] args2 = Arrays.copyOfRange(args, 2, args.length);
|
||||||
mainMethod.invoke(null, (Object) args2);
|
mainMethod.invoke(null, (Object) args2);
|
||||||
|
@ -37,7 +37,7 @@ public class Service {
|
|||||||
|
|
||||||
private static volatile boolean initialized = false;
|
private static volatile boolean initialized = false;
|
||||||
|
|
||||||
public Service(String ip, int port, Initialization initialization, MetricsServer metricsServer) {
|
public Service(String ip, int port, Initialization initialization, MetricsServer metricsServer, Runnable configureStaticFiles) {
|
||||||
this.initialization = initialization;
|
this.initialization = initialization;
|
||||||
|
|
||||||
serviceName = System.getProperty("service-name");
|
serviceName = System.getProperty("service-name");
|
||||||
@ -51,8 +51,7 @@ public class Service {
|
|||||||
|
|
||||||
logger.info("{} Listening to {}:{}", getClass().getSimpleName(), ip == null ? "" : ip, port);
|
logger.info("{} Listening to {}:{}", getClass().getSimpleName(), ip == null ? "" : ip, port);
|
||||||
|
|
||||||
Spark.staticFiles.expireTime(3600);
|
configureStaticFiles.run();
|
||||||
Spark.staticFiles.header("Cache-control", "public");
|
|
||||||
|
|
||||||
Spark.before(this::filterPublicRequests);
|
Spark.before(this::filterPublicRequests);
|
||||||
Spark.before(this::auditRequestIn);
|
Spark.before(this::auditRequestIn);
|
||||||
@ -66,24 +65,35 @@ public class Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Service(String ip, int port, Initialization initialization, MetricsServer metricsServer) {
|
||||||
|
this(ip, port, initialization, metricsServer, () -> {
|
||||||
|
// configureStaticFiles can't be an overridable method in Service because it may
|
||||||
|
// need to depend on parameters to the constructor, and super-constructors
|
||||||
|
// must run first
|
||||||
|
Spark.staticFiles.expireTime(3600);
|
||||||
|
Spark.staticFiles.header("Cache-control", "public");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void filterPublicRequests(Request request, Response response) {
|
private void filterPublicRequests(Request request, Response response) {
|
||||||
if (null != request.headers("X-Public")) {
|
if (null == request.headers("X-Public")) {
|
||||||
|
return;
|
||||||
String context = Optional
|
|
||||||
.ofNullable(request.headers("X-Context"))
|
|
||||||
.orElseGet(request::ip);
|
|
||||||
|
|
||||||
if (!request.pathInfo().startsWith("/public/")) {
|
|
||||||
logger.warn(httpMarker, "External connection to internal API: {} -> {} {}", context, request.requestMethod(), request.pathInfo());
|
|
||||||
Spark.halt(HttpStatus.SC_FORBIDDEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
String url = request.pathInfo();
|
|
||||||
if (request.queryString() != null) {
|
|
||||||
url = url + "?" + request.queryString();
|
|
||||||
}
|
|
||||||
logger.info(httpMarker, "PUBLIC {}: {} {}", Context.fromRequest(request).getIpHash().orElse("?"), request.requestMethod(), url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String context = Optional
|
||||||
|
.ofNullable(request.headers("X-Context"))
|
||||||
|
.orElseGet(request::ip);
|
||||||
|
|
||||||
|
if (!request.pathInfo().startsWith("/public/")) {
|
||||||
|
logger.warn(httpMarker, "External connection to internal API: {} -> {} {}", context, request.requestMethod(), request.pathInfo());
|
||||||
|
Spark.halt(HttpStatus.SC_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = request.pathInfo();
|
||||||
|
if (request.queryString() != null) {
|
||||||
|
url = url + "?" + request.queryString();
|
||||||
|
}
|
||||||
|
logger.info(httpMarker, "PUBLIC {}: {} {}", Context.fromRequest(request).getIpHash().orElse("?"), request.requestMethod(), url);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Object isInitialized(Request request, Response response) {
|
private Object isInitialized(Request request, Response response) {
|
||||||
|
@ -6,9 +6,9 @@ import com.google.inject.name.Named;
|
|||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
import nu.marginalia.gemini.GeminiService;
|
import nu.marginalia.gemini.GeminiService;
|
||||||
import nu.marginalia.gemini.gmi.GemtextDatabase;
|
import nu.marginalia.gemini.gmi.GemtextDatabase;
|
||||||
|
import nu.marginalia.gemini.gmi.GemtextDocument;
|
||||||
import nu.marginalia.util.graphics.dithering.FloydSteinbergDither;
|
import nu.marginalia.util.graphics.dithering.FloydSteinbergDither;
|
||||||
import nu.marginalia.util.graphics.dithering.Palettes;
|
import nu.marginalia.util.graphics.dithering.Palettes;
|
||||||
import nu.marginalia.gemini.gmi.GemtextDocument;
|
|
||||||
import nu.marginalia.wmsa.memex.change.GemtextTombstoneUpdateCaclulator;
|
import nu.marginalia.wmsa.memex.change.GemtextTombstoneUpdateCaclulator;
|
||||||
import nu.marginalia.wmsa.memex.model.MemexImage;
|
import nu.marginalia.wmsa.memex.model.MemexImage;
|
||||||
import nu.marginalia.wmsa.memex.model.MemexNode;
|
import nu.marginalia.wmsa.memex.model.MemexNode;
|
||||||
@ -16,7 +16,7 @@ import nu.marginalia.wmsa.memex.model.MemexNodeUrl;
|
|||||||
import nu.marginalia.wmsa.memex.renderer.MemexRendererers;
|
import nu.marginalia.wmsa.memex.renderer.MemexRendererers;
|
||||||
import nu.marginalia.wmsa.memex.system.MemexFileSystemMonitor;
|
import nu.marginalia.wmsa.memex.system.MemexFileSystemMonitor;
|
||||||
import nu.marginalia.wmsa.memex.system.MemexFileWriter;
|
import nu.marginalia.wmsa.memex.system.MemexFileWriter;
|
||||||
import nu.marginalia.wmsa.memex.system.MemexGitRepo;
|
import nu.marginalia.wmsa.memex.system.git.MemexGitRepo;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -5,23 +5,59 @@ import com.google.inject.Inject;
|
|||||||
import com.google.inject.Provider;
|
import com.google.inject.Provider;
|
||||||
import com.google.inject.name.Named;
|
import com.google.inject.name.Named;
|
||||||
import com.google.inject.name.Names;
|
import com.google.inject.name.Names;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import nu.marginalia.gemini.GeminiService;
|
||||||
|
import nu.marginalia.gemini.GeminiServiceDummy;
|
||||||
|
import nu.marginalia.gemini.GeminiServiceImpl;
|
||||||
import nu.marginalia.wmsa.memex.system.MemexFileWriter;
|
import nu.marginalia.wmsa.memex.system.MemexFileWriter;
|
||||||
|
import nu.marginalia.wmsa.memex.system.git.MemexGitRepo;
|
||||||
|
import nu.marginalia.wmsa.memex.system.git.MemexGitRepoDummy;
|
||||||
|
import nu.marginalia.wmsa.memex.system.git.MemexGitRepoImpl;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
public class MemexConfigurationModule extends AbstractModule {
|
public class MemexConfigurationModule extends AbstractModule {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(MemexConfigurationModule.class);
|
||||||
|
|
||||||
|
private static final String MEMEX_ROOT_PROPERTY = System.getProperty("memex-root", "/var/lib/wmsa/memex");
|
||||||
|
private static final String MEMEX_HTML_PROPERTY = System.getProperty("memex-html-resources", "/var/lib/wmsa/memex-html");
|
||||||
|
private static final String MEMEX_GMI_PROPERTY = System.getProperty("memex-gmi-resources", "/var/lib/wmsa/memex-gmi");
|
||||||
|
|
||||||
|
private static final boolean MEMEX_DISABLE_GIT = Boolean.getBoolean("memex-disable-git");
|
||||||
|
private static final boolean MEMEX_DISABLE_GEMINI = Boolean.getBoolean("memex-disable-gemini");
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
public MemexConfigurationModule() {
|
||||||
|
Thread.sleep(100);
|
||||||
|
}
|
||||||
|
|
||||||
public void configure() {
|
public void configure() {
|
||||||
bind(Path.class).annotatedWith(Names.named("memex-root")).toInstance(Path.of("/var/lib/wmsa/memex"));
|
bind(Path.class).annotatedWith(Names.named("memex-root")).toInstance(Path.of(MEMEX_ROOT_PROPERTY));
|
||||||
bind(Path.class).annotatedWith(Names.named("memex-html-resources")).toInstance(Path.of("/var/lib/wmsa/memex-html"));
|
bind(Path.class).annotatedWith(Names.named("memex-html-resources")).toInstance(Path.of(MEMEX_HTML_PROPERTY));
|
||||||
bind(Path.class).annotatedWith(Names.named("memex-gmi-resources")).toInstance(Path.of("/var/lib/wmsa/memex-gmi"));
|
bind(Path.class).annotatedWith(Names.named("memex-gmi-resources")).toInstance(Path.of(MEMEX_GMI_PROPERTY));
|
||||||
|
|
||||||
bind(String.class).annotatedWith(Names.named("tombestone-special-file")).toInstance("/special/tombstone.gmi");
|
bind(String.class).annotatedWith(Names.named("tombestone-special-file")).toInstance("/special/tombstone.gmi");
|
||||||
bind(String.class).annotatedWith(Names.named("redirects-special-file")).toInstance("/special/redirect.gmi");
|
bind(String.class).annotatedWith(Names.named("redirects-special-file")).toInstance("/special/redirect.gmi");
|
||||||
|
|
||||||
|
switchImpl(MemexGitRepo.class, MEMEX_DISABLE_GIT, MemexGitRepoDummy.class, MemexGitRepoImpl.class);
|
||||||
|
switchImpl(GeminiService.class, MEMEX_DISABLE_GEMINI, GeminiServiceDummy.class, GeminiServiceImpl.class);
|
||||||
|
|
||||||
bind(MemexFileWriter.class).annotatedWith(Names.named("html")).toProvider(MemexHtmlWriterProvider.class);
|
bind(MemexFileWriter.class).annotatedWith(Names.named("html")).toProvider(MemexHtmlWriterProvider.class);
|
||||||
bind(MemexFileWriter.class).annotatedWith(Names.named("gmi")).toProvider(MemexGmiWriterProvider.class);
|
bind(MemexFileWriter.class).annotatedWith(Names.named("gmi")).toProvider(MemexGmiWriterProvider.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<T> void switchImpl(Class<T> impl, boolean param, Class<? extends T> ifEnabled, Class<? extends T> ifDisabled) {
|
||||||
|
final Class<? extends T> choice;
|
||||||
|
if (param) {
|
||||||
|
choice = ifEnabled;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
choice = ifDisabled;
|
||||||
|
}
|
||||||
|
bind(impl).to(choice).asEagerSingleton();
|
||||||
|
}
|
||||||
|
|
||||||
public static class MemexHtmlWriterProvider implements Provider<MemexFileWriter> {
|
public static class MemexHtmlWriterProvider implements Provider<MemexFileWriter> {
|
||||||
private final Path path;
|
private final Path path;
|
||||||
|
@ -18,7 +18,7 @@ public class MemexMain extends MainClass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String... args) {
|
public static void main(String... args) {
|
||||||
init(ServiceDescriptor.EDGE_MEMEX, args);
|
init(ServiceDescriptor.MEMEX, args);
|
||||||
|
|
||||||
Injector injector = Guice.createInjector(
|
Injector injector = Guice.createInjector(
|
||||||
new MemexConfigurationModule(),
|
new MemexConfigurationModule(),
|
||||||
|
@ -3,6 +3,7 @@ package nu.marginalia.wmsa.memex;
|
|||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.name.Named;
|
import com.google.inject.name.Named;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
|
import nu.marginalia.gemini.gmi.GemtextDocument;
|
||||||
import nu.marginalia.gemini.gmi.renderer.GemtextRendererFactory;
|
import nu.marginalia.gemini.gmi.renderer.GemtextRendererFactory;
|
||||||
import nu.marginalia.wmsa.auth.client.AuthClient;
|
import nu.marginalia.wmsa.auth.client.AuthClient;
|
||||||
import nu.marginalia.wmsa.configuration.server.Context;
|
import nu.marginalia.wmsa.configuration.server.Context;
|
||||||
@ -10,12 +11,11 @@ import nu.marginalia.wmsa.configuration.server.Initialization;
|
|||||||
import nu.marginalia.wmsa.configuration.server.MetricsServer;
|
import nu.marginalia.wmsa.configuration.server.MetricsServer;
|
||||||
import nu.marginalia.wmsa.configuration.server.Service;
|
import nu.marginalia.wmsa.configuration.server.Service;
|
||||||
import nu.marginalia.wmsa.memex.change.GemtextMutation;
|
import nu.marginalia.wmsa.memex.change.GemtextMutation;
|
||||||
import nu.marginalia.gemini.gmi.GemtextDocument;
|
|
||||||
import nu.marginalia.wmsa.memex.change.update.GemtextDocumentUpdateCalculator;
|
import nu.marginalia.wmsa.memex.change.update.GemtextDocumentUpdateCalculator;
|
||||||
import nu.marginalia.wmsa.memex.renderer.MemexHtmlRenderer;
|
|
||||||
import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId;
|
import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId;
|
||||||
import nu.marginalia.wmsa.memex.model.MemexNodeUrl;
|
import nu.marginalia.wmsa.memex.model.MemexNodeUrl;
|
||||||
import nu.marginalia.wmsa.memex.model.render.*;
|
import nu.marginalia.wmsa.memex.model.render.*;
|
||||||
|
import nu.marginalia.wmsa.memex.renderer.MemexHtmlRenderer;
|
||||||
import org.apache.http.HttpStatus;
|
import org.apache.http.HttpStatus;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@ -49,9 +49,18 @@ public class MemexService extends Service {
|
|||||||
MemexHtmlRenderer renderer,
|
MemexHtmlRenderer renderer,
|
||||||
AuthClient authClient,
|
AuthClient authClient,
|
||||||
Initialization initialization,
|
Initialization initialization,
|
||||||
MetricsServer metricsServer) {
|
MetricsServer metricsServer,
|
||||||
|
@Named("memex-html-resources") Path memexHtmlDir
|
||||||
|
) {
|
||||||
|
|
||||||
super(ip, port, initialization, metricsServer);
|
super(ip, port, initialization, metricsServer, () -> {
|
||||||
|
staticFiles.externalLocation(memexHtmlDir.toString());
|
||||||
|
staticFiles.disableMimeTypeGuessing();
|
||||||
|
staticFiles.registerMimeType("gmi", "text/html");
|
||||||
|
staticFiles.registerMimeType("png", "text/html");
|
||||||
|
staticFiles.expireTime(60);
|
||||||
|
staticFiles.header("Cache-control", "public,proxy-revalidate");
|
||||||
|
});
|
||||||
|
|
||||||
this.updateCalculator = updateCalculator;
|
this.updateCalculator = updateCalculator;
|
||||||
this.memex = memex;
|
this.memex = memex;
|
||||||
|
@ -8,7 +8,7 @@ import nu.marginalia.wmsa.configuration.ServiceDescriptor;
|
|||||||
public class MemexApiClient extends AbstractDynamicClient {
|
public class MemexApiClient extends AbstractDynamicClient {
|
||||||
@Inject
|
@Inject
|
||||||
public MemexApiClient() {
|
public MemexApiClient() {
|
||||||
super(ServiceDescriptor.EDGE_MEMEX);
|
super(ServiceDescriptor.MEMEX);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,15 @@ import com.google.inject.Inject;
|
|||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
import com.google.inject.name.Named;
|
import com.google.inject.name.Named;
|
||||||
import nu.marginalia.wmsa.memex.model.MemexNodeUrl;
|
import nu.marginalia.wmsa.memex.model.MemexNodeUrl;
|
||||||
|
import nu.marginalia.wmsa.memex.system.git.MemexGitRepo;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.*;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class MemexSourceFileSystem {
|
public class MemexSourceFileSystem {
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
package nu.marginalia.wmsa.memex.system.git;
|
||||||
|
|
||||||
|
import nu.marginalia.wmsa.memex.model.MemexNodeUrl;
|
||||||
|
|
||||||
|
public interface MemexGitRepo {
|
||||||
|
void pull();
|
||||||
|
|
||||||
|
void remove(MemexNodeUrl url);
|
||||||
|
|
||||||
|
void add(MemexNodeUrl url);
|
||||||
|
|
||||||
|
void update(MemexNodeUrl url);
|
||||||
|
|
||||||
|
void rename(MemexNodeUrl src, MemexNodeUrl dst);
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package nu.marginalia.wmsa.memex.system.git;
|
||||||
|
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import nu.marginalia.wmsa.memex.model.MemexNodeUrl;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class MemexGitRepoDummy implements MemexGitRepo {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(MemexGitRepoDummy.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void pull() {
|
||||||
|
logger.info("Would perform a pull here");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove(MemexNodeUrl url) {
|
||||||
|
logger.info("Would perform a remove here");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void add(MemexNodeUrl url) {
|
||||||
|
logger.info("Would perform an add here");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(MemexNodeUrl url) {
|
||||||
|
logger.info("Would perform an update here");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void rename(MemexNodeUrl src, MemexNodeUrl dst) {
|
||||||
|
logger.info("Would perform a rename here");
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package nu.marginalia.wmsa.memex.system;
|
package nu.marginalia.wmsa.memex.system.git;
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
@ -10,7 +10,8 @@ import org.eclipse.jgit.api.Git;
|
|||||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
import org.eclipse.jgit.lib.Repository;
|
import org.eclipse.jgit.lib.Repository;
|
||||||
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
|
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
|
||||||
import org.eclipse.jgit.transport.*;
|
import org.eclipse.jgit.transport.JschConfigSessionFactory;
|
||||||
|
import org.eclipse.jgit.transport.SshSessionFactory;
|
||||||
import org.eclipse.jgit.util.FS;
|
import org.eclipse.jgit.util.FS;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@ -19,13 +20,13 @@ import java.io.IOException;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class MemexGitRepo {
|
public class MemexGitRepoImpl implements MemexGitRepo {
|
||||||
|
|
||||||
private final Git git;
|
private final Git git;
|
||||||
private final Logger logger = LoggerFactory.getLogger(MemexGitRepo.class);
|
private final Logger logger = LoggerFactory.getLogger(MemexGitRepoImpl.class);
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public MemexGitRepo(@Named("memex-root") Path root) throws IOException {
|
public MemexGitRepoImpl(@Named("memex-root") Path root) throws IOException {
|
||||||
|
|
||||||
FileRepositoryBuilder repositoryBuilder = new FileRepositoryBuilder();
|
FileRepositoryBuilder repositoryBuilder = new FileRepositoryBuilder();
|
||||||
|
|
||||||
@ -49,6 +50,7 @@ public class MemexGitRepo {
|
|||||||
pull();
|
pull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void pull() {
|
public void pull() {
|
||||||
try {
|
try {
|
||||||
git.pull().call();
|
git.pull().call();
|
||||||
@ -58,6 +60,7 @@ public class MemexGitRepo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void remove(MemexNodeUrl url) {
|
public void remove(MemexNodeUrl url) {
|
||||||
try {
|
try {
|
||||||
git.rm()
|
git.rm()
|
||||||
@ -72,6 +75,7 @@ public class MemexGitRepo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void add(MemexNodeUrl url) {
|
public void add(MemexNodeUrl url) {
|
||||||
try {
|
try {
|
||||||
git.add()
|
git.add()
|
||||||
@ -87,6 +91,7 @@ public class MemexGitRepo {
|
|||||||
logger.error("Git operation failed", ex);
|
logger.error("Git operation failed", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@Override
|
||||||
public void update(MemexNodeUrl url) {
|
public void update(MemexNodeUrl url) {
|
||||||
try {
|
try {
|
||||||
git.add()
|
git.add()
|
||||||
@ -105,6 +110,7 @@ public class MemexGitRepo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
public void rename(MemexNodeUrl src, MemexNodeUrl dst) {
|
public void rename(MemexNodeUrl src, MemexNodeUrl dst) {
|
||||||
try {
|
try {
|
||||||
git.rm().addFilepattern(filePattern(src)).call();
|
git.rm().addFilepattern(filePattern(src)).call();
|
@ -2,16 +2,18 @@ package nu.marginalia.wmsa.memex.change;
|
|||||||
|
|
||||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import nu.marginalia.gemini.GeminiService;
|
import nu.marginalia.gemini.GeminiServiceImpl;
|
||||||
import nu.marginalia.util.test.TestUtil;
|
import nu.marginalia.util.test.TestUtil;
|
||||||
import nu.marginalia.wmsa.memex.*;
|
import nu.marginalia.wmsa.memex.Memex;
|
||||||
|
import nu.marginalia.wmsa.memex.MemexData;
|
||||||
|
import nu.marginalia.wmsa.memex.MemexLoader;
|
||||||
import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId;
|
import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId;
|
||||||
import nu.marginalia.wmsa.memex.model.MemexNodeUrl;
|
import nu.marginalia.wmsa.memex.model.MemexNodeUrl;
|
||||||
import nu.marginalia.wmsa.memex.renderer.MemexRendererers;
|
import nu.marginalia.wmsa.memex.renderer.MemexRendererers;
|
||||||
import nu.marginalia.wmsa.memex.system.MemexFileSystemModifiedTimes;
|
import nu.marginalia.wmsa.memex.system.MemexFileSystemModifiedTimes;
|
||||||
import nu.marginalia.wmsa.memex.system.MemexFileWriter;
|
import nu.marginalia.wmsa.memex.system.MemexFileWriter;
|
||||||
import nu.marginalia.wmsa.memex.system.MemexGitRepo;
|
|
||||||
import nu.marginalia.wmsa.memex.system.MemexSourceFileSystem;
|
import nu.marginalia.wmsa.memex.system.MemexSourceFileSystem;
|
||||||
|
import nu.marginalia.wmsa.memex.system.git.MemexGitRepoImpl;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
@ -61,13 +63,13 @@ class GemtextChangeTest {
|
|||||||
var data = new MemexData();
|
var data = new MemexData();
|
||||||
|
|
||||||
memex = new Memex(data, null,
|
memex = new Memex(data, null,
|
||||||
Mockito.mock(MemexGitRepo.class), new MemexLoader(data, new MemexFileSystemModifiedTimes(),
|
Mockito.mock(MemexGitRepoImpl.class), new MemexLoader(data, new MemexFileSystemModifiedTimes(),
|
||||||
new MemexSourceFileSystem(tempDir, Mockito.mock(MemexGitRepo.class)),
|
new MemexSourceFileSystem(tempDir, Mockito.mock(MemexGitRepoImpl.class)),
|
||||||
tempDir, tombstonePath, redirectPath),
|
tempDir, tombstonePath, redirectPath),
|
||||||
Mockito.mock(MemexFileWriter.class),
|
Mockito.mock(MemexFileWriter.class),
|
||||||
null,
|
null,
|
||||||
Mockito.mock(MemexRendererers.class),
|
Mockito.mock(MemexRendererers.class),
|
||||||
Mockito.mock(GeminiService.class));
|
Mockito.mock(GeminiServiceImpl.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
|
@ -2,18 +2,20 @@ package nu.marginalia.wmsa.memex.change;
|
|||||||
|
|
||||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import nu.marginalia.gemini.GeminiService;
|
import nu.marginalia.gemini.GeminiServiceImpl;
|
||||||
import nu.marginalia.gemini.gmi.GemtextDocument;
|
import nu.marginalia.gemini.gmi.GemtextDocument;
|
||||||
import nu.marginalia.util.test.TestUtil;
|
import nu.marginalia.util.test.TestUtil;
|
||||||
import nu.marginalia.wmsa.memex.*;
|
import nu.marginalia.wmsa.memex.Memex;
|
||||||
|
import nu.marginalia.wmsa.memex.MemexData;
|
||||||
|
import nu.marginalia.wmsa.memex.MemexLoader;
|
||||||
import nu.marginalia.wmsa.memex.change.update.GemtextDocumentUpdateCalculator;
|
import nu.marginalia.wmsa.memex.change.update.GemtextDocumentUpdateCalculator;
|
||||||
import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId;
|
import nu.marginalia.wmsa.memex.model.MemexNodeHeadingId;
|
||||||
import nu.marginalia.wmsa.memex.model.MemexNodeUrl;
|
import nu.marginalia.wmsa.memex.model.MemexNodeUrl;
|
||||||
import nu.marginalia.wmsa.memex.renderer.MemexRendererers;
|
import nu.marginalia.wmsa.memex.renderer.MemexRendererers;
|
||||||
import nu.marginalia.wmsa.memex.system.MemexFileSystemModifiedTimes;
|
import nu.marginalia.wmsa.memex.system.MemexFileSystemModifiedTimes;
|
||||||
import nu.marginalia.wmsa.memex.system.MemexFileWriter;
|
import nu.marginalia.wmsa.memex.system.MemexFileWriter;
|
||||||
import nu.marginalia.wmsa.memex.system.MemexGitRepo;
|
|
||||||
import nu.marginalia.wmsa.memex.system.MemexSourceFileSystem;
|
import nu.marginalia.wmsa.memex.system.MemexSourceFileSystem;
|
||||||
|
import nu.marginalia.wmsa.memex.system.git.MemexGitRepoImpl;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
@ -67,12 +69,12 @@ class GemtextTaskUpdateTest {
|
|||||||
Files.createDirectory(tempDir.resolve("special"));
|
Files.createDirectory(tempDir.resolve("special"));
|
||||||
var data = new MemexData();
|
var data = new MemexData();
|
||||||
|
|
||||||
memex = new Memex(data, null, Mockito.mock(MemexGitRepo.class), new MemexLoader(data, new MemexFileSystemModifiedTimes(),
|
memex = new Memex(data, null, Mockito.mock(MemexGitRepoImpl.class), new MemexLoader(data, new MemexFileSystemModifiedTimes(),
|
||||||
new MemexSourceFileSystem(tempDir, Mockito.mock(MemexGitRepo.class)), tempDir, tombstonePath, redirectPath),
|
new MemexSourceFileSystem(tempDir, Mockito.mock(MemexGitRepoImpl.class)), tempDir, tombstonePath, redirectPath),
|
||||||
Mockito.mock(MemexFileWriter.class),
|
Mockito.mock(MemexFileWriter.class),
|
||||||
null,
|
null,
|
||||||
Mockito.mock(MemexRendererers.class),
|
Mockito.mock(MemexRendererers.class),
|
||||||
Mockito.mock(GeminiService.class));
|
Mockito.mock(GeminiServiceImpl.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
|
@ -2,15 +2,17 @@ package nu.marginalia.wmsa.memex.change;
|
|||||||
|
|
||||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import nu.marginalia.gemini.GeminiService;
|
import nu.marginalia.gemini.GeminiServiceImpl;
|
||||||
import nu.marginalia.util.test.TestUtil;
|
import nu.marginalia.util.test.TestUtil;
|
||||||
import nu.marginalia.wmsa.memex.*;
|
import nu.marginalia.wmsa.memex.Memex;
|
||||||
|
import nu.marginalia.wmsa.memex.MemexData;
|
||||||
|
import nu.marginalia.wmsa.memex.MemexLoader;
|
||||||
import nu.marginalia.wmsa.memex.model.MemexNodeUrl;
|
import nu.marginalia.wmsa.memex.model.MemexNodeUrl;
|
||||||
import nu.marginalia.wmsa.memex.renderer.MemexRendererers;
|
import nu.marginalia.wmsa.memex.renderer.MemexRendererers;
|
||||||
import nu.marginalia.wmsa.memex.system.MemexFileSystemModifiedTimes;
|
import nu.marginalia.wmsa.memex.system.MemexFileSystemModifiedTimes;
|
||||||
import nu.marginalia.wmsa.memex.system.MemexFileWriter;
|
import nu.marginalia.wmsa.memex.system.MemexFileWriter;
|
||||||
import nu.marginalia.wmsa.memex.system.MemexGitRepo;
|
|
||||||
import nu.marginalia.wmsa.memex.system.MemexSourceFileSystem;
|
import nu.marginalia.wmsa.memex.system.MemexSourceFileSystem;
|
||||||
|
import nu.marginalia.wmsa.memex.system.git.MemexGitRepoImpl;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
@ -64,13 +66,13 @@ class GemtextTombstoneUpdateCaclulatorTest {
|
|||||||
var data = new MemexData();
|
var data = new MemexData();
|
||||||
|
|
||||||
memex = new Memex(data, null,
|
memex = new Memex(data, null,
|
||||||
Mockito.mock(MemexGitRepo.class),
|
Mockito.mock(MemexGitRepoImpl.class),
|
||||||
new MemexLoader(data, new MemexFileSystemModifiedTimes(),
|
new MemexLoader(data, new MemexFileSystemModifiedTimes(),
|
||||||
new MemexSourceFileSystem(tempDir, Mockito.mock(MemexGitRepo.class)), tempDir, tombstonePath, redirectPath),
|
new MemexSourceFileSystem(tempDir, Mockito.mock(MemexGitRepoImpl.class)), tempDir, tombstonePath, redirectPath),
|
||||||
Mockito.mock(MemexFileWriter.class),
|
Mockito.mock(MemexFileWriter.class),
|
||||||
updateCaclulator,
|
updateCaclulator,
|
||||||
Mockito.mock(MemexRendererers.class),
|
Mockito.mock(MemexRendererers.class),
|
||||||
Mockito.mock(GeminiService.class));
|
Mockito.mock(GeminiServiceImpl.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
|
Loading…
Reference in New Issue
Block a user