mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-13 07:44:34 -05:00
Compare commits
15 Commits
renovate/m
...
41bb6d3dc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41bb6d3dc1 | ||
|
|
fbb383b1f1 | ||
|
|
558407d92c | ||
|
|
6870922784 | ||
|
|
a4b647972e | ||
|
|
16a14e6ac2 | ||
|
|
a2f29ec9dc | ||
|
|
82df985201 | ||
|
|
740db4f1ab | ||
|
|
c4711dec00 | ||
|
|
75d8d172aa | ||
|
|
81fb8c395d | ||
|
|
e93efa9627 | ||
|
|
03a95e6652 | ||
|
|
c117d380a3 |
4
.github/workflows/build_pull_request.yml
vendored
4
.github/workflows/build_pull_request.yml
vendored
@@ -67,7 +67,7 @@ jobs:
|
|||||||
export LD_PRELOAD="$(pwd)/scripts/resources/catch_abort.so"
|
export LD_PRELOAD="$(pwd)/scripts/resources/catch_abort.so"
|
||||||
JAR=$(ls ./server/build/*.jar| head -1)
|
JAR=$(ls ./server/build/*.jar| head -1)
|
||||||
set +e
|
set +e
|
||||||
timeout 30s java -DcrashOnFailedMigration=true \
|
timeout 30s java \
|
||||||
-Dsuwayomi.tachidesk.config.server.systemTrayEnabled=false \
|
-Dsuwayomi.tachidesk.config.server.systemTrayEnabled=false \
|
||||||
-Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false \
|
-Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false \
|
||||||
-Dsuwayomi.tachidesk.config.server.databaseType=POSTGRESQL \
|
-Dsuwayomi.tachidesk.config.server.databaseType=POSTGRESQL \
|
||||||
@@ -83,7 +83,7 @@ jobs:
|
|||||||
exit "$ecode"
|
exit "$ecode"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
timeout 30s java -DcrashOnFailedMigration=true \
|
timeout 30s java \
|
||||||
-Dsuwayomi.tachidesk.config.server.systemTrayEnabled=false \
|
-Dsuwayomi.tachidesk.config.server.systemTrayEnabled=false \
|
||||||
-Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false \
|
-Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false \
|
||||||
-jar "$JAR"
|
-jar "$JAR"
|
||||||
|
|||||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -10,13 +10,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|||||||
- .
|
- .
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- .
|
- (Database/H2) Use the latest H2 database engine
|
||||||
|
- (Startup) Crash on startup if an unrecoverable error happens
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- (CloudFlareInterceptor) Don't send the `cf_clearance` cookie back to Flaresolverr
|
- (**CloudFlareInterceptor**) Don't send the `cf_clearance` cookie back to Flaresolverr
|
||||||
- (WebUI) Handle serving non-default webui with "bundled"
|
- (**WebUI**) Handle serving non-default webui with "bundled"
|
||||||
- (WebUI) Wait until WebUI is ready to open in browser
|
- (**WebUI**) Wait until WebUI is ready to open in browser
|
||||||
- (Downloads) Truncate filenames by byte length to prevent "File name too long" IO errors
|
- (**Downloads**) Truncate filenames by byte length to prevent "File name too long" IO errors
|
||||||
|
- (**Extension**) Do not indicate an update is available when the extension is not installed
|
||||||
|
- (**Chapter**) Fix losing chapter data on failed chapter list update
|
||||||
|
- (**Chapter**) Fix database error when fetching chapter updates
|
||||||
|
- (**Manga/API**) Fix "mangas" graphql query with active sorting and using a postgresql database (QUERY "mangas")
|
||||||
|
|
||||||
## [v2.2.2100] + [WebUI: v20260508.01] - 2026-05-08
|
## [v2.2.2100] + [WebUI: v20260508.01] - 2026-05-08
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,69 @@
|
|||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
This page is laid out in several sections, where each section describes a specific problem, followed by one or more possible solutions.
|
||||||
|
|
||||||
|
At the end, you will find a General section, which is the nuclear option if nothing else works.
|
||||||
|
|
||||||
|
For further support, visit the [official Suwayomi Discord server](https://discord.gg/DDZdqZWaHA).
|
||||||
|
In such cases, it will be helpful to have logs ready. You can find them in [The Data Directory](./The-Data-Directory) in the logs directory.
|
||||||
|
|
||||||
|
**All steps below assume that you have stopped Suwayomi**.
|
||||||
|
|
||||||
|
|
||||||
|
## Broken database
|
||||||
|
|
||||||
|
- `failed due to
|
||||||
|
org.jetbrains.exposed.exceptions.ExposedSQLException: org.h2.jdbc.JdbcSQLSyntaxErrorException: Column "CATEGORY.SORT_ORDER" not found`
|
||||||
|
- `org.h2.jdbc.JdbcSQLSyntaxErrorException: Column "CHAPTER.KOREADER_HASH" not found`
|
||||||
|
- `java.lang.IllegalStateException: Unable to read the page at position 96170708817765466`
|
||||||
|
- Any other error text that includes "SQL Statement"
|
||||||
|
|
||||||
|
Your database is either corrupted or incompatible.
|
||||||
|
|
||||||
|
One of these is the cause:
|
||||||
|
- You were running a preview version and decided to downgrade to stable.
|
||||||
|
- You did not shut down Suwayomi properly.
|
||||||
|
- Suwayomi crashed in an unexpected way.
|
||||||
|
|
||||||
|
Solutions:
|
||||||
|
- If you downgraded, upgrade to preview again.
|
||||||
|
- Otherwise, you will need to reset and restore from a backup. See [General Troubleshooting](#general-troubleshooting) below.
|
||||||
|
|
||||||
|
|
||||||
|
## `HTTP error 429`
|
||||||
|
|
||||||
|
The source (or, if trackers are enabled, possibly the tracker) has blocked you for sending too many requests.
|
||||||
|
Note that Mass-Migration can result in an unexpectedly high number of requests to both the source and any configured trackers.
|
||||||
|
|
||||||
|
Solution: Use other/more sources, download less, and wait between request-heavy actions.
|
||||||
|
|
||||||
|
|
||||||
|
## Extension times out
|
||||||
|
|
||||||
|
- `Timed out waiting for 20000 ms…`
|
||||||
|
- `Timed out waiting for page list`
|
||||||
|
|
||||||
|
First, check if this is an extension issue or a Suwayomi issue.
|
||||||
|
On the manga page of the problematic entry, click "Open in WebView".
|
||||||
|
|
||||||
|
Solutions:
|
||||||
|
- If the WebView loads: The issue is with the extension. Search [the issues](https://github.com/Suwayomi/Suwayomi-Server/issues) and discord if there are known problems with that extension.
|
||||||
|
- If the WebView errors: Go to [The Data Directory](./The-Data-Directory) and remove the `bin` and `cache` folders.
|
||||||
|
- If the WebView still does not work after a restart, your installation is incomplete. On Linux, refer to [the README](https://github.com/Suwayomi/Suwayomi-Server#webview-support-gnulinux).
|
||||||
|
|
||||||
|
|
||||||
## General Troubleshooting
|
## General Troubleshooting
|
||||||
This guide will try to fix Suwayomi by reseting it to a clean installation state.
|
This guide will try to fix Suwayomi by reseting it to a clean installation state.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> This will remove all your data, including the library.
|
||||||
|
> Make sure you have copied your backups as described above!
|
||||||
|
|
||||||
- Make sure you have a recent backup of your library or create one in the app (if possible) because we **are going to wipe all Suwayomi data**.
|
- Make sure you have a recent backup of your library or create one in the app (if possible) because we **are going to wipe all Suwayomi data**.
|
||||||
- Make sure Suwayomi is not running (right click on tray icon and quit or kill it through the way your Operating System provides)
|
- Make sure Suwayomi is not running (right click on tray icon and quit or kill it through the way your Operating System provides)
|
||||||
- Clear all browsing data on your browser if you use Suwayomi from a browser.
|
- Clear all browsing data on your browser if you use Suwayomi from a browser.
|
||||||
- Delete the Suwayomi data directory located below and re-run the app.
|
- Delete the Suwayomi data directory located below and re-run the app. See the article [The Data Directory](./The-Data-Directory) for information on how to find it.
|
||||||
|
- If you wish to keep your downloads, you may also attempt to surgically remove only parts. You will need to remove `database.mv.db`, `database.trace.db`, `bin`, `cache`, `extensions`, `settings`, `webUI`. Removing only a subset of these files and folders may fail to resolve the problem.
|
||||||
Note: Replace `<Account>` with the currently logged in account/username on your pc.
|
- Open Suwayomi and go to Settings > Backup > Restore Backup, and select the latest backup you have.
|
||||||
|
- Restoring from backup does not restore your downloads. If you chose to keep them in the above step, you will now need to re-download all manga. Suwayomi will pick up on the existing files and not actually download anything that isn't new.
|
||||||
On Mac OS X : `/Users/<Account>/Library/Application Support/Tachidesk`
|
|
||||||
|
|
||||||
On Windows XP : `C:\Documents and Settings\<Account>\Application Data\Local Settings\Tachidesk`
|
|
||||||
|
|
||||||
On Windows 7 and later : `C:\Users\<Account>\AppData\Local\Tachidesk`
|
|
||||||
|
|
||||||
On Unix/Linux : `/home/<account>/.local/share/Tachidesk`
|
|
||||||
|
|
||||||
- In the case that you have to periodically perform this fix or the problem persists or the method failed to fix it, open an issue or Join the [Suwayomi discord server](https://discord.gg/DDZdqZWaHA) to hang out with the community and to receive support and help.
|
- In the case that you have to periodically perform this fix or the problem persists or the method failed to fix it, open an issue or Join the [Suwayomi discord server](https://discord.gg/DDZdqZWaHA) to hang out with the community and to receive support and help.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ coroutines = "1.11.0"
|
|||||||
serialization = "1.11.0"
|
serialization = "1.11.0"
|
||||||
jvmTarget = "21"
|
jvmTarget = "21"
|
||||||
okhttp = "5.3.2" # Major version is locked by Tachiyomi extensions
|
okhttp = "5.3.2" # Major version is locked by Tachiyomi extensions
|
||||||
javalin = "7.2.0"
|
javalin = "7.2.2"
|
||||||
jte = "3.2.4"
|
jte = "3.2.4"
|
||||||
jackson = "3.1.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
jackson = "3.1.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
||||||
exposed = "1.2.0"
|
exposed = "1.2.0"
|
||||||
@@ -37,7 +37,7 @@ serialization-xml-core = { module = "io.github.pdvrieze.xmlutil:core", version.r
|
|||||||
serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization-jvm", version.ref = "xmlserialization" }
|
serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization-jvm", version.ref = "xmlserialization" }
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
slf4japi = "org.slf4j:slf4j-api:2.0.17"
|
slf4japi = "org.slf4j:slf4j-api:2.0.18"
|
||||||
logback = "ch.qos.logback:logback-classic:1.5.32"
|
logback = "ch.qos.logback:logback-classic:1.5.32"
|
||||||
kotlinlogging = "io.github.oshai:kotlin-logging-jvm:8.0.02"
|
kotlinlogging = "io.github.oshai:kotlin-logging-jvm:8.0.02"
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ appdirs = "ca.gosyer:kotlin-multiplatform-appdirs:2.0.0"
|
|||||||
cache4k = "io.github.reactivecircus.cache4k:cache4k:0.14.0"
|
cache4k = "io.github.reactivecircus.cache4k:cache4k:0.14.0"
|
||||||
zip4j = "net.lingala.zip4j:zip4j:2.11.6"
|
zip4j = "net.lingala.zip4j:zip4j:2.11.6"
|
||||||
commonscompress = "org.apache.commons:commons-compress:1.28.0"
|
commonscompress = "org.apache.commons:commons-compress:1.28.0"
|
||||||
junrar = "com.github.junrar:junrar:7.5.10"
|
junrar = "com.github.junrar:junrar:7.6.0"
|
||||||
|
|
||||||
# AES/CBC/PKCS7Padding Cypher provider
|
# AES/CBC/PKCS7Padding Cypher provider
|
||||||
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84"
|
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84"
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
retries=0
|
retries=0
|
||||||
retryBackOffMs=500
|
retryBackOffMs=500
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
|||||||
* Metadata storage for clients, server/global level.
|
* Metadata storage for clients, server/global level.
|
||||||
*/
|
*/
|
||||||
object GlobalMetaTable : IntIdTable() {
|
object GlobalMetaTable : IntIdTable() {
|
||||||
val key = varchar("key", 256)
|
val key = varchar("meta_key", 256)
|
||||||
val value = varchar("value", 4096)
|
val value = varchar("value", 4096)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,11 @@ import org.jetbrains.exposed.v1.core.Op
|
|||||||
import org.jetbrains.exposed.v1.core.SortOrder
|
import org.jetbrains.exposed.v1.core.SortOrder
|
||||||
import org.jetbrains.exposed.v1.core.greater
|
import org.jetbrains.exposed.v1.core.greater
|
||||||
import org.jetbrains.exposed.v1.core.inList
|
import org.jetbrains.exposed.v1.core.inList
|
||||||
|
import org.jetbrains.exposed.v1.core.inSubQuery
|
||||||
import org.jetbrains.exposed.v1.core.less
|
import org.jetbrains.exposed.v1.core.less
|
||||||
import org.jetbrains.exposed.v1.core.like
|
import org.jetbrains.exposed.v1.core.like
|
||||||
import org.jetbrains.exposed.v1.jdbc.select
|
import org.jetbrains.exposed.v1.jdbc.select
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||||
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
||||||
@@ -243,13 +245,16 @@ class MangaQuery {
|
|||||||
): MangaNodeList {
|
): MangaNodeList {
|
||||||
val queryResults =
|
val queryResults =
|
||||||
transaction {
|
transaction {
|
||||||
val res =
|
val mangaIdsQuery =
|
||||||
MangaTable
|
MangaTable
|
||||||
.leftJoin(CategoryMangaTable)
|
.leftJoin(CategoryMangaTable)
|
||||||
.select(MangaTable.columns)
|
.select(MangaTable.id)
|
||||||
.withDistinctOn(MangaTable.id)
|
.withDistinct()
|
||||||
|
|
||||||
res.applyOps(condition, filter)
|
mangaIdsQuery.applyOps(condition, filter)
|
||||||
|
|
||||||
|
val res =
|
||||||
|
MangaTable.selectAll().where { MangaTable.id inSubQuery mangaIdsQuery }
|
||||||
|
|
||||||
if (order != null || orderBy != null || (last != null || before != null)) {
|
if (order != null || orderBy != null || (last != null || before != null)) {
|
||||||
val baseSort = listOf(MangaOrder(MangaOrderBy.ID, SortOrder.ASC))
|
val baseSort = listOf(MangaOrder(MangaOrderBy.ID, SortOrder.ASC))
|
||||||
|
|||||||
@@ -262,16 +262,14 @@ object Chapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// we got some clean up due
|
transaction {
|
||||||
if (chaptersIdsToDelete.isNotEmpty()) {
|
// we got some clean up due
|
||||||
DownloadManager.dequeue(chaptersIdsToDelete)
|
if (chaptersIdsToDelete.isNotEmpty()) {
|
||||||
transaction {
|
DownloadManager.dequeue(chaptersIdsToDelete)
|
||||||
PageTable.deleteWhere { chapter inList chaptersIdsToDelete }
|
PageTable.deleteWhere { chapter inList chaptersIdsToDelete }
|
||||||
ChapterTable.deleteWhere { id inList chaptersIdsToDelete }
|
ChapterTable.deleteWhere { id inList chaptersIdsToDelete }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
transaction {
|
|
||||||
if (chaptersToInsert.isNotEmpty()) {
|
if (chaptersToInsert.isNotEmpty()) {
|
||||||
ChapterTable
|
ChapterTable
|
||||||
.batchInsert(chaptersToInsert) { chapter ->
|
.batchInsert(chaptersToInsert) { chapter ->
|
||||||
@@ -287,6 +285,7 @@ object Chapter {
|
|||||||
this[ChapterTable.isRead] = false
|
this[ChapterTable.isRead] = false
|
||||||
this[ChapterTable.isBookmarked] = false
|
this[ChapterTable.isBookmarked] = false
|
||||||
this[ChapterTable.isDownloaded] = false
|
this[ChapterTable.isDownloaded] = false
|
||||||
|
this[ChapterTable.pageCount] = -1
|
||||||
|
|
||||||
// is recognized chapter number
|
// is recognized chapter number
|
||||||
if (chapter.chapterNumber >= 0f && chapter.chapterNumber in deletedChapterNumbers) {
|
if (chapter.chapterNumber >= 0f && chapter.chapterNumber in deletedChapterNumbers) {
|
||||||
|
|||||||
@@ -358,6 +358,7 @@ object Extension {
|
|||||||
} else {
|
} else {
|
||||||
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
||||||
it[isInstalled] = false
|
it[isInstalled] = false
|
||||||
|
it[hasUpdate] = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import suwayomi.tachidesk.manga.model.table.CategoryMetaTable.ref
|
|||||||
* Metadata storage for clients, about Category with id == [ref].
|
* Metadata storage for clients, about Category with id == [ref].
|
||||||
*/
|
*/
|
||||||
object CategoryMetaTable : IntIdTable() {
|
object CategoryMetaTable : IntIdTable() {
|
||||||
val key = varchar("key", 256)
|
val key = varchar("meta_key", 256)
|
||||||
val value = varchar("value", 4096)
|
val value = varchar("value", 4096)
|
||||||
val ref = reference("category_ref", CategoryTable, ReferenceOption.CASCADE)
|
val ref = reference("category_ref", CategoryTable, ReferenceOption.CASCADE)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import suwayomi.tachidesk.manga.model.table.ChapterMetaTable.ref
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
object ChapterMetaTable : IntIdTable() {
|
object ChapterMetaTable : IntIdTable() {
|
||||||
val key = varchar("key", 256)
|
val key = varchar("meta_key", 256)
|
||||||
val value = varchar("value", 4096)
|
val value = varchar("value", 4096)
|
||||||
val ref = reference("chapter_ref", ChapterTable, ReferenceOption.CASCADE)
|
val ref = reference("chapter_ref", ChapterTable, ReferenceOption.CASCADE)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import suwayomi.tachidesk.manga.model.table.MangaMetaTable.ref
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
object MangaMetaTable : IntIdTable() {
|
object MangaMetaTable : IntIdTable() {
|
||||||
val key = varchar("key", 256)
|
val key = varchar("meta_key", 256)
|
||||||
val value = varchar("value", 4096)
|
val value = varchar("value", 4096)
|
||||||
val ref = reference("manga_ref", MangaTable, ReferenceOption.CASCADE)
|
val ref = reference("manga_ref", MangaTable, ReferenceOption.CASCADE)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import suwayomi.tachidesk.manga.model.table.SourceMetaTable.ref
|
|||||||
* Metadata storage for clients, about Source with id == [ref].
|
* Metadata storage for clients, about Source with id == [ref].
|
||||||
*/
|
*/
|
||||||
object SourceMetaTable : IntIdTable() {
|
object SourceMetaTable : IntIdTable() {
|
||||||
val key = varchar("key", 256)
|
val key = varchar("meta_key", 256)
|
||||||
val value = varchar("value", 4096)
|
val value = varchar("value", 4096)
|
||||||
val ref = long("source_ref")
|
val ref = long("source_ref")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import android.content.Context
|
|||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import suwayomi.tachidesk.manga.impl.update.IUpdater
|
import suwayomi.tachidesk.manga.impl.update.IUpdater
|
||||||
import suwayomi.tachidesk.server.database.H2Migration
|
import suwayomi.tachidesk.server.database.H2Migration
|
||||||
|
import suwayomi.tachidesk.server.util.ExitCode
|
||||||
|
import suwayomi.tachidesk.server.util.shutdownApp
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -99,31 +101,35 @@ private val MIGRATIONS =
|
|||||||
|
|
||||||
fun runMigrations(applicationDirs: ApplicationDirs) {
|
fun runMigrations(applicationDirs: ApplicationDirs) {
|
||||||
val logger = KotlinLogging.logger("Migration")
|
val logger = KotlinLogging.logger("Migration")
|
||||||
|
try {
|
||||||
|
val migrationPreferences =
|
||||||
|
Injekt
|
||||||
|
.get<Application>()
|
||||||
|
.getSharedPreferences(
|
||||||
|
"migrations",
|
||||||
|
Context.MODE_PRIVATE,
|
||||||
|
)
|
||||||
|
val version = migrationPreferences.getInt("version", 0)
|
||||||
|
|
||||||
val migrationPreferences =
|
logger.info { "Running migrations, previous version $version, target version ${MIGRATIONS.size}" }
|
||||||
Injekt
|
|
||||||
.get<Application>()
|
|
||||||
.getSharedPreferences(
|
|
||||||
"migrations",
|
|
||||||
Context.MODE_PRIVATE,
|
|
||||||
)
|
|
||||||
val version = migrationPreferences.getInt("version", 0)
|
|
||||||
|
|
||||||
logger.info { "Running migrations, previous version $version, target version ${MIGRATIONS.size}" }
|
MIGRATIONS.forEachIndexed { index, (migrationName, migrationFunction) ->
|
||||||
|
val migrationVersion = index + 1
|
||||||
|
|
||||||
MIGRATIONS.forEachIndexed { index, (migrationName, migrationFunction) ->
|
val isMigrationRequired = version < migrationVersion
|
||||||
val migrationVersion = index + 1
|
if (!isMigrationRequired) {
|
||||||
|
logger.info { "Skipping migration version $migrationVersion: $migrationName" }
|
||||||
|
return@forEachIndexed
|
||||||
|
}
|
||||||
|
|
||||||
val isMigrationRequired = version < migrationVersion
|
logger.info { "Running migration version $migrationVersion: $migrationName" }
|
||||||
if (!isMigrationRequired) {
|
|
||||||
logger.info { "Skipping migration version $migrationVersion: $migrationName" }
|
migrationFunction(applicationDirs)
|
||||||
return@forEachIndexed
|
|
||||||
|
migrationPreferences.edit().putInt("version", migrationVersion).apply()
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
logger.info { "Running migration version $migrationVersion: $migrationName" }
|
logger.error(e) { "Failed to run migrations" }
|
||||||
|
shutdownApp(ExitCode.MigrationsRunFailure)
|
||||||
migrationFunction(applicationDirs)
|
|
||||||
|
|
||||||
migrationPreferences.edit().putInt("version", migrationVersion).apply()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -366,6 +366,7 @@ fun applicationSetup() {
|
|||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.error(e) { "Exception while creating initial server.conf" }
|
logger.error(e) { "Exception while creating initial server.conf" }
|
||||||
|
shutdownApp(ExitCode.SetupConfFileFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy local source icon
|
// copy local source icon
|
||||||
@@ -378,6 +379,7 @@ fun applicationSetup() {
|
|||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.error(e) { "Exception while copying Local source's icon" }
|
logger.error(e) { "Exception while copying Local source's icon" }
|
||||||
|
shutdownApp(ExitCode.LocalSourceIconCopyFailure)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fixes #119 , ref:
|
// fixes #119 , ref:
|
||||||
@@ -395,7 +397,12 @@ fun applicationSetup() {
|
|||||||
|
|
||||||
databaseUp()
|
databaseUp()
|
||||||
|
|
||||||
LocalSource.register()
|
try {
|
||||||
|
LocalSource.register()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error(e) { "Failed to setup LocalSource" }
|
||||||
|
shutdownApp(ExitCode.LocalSourceSetupFailure)
|
||||||
|
}
|
||||||
|
|
||||||
serverConfig.subscribeTo(
|
serverConfig.subscribeTo(
|
||||||
combine<Any, DatabaseSettings>(
|
combine<Any, DatabaseSettings>(
|
||||||
@@ -525,7 +532,7 @@ fun applicationSetup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
download { github() }
|
download { github { release("jbr-release-21.0.10b1163.108") } }
|
||||||
settings {
|
settings {
|
||||||
windowlessRenderingEnabled = true
|
windowlessRenderingEnabled = true
|
||||||
cachePath = (Path(applicationDirs.dataRoot) / "cache/kcef").toString()
|
cachePath = (Path(applicationDirs.dataRoot) / "cache/kcef").toString()
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import suwayomi.tachidesk.server.util.ExitCode
|
|||||||
import suwayomi.tachidesk.server.util.shutdownApp
|
import suwayomi.tachidesk.server.util.shutdownApp
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.sql.SQLException
|
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@@ -184,10 +183,8 @@ fun databaseUp() {
|
|||||||
}
|
}
|
||||||
val migrations = loadMigrationsFrom("suwayomi.tachidesk.server.database.migration", ServerConfig::class.java)
|
val migrations = loadMigrationsFrom("suwayomi.tachidesk.server.database.migration", ServerConfig::class.java)
|
||||||
runMigrations(migrations)
|
runMigrations(migrations)
|
||||||
} catch (e: SQLException) {
|
} catch (e: Exception) {
|
||||||
logger.error(e) { "Error up-to-database migration" }
|
logger.error(e) { "Error up-to-database migration" }
|
||||||
if (System.getProperty("crashOnFailedMigration").toBoolean()) {
|
shutdownApp(ExitCode.DbMigrationFailure)
|
||||||
shutdownApp(ExitCode.DbMigrationFailure)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,12 @@ import java.net.URLClassLoader
|
|||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import kotlin.io.path.Path
|
import kotlin.io.path.Path
|
||||||
import kotlin.io.path.absolutePathString
|
import kotlin.io.path.absolutePathString
|
||||||
|
import kotlin.io.path.bufferedReader
|
||||||
|
import kotlin.io.path.bufferedWriter
|
||||||
import kotlin.io.path.copyTo
|
import kotlin.io.path.copyTo
|
||||||
import kotlin.io.path.createDirectories
|
import kotlin.io.path.createDirectories
|
||||||
import kotlin.io.path.deleteExisting
|
import kotlin.io.path.deleteExisting
|
||||||
|
import kotlin.io.path.deleteIfExists
|
||||||
import kotlin.io.path.div
|
import kotlin.io.path.div
|
||||||
import kotlin.io.path.exists
|
import kotlin.io.path.exists
|
||||||
import kotlin.io.path.name
|
import kotlin.io.path.name
|
||||||
@@ -46,6 +49,10 @@ object H2Migration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val script = Path("$dbBase.${h2Old.substringAfterLast('.')}.sql")
|
val script = Path("$dbBase.${h2Old.substringAfterLast('.')}.sql")
|
||||||
|
script.deleteIfExists()
|
||||||
|
|
||||||
|
val modifiedScript = Path("$dbBase.${h2Old.substringAfterLast('.')}.modified.sql")
|
||||||
|
modifiedScript.deleteIfExists()
|
||||||
|
|
||||||
// Backup original database.
|
// Backup original database.
|
||||||
val backup = Path("$dbBase.mv.db.${h2Old.substringAfterLast('.')}.backup")
|
val backup = Path("$dbBase.mv.db.${h2Old.substringAfterLast('.')}.backup")
|
||||||
@@ -72,19 +79,32 @@ object H2Migration {
|
|||||||
libsDir.resolve("h2-$h2New.bin"),
|
libsDir.resolve("h2-$h2New.bin"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Delete attempted migration if failed previously
|
||||||
|
val newDatabase = Path(rootDir, "database.${h2New.substringAfterLast('.')}.mv.db")
|
||||||
|
newDatabase.deleteIfExists()
|
||||||
|
|
||||||
|
val modifiedNewDatabase = Path(rootDir, "database.${h2Old.substringAfterLast('.')}.modified.${h2New.substringAfterLast('.')}.mv.db")
|
||||||
|
modifiedNewDatabase.deleteIfExists()
|
||||||
|
|
||||||
runMigrationTool(
|
runMigrationTool(
|
||||||
migrationJar = migrationJar,
|
migrationJar = migrationJar,
|
||||||
libsDir = libsDir,
|
libsDir = libsDir,
|
||||||
mvStore = mvStore,
|
mvStore = mvStore,
|
||||||
script = script,
|
script = script,
|
||||||
|
modifiedScript = modifiedScript,
|
||||||
h2Old = h2Old,
|
h2Old = h2Old,
|
||||||
h2New = h2New,
|
h2New = h2New,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Move database to proper path
|
// Move database to proper path
|
||||||
val newDatabase = Path(rootDir, "database.${h2New.substringAfterLast('.')}.mv.db")
|
if (modifiedNewDatabase.exists()) {
|
||||||
newDatabase.copyTo(mvStore, overwrite = true)
|
modifiedNewDatabase.copyTo(mvStore, overwrite = true)
|
||||||
newDatabase.deleteExisting()
|
modifiedNewDatabase.deleteExisting()
|
||||||
|
newDatabase.deleteIfExists()
|
||||||
|
} else {
|
||||||
|
newDatabase.copyTo(mvStore, overwrite = true)
|
||||||
|
newDatabase.deleteExisting()
|
||||||
|
}
|
||||||
|
|
||||||
logger.info { "H2 migration completed successfully." }
|
logger.info { "H2 migration completed successfully." }
|
||||||
}
|
}
|
||||||
@@ -123,6 +143,7 @@ object H2Migration {
|
|||||||
libsDir: Path,
|
libsDir: Path,
|
||||||
mvStore: Path,
|
mvStore: Path,
|
||||||
script: Path,
|
script: Path,
|
||||||
|
modifiedScript: Path,
|
||||||
h2Old: String,
|
h2Old: String,
|
||||||
h2New: String,
|
h2New: String,
|
||||||
) {
|
) {
|
||||||
@@ -136,32 +157,77 @@ object H2Migration {
|
|||||||
val main =
|
val main =
|
||||||
clazz.getMethod("main", Array<String>::class.java)
|
clazz.getMethod("main", Array<String>::class.java)
|
||||||
|
|
||||||
main.invoke(
|
try {
|
||||||
null,
|
main.invoke(
|
||||||
arrayOf(
|
null,
|
||||||
// h2 driver dir
|
arrayOf(
|
||||||
"-l",
|
// h2 driver dir
|
||||||
libsDir.absolutePathString(),
|
"-l",
|
||||||
// from version
|
libsDir.absolutePathString(),
|
||||||
"-f",
|
// from version
|
||||||
h2Old,
|
"-f",
|
||||||
// to version
|
h2Old,
|
||||||
"-t",
|
// to version
|
||||||
h2New,
|
"-t",
|
||||||
// user
|
h2New,
|
||||||
"-u",
|
// user
|
||||||
"",
|
"-u",
|
||||||
// password
|
"",
|
||||||
"-p",
|
// password
|
||||||
"",
|
"-p",
|
||||||
// database.mv.db
|
"",
|
||||||
"-d",
|
// database.mv.db
|
||||||
mvStore.absolutePathString(),
|
"-d",
|
||||||
// database backup in SQL
|
mvStore.absolutePathString(),
|
||||||
"-s",
|
// database backup in SQL
|
||||||
script.absolutePathString(),
|
"-s",
|
||||||
),
|
script.absolutePathString(),
|
||||||
)
|
),
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Modify raw .sql file as needed for compatibility
|
||||||
|
if (e.stackTraceToString().contains("Unknown data type: \"DATETIME\"; SQL statement:") && script.exists()) {
|
||||||
|
script.bufferedReader().use { reader ->
|
||||||
|
modifiedScript.bufferedWriter().use { writer ->
|
||||||
|
reader.forEachLine { line ->
|
||||||
|
writer.write(
|
||||||
|
line.replace(
|
||||||
|
" \"EXECUTED_AT\" DATETIME(9) NOT NULL",
|
||||||
|
" \"EXECUTED_AT\" TIMESTAMP(9) NOT NULL",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
writer.newLine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main.invoke(
|
||||||
|
null,
|
||||||
|
arrayOf(
|
||||||
|
// h2 driver dir
|
||||||
|
"-l",
|
||||||
|
libsDir.absolutePathString(),
|
||||||
|
// from version
|
||||||
|
"-f",
|
||||||
|
h2Old,
|
||||||
|
// to version
|
||||||
|
"-t",
|
||||||
|
h2New,
|
||||||
|
// user
|
||||||
|
"-u",
|
||||||
|
"",
|
||||||
|
// password
|
||||||
|
"-p",
|
||||||
|
"",
|
||||||
|
// database.mv.db
|
||||||
|
"-d",
|
||||||
|
modifiedScript.absolutePathString(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,17 +10,10 @@ package suwayomi.tachidesk.server.database.migration
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import de.neonew.exposed.migrations.helpers.SQLMigration
|
import de.neonew.exposed.migrations.helpers.SQLMigration
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
|
import suwayomi.tachidesk.server.database.migration.helpers.toSqlName
|
||||||
|
|
||||||
@Suppress("ClassName", "unused")
|
@Suppress("ClassName", "unused")
|
||||||
class M0023_CategoryMetaRefFix : SQLMigration() {
|
class M0023_CategoryMetaRefFix : SQLMigration() {
|
||||||
fun String.toSqlName(): String =
|
|
||||||
TransactionManager.defaultDatabase!!.identifierManager.let {
|
|
||||||
it.quoteIfNecessary(
|
|
||||||
it.inProperCase(this),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val CategoryMetaTable by lazy { "CategoryMeta".toSqlName() }
|
private val CategoryMetaTable by lazy { "CategoryMeta".toSqlName() }
|
||||||
private val CategoryRefColumn by lazy { "category_ref".toSqlName() }
|
private val CategoryRefColumn by lazy { "category_ref".toSqlName() }
|
||||||
private val CategoryTable by lazy { "Category".toSqlName() }
|
private val CategoryTable by lazy { "Category".toSqlName() }
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ package suwayomi.tachidesk.server.database.migration
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import de.neonew.exposed.migrations.helpers.SQLMigration
|
import de.neonew.exposed.migrations.helpers.SQLMigration
|
||||||
|
import suwayomi.tachidesk.server.database.migration.helpers.toSqlName
|
||||||
|
|
||||||
@Suppress("ClassName", "unused")
|
@Suppress("ClassName", "unused")
|
||||||
class M0049_FixDuplicatedMetas : SQLMigration() {
|
class M0049_FixDuplicatedMetas : SQLMigration() {
|
||||||
@@ -15,7 +16,7 @@ class M0049_FixDuplicatedMetas : SQLMigration() {
|
|||||||
table: String,
|
table: String,
|
||||||
refColumn: String? = null,
|
refColumn: String? = null,
|
||||||
): String {
|
): String {
|
||||||
val groupBy = listOfNotNull(refColumn, "KEY").joinToString(", ")
|
val groupBy = listOfNotNull(refColumn, "KEY".toSqlName()).joinToString(", ")
|
||||||
|
|
||||||
return """
|
return """
|
||||||
DELETE FROM $table
|
DELETE FROM $table
|
||||||
@@ -30,10 +31,11 @@ class M0049_FixDuplicatedMetas : SQLMigration() {
|
|||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
}
|
}
|
||||||
|
|
||||||
override val sql: String =
|
override val sql: String by lazy {
|
||||||
createMigrationForTable("CATEGORYMETA", "CATEGORY_REF") +
|
createMigrationForTable("CATEGORYMETA", "CATEGORY_REF") +
|
||||||
createMigrationForTable("CHAPTERMETA", "CHAPTER_REF") +
|
createMigrationForTable("CHAPTERMETA", "CHAPTER_REF") +
|
||||||
createMigrationForTable("GLOBALMETA") +
|
createMigrationForTable("GLOBALMETA") +
|
||||||
createMigrationForTable("MANGAMETA", "MANGA_REF") +
|
createMigrationForTable("MANGAMETA", "MANGA_REF") +
|
||||||
createMigrationForTable("SOURCEMETA", "SOURCE_REF")
|
createMigrationForTable("SOURCEMETA", "SOURCE_REF")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package suwayomi.tachidesk.server.database.migration
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import de.neonew.exposed.migrations.helpers.SQLMigration
|
||||||
|
import suwayomi.tachidesk.graphql.types.DatabaseType
|
||||||
|
import suwayomi.tachidesk.server.database.migration.helpers.toSqlName
|
||||||
|
import suwayomi.tachidesk.server.serverConfig
|
||||||
|
|
||||||
|
@Suppress("ClassName", "unused")
|
||||||
|
class M0055_RenameMetaKeys : SQLMigration() {
|
||||||
|
fun postgresRename(table: String): String =
|
||||||
|
"ALTER TABLE $table " +
|
||||||
|
"RENAME COLUMN " + "KEY".toSqlName() + " TO META_KEY;"
|
||||||
|
|
||||||
|
fun h2Rename(table: String): String =
|
||||||
|
"ALTER TABLE $table " +
|
||||||
|
"ALTER COLUMN " + "KEY".toSqlName() + " RENAME TO META_KEY;"
|
||||||
|
|
||||||
|
fun createRenameMigration(table: String): String =
|
||||||
|
when (serverConfig.databaseType.value) {
|
||||||
|
DatabaseType.H2 -> h2Rename(table.toSqlName())
|
||||||
|
DatabaseType.POSTGRESQL -> postgresRename(table.toSqlName())
|
||||||
|
}
|
||||||
|
|
||||||
|
override val sql: String by lazy {
|
||||||
|
createRenameMigration("CATEGORYMETA") +
|
||||||
|
createRenameMigration("CHAPTERMETA") +
|
||||||
|
createRenameMigration("GLOBALMETA") +
|
||||||
|
createRenameMigration("MANGAMETA") +
|
||||||
|
createRenameMigration("SOURCEMETA")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,9 @@
|
|||||||
package suwayomi.tachidesk.server.database.migration.helpers
|
package suwayomi.tachidesk.server.database.migration.helpers
|
||||||
|
|
||||||
import de.neonew.exposed.migrations.helpers.SQLMigration
|
import de.neonew.exposed.migrations.helpers.SQLMigration
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
|
|
||||||
import suwayomi.tachidesk.graphql.types.DatabaseType
|
import suwayomi.tachidesk.graphql.types.DatabaseType
|
||||||
import suwayomi.tachidesk.server.serverConfig
|
import suwayomi.tachidesk.server.serverConfig
|
||||||
|
|
||||||
fun String.toSqlName(): String =
|
|
||||||
TransactionManager.current().db.identifierManager.let {
|
|
||||||
it.quoteIfNecessary(
|
|
||||||
it.inProperCase(this),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class RenameFieldMigration(
|
abstract class RenameFieldMigration(
|
||||||
tableName: String,
|
tableName: String,
|
||||||
originalName: String,
|
originalName: String,
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package suwayomi.tachidesk.server.database.migration.helpers
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
|
||||||
|
|
||||||
|
fun String.toSqlName(): String =
|
||||||
|
TransactionManager.current().db.identifierManager.let {
|
||||||
|
it.quoteIfNecessary(
|
||||||
|
it.inProperCase(this),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -21,6 +21,10 @@ enum class ExitCode(
|
|||||||
WebUISetupFailure(3),
|
WebUISetupFailure(3),
|
||||||
ConfigMigrationMisconfiguredFailure(4),
|
ConfigMigrationMisconfiguredFailure(4),
|
||||||
DbMigrationFailure(5),
|
DbMigrationFailure(5),
|
||||||
|
SetupConfFileFailed(6),
|
||||||
|
LocalSourceIconCopyFailure(7),
|
||||||
|
LocalSourceSetupFailure(8),
|
||||||
|
MigrationsRunFailure(9),
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shutdownApp(exitCode: ExitCode) {
|
fun shutdownApp(exitCode: ExitCode) {
|
||||||
|
|||||||
Reference in New Issue
Block a user