mirror of
https://gitlab.com/mangadex-pub/mangadex_at_home.git
synced 2024-01-19 02:48:37 +00:00
Compare commits
63 commits
2.0.0-rc10
...
master
Author | SHA1 | Date | |
---|---|---|---|
754c1de51d | |||
04f5addfaa | |||
4078d07053 | |||
a37f82e21e | |||
36ffe204a1 | |||
1984a994a4 | |||
aca092a141 | |||
ac246e5449 | |||
c9a5548770 | |||
aa0931dddc | |||
acda98ae55 | |||
08f72be1b0 | |||
85dce402df | |||
d198c27de3 | |||
c7830161c1 | |||
f651409e87 | |||
0cf608a66b | |||
0d490b3d5b | |||
0a7e738253 | |||
15f13eb7c3 | |||
ce94b21b03 | |||
baecff51be | |||
7dc9c21a8b | |||
7498ed0a4d | |||
ad392b30d8 | |||
85668ff207 | |||
f93481975c | |||
f3d35b60eb | |||
7f8b406164 | |||
10e394db1e | |||
d35d5b386e | |||
dd8e434024 | |||
a12d251bf4 | |||
52f23b0f53 | |||
22e318dafd | |||
5b154a6b20 | |||
b0dfc9ff12 | |||
49c19e7879 | |||
fe0fd429c3 | |||
f62eb0db00 | |||
5c6af7caa6 | |||
74083bd68e | |||
ae49f0098a | |||
4fed127e80 | |||
f9cf5729b4 | |||
8178f1b0d6 | |||
900e94c8a7 | |||
f44b6476a7 | |||
1be9ee8506 | |||
2252a9f02d | |||
a33a1dcc44 | |||
3812fcab47 | |||
2eb0a3d475 | |||
cb259a2c72 | |||
0a05cf05c1 | |||
964b97b1d5 | |||
1a0460edcd | |||
a68e93746d | |||
3e48be60df | |||
187b089946 | |||
0c19bb5c4c | |||
75beebd31d | |||
883c1db882 |
1
.gitattributes
vendored
1
.gitattributes
vendored
|
@ -1 +1,2 @@
|
|||
CHANGELOG.md merge=union
|
||||
* text=auto eol=lf
|
||||
|
|
113
.gitlab-ci.yml
113
.gitlab-ci.yml
|
@ -1,48 +1,101 @@
|
|||
stages:
|
||||
- build
|
||||
- publish
|
||||
- publish_latest
|
||||
- publish_docker
|
||||
- docker
|
||||
- push
|
||||
|
||||
cache:
|
||||
paths:
|
||||
- build/libs/
|
||||
|
||||
build:
|
||||
Gradle Build:
|
||||
image: openjdk:8
|
||||
stage: build
|
||||
only:
|
||||
- branches
|
||||
- tags
|
||||
- merge_requests
|
||||
before_script:
|
||||
- export VERSION="${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA}"
|
||||
script:
|
||||
- ./gradlew build
|
||||
- "ls -lah build/libs"
|
||||
|
||||
publish:
|
||||
image: alpine
|
||||
stage: publish
|
||||
before_script:
|
||||
- apk update && apk add git zip
|
||||
- export VERSION=`git describe --tags --dirty`
|
||||
script:
|
||||
- cp build/libs/mangadex_at_home-${VERSION}-all.jar ./
|
||||
- zip -r9 mangadex_at_home-${VERSION}.zip mangadex_at_home-${VERSION}-all.jar settings.sample.json
|
||||
variables:
|
||||
KUBERNETES_MEMORY_REQUEST: 3Gi
|
||||
KUBERNETES_MEMORY_LIMIT: 3Gi
|
||||
artifacts:
|
||||
name: "mangadex_at_home"
|
||||
paths:
|
||||
- "*.jar"
|
||||
- "mangadex_at_home-*.zip"
|
||||
- settings.sample.yaml
|
||||
- "build/libs/mangadex_at_home-*-all.jar"
|
||||
|
||||
publish_docker:
|
||||
image: docker:git
|
||||
Publish Artifacts:
|
||||
image: alpine
|
||||
stage: publish
|
||||
only:
|
||||
- tags
|
||||
needs:
|
||||
- Gradle Build
|
||||
variables:
|
||||
GIT_STRATEGY: none
|
||||
|
||||
before_script:
|
||||
- apk update && apk add zip
|
||||
- export VERSION="${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA}"
|
||||
script:
|
||||
- cp build/libs/mangadex_at_home-${VERSION}-all.jar ./
|
||||
- zip -r9 mangadex_at_home-${VERSION}.zip mangadex_at_home-${VERSION}-all.jar settings.sample.yaml
|
||||
artifacts:
|
||||
name: "mangadex_at_home"
|
||||
paths:
|
||||
- "mangadex_at_home-*-all.jar"
|
||||
- "mangadex_at_home-*.zip"
|
||||
- "settings.sample.yaml"
|
||||
|
||||
Docker Build:
|
||||
image: docker:20.10.8
|
||||
services:
|
||||
- docker:dind
|
||||
- docker:20.10.8-dind
|
||||
stage: docker
|
||||
needs:
|
||||
- Gradle Build
|
||||
|
||||
before_script:
|
||||
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin ${CI_REGISTRY}
|
||||
- export VERSION=`git describe --tags --dirty`
|
||||
- export VERSION="${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA}"
|
||||
- export BASE_TAG="git-$CI_COMMIT_SHORT_SHA"
|
||||
|
||||
script:
|
||||
- mv build/libs/mangadex_at_home-${VERSION}-all.jar build/libs/mangadex_at_home.jar
|
||||
- docker build -t ${CI_REGISTRY_IMAGE}:${VERSION} -t ${CI_REGISTRY_IMAGE}:latest .
|
||||
- docker push ${CI_REGISTRY_IMAGE}:${VERSION}
|
||||
- docker push ${CI_REGISTRY_IMAGE}:latest
|
||||
- docker build . -t $CI_REGISTRY_IMAGE:$BASE_TAG
|
||||
- docker push $CI_REGISTRY_IMAGE:$BASE_TAG
|
||||
|
||||
|
||||
.docker_push: &docker_push
|
||||
image: docker:20.10.8
|
||||
services:
|
||||
- docker:20.10.8-dind
|
||||
stage: push
|
||||
variables:
|
||||
GIT_STRATEGY: none
|
||||
|
||||
before_script:
|
||||
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin ${CI_REGISTRY}
|
||||
- export BASE_TAG="git-$CI_COMMIT_SHORT_SHA"
|
||||
- export SHORT_TAG="$(echo $CI_COMMIT_TAG | cut -d "." -f1)"
|
||||
|
||||
script:
|
||||
- docker pull $CI_REGISTRY_IMAGE:$BASE_TAG
|
||||
- docker tag $CI_REGISTRY_IMAGE:$BASE_TAG $CI_REGISTRY_IMAGE:$NEW_TAG
|
||||
- docker tag $CI_REGISTRY_IMAGE:$BASE_TAG $CI_REGISTRY_IMAGE:$SHORT_TAG
|
||||
- docker push $CI_REGISTRY_IMAGE --all-tags
|
||||
|
||||
Push Latest:
|
||||
<<: *docker_push
|
||||
needs:
|
||||
- Docker Build
|
||||
only:
|
||||
- master
|
||||
variables:
|
||||
NEW_TAG: latest
|
||||
|
||||
Push Tags:
|
||||
<<: *docker_push
|
||||
needs:
|
||||
- Docker Build
|
||||
only:
|
||||
- tags
|
||||
variables:
|
||||
NEW_TAG: $CI_COMMIT_TAG
|
||||
|
|
75
CHANGELOG.md
75
CHANGELOG.md
|
@ -17,6 +17,70 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Security
|
||||
|
||||
## [2.0.4] - 2023-08-07
|
||||
### Changed
|
||||
- [2023-08-07] Updated dependencies [@carbotaniuman].
|
||||
- [2023-04-06] Fixed DB contention issues [@carbotaniuman].
|
||||
- [2023-04-06] Make errors more useful [@carbotaniuman].
|
||||
|
||||
## [2.0.3] - 2022-02-17
|
||||
### Changed
|
||||
- [2022-02-17] Updated dependencies [@carbotaniuman].
|
||||
|
||||
### Fixed
|
||||
- [2022-02-17] Fix possible race condition in DB handling code [@carbotaniuman].
|
||||
- [2022-02-17] Missing ISO code no longer fails request [@carbotaniuman].
|
||||
|
||||
## [2.0.2] - 2022-02-16
|
||||
### Removed
|
||||
- [2022-02-16] Remove TLS 1.0 and 1.1 support [@carbotaniuman].
|
||||
|
||||
### Fixed
|
||||
- [2022-02-16] Fix uncatched exceptions killing threads and not being logged [@carbotaniuman].
|
||||
|
||||
## [2.0.1] - 2021-05-27
|
||||
### Added
|
||||
- [2021-05-27] Added SNI check to prevent people from simply scanning nodes [@carbotaniuman].
|
||||
|
||||
### Changed
|
||||
- [2021-05-21] Update metrics and fix cache directory leak [@carbotaniuman].
|
||||
- [2021-05-21] Change headers to be wildcards [@carbotaniuman].
|
||||
- [2021-05-27] Make sending the `Server` header configurable but off by default [@carbotaniuman].
|
||||
|
||||
## [2.0.0] - 2021-03-11
|
||||
### Changed
|
||||
- [2021-03-11] Switch back to HTTP/1.1 [@carbotaniuman].
|
||||
- [2021-03-11] Tune connection pool [@carbotaniuman].
|
||||
|
||||
### Fixed
|
||||
- [2021-03-11] Fixed Access-Control-Expose-Headers typo [@lflare].
|
||||
- [2021-03-11] Throw exceptions less frequently [@carbotaniuman].
|
||||
- [2021-03-11] Fix various Netty issues [@carbotaniuman].
|
||||
- [2021-03-11] Don't log IOUring and Epoll errors [@carbotaniuman].
|
||||
- [2021-03-11] Ignore IPV6 when pinging [@carbotaniuman].
|
||||
|
||||
## [2.0.0-rc14] - 2021-03-02
|
||||
### Changed
|
||||
- [2021-03-02] Fix Prometheus to 2.24.1 and Grafana to 7.4.0 [@_tde9].
|
||||
- [2021-03-02] Update and rearrange the embedded dashboard with the new Timeseries panel from Grafana 7.4 [@_tde9].
|
||||
- [2021-03-02] Update sample dashboard screenshot thanks to DLMSweet :smile: [@_tde9].
|
||||
- [2021-02-25] Use HTTP/2 to download when possible [@carbotaniuman].
|
||||
|
||||
### Fixed
|
||||
- [2021-02-21] Fix pipeline [@_tde9].
|
||||
|
||||
## [2.0.0-rc13] - 2021-02-19
|
||||
### Changed
|
||||
- [2021-02-19] Back to sqlite we go [@carbotaniuman].
|
||||
|
||||
## [2.0.0-rc12] - 2021-02-11
|
||||
### Fixed
|
||||
- [2021-02-11] Fixed stupid cross platform bug [@carbotaniuman].
|
||||
|
||||
## [2.0.0-rc11] - 2021-02-11
|
||||
### Fixed
|
||||
- [2021-02-11] Fixed docker bug [@carbotaniuman].
|
||||
|
||||
## [2.0.0-rc10] - 2021-02-11
|
||||
### Added
|
||||
- [2021-02-11] Add ability to disable io_uring and epoll [@carbotaniuman].
|
||||
|
@ -353,7 +417,16 @@ This release contains many breaking changes! Of note are the changes to the cach
|
|||
### Fixed
|
||||
- [2020-06-11] Tweaked logging configuration to reduce log file sizes by [@carbotaniuman].
|
||||
|
||||
[Unreleased]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0-rc10...HEAD
|
||||
[Unreleased]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.4...HEAD
|
||||
[2.0.4]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.3...2.0.4
|
||||
[2.0.3]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.2...2.0.3
|
||||
[2.0.2]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.1...2.0.2
|
||||
[2.0.1]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0...2.0.1
|
||||
[2.0.0]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0-rc14...2.0.0
|
||||
[2.0.0-rc14]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0-rc13...2.0.0-rc14
|
||||
[2.0.0-rc13]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0-rc12...2.0.0-rc13
|
||||
[2.0.0-rc12]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0-rc11...2.0.0-rc12
|
||||
[2.0.0-rc11]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0-rc10...2.0.0-rc11
|
||||
[2.0.0-rc10]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0-rc9...2.0.0-rc10
|
||||
[2.0.0-rc9]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0-rc8...2.0.0-rc9
|
||||
[2.0.0-rc8]: https://gitlab.com/mangadex/mangadex_at_home/-/compare/2.0.0-rc7...2.0.0-rc8
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM adoptopenjdk:15
|
||||
FROM eclipse-temurin:17-jre
|
||||
|
||||
WORKDIR /mangahome
|
||||
ADD /build/libs/mangadex_at_home.jar /mangahome/mangadex_at_home.jar
|
||||
|
|
103
build.gradle
103
build.gradle
|
@ -1,21 +1,26 @@
|
|||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
id "jacoco"
|
||||
id "java"
|
||||
id "org.jetbrains.kotlin.jvm" version "1.4.20"
|
||||
id "org.jetbrains.kotlin.kapt" version "1.4.0"
|
||||
id "org.jetbrains.kotlin.jvm" version "1.8.0"
|
||||
id "org.jetbrains.kotlin.kapt" version "1.8.0"
|
||||
id "application"
|
||||
id "com.github.johnrengelman.shadow" version "5.2.0"
|
||||
id "com.github.johnrengelman.shadow" version "7.0.0"
|
||||
id "com.diffplug.spotless" version "5.8.2"
|
||||
id "dev.afanasev.sekret" version "0.0.7"
|
||||
id "net.afanasev.sekret" version "0.1.1-RC3"
|
||||
id "com.palantir.git-version" version "0.12.3"
|
||||
}
|
||||
|
||||
group = "com.mangadex"
|
||||
version = "git describe --tags --dirty".execute().text.trim()
|
||||
mainClassName = "mdnet.MainKt"
|
||||
version = System.getenv("VERSION") ?: gitVersion()
|
||||
|
||||
application {
|
||||
mainClass = "mdnet.MainKt"
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
configurations {
|
||||
|
@ -23,47 +28,54 @@ configurations {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly group: "dev.afanasev", name: "sekret-annotation", version: "0.0.7"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect"
|
||||
|
||||
implementation group: "commons-io", name: "commons-io", version: "2.7"
|
||||
implementation group: "org.apache.commons", name: "commons-compress", version: "1.20"
|
||||
implementation group: "ch.qos.logback", name: "logback-classic", version: "1.3.0-alpha4"
|
||||
implementation group: "commons-io", name: "commons-io", version: "2.11.0"
|
||||
implementation group: "org.apache.commons", name: "commons-compress", version: "1.22"
|
||||
implementation group: "ch.qos.logback", name: "logback-classic", version: "1.3.6"
|
||||
|
||||
implementation group: "io.micrometer", name: "micrometer-registry-prometheus", version: "1.6.2"
|
||||
implementation group: "com.maxmind.geoip2", name: "geoip2", version: "2.15.0"
|
||||
implementation group: "io.micrometer", name: "micrometer-registry-prometheus", version: "1.8.3"
|
||||
implementation group: "com.maxmind.geoip2", name: "geoip2", version: "2.16.1"
|
||||
|
||||
implementation group: "org.http4k", name: "http4k-core", version: "$http_4k_version"
|
||||
implementation group: "org.http4k", name: "http4k-resilience4j", version: "$http_4k_version"
|
||||
implementation group: "io.github.resilience4j", name: "resilience4j-micrometer", version: "1.6.1"
|
||||
implementation group: "org.http4k", name: "http4k-format-jackson", version: "$http_4k_version"
|
||||
implementation group: "com.fasterxml.jackson.dataformat", name: "jackson-dataformat-yaml", version: "2.12.1"
|
||||
implementation group: "com.fasterxml.jackson.datatype", name: "jackson-datatype-jsr310", version: "2.12.1"
|
||||
implementation group: "org.http4k", name: "http4k-client-apache", version: "$http_4k_version"
|
||||
implementation group: "org.http4k", name: "http4k-metrics-micrometer", version: "$http_4k_version"
|
||||
implementation group: "org.http4k", name: "http4k-server-netty", version: "$http_4k_version"
|
||||
implementation group: "io.netty", name: "netty-transport-native-epoll", version: "4.1.58.Final", classifier: "linux-x86_64"
|
||||
implementation group: "io.netty.incubator", name: "netty-incubator-transport-native-io_uring", version: "0.0.3.Final", classifier: "linux-x86_64"
|
||||
testImplementation group: "org.http4k", name: "http4k-testing-kotest", version: "$http_4k_version"
|
||||
runtimeOnly group: "io.netty", name: "netty-tcnative-boringssl-static", version: "2.0.34.Final"
|
||||
implementation platform(group: "org.http4k", name: "http4k-bom", version: "4.41.3.0")
|
||||
implementation platform(group: "com.fasterxml.jackson", name: "jackson-bom", version: "2.14.2")
|
||||
implementation platform(group: "io.netty", name: "netty-bom", version: "4.1.91.Final")
|
||||
|
||||
implementation group: 'com.zaxxer', name: 'HikariCP', version: '4.0.1'
|
||||
implementation group: "com.h2database", name: "h2", version: "1.4.200"
|
||||
implementation group: "org.http4k", name: "http4k-core"
|
||||
implementation group: "org.http4k", name: "http4k-resilience4j"
|
||||
implementation group: "io.github.resilience4j", name: "resilience4j-micrometer", version: "1.7.1"
|
||||
implementation group: "org.http4k", name: "http4k-format-jackson"
|
||||
implementation group: "com.fasterxml.jackson.dataformat", name: "jackson-dataformat-yaml"
|
||||
implementation group: "com.fasterxml.jackson.datatype", name: "jackson-datatype-jsr310"
|
||||
implementation group: "org.http4k", name: "http4k-client-okhttp"
|
||||
implementation group: "org.http4k", name: "http4k-metrics-micrometer"
|
||||
implementation group: "org.http4k", name: "http4k-server-netty"
|
||||
implementation group: "io.netty", name: "netty-codec-haproxy"
|
||||
implementation group: "io.netty", name: "netty-transport-native-epoll", classifier: "linux-x86_64"
|
||||
implementation group: "io.netty.incubator", name: "netty-incubator-transport-native-io_uring", version: "0.0.19.Final", classifier: "linux-x86_64"
|
||||
testImplementation group: "org.http4k", name: "http4k-testing-kotest"
|
||||
runtimeOnly group: "io.netty", name: "netty-tcnative-boringssl-static", version: "2.0.59.Final"
|
||||
|
||||
implementation group: "com.zaxxer", name: "HikariCP", version: "4.0.3"
|
||||
implementation group: "org.xerial", name: "sqlite-jdbc", version: "3.41.2.1"
|
||||
implementation "org.ktorm:ktorm-core:$ktorm_version"
|
||||
implementation "org.ktorm:ktorm-jackson:$ktorm_version"
|
||||
|
||||
implementation "info.picocli:picocli:4.5.0"
|
||||
kapt "info.picocli:picocli-codegen:4.5.0"
|
||||
implementation "info.picocli:picocli:$picocli_version"
|
||||
kapt "info.picocli:picocli-codegen:$picocli_version"
|
||||
|
||||
testImplementation "io.kotest:kotest-runner-junit5:$kotest_version"
|
||||
testImplementation "io.kotest:kotest-assertions-core:$kotest_version"
|
||||
testImplementation "io.mockk:mockk:1.10.4"
|
||||
testImplementation "io.mockk:mockk:1.13.4"
|
||||
}
|
||||
|
||||
test {
|
||||
tasks.withType(Test).configureEach {
|
||||
useJUnitPlatform()
|
||||
javaLauncher = javaToolchains.launcherFor {
|
||||
languageVersion = JavaLanguageVersion.of(8)
|
||||
}
|
||||
}
|
||||
|
||||
task testDev(type: Test) {
|
||||
tasks.register("testDev", Test) {
|
||||
group = "verification"
|
||||
useJUnitPlatform()
|
||||
filter {
|
||||
|
@ -79,24 +91,18 @@ kapt {
|
|||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(8))
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
tasks.withType(KotlinCompile).configureEach {
|
||||
compilerOptions {
|
||||
freeCompilerArgs = ["-Xjsr305=strict"]
|
||||
}
|
||||
}
|
||||
|
||||
spotless {
|
||||
java {
|
||||
targetExclude("build/generated/**/*")
|
||||
eclipse()
|
||||
removeUnusedImports()
|
||||
trimTrailingWhitespace()
|
||||
endWithNewline()
|
||||
}
|
||||
kotlin {
|
||||
ktlint("0.40.0").userData(["disabled_rules": "no-wildcard-imports"])
|
||||
licenseHeaderFile "license_header"
|
||||
|
@ -112,9 +118,10 @@ tasks.register("generateVersion", Copy) {
|
|||
into "$buildDir/generated/java"
|
||||
expand templateContext
|
||||
}
|
||||
compileJava.dependsOn generateVersion
|
||||
sourceSets.main.java.srcDir generateVersion
|
||||
|
||||
sourceSets.main.java.srcDir generateVersion.outputs.files
|
||||
|
||||
// https://gist.github.com/medvedev/968119d7786966d9ed4442ae17aca279
|
||||
tasks.register("depsize") {
|
||||
description = 'Prints dependencies for "default" configuration'
|
||||
doLast() {
|
||||
|
|
|
@ -12,6 +12,8 @@ Once installed, you can check that it works by opening a command prompt and runn
|
|||
|
||||
## Run as a standalone container
|
||||
|
||||
*Note* Changes to `the docker-compose.yml` are coming, and as such, this instruction page will get reworked a bit.
|
||||
|
||||
Use either a specific image, preferrably the [latest image published](https://gitlab.com/mangadex-pub/mangadex_at_home/container_registry/1200259)
|
||||
|
||||
> While it might work, using `registry.gitlab.com/mangadex-pub/mangadex_at_home:latest` is a bad idea as we do not guarantee forward-compatibility
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 415 KiB After Width: | Height: | Size: 582 KiB |
|
@ -4,7 +4,7 @@ services:
|
|||
|
||||
mangadex-at-home:
|
||||
container_name: mangadex-at-home
|
||||
image: "registry.gitlab.com/mangadex-pub/mangadex_at_home:<version>"
|
||||
image: "registry.gitlab.com/mangadex-pub/mangadex_at_home:2"
|
||||
ports:
|
||||
- 443:443
|
||||
volumes:
|
||||
|
@ -12,7 +12,6 @@ services:
|
|||
- ./data/cache/:/mangahome/data/
|
||||
environment:
|
||||
JAVA_TOOL_OPTIONS: "-Xms1G -Xmx1G -XX:+UseShenandoahGC -Xss512K"
|
||||
privileged: true
|
||||
command: [
|
||||
"bash",
|
||||
"-c",
|
||||
|
@ -31,7 +30,7 @@ services:
|
|||
|
||||
prometheus:
|
||||
container_name: prometheus
|
||||
image: prom/prometheus
|
||||
image: prom/prometheus:v2.34.0
|
||||
user: "root"
|
||||
group_add:
|
||||
- 0
|
||||
|
@ -50,7 +49,7 @@ services:
|
|||
|
||||
grafana:
|
||||
container_name: grafana
|
||||
image: grafana/grafana
|
||||
image: grafana/grafana:8.4.3
|
||||
user: "root"
|
||||
group_add:
|
||||
- 0
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,3 @@
|
|||
http_4k_version=4.1.0.0
|
||||
exposed_version=0.26.2
|
||||
kotest_version=4.4.0.RC1
|
||||
ktorm_version=3.2.0
|
||||
kotest_version=5.5.5
|
||||
ktorm_version=3.6.0
|
||||
picocli_version=4.7.1
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
5
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,5 @@
|
|||
#Thu Jul 02 11:52:16 CDT 2020
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-all.zip
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
55
gradlew
vendored
55
gradlew
vendored
|
@ -1,5 +1,21 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
|
@ -28,7 +44,7 @@ APP_NAME="Gradle"
|
|||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m"'
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
@ -56,7 +72,7 @@ case "`uname`" in
|
|||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
MSYS* | MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
|
@ -66,6 +82,7 @@ esac
|
|||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
|
@ -109,10 +126,11 @@ if $darwin; then
|
|||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
|
@ -138,19 +156,19 @@ if $cygwin ; then
|
|||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
|
@ -159,14 +177,9 @@ save () {
|
|||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
|
43
gradlew.bat
vendored
43
gradlew.bat
vendored
|
@ -1,3 +1,19 @@
|
|||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
|
@ -13,15 +29,18 @@ if "%DIRNAME%" == "" set DIRNAME=.
|
|||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m"
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
@ -35,7 +54,7 @@ goto fail
|
|||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
|
@ -45,28 +64,14 @@ echo location of your Java installation.
|
|||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
|
|
@ -1,16 +1 @@
|
|||
pluginManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
jcenter()
|
||||
google()
|
||||
}
|
||||
resolutionStrategy {
|
||||
eachPlugin {
|
||||
if (requested.id.id == "com.squareup.sqldelight") {
|
||||
useModule("com.squareup.sqldelight:gradle-plugin:${requested.version}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = 'mangadex_at_home'
|
||||
|
|
|
@ -42,7 +42,7 @@ server_settings:
|
|||
# 443 is recommended for maximum appeal
|
||||
port: 443
|
||||
# This controls the value the server receives
|
||||
# for your uplaod speed
|
||||
# for your upload speed
|
||||
# Keep this as 0 to use the one currently stored
|
||||
# in the server, or set this higher if needed
|
||||
# This does not affect `max_kilobits_per_second` in any way
|
||||
|
@ -80,3 +80,20 @@ server_settings:
|
|||
# 0 defaults to (2 * your available processors)
|
||||
# https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Runtime.html#availableProcessors()
|
||||
threads: 0
|
||||
# Whether to enable support for HAProxy Proxy Protocol
|
||||
# If using a reverse proxy to forward requests to MD@H via
|
||||
# ssl passthrough, you can use Proxy Protocol to preserve
|
||||
# original IP if your reverse proxy supports it. This
|
||||
# will allow geo location metrics to work correctly.
|
||||
# https://www.haproxy.com/blog/haproxy/proxy-protocol/
|
||||
enable_proxy_protocol: false
|
||||
|
||||
|
||||
# Settings intended for advanced use cases or tinkering
|
||||
dev_settings:
|
||||
# The url to override the MD@H backend with
|
||||
dev_url: ~
|
||||
# Whether to disable the sni check for mangadex.network and localhost
|
||||
disable_sni_check: false
|
||||
# Whether to send the server header or not, defaults to false
|
||||
send_server_header: false
|
112
src/main/java/mdnet/cache/CachingInputStream.java
vendored
112
src/main/java/mdnet/cache/CachingInputStream.java
vendored
|
@ -1,112 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package mdnet.cache;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.io.input.ProxyInputStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
import static org.apache.commons.io.IOUtils.EOF;
|
||||
|
||||
public class CachingInputStream extends ProxyInputStream {
|
||||
private final OutputStream cache;
|
||||
private final ExecutorService executor;
|
||||
private final Runnable onClose;
|
||||
|
||||
public CachingInputStream(InputStream response, ExecutorService executor, OutputStream cache, Runnable onClose) {
|
||||
super(response);
|
||||
this.executor = executor;
|
||||
this.cache = cache;
|
||||
this.onClose = onClose;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (read() == EOF) {
|
||||
try {
|
||||
in.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
try {
|
||||
cache.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
onClose.run();
|
||||
|
||||
return;
|
||||
}
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
IOUtils.copy(in, cache);
|
||||
} catch (IOException ignored) {
|
||||
} finally {
|
||||
try {
|
||||
in.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
try {
|
||||
cache.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
onClose.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
final int ch = super.read();
|
||||
if (ch != EOF) {
|
||||
try {
|
||||
cache.write(ch);
|
||||
} catch (IOException ignored) {
|
||||
// don't let write failures affect the image loading
|
||||
}
|
||||
}
|
||||
return ch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(final byte[] bts, final int st, final int end) throws IOException {
|
||||
final int n = super.read(bts, st, end);
|
||||
if (n != EOF) {
|
||||
try {
|
||||
cache.write(bts, st, n);
|
||||
} catch (IOException ignored) {
|
||||
// don't let write failures affect the image loading
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(final byte[] bts) throws IOException {
|
||||
final int n = super.read(bts);
|
||||
if (n != EOF) {
|
||||
try {
|
||||
cache.write(bts, 0, n);
|
||||
} catch (IOException ignored) {
|
||||
// don't let write failures affect the image loading
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
}
|
|
@ -23,16 +23,17 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule
|
|||
import mdnet.ServerHandlerJackson.auto
|
||||
import mdnet.logging.info
|
||||
import mdnet.settings.*
|
||||
import org.http4k.client.ApacheClient
|
||||
import org.http4k.core.Body
|
||||
import org.http4k.core.HttpHandler
|
||||
import org.http4k.core.Method
|
||||
import org.http4k.core.Request
|
||||
import okhttp3.Dns
|
||||
import okhttp3.OkHttpClient
|
||||
import org.http4k.client.OkHttp
|
||||
import org.http4k.core.*
|
||||
import org.http4k.format.ConfigurableJackson
|
||||
import org.http4k.format.asConfigurable
|
||||
import org.http4k.format.withStandardMappings
|
||||
import org.http4k.lens.LensFailure
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.net.Inet4Address
|
||||
import java.net.InetAddress
|
||||
|
||||
object ServerHandlerJackson : ConfigurableJackson(
|
||||
KotlinModule()
|
||||
|
@ -43,8 +44,16 @@ object ServerHandlerJackson : ConfigurableJackson(
|
|||
)
|
||||
|
||||
class BackendApi(private val settings: ClientSettings) {
|
||||
private val client = OkHttp(
|
||||
client = OkHttpClient.Builder()
|
||||
.dns(object : Dns {
|
||||
override fun lookup(hostname: String): List<InetAddress> {
|
||||
return Dns.SYSTEM.lookup(hostname).filterIsInstance<Inet4Address>()
|
||||
}
|
||||
})
|
||||
.build()
|
||||
)
|
||||
private val serverAddress = settings.devSettings.devUrl ?: SERVER_ADDRESS
|
||||
private val client = ApacheClient()
|
||||
|
||||
fun logoutFromControl(): Boolean {
|
||||
val serverSettings = settings.serverSettings
|
||||
|
@ -115,7 +124,7 @@ class BackendApi(private val settings: ClientSettings) {
|
|||
try {
|
||||
PING_FAILURE_LENS(response)
|
||||
} catch (e: LensFailure) {
|
||||
PingFailure(response.status.code, response.status.description)
|
||||
PingFailure(response.status.code, response.bodyString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ package mdnet
|
|||
import java.time.Duration
|
||||
|
||||
object Constants {
|
||||
const val CLIENT_BUILD = 27
|
||||
const val CLIENT_BUILD = 32
|
||||
|
||||
@JvmField val MAX_AGE_CACHE: Duration = Duration.ofDays(14)
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@ import java.io.File
|
|||
import java.lang.IllegalArgumentException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.isRegularFile
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
CommandLine(Main()).execute(*args)
|
||||
|
@ -34,11 +33,11 @@ fun main(args: Array<String>) {
|
|||
|
||||
@CommandLine.Command(name = "java -jar <jar>", usageHelpWidth = 120, version = ["Client Version ${BuildInfo.VERSION} (Build ${Constants.CLIENT_BUILD})"])
|
||||
class Main : Runnable {
|
||||
@field:CommandLine.Option(names = ["-s", "--settings"], defaultValue = ".\\settings.yaml", paramLabel = "<settings>", description = ["the settings file (default: \${DEFAULT-VALUE})"])
|
||||
@field:CommandLine.Option(names = ["-s", "--settings"], defaultValue = ".\${sys:file.separator}settings.yaml", paramLabel = "<settings>", description = ["the settings file (default: \${DEFAULT-VALUE})"])
|
||||
lateinit var settingsFile: File
|
||||
@field:CommandLine.Option(names = ["-d", "--database"], defaultValue = ".\\", paramLabel = "<settings>", description = ["the database folder (default: \${DEFAULT-VALUE})"])
|
||||
@field:CommandLine.Option(names = ["-d", "--database"], defaultValue = ".\${sys:file.separator}", paramLabel = "<settings>", description = ["the database folder (default: \${DEFAULT-VALUE})"])
|
||||
lateinit var databaseFolder: Path
|
||||
@field:CommandLine.Option(names = ["-c", "--cache"], defaultValue = ".\\images", paramLabel = "<settings>", description = ["the cache folder (default: \${DEFAULT-VALUE})"])
|
||||
@field:CommandLine.Option(names = ["-c", "--cache"], defaultValue = ".\${sys:file.separator}images", paramLabel = "<settings>", description = ["the cache folder (default: \${DEFAULT-VALUE})"])
|
||||
lateinit var cacheFolder: Path
|
||||
@field:CommandLine.Option(names = ["-h", "--help"], usageHelp = true, description = ["show this help message and exit"])
|
||||
var helpRequested: Boolean = false
|
||||
|
@ -68,30 +67,16 @@ class Main : Runnable {
|
|||
""".trimIndent()
|
||||
)
|
||||
|
||||
if (!Files.isDirectory(databaseFolder) || Files.isRegularFile(databaseFolder.resolveSibling(databaseFolder.fileName.toString() + ".mv.db"))) {
|
||||
println()
|
||||
println()
|
||||
println(
|
||||
"""the --database option now takes in the folder with the database file!
|
||||
|(it previously took in the path to the file without any extensions)
|
||||
|if you are using docker update your docker mount settings!
|
||||
|if you are not, manually move update your --database args!
|
||||
|note: the database file itself should be named metadata.{extension}
|
||||
|where {extension} can be `.db` or `.mv.db`
|
||||
""".trimMargin()
|
||||
)
|
||||
println()
|
||||
println()
|
||||
|
||||
throw IllegalArgumentException()
|
||||
if (!Files.isDirectory(databaseFolder)) {
|
||||
throw IllegalArgumentException("Expected $databaseFolder to be a folder, was a file")
|
||||
}
|
||||
|
||||
if (!Files.isDirectory(databaseFolder)) {
|
||||
if (Files.isRegularFile(databaseFolder)) {
|
||||
throw IllegalArgumentException("Database folder $databaseFolder must be a directory")
|
||||
}
|
||||
|
||||
if (!Files.isDirectory(cacheFolder)) {
|
||||
throw IllegalArgumentException("Database folder $cacheFolder must be a directory")
|
||||
if (Files.isRegularFile(cacheFolder)) {
|
||||
throw IllegalArgumentException("Cache folder $cacheFolder must be a directory")
|
||||
}
|
||||
|
||||
val client = MangaDexClient(settingsFile, databaseFolder, cacheFolder)
|
||||
|
|
|
@ -65,9 +65,11 @@ class MangaDexClient(private val settingsFile: File, databaseFolder: Path, cache
|
|||
|
||||
LOGGER.info { "Client settings loaded: $settings" }
|
||||
|
||||
Class.forName("org.sqlite.JDBC")
|
||||
|
||||
val config = HikariConfig()
|
||||
val db = databaseFolder.resolve("metadata")
|
||||
config.jdbcUrl = "jdbc:h2:$db"
|
||||
val db = databaseFolder.resolve("metadata.db")
|
||||
config.jdbcUrl = "jdbc:sqlite:$db"
|
||||
config.addDataSourceProperty("cachePrepStmts", "true")
|
||||
config.addDataSourceProperty("prepStmtCacheSize", "100")
|
||||
config.addDataSourceProperty("prepStmtCacheSqlLimit", "1000")
|
||||
|
@ -196,8 +198,8 @@ class MangaDexClient(private val settingsFile: File, databaseFolder: Path, cache
|
|||
}
|
||||
|
||||
private fun validateSettings(settings: ClientSettings) {
|
||||
if (settings.maxCacheSizeInMebibytes < 20480) {
|
||||
throw ClientSettingsException("Config Error: Invalid max cache size, must be >= 20480 MiB (20 GiB)")
|
||||
if (settings.maxCacheSizeInMebibytes < 40960) {
|
||||
throw ClientSettingsException("Config Error: Invalid max cache size, must be >= 40960 MiB (40 GiB)")
|
||||
}
|
||||
|
||||
fun isSecretValid(clientSecret: String): Boolean {
|
||||
|
|
|
@ -18,30 +18,29 @@ along with this MangaDex@Home. If not, see <http://www.gnu.org/licenses/>.
|
|||
*/
|
||||
package mdnet
|
||||
|
||||
import io.micrometer.core.instrument.Gauge
|
||||
import io.micrometer.core.instrument.binder.BaseUnits
|
||||
import io.micrometer.prometheus.PrometheusConfig
|
||||
import io.micrometer.prometheus.PrometheusMeterRegistry
|
||||
import mdnet.cache.ImageStorage
|
||||
import mdnet.data.Statistics
|
||||
import mdnet.logging.error
|
||||
import mdnet.logging.info
|
||||
import mdnet.logging.warn
|
||||
import mdnet.metrics.DefaultMicrometerMetrics
|
||||
import mdnet.server.getServer
|
||||
import mdnet.settings.ClientSettings
|
||||
import mdnet.settings.RemoteSettings
|
||||
import org.apache.hc.client5.http.config.RequestConfig
|
||||
import org.apache.hc.client5.http.cookie.StandardCookieSpec
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClients
|
||||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder
|
||||
import org.apache.hc.core5.util.TimeValue
|
||||
import org.apache.hc.core5.util.Timeout
|
||||
import org.http4k.client.ApacheClient
|
||||
import okhttp3.ConnectionPool
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Protocol
|
||||
import org.http4k.client.OkHttp
|
||||
import org.http4k.core.BodyMode
|
||||
import org.http4k.core.then
|
||||
import org.http4k.filter.ClientFilters
|
||||
import org.http4k.filter.MicrometerMetrics
|
||||
import org.http4k.server.Http4kServer
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
@ -73,26 +72,24 @@ class ServerManager(
|
|||
private val executor = Executors.newSingleThreadScheduledExecutor()
|
||||
private val registry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
|
||||
private val statistics = Statistics()
|
||||
private val connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
|
||||
.setMaxConnTotal(500)
|
||||
.setMaxConnPerRoute(500)
|
||||
.build()
|
||||
private val apache = ClientFilters.MicrometerMetrics.RequestCounter(registry)
|
||||
|
||||
private val okhttp = ClientFilters.MicrometerMetrics.RequestCounter(registry)
|
||||
.then(ClientFilters.MicrometerMetrics.RequestTimer(registry))
|
||||
.then(
|
||||
ApacheClient(
|
||||
responseBodyMode = BodyMode.Stream,
|
||||
client = HttpClients.custom()
|
||||
.disableConnectionState()
|
||||
.setDefaultRequestConfig(
|
||||
RequestConfig.custom()
|
||||
.setCookieSpec(StandardCookieSpec.IGNORE)
|
||||
.setConnectTimeout(Timeout.ofSeconds(2))
|
||||
.setResponseTimeout(Timeout.ofSeconds(2))
|
||||
.setConnectionRequestTimeout(Timeout.ofSeconds(1))
|
||||
.build()
|
||||
OkHttp(
|
||||
bodyMode = BodyMode.Stream,
|
||||
client = OkHttpClient.Builder()
|
||||
.connectTimeout(Duration.ofSeconds(2))
|
||||
.connectionPool(
|
||||
ConnectionPool(
|
||||
maxIdleConnections = 100,
|
||||
keepAliveDuration = 1,
|
||||
timeUnit = TimeUnit.MINUTES
|
||||
)
|
||||
)
|
||||
.setConnectionManager(connectionManager)
|
||||
.writeTimeout(Duration.ofSeconds(10))
|
||||
.readTimeout(Duration.ofSeconds(10))
|
||||
.protocols(listOf(Protocol.HTTP_1_1))
|
||||
.build()
|
||||
)
|
||||
)
|
||||
|
@ -109,7 +106,19 @@ class ServerManager(
|
|||
|
||||
fun start() {
|
||||
LOGGER.info { "Image server starting" }
|
||||
DefaultMicrometerMetrics(registry, storage.cacheDirectory)
|
||||
DefaultMicrometerMetrics(registry)
|
||||
Gauge.builder(
|
||||
"cache.used",
|
||||
storage,
|
||||
{ it.size.toDouble() }
|
||||
).baseUnit(BaseUnits.BYTES).register(registry)
|
||||
|
||||
Gauge.builder(
|
||||
"cache.max",
|
||||
storage,
|
||||
{ it.maxSize.toDouble() }
|
||||
).baseUnit(BaseUnits.BYTES).register(registry)
|
||||
|
||||
loginAndStartServer()
|
||||
|
||||
var lastBytesSent = statistics.bytesSent.get()
|
||||
|
@ -178,7 +187,7 @@ class ServerManager(
|
|||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LOGGER.error(e) { "Main loop failed" }
|
||||
LOGGER.warn(e) { "Main loop failed" }
|
||||
}
|
||||
},
|
||||
5, 5, TimeUnit.SECONDS
|
||||
|
@ -205,20 +214,6 @@ class ServerManager(
|
|||
45, 45, TimeUnit.SECONDS
|
||||
)
|
||||
|
||||
executor.scheduleWithFixedDelay(
|
||||
{
|
||||
try {
|
||||
LOGGER.info { "Closing old Apache HTTP connections" }
|
||||
|
||||
connectionManager.closeExpired()
|
||||
connectionManager.closeIdle(TimeValue.ofSeconds(30))
|
||||
} catch (e: Exception) {
|
||||
LOGGER.warn(e) { "Old Apache HTTP connection closer failed" }
|
||||
}
|
||||
},
|
||||
45, 45, TimeUnit.SECONDS
|
||||
)
|
||||
|
||||
LOGGER.info { "Image server has started" }
|
||||
}
|
||||
|
||||
|
@ -268,10 +263,11 @@ class ServerManager(
|
|||
storage,
|
||||
remoteSettings,
|
||||
settings.serverSettings,
|
||||
settings.devSettings,
|
||||
settings.metricsSettings,
|
||||
statistics,
|
||||
registry,
|
||||
apache,
|
||||
okhttp,
|
||||
).start()
|
||||
|
||||
this.state = Running(server, remoteSettings)
|
||||
|
|
114
src/main/kotlin/mdnet/cache/CachingInputStream.kt
vendored
Normal file
114
src/main/kotlin/mdnet/cache/CachingInputStream.kt
vendored
Normal file
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
Mangadex@Home
|
||||
Copyright (c) 2020, MangaDex Network
|
||||
This file is part of MangaDex@Home.
|
||||
|
||||
MangaDex@Home is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
MangaDex@Home is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this MangaDex@Home. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package mdnet.cache
|
||||
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.io.input.ProxyInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.lang.Runnable
|
||||
import java.util.concurrent.ExecutorService
|
||||
import kotlin.Throws
|
||||
|
||||
class CachingInputStream(
|
||||
response: InputStream?,
|
||||
private val executor: ExecutorService,
|
||||
private val cache: OutputStream,
|
||||
private val onClose: Runnable
|
||||
) : ProxyInputStream(response) {
|
||||
private var eofReached = false
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun close() {
|
||||
if (eofReached) {
|
||||
try {
|
||||
`in`.close()
|
||||
} catch (ignored: IOException) {
|
||||
}
|
||||
try {
|
||||
cache.close()
|
||||
} catch (ignored: IOException) {
|
||||
}
|
||||
onClose.run()
|
||||
} else {
|
||||
executor.submit {
|
||||
try {
|
||||
IOUtils.copy(`in`, cache)
|
||||
} catch (ignored: IOException) {
|
||||
} finally {
|
||||
try {
|
||||
`in`.close()
|
||||
} catch (ignored: IOException) {
|
||||
}
|
||||
try {
|
||||
cache.close()
|
||||
} catch (ignored: IOException) {
|
||||
}
|
||||
onClose.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(): Int {
|
||||
val ch = super.read()
|
||||
if (ch != IOUtils.EOF) {
|
||||
try {
|
||||
cache.write(ch)
|
||||
} catch (ignored: IOException) {
|
||||
// don't let write failures affect the image loading
|
||||
}
|
||||
} else {
|
||||
eofReached = true
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(bts: ByteArray, st: Int, end: Int): Int {
|
||||
val n = super.read(bts, st, end)
|
||||
if (n != IOUtils.EOF) {
|
||||
try {
|
||||
cache.write(bts, st, n)
|
||||
} catch (ignored: IOException) {
|
||||
// don't let write failures affect the image loading
|
||||
}
|
||||
} else {
|
||||
eofReached = true
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(bts: ByteArray): Int {
|
||||
val n = super.read(bts)
|
||||
if (n != IOUtils.EOF) {
|
||||
try {
|
||||
cache.write(bts, 0, n)
|
||||
} catch (ignored: IOException) {
|
||||
// don't let write failures affect the image loading
|
||||
}
|
||||
} else {
|
||||
eofReached = true
|
||||
}
|
||||
return n
|
||||
}
|
||||
}
|
179
src/main/kotlin/mdnet/cache/ImageStorage.kt
vendored
179
src/main/kotlin/mdnet/cache/ImageStorage.kt
vendored
|
@ -25,16 +25,18 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
|||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import mdnet.logging.info
|
||||
import mdnet.logging.trace
|
||||
import mdnet.logging.warn
|
||||
import org.apache.commons.io.file.PathUtils
|
||||
import org.ktorm.database.Database
|
||||
import org.ktorm.dsl.*
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.*
|
||||
import java.nio.file.*
|
||||
import java.sql.SQLIntegrityConstraintViolationException
|
||||
import java.sql.SQLException
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.*
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
|
||||
data class ImageMetadata(
|
||||
|
@ -55,13 +57,14 @@ data class Image(val data: ImageMetadata, val stream: InputStream)
|
|||
*/
|
||||
class ImageStorage(
|
||||
var maxSize: Long,
|
||||
val cacheDirectory: Path,
|
||||
private val cacheDirectory: Path,
|
||||
private val database: Database,
|
||||
autoPrune: Boolean = true
|
||||
) {
|
||||
) : AutoCloseable {
|
||||
private val tempCacheDirectory = cacheDirectory.resolve("tmp")
|
||||
private val databaseLock = ReentrantLock()
|
||||
|
||||
private val evictor: ScheduledExecutorService = Executors.newScheduledThreadPool(2)
|
||||
private val evictor: ScheduledExecutorService = Executors.newScheduledThreadPool(1)
|
||||
private val queue = LinkedBlockingQueue<String>(1000)
|
||||
|
||||
/**
|
||||
|
@ -89,34 +92,55 @@ class ImageStorage(
|
|||
|
||||
evictor.scheduleWithFixedDelay(
|
||||
{
|
||||
val toUpdate = HashSet<String>()
|
||||
queue.drainTo(toUpdate)
|
||||
val now = Instant.now()
|
||||
try {
|
||||
val toUpdate = HashSet<String>()
|
||||
queue.drainTo(toUpdate)
|
||||
|
||||
LOGGER.info { "Updating LRU times for ${toUpdate.size} entries" }
|
||||
database.batchUpdate(DbImage) {
|
||||
for (id in toUpdate) {
|
||||
item {
|
||||
set(DbImage.accessed, now)
|
||||
where {
|
||||
DbImage.id eq id
|
||||
}
|
||||
}
|
||||
if (toUpdate.isEmpty()) {
|
||||
LOGGER.info { "Updating LRU times for ${toUpdate.size} entries" }
|
||||
} else {
|
||||
LOGGER.info { "Skipping empty LRU update" }
|
||||
}
|
||||
|
||||
val now = Instant.now()
|
||||
|
||||
if (databaseLock.tryLock(500, TimeUnit.MILLISECONDS)) {
|
||||
try {
|
||||
database.batchUpdate(DbImage) {
|
||||
for (id in toUpdate) {
|
||||
item {
|
||||
set(DbImage.accessed, now)
|
||||
where {
|
||||
DbImage.id eq id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
databaseLock.unlock()
|
||||
}
|
||||
} else {
|
||||
LOGGER.warn { "High contention for database lock, bailing LRU update" }
|
||||
}
|
||||
calculateSize()
|
||||
} catch (e: Exception) {
|
||||
LOGGER.warn(e) { "Error updating LRU $this" }
|
||||
}
|
||||
calculateSize()
|
||||
},
|
||||
1, 1, TimeUnit.MINUTES
|
||||
15, 30, TimeUnit.SECONDS
|
||||
)
|
||||
|
||||
// evict LRU cache every 3 minutes
|
||||
if (autoPrune) {
|
||||
evictor.scheduleWithFixedDelay(
|
||||
{
|
||||
calculateSize()
|
||||
pruneImages()
|
||||
try {
|
||||
calculateSize()
|
||||
pruneImages()
|
||||
} catch (e: Exception) {
|
||||
LOGGER.warn(e) { "Error pruning images" }
|
||||
}
|
||||
},
|
||||
0, 3, TimeUnit.MINUTES
|
||||
0, 1, TimeUnit.MINUTES
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -130,28 +154,30 @@ class ImageStorage(
|
|||
LOGGER.info { "Cache at $size out of $maxSize bytes" }
|
||||
// we need to prune the cache now
|
||||
if (size > maxSize * 0.95) {
|
||||
val toClear = size - (maxSize * 0.9).toLong()
|
||||
var toClear = size - (maxSize * 0.9).toLong()
|
||||
LOGGER.info { "Evicting at least $toClear bytes from cache" }
|
||||
|
||||
val list = database.useConnection { conn ->
|
||||
conn.prepareStatement(IMAGES_TO_PRUNE).apply {
|
||||
setLong(1, toClear)
|
||||
}.use { stmt ->
|
||||
stmt.executeQuery().let {
|
||||
val ret = ArrayList<String>()
|
||||
while (toClear > 0) {
|
||||
val list = database.useConnection { conn ->
|
||||
conn.prepareStatement(IMAGES_TO_PRUNE).use { stmt ->
|
||||
stmt.executeQuery().let {
|
||||
val ret = ArrayList<Pair<String, Int>>()
|
||||
|
||||
while (it.next()) {
|
||||
ret.add(it.getString(1))
|
||||
while (it.next()) {
|
||||
ret.add(it.getString(1) to it.getInt(2))
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (id in list) {
|
||||
LOGGER.info { "Evicting images $id from cache" }
|
||||
deleteImage(id)
|
||||
val count = list.fold(0) { sum, (id, num) ->
|
||||
deleteImage(id)
|
||||
sum + num
|
||||
}
|
||||
LOGGER.info { "Evicting $count bytes from cache this loop" }
|
||||
toClear -= count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -216,24 +242,34 @@ class ImageStorage(
|
|||
private fun deleteImage(id: String) {
|
||||
LOGGER.trace { "Deleting image $id from cache" }
|
||||
|
||||
database.useTransaction {
|
||||
val path = getTempPath()
|
||||
val path = getTempPath()
|
||||
|
||||
try {
|
||||
Files.move(
|
||||
getPath(id),
|
||||
path,
|
||||
StandardCopyOption.ATOMIC_MOVE
|
||||
)
|
||||
|
||||
Files.deleteIfExists(path)
|
||||
} catch (_: IOException) {
|
||||
}
|
||||
|
||||
// it is safe, but not optimal, for the
|
||||
// DB write to fail after we've grabbed the file,
|
||||
// as that just inflates the count.
|
||||
// it will get resolved when the file gets grabbed again,
|
||||
// or if the cache gets pruned.
|
||||
if (databaseLock.tryLock(500, TimeUnit.MILLISECONDS)) {
|
||||
try {
|
||||
Files.move(
|
||||
getPath(id),
|
||||
path,
|
||||
StandardCopyOption.ATOMIC_MOVE
|
||||
)
|
||||
|
||||
Files.deleteIfExists(path)
|
||||
} catch (e: IOException) {
|
||||
// a failure means the image did not exist
|
||||
} finally {
|
||||
database.delete(DbImage) {
|
||||
DbImage.id eq id
|
||||
}
|
||||
} finally {
|
||||
databaseLock.unlock()
|
||||
}
|
||||
} else {
|
||||
LOGGER.warn { "High contention for database lock, bailing image delete write" }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -251,7 +287,7 @@ class ImageStorage(
|
|||
}
|
||||
}
|
||||
|
||||
fun close() {
|
||||
override fun close() {
|
||||
evictor.shutdown()
|
||||
evictor.awaitTermination(10, TimeUnit.SECONDS)
|
||||
}
|
||||
|
@ -282,12 +318,10 @@ class ImageStorage(
|
|||
|
||||
private inner class WriterImpl(private val id: String, metadata: ImageMetadata) : Writer {
|
||||
val tempPath = getTempPath()
|
||||
override val stream: OutputStream
|
||||
override val stream: OutputStream = Files.newOutputStream(tempPath, StandardOpenOption.CREATE_NEW)
|
||||
val metadataSize: Int
|
||||
|
||||
init {
|
||||
stream = Files.newOutputStream(tempPath, StandardOpenOption.CREATE_NEW)
|
||||
|
||||
val dataOutputStream = DataOutputStream(stream)
|
||||
dataOutputStream.writeUTF(
|
||||
JACKSON.writeValueAsString(metadata)
|
||||
|
@ -309,21 +343,27 @@ class ImageStorage(
|
|||
|
||||
Files.createDirectories(getPath(id).parent)
|
||||
|
||||
try {
|
||||
database.insert(DbImage) {
|
||||
set(DbImage.id, id)
|
||||
set(DbImage.accessed, Instant.now())
|
||||
set(DbImage.size, metadataSize + bytes)
|
||||
if (databaseLock.tryLock(500, TimeUnit.MILLISECONDS)) {
|
||||
try {
|
||||
database.insert(DbImage) {
|
||||
set(DbImage.id, id)
|
||||
set(DbImage.accessed, Instant.now())
|
||||
set(DbImage.size, metadataSize + bytes)
|
||||
}
|
||||
} catch (e: SQLException) {
|
||||
// someone got to us before this (TOCTOU)
|
||||
// there are 2 situations here
|
||||
// one is that the
|
||||
// other write died in between writing the DB and
|
||||
// moving the file
|
||||
// the other is that we have raced and the other
|
||||
// is about to write the file
|
||||
// we handle this below
|
||||
} finally {
|
||||
databaseLock.unlock()
|
||||
}
|
||||
} catch (e: SQLIntegrityConstraintViolationException) {
|
||||
// someone got to us before this (TOCTOU)
|
||||
// there are 2 situations here
|
||||
// one is that the
|
||||
// other write died in between writing the DB and
|
||||
// moving the file
|
||||
// the other is that we have raced and the other
|
||||
// is about to write the file
|
||||
// we handle this below
|
||||
} else {
|
||||
LOGGER.warn { "High contention for database lock, bailing DB write" }
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -332,7 +372,7 @@ class ImageStorage(
|
|||
getPath(id),
|
||||
StandardCopyOption.ATOMIC_MOVE
|
||||
)
|
||||
} catch (e: FileAlreadyExistsException) {
|
||||
} catch (e: IOException) {
|
||||
// the file already exists
|
||||
// so we must lost the race
|
||||
// delete our local copy
|
||||
|
@ -362,7 +402,6 @@ class ImageStorage(
|
|||
private val JACKSON: ObjectMapper = jacksonObjectMapper()
|
||||
|
||||
private fun String.toCachePath() =
|
||||
this.substring(0, 3).replace(".(?!$)".toRegex(), "$0 ").split(" ".toRegex()).reversed()
|
||||
.plus(this).joinToString(File.separator)
|
||||
this.substring(0, 3).split("").reversed().drop(1).joinToString(File.separator) + this
|
||||
}
|
||||
}
|
||||
|
|
10
src/main/kotlin/mdnet/cache/metadata.kt
vendored
10
src/main/kotlin/mdnet/cache/metadata.kt
vendored
|
@ -34,16 +34,12 @@ create table if not exists Images(
|
|||
accessed timestamp not null default CURRENT_TIMESTAMP,
|
||||
disk_size integer as ((size + 4095) / 4096 * 4096)
|
||||
);
|
||||
create index if not exists Images_lastAccessed_idx on Images(accessed, disk_size, id);
|
||||
drop index if exists Images_lastAccessed_idx;
|
||||
create index if not exists Images_accessed on Images(accessed);
|
||||
"""
|
||||
|
||||
const val SIZE_TAKEN_SQL = "select sum(disk_size) from Images"
|
||||
|
||||
const val IMAGES_TO_PRUNE = """
|
||||
select id from (
|
||||
select id, sum(disk_size)
|
||||
OVER (order by accessed rows unbounded preceding exclude current row)
|
||||
as RunningTotal from Images
|
||||
) as X
|
||||
WHERE coalesce(X.RunningTotal, 0) <= ?;
|
||||
select id, disk_size from Images order by accessed asc limit 1000
|
||||
"""
|
||||
|
|
|
@ -23,4 +23,4 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming
|
|||
import java.time.OffsetDateTime
|
||||
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
|
||||
data class Token(val expires: OffsetDateTime, val hash: String, val clientId: String? = null, val ip: String? = null)
|
||||
data class Token(val expires: OffsetDateTime, val hash: String, val clientId: String)
|
||||
|
|
|
@ -19,7 +19,6 @@ along with this MangaDex@Home. If not, see <http://www.gnu.org/licenses/>.
|
|||
package mdnet.metrics
|
||||
|
||||
import io.micrometer.core.instrument.Tag
|
||||
import io.micrometer.core.instrument.binder.jvm.DiskSpaceMetrics
|
||||
import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics
|
||||
import io.micrometer.core.instrument.binder.jvm.JvmHeapPressureMetrics
|
||||
import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics
|
||||
|
@ -30,9 +29,8 @@ import io.micrometer.core.instrument.binder.system.ProcessorMetrics
|
|||
import io.micrometer.core.instrument.binder.system.UptimeMetrics
|
||||
import io.micrometer.prometheus.PrometheusMeterRegistry
|
||||
import mdnet.BuildInfo
|
||||
import java.nio.file.Path
|
||||
|
||||
class DefaultMicrometerMetrics(registry: PrometheusMeterRegistry, cacheDirectory: Path) {
|
||||
class DefaultMicrometerMetrics(registry: PrometheusMeterRegistry) {
|
||||
init {
|
||||
UptimeMetrics(
|
||||
mutableListOf(
|
||||
|
@ -47,6 +45,5 @@ class DefaultMicrometerMetrics(registry: PrometheusMeterRegistry, cacheDirectory
|
|||
JvmHeapPressureMetrics().bindTo(registry)
|
||||
FileDescriptorMetrics().bindTo(registry)
|
||||
LogbackMetrics().bindTo(registry)
|
||||
DiskSpaceMetrics(cacheDirectory.toFile()).bindTo(registry)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,8 +25,7 @@ import io.micrometer.prometheus.PrometheusMeterRegistry
|
|||
import mdnet.logging.debug
|
||||
import mdnet.logging.warn
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.http4k.client.ApacheClient
|
||||
import org.http4k.client.OkHttp
|
||||
import org.http4k.core.Filter
|
||||
import org.http4k.core.HttpHandler
|
||||
import org.http4k.core.Method
|
||||
|
@ -37,7 +36,6 @@ import org.slf4j.Logger
|
|||
import org.slf4j.LoggerFactory
|
||||
import java.net.InetAddress
|
||||
import java.net.UnknownHostException
|
||||
import java.nio.file.Files
|
||||
|
||||
class GeoIpMetricsFilter(
|
||||
private val databaseReader: DatabaseReader?,
|
||||
|
@ -64,7 +62,10 @@ class GeoIpMetricsFilter(
|
|||
val inetAddress = InetAddress.getByName(sourceIp)
|
||||
if (!inetAddress.isLoopbackAddress && !inetAddress.isAnyLocalAddress) {
|
||||
val country = databaseReader!!.country(inetAddress)
|
||||
recordCountry(country.country.isoCode)
|
||||
|
||||
if (country.country.isoCode != null) {
|
||||
recordCountry(country.country.isoCode)
|
||||
}
|
||||
}
|
||||
} catch (e: GeoIp2Exception) {
|
||||
// do not disclose ip here, for privacy of logs
|
||||
|
@ -92,7 +93,8 @@ class GeoIpMetricsFilterBuilder(
|
|||
private val license: String,
|
||||
private val registry: PrometheusMeterRegistry,
|
||||
) {
|
||||
val client = ApacheClient()
|
||||
private val client = OkHttp()
|
||||
|
||||
fun build(): GeoIpMetricsFilter {
|
||||
return if (enableGeoIp) {
|
||||
LOGGER.info("GeoIp initialising")
|
||||
|
@ -105,32 +107,27 @@ class GeoIpMetricsFilterBuilder(
|
|||
}
|
||||
|
||||
private fun initDatabase(): DatabaseReader {
|
||||
val databaseFileDir = Files.createTempDirectory("mangadex-geoip")
|
||||
val databaseFile = Files.createTempFile(databaseFileDir, "geoip2_country", ".mmdb")
|
||||
|
||||
val geoIpDatabaseUri = GEOIP2_COUNTRY_URI_FORMAT.format(license)
|
||||
val response = client(Request(Method.GET, geoIpDatabaseUri))
|
||||
if (response.status != Status.OK) {
|
||||
throw IllegalStateException("Couldn't download GeoIP 2 database (http status: ${response.status})")
|
||||
}
|
||||
|
||||
response.use {
|
||||
val archiveStream = TarArchiveInputStream(it.body.gunzippedStream().stream)
|
||||
var entry = archiveStream.nextTarEntry
|
||||
while (!entry.name.endsWith(".mmdb")) {
|
||||
LOGGER.debug { "Skipped non-database file: ${entry.name}" }
|
||||
entry = archiveStream.nextTarEntry
|
||||
return response.use { data ->
|
||||
TarArchiveInputStream(data.body.gunzippedStream().stream).use {
|
||||
var entry = it.nextTarEntry
|
||||
while (!entry.name.endsWith(".mmdb")) {
|
||||
LOGGER.debug { "Skipped non-database file: ${entry.name}" }
|
||||
entry = it.nextTarEntry
|
||||
}
|
||||
|
||||
// reads only the current entry to its end
|
||||
DatabaseReader
|
||||
.Builder(it)
|
||||
.withCache(CHMCache())
|
||||
.build()
|
||||
}
|
||||
|
||||
// reads only the current entry to its end
|
||||
val dbBytes = IOUtils.toByteArray(archiveStream)
|
||||
Files.write(databaseFile, dbBytes)
|
||||
}
|
||||
|
||||
return DatabaseReader
|
||||
.Builder(databaseFile.toFile())
|
||||
.withCache(CHMCache())
|
||||
.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -28,7 +28,7 @@ class PostTransactionLabeler : HttpTransactionLabeler {
|
|||
"method" to transaction.request.method.toString(),
|
||||
"status" to transaction.response.status.code.toString(),
|
||||
"path" to transaction.routingGroup,
|
||||
"cache" to (transaction.response.header("X-Cache") ?: "MISS").toUpperCase()
|
||||
"cache" to (transaction.response.header("X-Cache") ?: "MISS").uppercase()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ along with this MangaDex@Home. If not, see <http://www.gnu.org/licenses/>.
|
|||
package mdnet.netty
|
||||
|
||||
import io.netty.bootstrap.ServerBootstrap
|
||||
import io.netty.buffer.ByteBuf
|
||||
import io.netty.channel.*
|
||||
import io.netty.channel.epoll.Epoll
|
||||
import io.netty.channel.epoll.EpollEventLoopGroup
|
||||
|
@ -27,9 +28,17 @@ import io.netty.channel.nio.NioEventLoopGroup
|
|||
import io.netty.channel.socket.SocketChannel
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel
|
||||
import io.netty.handler.codec.DecoderException
|
||||
import io.netty.handler.codec.ProtocolDetectionResult
|
||||
import io.netty.handler.codec.ProtocolDetectionState
|
||||
import io.netty.handler.codec.haproxy.HAProxyMessage
|
||||
import io.netty.handler.codec.haproxy.HAProxyMessageDecoder
|
||||
import io.netty.handler.codec.haproxy.HAProxyProtocolVersion
|
||||
import io.netty.handler.codec.http.FullHttpRequest
|
||||
import io.netty.handler.codec.http.HttpObjectAggregator
|
||||
import io.netty.handler.codec.http.HttpServerCodec
|
||||
import io.netty.handler.codec.http.HttpServerKeepAliveHandler
|
||||
import io.netty.handler.ssl.SniCompletionEvent
|
||||
import io.netty.handler.ssl.SniHandler
|
||||
import io.netty.handler.ssl.SslContextBuilder
|
||||
import io.netty.handler.stream.ChunkedWriteHandler
|
||||
import io.netty.handler.timeout.ReadTimeoutException
|
||||
|
@ -41,6 +50,10 @@ import io.netty.handler.traffic.TrafficCounter
|
|||
import io.netty.incubator.channel.uring.IOUring
|
||||
import io.netty.incubator.channel.uring.IOUringEventLoopGroup
|
||||
import io.netty.incubator.channel.uring.IOUringServerSocketChannel
|
||||
import io.netty.util.AttributeKey
|
||||
import io.netty.util.AttributeMap
|
||||
import io.netty.util.DomainWildcardMappingBuilder
|
||||
import io.netty.util.ReferenceCountUtil
|
||||
import io.netty.util.concurrent.DefaultEventExecutorGroup
|
||||
import io.netty.util.internal.SystemPropertyUtil
|
||||
import mdnet.Constants
|
||||
|
@ -48,8 +61,9 @@ import mdnet.data.Statistics
|
|||
import mdnet.logging.info
|
||||
import mdnet.logging.trace
|
||||
import mdnet.logging.warn
|
||||
import mdnet.settings.DevSettings
|
||||
import mdnet.settings.RemoteSettings
|
||||
import mdnet.settings.ServerSettings
|
||||
import mdnet.settings.TlsCert
|
||||
import org.http4k.core.HttpHandler
|
||||
import org.http4k.server.Http4kChannelHandler
|
||||
import org.http4k.server.Http4kServer
|
||||
|
@ -64,6 +78,7 @@ import java.security.PrivateKey
|
|||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.net.ssl.SSLException
|
||||
|
||||
sealed class NettyTransport(threads: Int) {
|
||||
|
@ -76,12 +91,6 @@ sealed class NettyTransport(threads: Int) {
|
|||
}
|
||||
)
|
||||
|
||||
fun shutdownGracefully() {
|
||||
bossGroup.shutdownGracefully().sync()
|
||||
workerGroup.shutdownGracefully().sync()
|
||||
executor.shutdownGracefully().sync()
|
||||
}
|
||||
|
||||
private class NioTransport(threads: Int) : NettyTransport(threads) {
|
||||
override val bossGroup = NioEventLoopGroup(1)
|
||||
override val workerGroup = NioEventLoopGroup(8)
|
||||
|
@ -105,10 +114,10 @@ sealed class NettyTransport(threads: Int) {
|
|||
private fun defaultNumThreads() = Runtime.getRuntime().availableProcessors() * 2
|
||||
|
||||
fun bestForPlatform(threads: Int): NettyTransport {
|
||||
val name = SystemPropertyUtil.get("os.name").toLowerCase(Locale.UK).trim { it <= ' ' }
|
||||
val name = SystemPropertyUtil.get("os.name").lowercase(Locale.US).trim { it <= ' ' }
|
||||
|
||||
val threadsToUse = if (threads == 0) defaultNumThreads() else threads
|
||||
LOGGER.info { "Choosing a transport using $threadsToUse threads" }
|
||||
LOGGER.info { "Choosing a transport with $threadsToUse threads" }
|
||||
|
||||
if (name.startsWith("linux")) {
|
||||
if (!SystemPropertyUtil.get("no-iouring").toBoolean()) {
|
||||
|
@ -116,8 +125,8 @@ sealed class NettyTransport(threads: Int) {
|
|||
LOGGER.info { "Using IOUring transport" }
|
||||
return IOUringTransport(threadsToUse)
|
||||
} else {
|
||||
LOGGER.info(IOUring.unavailabilityCause()) {
|
||||
"IOUring transport not available"
|
||||
LOGGER.info {
|
||||
"IOUring transport not available (this may be normal)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -127,8 +136,8 @@ sealed class NettyTransport(threads: Int) {
|
|||
LOGGER.info { "Using Epoll transport" }
|
||||
return EpollTransport(threadsToUse)
|
||||
} else {
|
||||
LOGGER.info(Epoll.unavailabilityCause()) {
|
||||
"Epoll transport not available"
|
||||
LOGGER.info {
|
||||
"Epoll transport not available (this may be normal)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -141,11 +150,12 @@ sealed class NettyTransport(threads: Int) {
|
|||
}
|
||||
|
||||
class Netty(
|
||||
private val tls: TlsCert,
|
||||
private val remoteSettings: RemoteSettings,
|
||||
private val serverSettings: ServerSettings,
|
||||
private val devSettings: DevSettings,
|
||||
private val statistics: Statistics
|
||||
) : ServerConfig {
|
||||
override fun toServer(httpHandler: HttpHandler): Http4kServer = object : Http4kServer {
|
||||
override fun toServer(http: HttpHandler): Http4kServer = object : Http4kServer {
|
||||
private val transport = NettyTransport.bestForPlatform(serverSettings.threads)
|
||||
|
||||
private lateinit var channel: Channel
|
||||
|
@ -160,11 +170,12 @@ class Netty(
|
|||
|
||||
override fun start(): Http4kServer = apply {
|
||||
LOGGER.info { "Starting Netty!" }
|
||||
val tls = remoteSettings.tls!!
|
||||
|
||||
val certs = getX509Certs(tls.certificate)
|
||||
val sslContext = SslContextBuilder
|
||||
.forServer(getPrivateKey(tls.privateKey), certs)
|
||||
.protocols("TLSv1.3", "TLSv1.2", "TLSv1.1", "TLSv1")
|
||||
.protocols("TLSv1.3", "TLSv1.2")
|
||||
.build()
|
||||
|
||||
val bootstrap = ServerBootstrap()
|
||||
|
@ -172,12 +183,88 @@ class Netty(
|
|||
.channelFactory(transport.factory)
|
||||
.childHandler(object : ChannelInitializer<SocketChannel>() {
|
||||
public override fun initChannel(ch: SocketChannel) {
|
||||
ch.pipeline().addLast("ssl", sslContext.newHandler(ch.alloc()))
|
||||
if (serverSettings.enableProxyProtocol) {
|
||||
ch.pipeline().addLast(
|
||||
"proxyProtocol",
|
||||
object : ChannelInboundHandlerAdapter() {
|
||||
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
|
||||
if (msg is ByteBuf) {
|
||||
// Since the builtin `HAProxyMessageDecoder` will break non Proxy-Protocol requests
|
||||
// we need to use its detection capabilities to only add it when needed.
|
||||
val result: ProtocolDetectionResult<HAProxyProtocolVersion> = HAProxyMessageDecoder.detectProtocol(msg)
|
||||
if (result.state() == ProtocolDetectionState.DETECTED) {
|
||||
ctx.pipeline().addAfter("proxyProtocol", null, HAProxyMessageDecoder())
|
||||
ctx.pipeline().remove(this)
|
||||
}
|
||||
}
|
||||
super.channelRead(ctx, msg)
|
||||
}
|
||||
}
|
||||
)
|
||||
ch.pipeline().addLast(
|
||||
"saveOriginalIp",
|
||||
object : SimpleChannelInboundHandler<HAProxyMessage>() {
|
||||
override fun channelRead0(ctx: ChannelHandlerContext, msg: HAProxyMessage) {
|
||||
// Store proxy IP in an attribute for later use after HTTP request is extracted.
|
||||
// Using an attribute ensures the value is scoped to this channel.
|
||||
(ctx as AttributeMap).attr(HAPROXY_SOURCE).set(msg.sourceAddress())
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
ch.pipeline().addLast(
|
||||
"ssl",
|
||||
SniHandler(DomainWildcardMappingBuilder(sslContext).build())
|
||||
)
|
||||
|
||||
ch.pipeline().addLast(
|
||||
"dropHostname",
|
||||
object : ChannelInboundHandlerAdapter() {
|
||||
private val hostToTest = remoteSettings.url.authority.let {
|
||||
it.substring(0, it.lastIndexOf(":"))
|
||||
}
|
||||
|
||||
override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) {
|
||||
if (evt is SniCompletionEvent) {
|
||||
if (!devSettings.disableSniCheck) {
|
||||
if (evt.hostname() != null &&
|
||||
!evt.hostname().endsWith(hostToTest) &&
|
||||
!evt.hostname().endsWith("localhost")
|
||||
) {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ctx.fireUserEventTriggered(evt)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ch.pipeline().addLast("codec", HttpServerCodec())
|
||||
ch.pipeline().addLast("keepAlive", HttpServerKeepAliveHandler())
|
||||
ch.pipeline().addLast("aggregator", HttpObjectAggregator(65536))
|
||||
|
||||
if (serverSettings.enableProxyProtocol) {
|
||||
ch.pipeline().addLast(
|
||||
"setForwardHeader",
|
||||
object : SimpleChannelInboundHandler<FullHttpRequest>(false) {
|
||||
override fun channelRead0(ctx: ChannelHandlerContext, request: FullHttpRequest) {
|
||||
// The geo-location code already supports the `Forwarded` header so setting
|
||||
// it is the easiest way to introduce the original IP downstream.
|
||||
if ((ctx as AttributeMap).hasAttr(HAPROXY_SOURCE)) {
|
||||
val addr = (ctx as AttributeMap).attr(HAPROXY_SOURCE).get()
|
||||
request.headers().set("Forwarded", addr)
|
||||
}
|
||||
// Since we're modifying the request without handling it, we must
|
||||
// call retain to ensure it will still be available downstream.
|
||||
ReferenceCountUtil.retain(request)
|
||||
ctx.fireChannelRead(request)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
ch.pipeline().addLast("burstLimiter", burstLimiter)
|
||||
|
||||
ch.pipeline().addLast(
|
||||
|
@ -190,7 +277,7 @@ class Netty(
|
|||
)
|
||||
|
||||
ch.pipeline().addLast("streamer", ChunkedWriteHandler())
|
||||
ch.pipeline().addLast(transport.executor, "handler", Http4kChannelHandler(httpHandler))
|
||||
ch.pipeline().addLast(transport.executor, "handler", Http4kChannelHandler(http))
|
||||
|
||||
ch.pipeline().addLast(
|
||||
"exceptions",
|
||||
|
@ -215,15 +302,20 @@ class Netty(
|
|||
}
|
||||
|
||||
override fun stop() = apply {
|
||||
transport.shutdownGracefully()
|
||||
channel.closeFuture().sync()
|
||||
channel.close().sync()
|
||||
transport.run {
|
||||
bossGroup.shutdownGracefully(0, 500, TimeUnit.MILLISECONDS).sync()
|
||||
workerGroup.shutdownGracefully(0, 500, TimeUnit.MILLISECONDS).sync()
|
||||
executor.shutdownGracefully(0, 500, TimeUnit.MILLISECONDS).sync()
|
||||
}
|
||||
}
|
||||
|
||||
override fun port(): Int = serverSettings.port
|
||||
override fun port(): Int = (channel.localAddress() as InetSocketAddress).port
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOGGER = LoggerFactory.getLogger(Netty::class.java)
|
||||
private val HAPROXY_SOURCE = AttributeKey.newInstance<String>("haproxy_source")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ class ImageServer(
|
|||
registry: PrometheusMeterRegistry
|
||||
) {
|
||||
private val executor = Executors.newCachedThreadPool()
|
||||
private val cacheLookupTimer = Timer.builder("cache_lookup")
|
||||
private val cacheLookupTimer = Timer.builder("cache.lookup")
|
||||
.publishPercentiles(0.5, 0.75, 0.9, 0.99)
|
||||
.register(registry)
|
||||
|
||||
|
@ -147,7 +147,7 @@ class ImageServer(
|
|||
}
|
||||
respondWithImage(tee, contentLength, contentType, lastModified, false)
|
||||
} else {
|
||||
LOGGER.info { "Request for $sanitizedUri is being served due to write errors" }
|
||||
LOGGER.info { "Request for $sanitizedUri is being served as the cache is full" }
|
||||
respondWithImage(mdResponse.body.stream, contentLength, contentType, lastModified, false)
|
||||
}
|
||||
}
|
||||
|
@ -174,17 +174,17 @@ class ImageServer(
|
|||
|
||||
companion object {
|
||||
private val LOGGER = LoggerFactory.getLogger(ImageServer::class.java)
|
||||
private fun String.isImageMimetype() = this.toLowerCase().startsWith("image/")
|
||||
private fun String.isImageMimetype() = this.lowercase().startsWith("image/")
|
||||
|
||||
private fun baseHandler(): Filter =
|
||||
CachingFilters.Response.MaxAge(Clock.systemUTC(), Constants.MAX_AGE_CACHE)
|
||||
.then { next: HttpHandler ->
|
||||
{ request: Request ->
|
||||
val response = next(request)
|
||||
response.header("access-control-allow-origin", "https://mangadex.org")
|
||||
.header("access-control-allow-headers", "*")
|
||||
response.header("access-control-allow-origin", "*")
|
||||
.header("access-control-expose-headers", "*")
|
||||
.header("access-control-allow-methods", "GET")
|
||||
.header("timing-allow-origin", "https://mangadex.org")
|
||||
.header("timing-allow-origin", "*")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig
|
|||
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry
|
||||
import io.github.resilience4j.micrometer.tagged.TaggedCircuitBreakerMetrics
|
||||
import io.micrometer.core.instrument.FunctionCounter
|
||||
import io.micrometer.core.instrument.binder.BaseUnits
|
||||
import io.micrometer.prometheus.PrometheusMeterRegistry
|
||||
import mdnet.cache.ImageStorage
|
||||
import mdnet.data.Statistics
|
||||
|
@ -30,6 +31,7 @@ import mdnet.logging.warn
|
|||
import mdnet.metrics.GeoIpMetricsFilterBuilder
|
||||
import mdnet.metrics.PostTransactionLabeler
|
||||
import mdnet.netty.Netty
|
||||
import mdnet.settings.DevSettings
|
||||
import mdnet.settings.MetricsSettings
|
||||
import mdnet.settings.RemoteSettings
|
||||
import mdnet.settings.ServerSettings
|
||||
|
@ -45,12 +47,17 @@ fun getServer(
|
|||
storage: ImageStorage,
|
||||
remoteSettings: RemoteSettings,
|
||||
serverSettings: ServerSettings,
|
||||
devSettings: DevSettings,
|
||||
metricsSettings: MetricsSettings,
|
||||
statistics: Statistics,
|
||||
registry: PrometheusMeterRegistry,
|
||||
client: HttpHandler
|
||||
): Http4kServer {
|
||||
val circuitBreakerRegistry = CircuitBreakerRegistry.ofDefaults()
|
||||
TaggedCircuitBreakerMetrics
|
||||
.ofCircuitBreakerRegistry(circuitBreakerRegistry)
|
||||
.bindTo(registry)
|
||||
|
||||
val circuitBreaker = circuitBreakerRegistry.circuitBreaker(
|
||||
"upstream",
|
||||
CircuitBreakerConfig.custom()
|
||||
|
@ -70,20 +77,16 @@ fun getServer(
|
|||
}
|
||||
|
||||
circuitBreaker.eventPublisher.onReset {
|
||||
LOGGER.warn { "Circuit breaker has rest" }
|
||||
LOGGER.warn { "Circuit breaker has reset" }
|
||||
}
|
||||
|
||||
circuitBreaker.eventPublisher.onStateTransition {
|
||||
LOGGER.warn { "Circuit breaker has moved from ${it.stateTransition.fromState} to ${it.stateTransition.toState}" }
|
||||
}
|
||||
|
||||
TaggedCircuitBreakerMetrics
|
||||
.ofCircuitBreakerRegistry(circuitBreakerRegistry)
|
||||
.bindTo(registry)
|
||||
|
||||
val circuited = ResilienceFilters.CircuitBreak(
|
||||
circuitBreaker,
|
||||
isError = { r: Response -> !r.status.successful }
|
||||
isError = { r: Response -> r.status.serverError }
|
||||
)
|
||||
|
||||
val upstream = ClientFilters.MicrometerMetrics.RequestTimer(registry)
|
||||
|
@ -98,20 +101,22 @@ fun getServer(
|
|||
)
|
||||
|
||||
FunctionCounter.builder(
|
||||
"client_sent_bytes",
|
||||
statistics,
|
||||
{ it.bytesSent.get().toDouble() }
|
||||
).register(registry)
|
||||
"client.sent",
|
||||
statistics
|
||||
) { it.bytesSent.get().toDouble() }
|
||||
.baseUnit(BaseUnits.BYTES).register(registry)
|
||||
|
||||
val verifier = TokenVerifier(
|
||||
tokenKey = remoteSettings.tokenKey,
|
||||
shouldVerify = { chapter, _ ->
|
||||
!remoteSettings.disableTokens && !(chapter == "1b682e7b24ae7dbdc5064eeeb8e8e353" || chapter == "8172a46adc798f4f4ace6663322a383e")
|
||||
}
|
||||
isDisabled = devSettings.disableTokenValidation,
|
||||
)
|
||||
|
||||
if (devSettings.disableTokenValidation) {
|
||||
LOGGER.warn { "Token validation has been explicitly disabled. This should only be used for testing!" }
|
||||
}
|
||||
|
||||
return timeRequest()
|
||||
.then(addCommonHeaders())
|
||||
.then(addCommonHeaders(devSettings.sendServerHeader))
|
||||
.then(catchAllHideDetails())
|
||||
.then(
|
||||
routes(
|
||||
|
@ -144,7 +149,7 @@ fun getServer(
|
|||
GeoIpMetricsFilterBuilder(metricsSettings.enableGeoip, metricsSettings.geoipLicenseKey, registry).build()
|
||||
)
|
||||
)
|
||||
.asServer(Netty(remoteSettings.tls!!, serverSettings, statistics))
|
||||
.asServer(Netty(remoteSettings, serverSettings, devSettings, statistics))
|
||||
}
|
||||
|
||||
private val LOGGER = LoggerFactory.getLogger(ImageServer::class.java)
|
||||
|
|
|
@ -37,56 +37,57 @@ import org.slf4j.LoggerFactory
|
|||
import java.time.OffsetDateTime
|
||||
import java.util.Base64
|
||||
|
||||
class TokenVerifier(tokenKey: ByteArray, private val shouldVerify: (String, String) -> Boolean) : Filter {
|
||||
class TokenVerifier(tokenKey: ByteArray, private val isDisabled: Boolean) : Filter {
|
||||
private val box = TweetNaclFast.SecretBox(tokenKey)
|
||||
|
||||
override fun invoke(next: HttpHandler): HttpHandler {
|
||||
return then@{
|
||||
if (isDisabled) {
|
||||
return@then next(it)
|
||||
}
|
||||
|
||||
val chapterHash = Path.of("chapterHash")(it)
|
||||
val fileName = Path.of("fileName")(it)
|
||||
|
||||
if (shouldVerify(chapterHash, fileName)) {
|
||||
val cleanedUri = it.uri.path.replaceBefore("/data", "/{token}")
|
||||
val cleanedUri = it.uri.path.replaceBefore("/data", "/{token}")
|
||||
|
||||
val tokenArr = try {
|
||||
val toDecode = try {
|
||||
Path.of("token")(it)
|
||||
} catch (e: LensFailure) {
|
||||
LOGGER.info(e) { "Request for $cleanedUri rejected for missing token" }
|
||||
return@then Response(Status.FORBIDDEN).body("Token is missing")
|
||||
}
|
||||
Base64.getUrlDecoder().decode(toDecode)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
LOGGER.info(e) { "Request for $cleanedUri rejected for non-base64 token" }
|
||||
return@then Response(Status.FORBIDDEN).body("Token is invalid base64")
|
||||
val tokenArr = try {
|
||||
val toDecode = try {
|
||||
Path.of("token")(it)
|
||||
} catch (e: LensFailure) {
|
||||
LOGGER.info(e) { "Request for $cleanedUri rejected for missing token" }
|
||||
return@then Response(Status.FORBIDDEN).body("Token is missing")
|
||||
}
|
||||
if (tokenArr.size < 24) {
|
||||
LOGGER.info { "Request for $cleanedUri rejected for invalid token" }
|
||||
return@then Response(Status.FORBIDDEN)
|
||||
}
|
||||
val token = try {
|
||||
JACKSON.readValue<Token>(
|
||||
box.open(tokenArr.sliceArray(24 until tokenArr.size), tokenArr.sliceArray(0 until 24)).apply {
|
||||
if (this == null) {
|
||||
LOGGER.info { "Request for $cleanedUri rejected for invalid token" }
|
||||
return@then Response(Status.FORBIDDEN)
|
||||
}
|
||||
Base64.getUrlDecoder().decode(toDecode)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
LOGGER.info(e) { "Request for $cleanedUri rejected for non-base64 token" }
|
||||
return@then Response(Status.FORBIDDEN).body("Token is invalid base64")
|
||||
}
|
||||
if (tokenArr.size < 24) {
|
||||
LOGGER.info { "Request for $cleanedUri rejected for invalid token" }
|
||||
return@then Response(Status.FORBIDDEN)
|
||||
}
|
||||
val token = try {
|
||||
JACKSON.readValue<Token>(
|
||||
box.open(tokenArr.sliceArray(24 until tokenArr.size), tokenArr.sliceArray(0 until 24)).apply {
|
||||
if (this == null) {
|
||||
LOGGER.info { "Request for $cleanedUri rejected for invalid token" }
|
||||
return@then Response(Status.FORBIDDEN)
|
||||
}
|
||||
)
|
||||
} catch (e: JsonProcessingException) {
|
||||
LOGGER.info(e) { "Request for $cleanedUri rejected for invalid token" }
|
||||
return@then Response(Status.FORBIDDEN).body("Token is invalid")
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (e: JsonProcessingException) {
|
||||
LOGGER.info(e) { "Request for $cleanedUri rejected for invalid token" }
|
||||
return@then Response(Status.FORBIDDEN).body("Token is invalid")
|
||||
}
|
||||
|
||||
if (OffsetDateTime.now().isAfter(token.expires)) {
|
||||
LOGGER.info { "Request for $cleanedUri rejected for expired token" }
|
||||
return@then Response(Status.GONE).body("Token has expired")
|
||||
}
|
||||
if (OffsetDateTime.now().isAfter(token.expires)) {
|
||||
LOGGER.info { "Request for $cleanedUri rejected for expired token" }
|
||||
return@then Response(Status.GONE).body("Token has expired")
|
||||
}
|
||||
|
||||
if (token.hash != chapterHash) {
|
||||
LOGGER.info { "Request for $cleanedUri rejected for inapplicable token" }
|
||||
return@then Response(Status.FORBIDDEN).body("Token is inapplicable for the image")
|
||||
}
|
||||
if (token.hash != chapterHash) {
|
||||
LOGGER.info { "Request for $cleanedUri rejected for inapplicable token" }
|
||||
return@then Response(Status.FORBIDDEN).body("Token is inapplicable for the image")
|
||||
}
|
||||
|
||||
return@then next(it)
|
||||
|
|
|
@ -35,12 +35,17 @@ import java.util.*
|
|||
private val HTTP_TIME_FORMATTER = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss O", Locale.ENGLISH)
|
||||
private val LOGGER = LoggerFactory.getLogger("Application")
|
||||
|
||||
fun addCommonHeaders(): Filter {
|
||||
fun addCommonHeaders(sendServerHeader: Boolean): Filter {
|
||||
return Filter { next: HttpHandler ->
|
||||
{ request: Request ->
|
||||
val response = next(request)
|
||||
response.header("Date", HTTP_TIME_FORMATTER.format(ZonedDateTime.now(ZoneOffset.UTC)))
|
||||
.header("Server", "MangaDex@Home Node ${BuildInfo.VERSION} (${Constants.CLIENT_BUILD})")
|
||||
response.header("Date", HTTP_TIME_FORMATTER.format(ZonedDateTime.now(ZoneOffset.UTC))).let {
|
||||
if (sendServerHeader) {
|
||||
it.header("Server", "MangaDex@Home Node ${BuildInfo.VERSION} (${Constants.CLIENT_BUILD})")
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +56,8 @@ fun catchAllHideDetails(): Filter {
|
|||
try {
|
||||
next(request)
|
||||
} catch (e: Exception) {
|
||||
LOGGER.warn(e) { "Request error detected" }
|
||||
val cleanedUri = request.uri.path.replaceBefore("/data", "/{token}")
|
||||
LOGGER.warn(e) { "Request for $cleanedUri errored" }
|
||||
|
||||
Response(Status.INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ package mdnet.server
|
|||
|
||||
import java.security.MessageDigest
|
||||
|
||||
fun md5Bytes(stringToHash: String) =
|
||||
fun md5Bytes(stringToHash: String): ByteArray =
|
||||
MessageDigest.getInstance("MD5")
|
||||
.digest(stringToHash.toByteArray())
|
||||
|
||||
|
|
|
@ -20,7 +20,6 @@ package mdnet.settings
|
|||
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming
|
||||
import dev.afanasev.sekret.Secret
|
||||
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
|
||||
data class ClientSettings(
|
||||
|
@ -33,7 +32,7 @@ data class ClientSettings(
|
|||
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
|
||||
data class ServerSettings(
|
||||
@field:Secret val secret: String,
|
||||
val secret: String,
|
||||
val externalPort: Int = 0,
|
||||
val gracefulShutdownWaitSeconds: Int = 60,
|
||||
val hostname: String = "0.0.0.0",
|
||||
|
@ -43,15 +42,27 @@ data class ServerSettings(
|
|||
val externalIp: String? = null,
|
||||
val port: Int = 443,
|
||||
val threads: Int = 0,
|
||||
)
|
||||
val enableProxyProtocol: Boolean = false,
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "ServerSettings(secret=<redacted>, externalPort=$externalPort, gracefulShutdownWaitSeconds=$gracefulShutdownWaitSeconds, hostname='$hostname', maxKilobitsPerSecond=$maxKilobitsPerSecond, externalMaxKilobitsPerSecond=$externalMaxKilobitsPerSecond, maxMebibytesPerHour=$maxMebibytesPerHour, externalIp=$externalIp, port=$port, threads=$threads, enableProxyProtocol=$enableProxyProtocol)"
|
||||
}
|
||||
}
|
||||
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
|
||||
data class DevSettings(
|
||||
val devUrl: String? = null
|
||||
val devUrl: String? = null,
|
||||
val disableSniCheck: Boolean = false,
|
||||
val sendServerHeader: Boolean = false,
|
||||
val disableTokenValidation: Boolean = false,
|
||||
)
|
||||
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
|
||||
data class MetricsSettings(
|
||||
val enableGeoip: Boolean = false,
|
||||
@field:Secret val geoipLicenseKey: String = "none"
|
||||
)
|
||||
val geoipLicenseKey: String = "none"
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "MetricsSettings(enableGeoip=$enableGeoip, geoipLicenseKey=<redacted>)"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ package mdnet.settings
|
|||
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming
|
||||
import dev.afanasev.sekret.Secret
|
||||
import org.http4k.core.Uri
|
||||
|
||||
sealed class PingResult
|
||||
|
@ -36,12 +35,14 @@ data class RemoteSettings(
|
|||
val imageServer: Uri,
|
||||
val latestBuild: Int,
|
||||
val url: Uri,
|
||||
@field:Secret val tokenKey: ByteArray,
|
||||
val clientId: String,
|
||||
val tokenKey: ByteArray,
|
||||
val compromised: Boolean,
|
||||
val paused: Boolean,
|
||||
val disableTokens: Boolean = false,
|
||||
val tls: TlsCert?
|
||||
) : PingResult() {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
@ -51,6 +52,7 @@ data class RemoteSettings(
|
|||
if (imageServer != other.imageServer) return false
|
||||
if (latestBuild != other.latestBuild) return false
|
||||
if (url != other.url) return false
|
||||
if (clientId != other.clientId) return false
|
||||
if (!tokenKey.contentEquals(other.tokenKey)) return false
|
||||
if (compromised != other.compromised) return false
|
||||
if (paused != other.paused) return false
|
||||
|
@ -64,6 +66,7 @@ data class RemoteSettings(
|
|||
var result = imageServer.hashCode()
|
||||
result = 31 * result + latestBuild
|
||||
result = 31 * result + url.hashCode()
|
||||
result = 31 * result + clientId.hashCode()
|
||||
result = 31 * result + tokenKey.contentHashCode()
|
||||
result = 31 * result + compromised.hashCode()
|
||||
result = 31 * result + paused.hashCode()
|
||||
|
@ -71,11 +74,19 @@ data class RemoteSettings(
|
|||
result = 31 * result + (tls?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "RemoteSettings(imageServer=$imageServer, latestBuild=$latestBuild, url=$url, clientId='$clientId', tokenKey=<redacted>, compromised=$compromised, paused=$paused, disableTokens=$disableTokens, tls=$tls)"
|
||||
}
|
||||
}
|
||||
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
|
||||
data class TlsCert(
|
||||
val createdAt: String,
|
||||
@field:Secret val privateKey: String,
|
||||
@field:Secret val certificate: String
|
||||
)
|
||||
val privateKey: String,
|
||||
val certificate: String
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "TlsCert(createdAt='$createdAt', privateKey=<redacted>, certificate=<redacted>)"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,20 +20,27 @@ package mdnet.settings
|
|||
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming
|
||||
import dev.afanasev.sekret.Secret
|
||||
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
|
||||
data class SettingsRequest(
|
||||
@field:Secret val secret: String,
|
||||
val secret: String,
|
||||
val ipAddress: String?,
|
||||
val port: Int,
|
||||
val diskSpace: Long,
|
||||
val networkSpeed: Long,
|
||||
val buildVersion: Int,
|
||||
val tlsCreatedAt: String?,
|
||||
)
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "SettingsRequest(secret=<redacted>, ipAddress=$ipAddress, port=$port, diskSpace=$diskSpace, networkSpeed=$networkSpeed, buildVersion=$buildVersion, tlsCreatedAt=$tlsCreatedAt)"
|
||||
}
|
||||
}
|
||||
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
|
||||
data class LogoutRequest(
|
||||
@field:Secret val secret: String,
|
||||
)
|
||||
val secret: String,
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "LogoutRequest(secret=<redacted>)"
|
||||
}
|
||||
}
|
||||
|
|
30
src/test/kotlin/mdnet/cache/ImageStorageTest.kt
vendored
30
src/test/kotlin/mdnet/cache/ImageStorageTest.kt
vendored
|
@ -32,18 +32,20 @@ import io.kotest.matchers.nulls.shouldNotBeNull
|
|||
import io.kotest.matchers.shouldBe
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.ktorm.database.Database
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.minutes
|
||||
|
||||
class ImageStorageTest : FreeSpec() {
|
||||
override fun isolationMode() = IsolationMode.InstancePerTest
|
||||
|
||||
init {
|
||||
val imageStorage = ImageStorage(
|
||||
maxSize = 5,
|
||||
cacheDirectory = tempdir().toPath(),
|
||||
database = Database.connect("jdbc:h2:${tempfile()}"),
|
||||
autoPrune = false,
|
||||
val imageStorage = autoClose(
|
||||
ImageStorage(
|
||||
maxSize = 5,
|
||||
cacheDirectory = tempdir().toPath(),
|
||||
database = Database.connect("jdbc:sqlite:${tempfile()}"),
|
||||
autoPrune = false,
|
||||
)
|
||||
)
|
||||
|
||||
val testMeta = ImageMetadata("a", "a", 123)
|
||||
|
@ -81,7 +83,8 @@ class ImageStorageTest : FreeSpec() {
|
|||
writer.stream.write(ByteArray(12))
|
||||
writer.abort()
|
||||
|
||||
"should not update size" {
|
||||
"should not update size even if calculated" {
|
||||
imageStorage.calculateSize()
|
||||
imageStorage.size.shouldBeZero()
|
||||
}
|
||||
}
|
||||
|
@ -157,14 +160,17 @@ class ImageStorageSlowTest : FreeSpec() {
|
|||
override fun isolationMode() = IsolationMode.InstancePerTest
|
||||
|
||||
init {
|
||||
val imageStorage = ImageStorage(
|
||||
maxSize = 4097,
|
||||
cacheDirectory = tempdir().toPath(),
|
||||
database = Database.connect("jdbc:h2:${tempfile()}"),
|
||||
val imageStorage = autoClose(
|
||||
ImageStorage(
|
||||
maxSize = 4097,
|
||||
cacheDirectory = tempdir().toPath(),
|
||||
database = Database.connect("jdbc:sqlite:${tempfile()}"),
|
||||
)
|
||||
)
|
||||
|
||||
"autoPrune" - {
|
||||
"should update size eventually" {
|
||||
println("1 - $imageStorage")
|
||||
val writer = imageStorage.storeImage("test", ImageMetadata("a", "a", 4096))
|
||||
writer.shouldNotBeNull()
|
||||
|
||||
|
@ -177,6 +183,7 @@ class ImageStorageSlowTest : FreeSpec() {
|
|||
}
|
||||
|
||||
"should prune if insufficient size eventually" {
|
||||
println("2 - $imageStorage")
|
||||
imageStorage.maxSize = 10000
|
||||
|
||||
val writer = imageStorage.storeImage("test", ImageMetadata("a", "a", 123))
|
||||
|
@ -185,6 +192,7 @@ class ImageStorageSlowTest : FreeSpec() {
|
|||
writer.stream.write(ByteArray(8192))
|
||||
writer.commit(8192).shouldBeTrue()
|
||||
|
||||
imageStorage.calculateSize()
|
||||
eventually(5.minutes) {
|
||||
imageStorage.size.shouldBeZero()
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ import org.http4k.core.Response
|
|||
import org.http4k.core.Status
|
||||
import org.http4k.kotest.shouldHaveHeader
|
||||
import org.http4k.kotest.shouldHaveStatus
|
||||
import org.http4k.kotest.shouldNotHaveHeader
|
||||
import org.http4k.routing.bind
|
||||
import org.http4k.routing.routes
|
||||
import org.ktorm.database.Database
|
||||
|
@ -106,14 +107,22 @@ class ImageServerTest : FreeSpec() {
|
|||
response.close()
|
||||
}
|
||||
}
|
||||
|
||||
"should not have Server header" {
|
||||
val response = handler(Request(Method.GET, "/data/02181a8f5fe8cd408720a771dd129fd8/T2.png"))
|
||||
response.shouldNotHaveHeader("Server")
|
||||
response.close()
|
||||
}
|
||||
}
|
||||
|
||||
"with real cache" - {
|
||||
val storage = ImageStorage(
|
||||
maxSize = 100000,
|
||||
cacheDirectory = tempdir().toPath(),
|
||||
database = Database.connect("jdbc:h2:${tempfile()}"),
|
||||
autoPrune = false,
|
||||
val storage = autoClose(
|
||||
ImageStorage(
|
||||
maxSize = 100000,
|
||||
cacheDirectory = tempdir().toPath(),
|
||||
database = Database.connect("jdbc:sqlite:${tempfile()}"),
|
||||
autoPrune = false,
|
||||
)
|
||||
)
|
||||
|
||||
val server = ImageServer(
|
||||
|
@ -154,11 +163,13 @@ class ImageServerTest : FreeSpec() {
|
|||
"failed upstream responses" - {
|
||||
val client = mockk<HttpHandler>()
|
||||
|
||||
val storage = ImageStorage(
|
||||
maxSize = 100000,
|
||||
cacheDirectory = tempdir().toPath(),
|
||||
database = Database.connect("jdbc:h2:${tempfile()}"),
|
||||
autoPrune = false,
|
||||
val storage = autoClose(
|
||||
ImageStorage(
|
||||
maxSize = 100000,
|
||||
cacheDirectory = tempdir().toPath(),
|
||||
database = Database.connect("jdbc:sqlite:${tempfile()}"),
|
||||
autoPrune = false,
|
||||
)
|
||||
)
|
||||
|
||||
val server = ImageServer(
|
||||
|
|
|
@ -31,9 +31,7 @@ class TokenVerifierTest : FreeSpec() {
|
|||
val clientKeys = TweetNaclFast.Box.keyPair()
|
||||
val box = TweetNaclFast.Box(clientKeys.publicKey, remoteKeys.secretKey)
|
||||
|
||||
val backend = TokenVerifier(box.before()) { _, _ ->
|
||||
true
|
||||
}.then {
|
||||
val backend = TokenVerifier(box.before(), false).then {
|
||||
Response(Status.OK)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue