mirror of
https://gitlab.com/mangadex-pub/mangadex_at_home.git
synced 2024-01-19 02:48:37 +00:00
Compare commits
48 commits
2.0.0-rc14
...
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 |
1
.gitattributes
vendored
1
.gitattributes
vendored
|
@ -1 +1,2 @@
|
|||
CHANGELOG.md merge=union
|
||||
* text=auto eol=lf
|
||||
|
|
|
@ -1,36 +1,42 @@
|
|||
stages:
|
||||
- build
|
||||
- publish
|
||||
- publish_latest
|
||||
- publish_docker
|
||||
- docker
|
||||
- push
|
||||
|
||||
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:
|
||||
- export VERSION="$CI_COMMIT_REF_NAME"
|
||||
- ./gradlew build
|
||||
- "ls -lah build/libs"
|
||||
cache:
|
||||
key: "mangadex_at_home-build"
|
||||
paths:
|
||||
- /root/.gradle
|
||||
variables:
|
||||
KUBERNETES_MEMORY_REQUEST: 3Gi
|
||||
KUBERNETES_MEMORY_LIMIT: 3Gi
|
||||
artifacts:
|
||||
name: "mangadex_at_home"
|
||||
paths:
|
||||
- "build/libs/mangadex_at_home-*-all.jar"
|
||||
|
||||
publish:
|
||||
Publish Artifacts:
|
||||
image: alpine
|
||||
stage: publish
|
||||
needs:
|
||||
- Gradle Build
|
||||
variables:
|
||||
GIT_STRATEGY: none
|
||||
|
||||
before_script:
|
||||
- apk update && apk add git zip
|
||||
- export VERSION="$CI_COMMIT_REF_NAME"
|
||||
- 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.json
|
||||
dependencies:
|
||||
- build
|
||||
- zip -r9 mangadex_at_home-${VERSION}.zip mangadex_at_home-${VERSION}-all.jar settings.sample.yaml
|
||||
artifacts:
|
||||
name: "mangadex_at_home"
|
||||
paths:
|
||||
|
@ -38,19 +44,58 @@ publish:
|
|||
- "mangadex_at_home-*.zip"
|
||||
- "settings.sample.yaml"
|
||||
|
||||
publish_docker:
|
||||
image: docker:git
|
||||
stage: publish
|
||||
only:
|
||||
- tags
|
||||
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="$CI_COMMIT_REF_NAME"
|
||||
dependencies:
|
||||
- build
|
||||
- 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}
|
||||
- docker push ${CI_REGISTRY_IMAGE}:${VERSION}
|
||||
- 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
|
||||
|
|
61
CHANGELOG.md
61
CHANGELOG.md
|
@ -17,15 +17,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Security
|
||||
|
||||
## [2.0.0-rc14] - 2021-03-02
|
||||
## [2.0.4] - 2023-08-07
|
||||
### Changed
|
||||
- [2020-02-10] Fix Prometheus to 2.24.1 and Grafana to 7.4.0 [@_tde9].
|
||||
- [2020-02-10] Update and rearrange the embedded dashboard with the new Timeseries panel from Grafana 7.4 [@_tde9].
|
||||
- [2020-02-10] Update sample dashboard screenshot thanks to DLMSweet :smile: [@_tde9].
|
||||
- [2020-02-25] Use HTTP/2 to download when possible [@carbotaniuman].
|
||||
- [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
|
||||
- [2020-02-21] Fix pipeline [@_tde9].
|
||||
- [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
|
||||
|
@ -375,7 +417,12 @@ 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-rc14...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
|
||||
|
|
|
@ -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
|
||||
|
|
92
build.gradle
92
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.30"
|
||||
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 = System.getenv().getOrDefault("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,52 +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-bom", version: "4.3.5.4"
|
||||
|
||||
implementation platform(group: "org.http4k", name: "http4k-bom", version: "4.3.5.4")
|
||||
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: "org.http4k", name: "http4k-core"
|
||||
implementation group: "org.http4k", name: "http4k-resilience4j"
|
||||
implementation group: "io.github.resilience4j", name: "resilience4j-micrometer", version: "1.6.1"
|
||||
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", version: "2.12.1"
|
||||
implementation group: "com.fasterxml.jackson.datatype", name: "jackson-datatype-jsr310", version: "2.12.1"
|
||||
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-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"
|
||||
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.34.Final"
|
||||
runtimeOnly group: "io.netty", name: "netty-tcnative-boringssl-static", version: "2.0.59.Final"
|
||||
|
||||
implementation group: 'com.zaxxer', name: 'HikariCP', version: '4.0.1'
|
||||
implementation group: "com.h2database", name: "h2", version: "1.4.200"
|
||||
implementation group: 'org.xerial', name: 'sqlite-jdbc', version: '3.34.0'
|
||||
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 {
|
||||
|
@ -84,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"
|
||||
|
@ -117,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
|
||||
|
|
|
@ -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:v2.24.1
|
||||
image: prom/prometheus:v2.34.0
|
||||
user: "root"
|
||||
group_add:
|
||||
- 0
|
||||
|
@ -50,7 +49,7 @@ services:
|
|||
|
||||
grafana:
|
||||
container_name: grafana
|
||||
image: grafana/grafana:7.4.0
|
||||
image: grafana/grafana:8.4.3
|
||||
user: "root"
|
||||
group_add:
|
||||
- 0
|
||||
|
|
|
@ -14,10 +14,10 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"editable": false,
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 1,
|
||||
"iteration": 1612974883967,
|
||||
"iteration": 1617066952719,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
|
@ -115,7 +115,7 @@
|
|||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 8,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 1
|
||||
},
|
||||
|
@ -250,8 +250,8 @@
|
|||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 8,
|
||||
"x": 8,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 1
|
||||
},
|
||||
"id": 19,
|
||||
|
@ -310,6 +310,167 @@
|
|||
"title": "RAM",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"cacheTimeout": null,
|
||||
"datasource": "Prometheus",
|
||||
"description": "Cache size allocated, does not include metadata or other overhead, but does somewhat account for block sizes.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 30,
|
||||
"gradientMode": "hue",
|
||||
"hideFrom": {
|
||||
"graph": false,
|
||||
"legend": false,
|
||||
"tooltip": false
|
||||
},
|
||||
"lineInterpolation": "linear",
|
||||
"lineStyle": {
|
||||
"fill": "solid"
|
||||
},
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": true
|
||||
},
|
||||
"mappings": [],
|
||||
"min": 0,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "decbytes"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "Max"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "dark-red",
|
||||
"mode": "fixed"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "Used"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "dark-green",
|
||||
"mode": "fixed"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "Max"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.fillOpacity",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"id": "custom.lineWidth",
|
||||
"value": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 6,
|
||||
"x": 12,
|
||||
"y": 1
|
||||
},
|
||||
"id": 57,
|
||||
"interval": null,
|
||||
"links": [],
|
||||
"options": {
|
||||
"graph": {},
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"min",
|
||||
"max",
|
||||
"mean",
|
||||
"last"
|
||||
],
|
||||
"displayMode": "table",
|
||||
"placement": "bottom"
|
||||
},
|
||||
"tooltipOptions": {
|
||||
"mode": "multi"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "7.4.0",
|
||||
"targets": [
|
||||
{
|
||||
"aggregation": "Last",
|
||||
"decimals": 2,
|
||||
"displayAliasType": "Warning / Critical",
|
||||
"displayType": "Regular",
|
||||
"displayValueWithAlias": "Never",
|
||||
"expr": "sum(cache_used_bytes)",
|
||||
"hide": false,
|
||||
"interval": "",
|
||||
"legendFormat": "Used",
|
||||
"refId": "A",
|
||||
"units": "none",
|
||||
"valueHandler": "Number Threshold"
|
||||
},
|
||||
{
|
||||
"aggregation": "Last",
|
||||
"decimals": 2,
|
||||
"displayAliasType": "Warning / Critical",
|
||||
"displayType": "Regular",
|
||||
"displayValueWithAlias": "Never",
|
||||
"expr": "sum(cache_max_bytes)",
|
||||
"hide": false,
|
||||
"instant": false,
|
||||
"interval": "",
|
||||
"legendFormat": "Max",
|
||||
"refId": "B",
|
||||
"units": "none",
|
||||
"valueHandler": "Number Threshold"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Cache",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"cacheTimeout": null,
|
||||
"datasource": "Prometheus",
|
||||
|
@ -408,8 +569,8 @@
|
|||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 8,
|
||||
"x": 16,
|
||||
"w": 6,
|
||||
"x": 18,
|
||||
"y": 1
|
||||
},
|
||||
"id": 20,
|
||||
|
@ -1792,7 +1953,7 @@
|
|||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"collapsed": true,
|
||||
"collapsed": false,
|
||||
"datasource": null,
|
||||
"gridPos": {
|
||||
"h": 1,
|
||||
|
@ -1801,181 +1962,180 @@
|
|||
"y": 27
|
||||
},
|
||||
"id": 53,
|
||||
"panels": [
|
||||
{
|
||||
"circleMaxSize": "10",
|
||||
"circleMinSize": "1",
|
||||
"colors": [
|
||||
"#73BF69",
|
||||
"#FADE2A",
|
||||
"#C4162A"
|
||||
],
|
||||
"datasource": "Prometheus",
|
||||
"decimals": 0,
|
||||
"esMetric": "Count",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 15,
|
||||
"w": 11,
|
||||
"x": 0,
|
||||
"y": 28
|
||||
},
|
||||
"hideEmpty": true,
|
||||
"hideZero": true,
|
||||
"id": 39,
|
||||
"initialZoom": "2",
|
||||
"locationData": "countries",
|
||||
"mapCenter": "custom",
|
||||
"mapCenterLatitude": "30",
|
||||
"mapCenterLongitude": "20",
|
||||
"maxDataPoints": 1,
|
||||
"mouseWheelZoom": false,
|
||||
"pluginVersion": "7.3.4",
|
||||
"showLegend": true,
|
||||
"stickyLabels": false,
|
||||
"tableQueryOptions": {
|
||||
"geohashField": "geohash",
|
||||
"labelField": "country",
|
||||
"latitudeField": "latitude",
|
||||
"longitudeField": "longitude",
|
||||
"metricField": "metric",
|
||||
"queryType": "geohash"
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(increase(requests_country_counts_total[$__range])) by (country)",
|
||||
"format": "time_series",
|
||||
"instant": true,
|
||||
"interval": "",
|
||||
"legendFormat": "{{ country }}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": "1000,10000",
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Origin of requests over timerange",
|
||||
"type": "grafana-worldmap-panel",
|
||||
"unitPlural": "",
|
||||
"unitSingle": "",
|
||||
"unitSingular": "",
|
||||
"valueName": "current"
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": true,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": null,
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {},
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": []
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"fill": 8,
|
||||
"fillGradient": 0,
|
||||
"gridPos": {
|
||||
"h": 15,
|
||||
"w": 13,
|
||||
"x": 11,
|
||||
"y": 28
|
||||
},
|
||||
"hiddenSeries": false,
|
||||
"id": 41,
|
||||
"legend": {
|
||||
"alignAsTable": true,
|
||||
"avg": true,
|
||||
"current": true,
|
||||
"hideEmpty": true,
|
||||
"hideZero": true,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"rightSide": true,
|
||||
"show": true,
|
||||
"sort": "avg",
|
||||
"sortDesc": true,
|
||||
"total": false,
|
||||
"values": true
|
||||
},
|
||||
"lines": false,
|
||||
"linewidth": 2,
|
||||
"nullPointMode": "null as zero",
|
||||
"options": {
|
||||
"alertThreshold": false
|
||||
},
|
||||
"percentage": false,
|
||||
"pluginVersion": "7.4.0",
|
||||
"pointradius": 0.5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": true,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "topk(5, sum(rate(requests_country_counts_total[$itvl])) by (country))",
|
||||
"interval": "$itvl",
|
||||
"legendFormat": "{{ country }}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "Country spread [$itvl]",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 2,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"$$hashKey": "object:104",
|
||||
"format": "reqps",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"$$hashKey": "object:105",
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": false
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"panels": [],
|
||||
"title": "GeoIP",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"circleMaxSize": "10",
|
||||
"circleMinSize": "1",
|
||||
"colors": [
|
||||
"#73BF69",
|
||||
"#FADE2A",
|
||||
"#C4162A"
|
||||
],
|
||||
"datasource": "Prometheus",
|
||||
"decimals": 0,
|
||||
"esMetric": "Count",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 15,
|
||||
"w": 11,
|
||||
"x": 0,
|
||||
"y": 28
|
||||
},
|
||||
"hideEmpty": true,
|
||||
"hideZero": true,
|
||||
"id": 39,
|
||||
"initialZoom": "2",
|
||||
"locationData": "countries",
|
||||
"mapCenter": "custom",
|
||||
"mapCenterLatitude": "30",
|
||||
"mapCenterLongitude": "20",
|
||||
"maxDataPoints": 1,
|
||||
"mouseWheelZoom": false,
|
||||
"pluginVersion": "7.3.4",
|
||||
"showLegend": true,
|
||||
"stickyLabels": false,
|
||||
"tableQueryOptions": {
|
||||
"geohashField": "geohash",
|
||||
"labelField": "country",
|
||||
"latitudeField": "latitude",
|
||||
"longitudeField": "longitude",
|
||||
"metricField": "metric",
|
||||
"queryType": "geohash"
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(increase(requests_country_counts_total[$__range])) by (country)",
|
||||
"format": "time_series",
|
||||
"instant": true,
|
||||
"interval": "",
|
||||
"legendFormat": "{{ country }}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": "1000,10000",
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Origin of requests over timerange",
|
||||
"type": "grafana-worldmap-panel",
|
||||
"unitPlural": "",
|
||||
"unitSingle": "",
|
||||
"unitSingular": "",
|
||||
"valueName": "current"
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": true,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": null,
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {},
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": []
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"fill": 8,
|
||||
"fillGradient": 0,
|
||||
"gridPos": {
|
||||
"h": 15,
|
||||
"w": 13,
|
||||
"x": 11,
|
||||
"y": 28
|
||||
},
|
||||
"hiddenSeries": false,
|
||||
"id": 41,
|
||||
"legend": {
|
||||
"alignAsTable": true,
|
||||
"avg": true,
|
||||
"current": true,
|
||||
"hideEmpty": true,
|
||||
"hideZero": true,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"rightSide": true,
|
||||
"show": true,
|
||||
"sort": "avg",
|
||||
"sortDesc": true,
|
||||
"total": false,
|
||||
"values": true
|
||||
},
|
||||
"lines": false,
|
||||
"linewidth": 2,
|
||||
"nullPointMode": "null as zero",
|
||||
"options": {
|
||||
"alertThreshold": false
|
||||
},
|
||||
"percentage": false,
|
||||
"pluginVersion": "7.4.0",
|
||||
"pointradius": 0.5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": true,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "topk(5, sum(rate(requests_country_counts_total[$itvl])) by (country))",
|
||||
"interval": "$itvl",
|
||||
"legendFormat": "{{ country }}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "Country spread [$itvl]",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 2,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"$$hashKey": "object:104",
|
||||
"format": "reqps",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"$$hashKey": "object:105",
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": false
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"refresh": "30s",
|
||||
|
@ -2069,12 +2229,12 @@
|
|||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-30m",
|
||||
"from": "now-5m",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "MangaDex@Home - Personal client dashboard",
|
||||
"uid": "a7sZAw2Mk",
|
||||
"version": 4
|
||||
}
|
||||
"version": 2
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
http_4k_version=4.3.0.0
|
||||
kotest_version=4.4.1
|
||||
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,15 +23,17 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule
|
|||
import mdnet.ServerHandlerJackson.auto
|
||||
import mdnet.logging.info
|
||||
import mdnet.settings.*
|
||||
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()
|
||||
|
@ -41,7 +43,16 @@ object ServerHandlerJackson : ConfigurableJackson(
|
|||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
)
|
||||
|
||||
class BackendApi(private val settings: ClientSettings, private val client: HttpHandler) {
|
||||
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
|
||||
|
||||
fun logoutFromControl(): Boolean {
|
||||
|
@ -113,7 +124,7 @@ class BackendApi(private val settings: ClientSettings, private val client: HttpH
|
|||
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 = 29
|
||||
const val CLIENT_BUILD = 32
|
||||
|
||||
@JvmField val MAX_AGE_CACHE: Duration = Duration.ofDays(14)
|
||||
|
||||
|
|
|
@ -67,26 +67,8 @@ 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`
|
||||
|
|
||||
|If this is your first time seeing this message, please check out the support
|
||||
|channel as things HAVE changed. Failure to do so WILL require
|
||||
|a cache wipe.
|
||||
""".trimMargin()
|
||||
)
|
||||
println()
|
||||
println()
|
||||
|
||||
throw IllegalArgumentException()
|
||||
if (!Files.isDirectory(databaseFolder)) {
|
||||
throw IllegalArgumentException("Expected $databaseFolder to be a folder, was a file")
|
||||
}
|
||||
|
||||
if (Files.isRegularFile(databaseFolder)) {
|
||||
|
@ -97,8 +79,6 @@ class Main : Runnable {
|
|||
throw IllegalArgumentException("Cache folder $cacheFolder must be a directory")
|
||||
}
|
||||
|
||||
migrate(databaseFolder)
|
||||
|
||||
val client = MangaDexClient(settingsFile, databaseFolder, cacheFolder)
|
||||
val hook = Thread {
|
||||
client.shutdown()
|
||||
|
|
|
@ -198,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 {
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
/*
|
||||
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
|
||||
|
||||
import mdnet.cache.DbImage
|
||||
import mdnet.cache.INIT_TABLE
|
||||
import org.ktorm.database.Database
|
||||
import org.ktorm.dsl.*
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
fun main() {
|
||||
migrate(Paths.get("./"))
|
||||
}
|
||||
|
||||
fun migrate(path: Path) {
|
||||
val h2file = path.resolve("metadata.mv.db")
|
||||
if (!Files.exists(h2file)) {
|
||||
return
|
||||
}
|
||||
|
||||
println("Migrating database - this may take a long time")
|
||||
|
||||
Class.forName("org.sqlite.JDBC")
|
||||
|
||||
val sqliteDb = path.resolve("metadata.db")
|
||||
Files.deleteIfExists(sqliteDb)
|
||||
|
||||
val sqlite = Database.connect("jdbc:sqlite:$sqliteDb")
|
||||
sqlite.useConnection { conn ->
|
||||
conn.prepareStatement(INIT_TABLE).use {
|
||||
it.execute()
|
||||
}
|
||||
}
|
||||
|
||||
val db = path.resolve("metadata")
|
||||
|
||||
val h2 = Database.connect("jdbc:h2:$db")
|
||||
h2.useConnection { conn ->
|
||||
conn.prepareStatement(INIT_TABLE).use {
|
||||
it.execute()
|
||||
}
|
||||
}
|
||||
|
||||
h2.from(DbImage).select().asIterable().chunked(1000).forEach { list ->
|
||||
sqlite.batchInsert(DbImage) {
|
||||
for (data in list) {
|
||||
item {
|
||||
set(DbImage.id, data[DbImage.id])
|
||||
set(DbImage.accessed, data[DbImage.accessed])
|
||||
set(DbImage.size, data[DbImage.size])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Files.move(h2file, path.resolve("metadata.mv.db.old"))
|
||||
}
|
|
@ -18,18 +18,19 @@ 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 io.netty.util.internal.SystemPropertyUtil
|
||||
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 okhttp3.ConnectionPool
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Protocol
|
||||
import org.http4k.client.OkHttp
|
||||
|
@ -78,17 +79,17 @@ class ServerManager(
|
|||
OkHttp(
|
||||
bodyMode = BodyMode.Stream,
|
||||
client = OkHttpClient.Builder()
|
||||
.callTimeout(Duration.ofSeconds(30))
|
||||
.connectTimeout(Duration.ofSeconds(1))
|
||||
.writeTimeout(Duration.ofSeconds(5))
|
||||
.readTimeout(Duration.ofSeconds(5))
|
||||
.let {
|
||||
if (SystemPropertyUtil.get("no-client-http2").toBoolean()) {
|
||||
it.protocols(listOf(Protocol.HTTP_1_1))
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
.connectTimeout(Duration.ofSeconds(2))
|
||||
.connectionPool(
|
||||
ConnectionPool(
|
||||
maxIdleConnections = 100,
|
||||
keepAliveDuration = 1,
|
||||
timeUnit = TimeUnit.MINUTES
|
||||
)
|
||||
)
|
||||
.writeTimeout(Duration.ofSeconds(10))
|
||||
.readTimeout(Duration.ofSeconds(10))
|
||||
.protocols(listOf(Protocol.HTTP_1_1))
|
||||
.build()
|
||||
)
|
||||
)
|
||||
|
@ -100,12 +101,24 @@ class ServerManager(
|
|||
|
||||
init {
|
||||
state = Uninitialized
|
||||
backendApi = BackendApi(settings, OkHttp())
|
||||
backendApi = BackendApi(settings)
|
||||
}
|
||||
|
||||
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()
|
||||
|
@ -174,7 +187,7 @@ class ServerManager(
|
|||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LOGGER.error(e) { "Main loop failed" }
|
||||
LOGGER.warn(e) { "Main loop failed" }
|
||||
}
|
||||
},
|
||||
5, 5, TimeUnit.SECONDS
|
||||
|
@ -250,6 +263,7 @@ class ServerManager(
|
|||
storage,
|
||||
remoteSettings,
|
||||
settings.serverSettings,
|
||||
settings.devSettings,
|
||||
settings.metricsSettings,
|
||||
statistics,
|
||||
registry,
|
||||
|
|
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
|
||||
}
|
||||
}
|
111
src/main/kotlin/mdnet/cache/ImageStorage.kt
vendored
111
src/main/kotlin/mdnet/cache/ImageStorage.kt
vendored
|
@ -25,6 +25,7 @@ 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.*
|
||||
|
@ -35,6 +36,7 @@ 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,36 +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" }
|
||||
synchronized(database) {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -230,14 +252,24 @@ class ImageStorage(
|
|||
)
|
||||
|
||||
Files.deleteIfExists(path)
|
||||
} catch (e: IOException) {
|
||||
// a failure means the image did not exist
|
||||
} finally {
|
||||
synchronized(database) {
|
||||
} 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 {
|
||||
database.delete(DbImage) {
|
||||
DbImage.id eq id
|
||||
}
|
||||
} finally {
|
||||
databaseLock.unlock()
|
||||
}
|
||||
} else {
|
||||
LOGGER.warn { "High contention for database lock, bailing image delete write" }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -255,7 +287,7 @@ class ImageStorage(
|
|||
}
|
||||
}
|
||||
|
||||
fun close() {
|
||||
override fun close() {
|
||||
evictor.shutdown()
|
||||
evictor.awaitTermination(10, TimeUnit.SECONDS)
|
||||
}
|
||||
|
@ -311,23 +343,27 @@ class ImageStorage(
|
|||
|
||||
Files.createDirectories(getPath(id).parent)
|
||||
|
||||
try {
|
||||
synchronized(database) {
|
||||
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: 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
|
||||
} else {
|
||||
LOGGER.warn { "High contention for database lock, bailing DB write" }
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -366,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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,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
|
||||
|
|
|
@ -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,7 +125,7 @@ sealed class NettyTransport(threads: Int) {
|
|||
LOGGER.info { "Using IOUring transport" }
|
||||
return IOUringTransport(threadsToUse)
|
||||
} else {
|
||||
LOGGER.info(IOUring.unavailabilityCause()) {
|
||||
LOGGER.info {
|
||||
"IOUring transport not available (this may be normal)"
|
||||
}
|
||||
}
|
||||
|
@ -127,7 +136,7 @@ sealed class NettyTransport(threads: Int) {
|
|||
LOGGER.info { "Using Epoll transport" }
|
||||
return EpollTransport(threadsToUse)
|
||||
} else {
|
||||
LOGGER.info(Epoll.unavailabilityCause()) {
|
||||
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",
|
||||
|
@ -216,14 +303,19 @@ class Netty(
|
|||
|
||||
override fun stop() = apply {
|
||||
channel.close().sync()
|
||||
transport.shutdownGracefully()
|
||||
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,6 +47,7 @@ fun getServer(
|
|||
storage: ImageStorage,
|
||||
remoteSettings: RemoteSettings,
|
||||
serverSettings: ServerSettings,
|
||||
devSettings: DevSettings,
|
||||
metricsSettings: MetricsSettings,
|
||||
statistics: Statistics,
|
||||
registry: PrometheusMeterRegistry,
|
||||
|
@ -74,7 +77,7 @@ fun getServer(
|
|||
}
|
||||
|
||||
circuitBreaker.eventPublisher.onReset {
|
||||
LOGGER.warn { "Circuit breaker has rest" }
|
||||
LOGGER.warn { "Circuit breaker has reset" }
|
||||
}
|
||||
|
||||
circuitBreaker.eventPublisher.onStateTransition {
|
||||
|
@ -83,7 +86,7 @@ fun getServer(
|
|||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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