mirror of
https://gitlab.com/mangadex-pub/mangadex_at_home.git
synced 2024-01-19 02:48:37 +00:00
Minor changes + new CLI
This commit is contained in:
parent
398ab05788
commit
9d07c18de1
|
@ -6,15 +6,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Added
|
### Added
|
||||||
|
- [2020-08-11] New CLI for specifying database location, cache folder, and settings [@carbotaniuman].
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
-- [2020-08-11] Change logging defaults [@carbotaniuman].
|
- [2020-08-11] Change logging defaults [@carbotaniuman].
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
- [2020-08-11] Bugs relating to `settings.json` changes [@carbotaniuman].
|
||||||
|
- [2020-08-11] Logs taking up an absurd amount of space [@carbotaniuman].
|
||||||
|
- [2020-08-11] Random crashes for no reason [@carbotaniuman].
|
||||||
|
- [2020-08-11] SQLException is noww properly handled [@carbotaniuman].
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
|
|
10
build.gradle
10
build.gradle
|
@ -1,6 +1,7 @@
|
||||||
plugins {
|
plugins {
|
||||||
id "java"
|
id "java"
|
||||||
id "org.jetbrains.kotlin.jvm" version "1.3.72"
|
id "org.jetbrains.kotlin.jvm" version "1.3.72"
|
||||||
|
id "org.jetbrains.kotlin.kapt" version "1.3.72"
|
||||||
id "application"
|
id "application"
|
||||||
id "com.github.johnrengelman.shadow" version "5.2.0"
|
id "com.github.johnrengelman.shadow" version "5.2.0"
|
||||||
id "com.diffplug.gradle.spotless" version "4.4.0"
|
id "com.diffplug.gradle.spotless" version "4.4.0"
|
||||||
|
@ -45,6 +46,15 @@ dependencies {
|
||||||
|
|
||||||
implementation "com.goterl.lazycode:lazysodium-java:4.3.0"
|
implementation "com.goterl.lazycode:lazysodium-java:4.3.0"
|
||||||
implementation "net.java.dev.jna:jna:5.5.0"
|
implementation "net.java.dev.jna:jna:5.5.0"
|
||||||
|
|
||||||
|
implementation "info.picocli:picocli:4.5.0"
|
||||||
|
kapt "info.picocli:picocli-codegen:4.5.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
kapt {
|
||||||
|
arguments {
|
||||||
|
arg("project", "${project.group}/${project.name}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
|
|
|
@ -19,15 +19,48 @@ along with this MangaDex@Home. If not, see <http://www.gnu.org/licenses/>.
|
||||||
package mdnet.base
|
package mdnet.base
|
||||||
|
|
||||||
import ch.qos.logback.classic.LoggerContext
|
import ch.qos.logback.classic.LoggerContext
|
||||||
|
import java.io.File
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
import mdnet.BuildInfo
|
import mdnet.BuildInfo
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import picocli.CommandLine
|
||||||
|
|
||||||
object Main {
|
object Main {
|
||||||
private val LOGGER = LoggerFactory.getLogger(Main::class.java)
|
private val LOGGER = LoggerFactory.getLogger(Main::class.java)
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
|
CommandLine(ClientArgs()).execute(*args)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dieWithError(e: Throwable): Nothing {
|
||||||
|
LOGGER.error(e) { "Critical Error" }
|
||||||
|
(LoggerFactory.getILoggerFactory() as LoggerContext).stop()
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dieWithError(error: String): Nothing {
|
||||||
|
LOGGER.error { "Critical Error: $error" }
|
||||||
|
|
||||||
|
(LoggerFactory.getILoggerFactory() as LoggerContext).stop()
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CommandLine.Command(name = "java -jar <jar>", usageHelpWidth = 120, version = ["Client Version ${BuildInfo.VERSION} (Build ${Constants.CLIENT_BUILD})"])
|
||||||
|
data class ClientArgs(
|
||||||
|
@field:CommandLine.Option(names = ["-s", "--settings"], defaultValue = "settings.json", paramLabel = "<settings>", description = ["the settings file (default: \${DEFAULT-VALUE})"])
|
||||||
|
var settingsFile: File = File("settings.json"),
|
||||||
|
@field:CommandLine.Option(names = ["-d", "--database"], defaultValue = "cache\${sys:file.separator}data.db", paramLabel = "<settings>", description = ["the database file (default: \${DEFAULT-VALUE})"])
|
||||||
|
var databaseFile: File = File("cache${File.separator}data.db"),
|
||||||
|
@field:CommandLine.Option(names = ["-c", "--cache"], defaultValue = "cache", paramLabel = "<settings>", description = ["the cache folder (default: \${DEFAULT-VALUE})"])
|
||||||
|
var cacheFolder: File = File("cache"),
|
||||||
|
@field:CommandLine.Option(names = ["-h", "--help"], usageHelp = true, description = ["show this help message and exit"])
|
||||||
|
var helpRequested: Boolean = false,
|
||||||
|
@field:CommandLine.Option(names = ["-v", "--version"], versionHelp = true, description = ["show the version message and exit"])
|
||||||
|
var versionRequested: Boolean = false
|
||||||
|
) : Runnable {
|
||||||
|
override fun run() {
|
||||||
println(
|
println(
|
||||||
"Mangadex@Home Client Version ${BuildInfo.VERSION} (Build ${Constants.CLIENT_BUILD}) initializing"
|
"Mangadex@Home Client Version ${BuildInfo.VERSION} (Build ${Constants.CLIENT_BUILD}) initializing"
|
||||||
)
|
)
|
||||||
|
@ -48,31 +81,11 @@ object Main {
|
||||||
along with Mangadex@Home. If not, see <https://www.gnu.org/licenses/>.
|
along with Mangadex@Home. If not, see <https://www.gnu.org/licenses/>.
|
||||||
""".trimIndent())
|
""".trimIndent())
|
||||||
|
|
||||||
var file = "settings.json"
|
val client = MangaDexClient(settingsFile, databaseFile, cacheFolder)
|
||||||
if (args.size == 1) {
|
|
||||||
file = args[0]
|
|
||||||
} else if (args.isNotEmpty()) {
|
|
||||||
dieWithError("Expected one argument: path to config file, or nothing")
|
|
||||||
}
|
|
||||||
|
|
||||||
val client = MangaDexClient(file)
|
|
||||||
Runtime.getRuntime().addShutdownHook(Thread {
|
Runtime.getRuntime().addShutdownHook(Thread {
|
||||||
client.shutdown()
|
client.shutdown()
|
||||||
(LoggerFactory.getILoggerFactory() as LoggerContext).stop()
|
(LoggerFactory.getILoggerFactory() as LoggerContext).stop()
|
||||||
})
|
})
|
||||||
client.runLoop()
|
client.runLoop()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dieWithError(e: Throwable): Nothing {
|
|
||||||
LOGGER.error(e) { "Critical Error" }
|
|
||||||
(LoggerFactory.getILoggerFactory() as LoggerContext).stop()
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dieWithError(error: String): Nothing {
|
|
||||||
LOGGER.error { "Critical Error: $error" }
|
|
||||||
|
|
||||||
(LoggerFactory.getILoggerFactory() as LoggerContext).stop()
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,14 +38,16 @@ import mdnet.base.settings.*
|
||||||
import mdnet.cache.DiskLruCache
|
import mdnet.cache.DiskLruCache
|
||||||
import mdnet.cache.HeaderMismatchException
|
import mdnet.cache.HeaderMismatchException
|
||||||
import org.http4k.server.Http4kServer
|
import org.http4k.server.Http4kServer
|
||||||
|
import org.jetbrains.exposed.sql.Database
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
// Exception class to handle when Client Settings have invalid values
|
// Exception class to handle when Client Settings have invalid values
|
||||||
class ClientSettingsException(message: String) : Exception(message)
|
class ClientSettingsException(message: String) : Exception(message)
|
||||||
|
|
||||||
class MangaDexClient(private val clientSettingsFile: String) {
|
class MangaDexClient(private val settingsFile: File, databaseFile: File, cacheFolder: File) {
|
||||||
// just for scheduling one task, so single-threaded
|
// just for scheduling one task, so single-threaded
|
||||||
private val executor = Executors.newSingleThreadScheduledExecutor()
|
private val executor = Executors.newSingleThreadScheduledExecutor()
|
||||||
|
private val database: Database
|
||||||
private val cache: DiskLruCache
|
private val cache: DiskLruCache
|
||||||
private var settings: ClientSettings
|
private var settings: ClientSettings
|
||||||
|
|
||||||
|
@ -65,9 +67,13 @@ class MangaDexClient(private val clientSettingsFile: String) {
|
||||||
dieWithError(e)
|
dieWithError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOGGER.info { "Client settings loaded: $settings" }
|
||||||
|
|
||||||
|
database = Database.connect("jdbc:sqlite:$databaseFile", "org.sqlite.JDBC")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
cache = DiskLruCache.open(
|
cache = DiskLruCache.open(
|
||||||
File("cache"), 1, 1,
|
cacheFolder, 1, 1,
|
||||||
(settings.maxCacheSizeInMebibytes * 1024 * 1024 * 0.8).toLong() /* MiB to bytes */
|
(settings.maxCacheSizeInMebibytes * 1024 * 1024 * 0.8).toLong() /* MiB to bytes */
|
||||||
)
|
)
|
||||||
} catch (e: HeaderMismatchException) {
|
} catch (e: HeaderMismatchException) {
|
||||||
|
@ -88,6 +94,9 @@ class MangaDexClient(private val clientSettingsFile: String) {
|
||||||
LOGGER.warn(e) { "Reload of ClientSettings failed" }
|
LOGGER.warn(e) { "Reload of ClientSettings failed" }
|
||||||
}
|
}
|
||||||
}, 1, 1, TimeUnit.MINUTES)
|
}, 1, 1, TimeUnit.MINUTES)
|
||||||
|
|
||||||
|
startImageServer()
|
||||||
|
startWebUi()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Precondition: settings must be filled with up-to-date settings and `imageServer` must not be null
|
// Precondition: settings must be filled with up-to-date settings and `imageServer` must not be null
|
||||||
|
@ -96,37 +105,53 @@ class MangaDexClient(private val clientSettingsFile: String) {
|
||||||
val imageServer = requireNotNull(imageServer)
|
val imageServer = requireNotNull(imageServer)
|
||||||
|
|
||||||
if (webUi != null) throw AssertionError()
|
if (webUi != null) throw AssertionError()
|
||||||
|
LOGGER.info { "WebUI starting" }
|
||||||
webUi = getUiServer(webSettings, imageServer.statistics, imageServer.statsMap).also {
|
webUi = getUiServer(webSettings, imageServer.statistics, imageServer.statsMap).also {
|
||||||
it.start()
|
it.start()
|
||||||
}
|
}
|
||||||
LOGGER.info { "WebUI was successfully started" }
|
LOGGER.info { "WebUI started" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Precondition: settings must be filled with up-to-date settings
|
// Precondition: settings must be filled with up-to-date settings
|
||||||
private fun startImageServer() {
|
private fun startImageServer() {
|
||||||
if (imageServer != null) throw AssertionError()
|
if (imageServer != null) throw AssertionError()
|
||||||
imageServer = ServerManager(settings.serverSettings, settings.devSettings, settings.maxCacheSizeInMebibytes, cache).also {
|
LOGGER.info { "Server manager starting" }
|
||||||
|
imageServer = ServerManager(settings.serverSettings, settings.devSettings, settings.maxCacheSizeInMebibytes, cache, database).also {
|
||||||
it.start()
|
it.start()
|
||||||
}
|
}
|
||||||
LOGGER.info { "Server manager was successfully started" }
|
LOGGER.info { "Server manager started" }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopImageServer() {
|
private fun stopImageServer() {
|
||||||
|
LOGGER.info { "Server manager stopping" }
|
||||||
requireNotNull(imageServer).shutdown()
|
requireNotNull(imageServer).shutdown()
|
||||||
LOGGER.info { "Server manager was successfully stopped" }
|
imageServer = null
|
||||||
|
LOGGER.info { "Server manager stopped" }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopWebUi() {
|
private fun stopWebUi() {
|
||||||
|
LOGGER.info { "WebUI stopping" }
|
||||||
requireNotNull(webUi).stop()
|
requireNotNull(webUi).stop()
|
||||||
LOGGER.info { "Server manager was successfully stopped" }
|
webUi = null
|
||||||
|
LOGGER.info { "WebUI stopped" }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shutdown() {
|
fun shutdown() {
|
||||||
LOGGER.info { "Mangadex@Home Client shutting down" }
|
LOGGER.info { "Mangadex@Home Client shutting down" }
|
||||||
stopWebUi()
|
if (webUi != null) {
|
||||||
stopImageServer()
|
stopWebUi()
|
||||||
|
}
|
||||||
|
if (imageServer != null) {
|
||||||
|
stopImageServer()
|
||||||
|
}
|
||||||
LOGGER.info { "Mangadex@Home Client has shut down" }
|
LOGGER.info { "Mangadex@Home Client has shut down" }
|
||||||
|
|
||||||
|
try {
|
||||||
|
cache.close()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
LOGGER.error(e) { "Cache failed to close" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -142,6 +167,7 @@ class MangaDexClient(private val clientSettingsFile: String) {
|
||||||
LOGGER.info { "Client settings unchanged" }
|
LOGGER.info { "Client settings unchanged" }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
LOGGER.info { "New settings loaded: $newSettings" }
|
||||||
|
|
||||||
cache.maxSize = (newSettings.maxCacheSizeInMebibytes * 1024 * 1024 * 0.8).toLong()
|
cache.maxSize = (newSettings.maxCacheSizeInMebibytes * 1024 * 1024 * 0.8).toLong()
|
||||||
|
|
||||||
|
@ -215,7 +241,7 @@ class MangaDexClient(private val clientSettingsFile: String) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun readClientSettings(): ClientSettings {
|
private fun readClientSettings(): ClientSettings {
|
||||||
return JACKSON.readValue<ClientSettings>(FileReader(clientSettingsFile)).apply(::validateSettings)
|
return JACKSON.readValue<ClientSettings>(FileReader(settingsFile)).apply(::validateSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -5,7 +5,6 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature
|
import com.fasterxml.jackson.databind.SerializationFeature
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
import java.io.IOException
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.LinkedHashMap
|
import java.util.LinkedHashMap
|
||||||
|
@ -21,6 +20,7 @@ import mdnet.base.settings.RemoteSettings
|
||||||
import mdnet.base.settings.ServerSettings
|
import mdnet.base.settings.ServerSettings
|
||||||
import mdnet.cache.DiskLruCache
|
import mdnet.cache.DiskLruCache
|
||||||
import org.http4k.server.Http4kServer
|
import org.http4k.server.Http4kServer
|
||||||
|
import org.jetbrains.exposed.sql.Database
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
sealed class State
|
sealed class State
|
||||||
|
@ -33,7 +33,7 @@ data class GracefulStop(val lastRunning: Running, val counts: Int = 0, val nextS
|
||||||
// server is currently running
|
// server is currently running
|
||||||
data class Running(val server: Http4kServer, val settings: RemoteSettings, val serverSettings: ServerSettings, val devSettings: DevSettings) : State()
|
data class Running(val server: Http4kServer, val settings: RemoteSettings, val serverSettings: ServerSettings, val devSettings: DevSettings) : State()
|
||||||
|
|
||||||
class ServerManager(serverSettings: ServerSettings, devSettings: DevSettings, maxCacheSizeInMebibytes: Long, private val cache: DiskLruCache) {
|
class ServerManager(serverSettings: ServerSettings, devSettings: DevSettings, maxCacheSizeInMebibytes: Long, private val cache: DiskLruCache, private val database: Database) {
|
||||||
// this must remain single-threaded because of how the state mechanism works
|
// this must remain single-threaded because of how the state mechanism works
|
||||||
private val executor = Executors.newSingleThreadScheduledExecutor()
|
private val executor = Executors.newSingleThreadScheduledExecutor()
|
||||||
|
|
||||||
|
@ -68,6 +68,7 @@ class ServerManager(serverSettings: ServerSettings, devSettings: DevSettings, ma
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
|
LOGGER.info { "Image server starting" }
|
||||||
loginAndStartServer()
|
loginAndStartServer()
|
||||||
statsMap[Instant.now()] = statistics.get()
|
statsMap[Instant.now()] = statistics.get()
|
||||||
|
|
||||||
|
@ -165,6 +166,8 @@ class ServerManager(serverSettings: ServerSettings, devSettings: DevSettings, ma
|
||||||
LOGGER.warn(e) { "Graceful shutdown checker failed" }
|
LOGGER.warn(e) { "Graceful shutdown checker failed" }
|
||||||
}
|
}
|
||||||
}, 45, 45, TimeUnit.SECONDS)
|
}, 45, 45, TimeUnit.SECONDS)
|
||||||
|
|
||||||
|
LOGGER.info { "Image server has started" }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pingControl() {
|
private fun pingControl() {
|
||||||
|
@ -197,7 +200,7 @@ class ServerManager(serverSettings: ServerSettings, devSettings: DevSettings, ma
|
||||||
|
|
||||||
val remoteSettings = serverHandler.loginToControl()
|
val remoteSettings = serverHandler.loginToControl()
|
||||||
?: Main.dieWithError("Failed to get a login response from server - check API secret for validity")
|
?: Main.dieWithError("Failed to get a login response from server - check API secret for validity")
|
||||||
val server = getServer(cache, remoteSettings, state.serverSettings, statistics, isHandled).start()
|
val server = getServer(cache, database, remoteSettings, state.serverSettings, statistics, isHandled).start()
|
||||||
|
|
||||||
if (remoteSettings.latestBuild > Constants.CLIENT_BUILD) {
|
if (remoteSettings.latestBuild > Constants.CLIENT_BUILD) {
|
||||||
LOGGER.warn {
|
LOGGER.warn {
|
||||||
|
@ -253,12 +256,6 @@ class ServerManager(serverSettings: ServerSettings, devSettings: DevSettings, ma
|
||||||
}, 0, TimeUnit.SECONDS)
|
}, 0, TimeUnit.SECONDS)
|
||||||
latch.await()
|
latch.await()
|
||||||
|
|
||||||
try {
|
|
||||||
cache.close()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
LOGGER.error(e) { "Cache failed to close" }
|
|
||||||
}
|
|
||||||
|
|
||||||
executor.shutdown()
|
executor.shutdown()
|
||||||
LOGGER.info { "Image server has shut down" }
|
LOGGER.info { "Image server has shut down" }
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,9 +57,9 @@ import org.http4k.server.Http4kServer
|
||||||
import org.http4k.server.ServerConfig
|
import org.http4k.server.ServerConfig
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
private val LOGGER = LoggerFactory.getLogger("Application")
|
private val LOGGER = LoggerFactory.getLogger("AppNetty")
|
||||||
|
|
||||||
class Netty(private val tls: TlsCert, internal val serverSettings: ServerSettings, private val statistics: AtomicReference<Statistics>) : ServerConfig {
|
class Netty(private val tls: TlsCert, private val serverSettings: ServerSettings, private val statistics: AtomicReference<Statistics>) : ServerConfig {
|
||||||
override fun toServer(httpHandler: HttpHandler): Http4kServer = object : Http4kServer {
|
override fun toServer(httpHandler: HttpHandler): Http4kServer = object : Http4kServer {
|
||||||
private val masterGroup = NioEventLoopGroup(serverSettings.threads)
|
private val masterGroup = NioEventLoopGroup(serverSettings.threads)
|
||||||
private val workerGroup = NioEventLoopGroup(serverSettings.threads)
|
private val workerGroup = NioEventLoopGroup(serverSettings.threads)
|
||||||
|
|
|
@ -32,7 +32,7 @@ private const val PKCS_1_PEM_FOOTER = "-----END RSA PRIVATE KEY-----"
|
||||||
private const val PKCS_8_PEM_HEADER = "-----BEGIN PRIVATE KEY-----"
|
private const val PKCS_8_PEM_HEADER = "-----BEGIN PRIVATE KEY-----"
|
||||||
private const val PKCS_8_PEM_FOOTER = "-----END PRIVATE KEY-----"
|
private const val PKCS_8_PEM_FOOTER = "-----END PRIVATE KEY-----"
|
||||||
|
|
||||||
internal fun loadKey(keyDataString: String): PrivateKey? {
|
fun loadKey(keyDataString: String): PrivateKey? {
|
||||||
if (keyDataString.contains(PKCS_1_PEM_HEADER)) {
|
if (keyDataString.contains(PKCS_1_PEM_HEADER)) {
|
||||||
val fixedString = keyDataString.replace(PKCS_1_PEM_HEADER, "").replace(
|
val fixedString = keyDataString.replace(PKCS_1_PEM_HEADER, "").replace(
|
||||||
PKCS_1_PEM_FOOTER, "")
|
PKCS_1_PEM_FOOTER, "")
|
||||||
|
|
|
@ -32,7 +32,6 @@ import io.netty.handler.codec.http.HttpServerCodec
|
||||||
import io.netty.handler.codec.http.HttpServerKeepAliveHandler
|
import io.netty.handler.codec.http.HttpServerKeepAliveHandler
|
||||||
import io.netty.handler.stream.ChunkedWriteHandler
|
import io.netty.handler.stream.ChunkedWriteHandler
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import org.http4k.core.HttpHandler
|
import org.http4k.core.HttpHandler
|
||||||
import org.http4k.server.Http4kChannelHandler
|
import org.http4k.server.Http4kChannelHandler
|
||||||
import org.http4k.server.Http4kServer
|
import org.http4k.server.Http4kServer
|
||||||
|
@ -67,9 +66,9 @@ class WebUiNetty(private val hostname: String, private val port: Int) : ServerCo
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() = apply {
|
override fun stop() = apply {
|
||||||
masterGroup.shutdownGracefully(5, 15, TimeUnit.SECONDS).sync()
|
closeFuture.cancel(false)
|
||||||
workerGroup.shutdownGracefully(5, 15, TimeUnit.SECONDS).sync()
|
workerGroup.shutdownGracefully()
|
||||||
closeFuture.sync()
|
masterGroup.shutdownGracefully()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun port(): Int = address.port
|
override fun port(): Int = address.port
|
||||||
|
|
|
@ -66,6 +66,7 @@ import org.http4k.routing.bind
|
||||||
import org.http4k.routing.routes
|
import org.http4k.routing.routes
|
||||||
import org.http4k.server.Http4kServer
|
import org.http4k.server.Http4kServer
|
||||||
import org.http4k.server.asServer
|
import org.http4k.server.asServer
|
||||||
|
import org.jetbrains.exposed.exceptions.ExposedSQLException
|
||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.sql.Database
|
||||||
import org.jetbrains.exposed.sql.SchemaUtils
|
import org.jetbrains.exposed.sql.SchemaUtils
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
@ -250,13 +251,20 @@ class ImageServer(
|
||||||
LOGGER.trace { "Request for $sanitizedUri is being cached and served" }
|
LOGGER.trace { "Request for $sanitizedUri is being cached and served" }
|
||||||
|
|
||||||
if (imageDatum == null) {
|
if (imageDatum == null) {
|
||||||
synchronized(database) {
|
try {
|
||||||
transaction(database) {
|
synchronized(database) {
|
||||||
ImageDatum.new(imageId) {
|
transaction(database) {
|
||||||
this.contentType = contentType
|
ImageDatum.new(imageId) {
|
||||||
this.lastModified = lastModified
|
this.contentType = contentType
|
||||||
|
this.lastModified = lastModified
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (_: ExposedSQLException) {
|
||||||
|
// some other code got to the database first, fall back to just serving
|
||||||
|
editor.abort()
|
||||||
|
LOGGER.trace { "Request for $sanitizedUri is being served" }
|
||||||
|
respondWithImage(mdResponse.body.stream, contentLength, contentType, lastModified, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -331,8 +339,7 @@ class ImageServer(
|
||||||
|
|
||||||
private fun String.isImageMimetype() = this.toLowerCase().startsWith("image/")
|
private fun String.isImageMimetype() = this.toLowerCase().startsWith("image/")
|
||||||
|
|
||||||
fun getServer(cache: DiskLruCache, remoteSettings: RemoteSettings, serverSettings: ServerSettings, statistics: AtomicReference<Statistics>, isHandled: AtomicBoolean): Http4kServer {
|
fun getServer(cache: DiskLruCache, database: Database, remoteSettings: RemoteSettings, serverSettings: ServerSettings, statistics: AtomicReference<Statistics>, isHandled: AtomicBoolean): Http4kServer {
|
||||||
val database = Database.connect("jdbc:sqlite:cache/data.db", "org.sqlite.JDBC")
|
|
||||||
val client = ApacheClient(responseBodyMode = BodyMode.Stream, client = HttpClients.custom()
|
val client = ApacheClient(responseBodyMode = BodyMode.Stream, client = HttpClients.custom()
|
||||||
.disableConnectionState()
|
.disableConnectionState()
|
||||||
.setDefaultRequestConfig(
|
.setDefaultRequestConfig(
|
||||||
|
|
|
@ -27,7 +27,7 @@ import javax.crypto.Cipher
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
@Throws(SodiumException::class)
|
@Throws(SodiumException::class)
|
||||||
internal fun LazySodiumJava.cryptoBoxOpenEasyAfterNm(cipherBytes: ByteArray, nonce: ByteArray, sharedKey: ByteArray): String {
|
fun LazySodiumJava.cryptoBoxOpenEasyAfterNm(cipherBytes: ByteArray, nonce: ByteArray, sharedKey: ByteArray): String {
|
||||||
if (!Box.Checker.checkNonce(nonce.size)) {
|
if (!Box.Checker.checkNonce(nonce.size)) {
|
||||||
throw SodiumException("Incorrect nonce length.")
|
throw SodiumException("Incorrect nonce length.")
|
||||||
}
|
}
|
||||||
|
@ -44,18 +44,18 @@ internal fun LazySodiumJava.cryptoBoxOpenEasyAfterNm(cipherBytes: ByteArray, non
|
||||||
return str(message)
|
return str(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun getRc4(key: ByteArray): Cipher {
|
fun getRc4(key: ByteArray): Cipher {
|
||||||
val rc4 = Cipher.getInstance("RC4")
|
val rc4 = Cipher.getInstance("RC4")
|
||||||
rc4.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "RC4"))
|
rc4.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "RC4"))
|
||||||
return rc4
|
return rc4
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun md5Bytes(stringToHash: String): ByteArray {
|
fun md5Bytes(stringToHash: String): ByteArray {
|
||||||
val digest = MessageDigest.getInstance("MD5")
|
val digest = MessageDigest.getInstance("MD5")
|
||||||
return digest.digest(stringToHash.toByteArray())
|
return digest.digest(stringToHash.toByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun printHexString(bytes: ByteArray): String {
|
fun printHexString(bytes: ByteArray): String {
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
for (b in bytes) {
|
for (b in bytes) {
|
||||||
sb.append(String.format("%02x", b))
|
sb.append(String.format("%02x", b))
|
||||||
|
|
|
@ -24,13 +24,43 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming
|
||||||
import dev.afanasev.sekret.Secret
|
import dev.afanasev.sekret.Secret
|
||||||
|
|
||||||
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
||||||
data class ClientSettings(
|
class ClientSettings(
|
||||||
val maxCacheSizeInMebibytes: Long = 20480,
|
val maxCacheSizeInMebibytes: Long = 20480,
|
||||||
@JsonUnwrapped
|
|
||||||
val serverSettings: ServerSettings = ServerSettings(),
|
|
||||||
val webSettings: WebSettings? = null,
|
val webSettings: WebSettings? = null,
|
||||||
val devSettings: DevSettings = DevSettings(isDev = false)
|
val devSettings: DevSettings = DevSettings(isDev = false)
|
||||||
)
|
) {
|
||||||
|
// FIXME: jackson doesn't work with data classes and JsonUnwrapped
|
||||||
|
// fix this in 2.0 when we can break the settings file
|
||||||
|
// and remove the `@JsonUnwrapped`
|
||||||
|
@field:JsonUnwrapped
|
||||||
|
lateinit var serverSettings: ServerSettings
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as ClientSettings
|
||||||
|
|
||||||
|
if (maxCacheSizeInMebibytes != other.maxCacheSizeInMebibytes) return false
|
||||||
|
if (webSettings != other.webSettings) return false
|
||||||
|
if (devSettings != other.devSettings) return false
|
||||||
|
if (serverSettings != other.serverSettings) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = maxCacheSizeInMebibytes.hashCode()
|
||||||
|
result = 31 * result + (webSettings?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + devSettings.hashCode()
|
||||||
|
result = 31 * result + serverSettings.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "ClientSettings(maxCacheSizeInMebibytes=$maxCacheSizeInMebibytes, webSettings=$webSettings, devSettings=$devSettings, serverSettings=$serverSettings)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
||||||
data class ServerSettings(
|
data class ServerSettings(
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
</filter>
|
</filter>
|
||||||
|
|
||||||
<file>log/latest.log</file>
|
<file>log/latest.log</file>
|
||||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
<fileNamePattern>log/logFile.%d{yyyy-MM-dd_HH}.log</fileNamePattern>
|
<fileNamePattern>log/logFile.%d{yyyy-MM-dd_HH}.%i.log</fileNamePattern>
|
||||||
<maxHistory>12</maxHistory>
|
<maxHistory>12</maxHistory>
|
||||||
<maxFileSize>100MB</maxFileSize>
|
<maxFileSize>100MB</maxFileSize>
|
||||||
<totalSizeCap>1GB</totalSizeCap>
|
<totalSizeCap>1GB</totalSizeCap>
|
||||||
</rollingPolicy>
|
</rollingPolicy>-->
|
||||||
|
|
||||||
<encoder>
|
<encoder>
|
||||||
<pattern>%d{YYYY-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
|
<pattern>%d{YYYY-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
|
||||||
|
@ -37,6 +37,6 @@
|
||||||
<appender-ref ref="ASYNC"/>
|
<appender-ref ref="ASYNC"/>
|
||||||
</root>
|
</root>
|
||||||
|
|
||||||
<logger name="Exposed" level="ERROR"/
|
<logger name="Exposed" level="ERROR"/>
|
||||||
<logger name="io.netty" level="INFO"/>
|
<logger name="io.netty" level="INFO"/>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|
Loading…
Reference in a new issue