mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-14 08:14:34 -05:00
Compare commits
4 Commits
5bdb945406
...
renovate/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5561761020 | ||
|
|
a210153ed1 | ||
|
|
b40447c4f9 | ||
|
|
193dd1ee84 |
@@ -9,9 +9,6 @@ package xyz.nulldev.androidcompat.util
|
|||||||
|
|
||||||
// adopted from: https://github.com/tachiyomiorg/tachiyomi/blob/4cefbce7c34e724b409b6ba127f3c6c5c346ad8d/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt
|
// adopted from: https://github.com/tachiyomiorg/tachiyomi/blob/4cefbce7c34e724b409b6ba127f3c6c5c346ad8d/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt
|
||||||
object SafePath {
|
object SafePath {
|
||||||
private const val MAX_FILENAME_CHARS = 240
|
|
||||||
private const val MAX_FILENAME_UTF8_BYTES = 240
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mutate the given filename to make it valid for a FAT filesystem,
|
* Mutate the given filename to make it valid for a FAT filesystem,
|
||||||
* replacing any invalid characters with "_". This method doesn't allow hidden files (starting
|
* replacing any invalid characters with "_". This method doesn't allow hidden files (starting
|
||||||
@@ -30,41 +27,11 @@ object SafePath {
|
|||||||
sb.append('_')
|
sb.append('_')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Even though vfat allows 255 UCS-2 chars, we might eventually write to
|
||||||
return truncateFilename(sb.toString())
|
// ext4 through a FUSE layer, so use that limit minus 15 reserved characters.
|
||||||
|
return sb.toString().take(240)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun truncateFilename(filename: String): String {
|
|
||||||
// Keep a safety margin under common filesystem limits and satisfy both
|
|
||||||
// character count and UTF-8 byte-length constraints.
|
|
||||||
val output = StringBuilder(minOf(filename.length, MAX_FILENAME_CHARS))
|
|
||||||
var usedBytes = 0
|
|
||||||
var index = 0
|
|
||||||
|
|
||||||
while (index < filename.length && output.length < MAX_FILENAME_CHARS) {
|
|
||||||
val codePoint = Character.codePointAt(filename, index)
|
|
||||||
val codePointBytes = utf8ByteCount(codePoint)
|
|
||||||
|
|
||||||
if (usedBytes + codePointBytes > MAX_FILENAME_UTF8_BYTES) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
output.appendCodePoint(codePoint)
|
|
||||||
usedBytes += codePointBytes
|
|
||||||
index += Character.charCount(codePoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
return output.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun utf8ByteCount(codePoint: Int): Int =
|
|
||||||
when {
|
|
||||||
codePoint <= 0x7f -> 1
|
|
||||||
codePoint <= 0x7ff -> 2
|
|
||||||
codePoint <= 0xffff -> 3
|
|
||||||
else -> 4
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the given character is a valid filename character, false otherwise.
|
* Returns true if the given character is a valid filename character, false otherwise.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -13,10 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|||||||
- .
|
- .
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- (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
|
|
||||||
- (Downloads) Truncate filenames by byte length to prevent "File name too long" IO errors
|
|
||||||
|
|
||||||
## [v2.2.2100] + [WebUI: v20260508.01] - 2026-05-08
|
## [v2.2.2100] + [WebUI: v20260508.01] - 2026-05-08
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ 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.0"
|
||||||
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.2" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
||||||
exposed = "0.61.0"
|
exposed = "0.61.0"
|
||||||
dex2jar = "2.4.36"
|
dex2jar = "2.4.36"
|
||||||
polyglot = "25.0.3"
|
polyglot = "25.0.3"
|
||||||
@@ -54,7 +54,7 @@ javalin-openapi = { module = "io.javalin:javalin-openapi", version.ref = "javali
|
|||||||
javalin-rendering = { module = "io.javalin:javalin-rendering-jte", version.ref = "javalin" }
|
javalin-rendering = { module = "io.javalin:javalin-rendering-jte", version.ref = "javalin" }
|
||||||
jackson-databind = { module = "tools.jackson.core:jackson-databind", version.ref = "jackson" }
|
jackson-databind = { module = "tools.jackson.core:jackson-databind", version.ref = "jackson" }
|
||||||
jackson-kotlin = { module = "tools.jackson.module:jackson-module-kotlin", version.ref = "jackson" }
|
jackson-kotlin = { module = "tools.jackson.module:jackson-module-kotlin", version.ref = "jackson" }
|
||||||
jackson-annotations = "com.fasterxml.jackson.core:jackson-annotations:2.21"
|
jackson-annotations = "com.fasterxml.jackson.core:jackson-annotations:2.20"
|
||||||
jte = { module = "gg.jte:jte", version.ref = "jte" }
|
jte = { module = "gg.jte:jte", version.ref = "jte" }
|
||||||
kte = { module = "gg.jte:jte-kotlin", version.ref = "jte" }
|
kte = { module = "gg.jte:jte-kotlin", version.ref = "jte" }
|
||||||
|
|
||||||
|
|||||||
@@ -171,7 +171,6 @@ tasks {
|
|||||||
"Implementation-Vendor" to "The Suwayomi Project",
|
"Implementation-Vendor" to "The Suwayomi Project",
|
||||||
"Specification-Version" to getTachideskVersion(),
|
"Specification-Version" to getTachideskVersion(),
|
||||||
"Implementation-Version" to getTachideskRevision(),
|
"Implementation-Version" to getTachideskRevision(),
|
||||||
"Multi-Release" to true, // needed for polyglot
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
archiveBaseName.set(rootProject.name)
|
archiveBaseName.set(rootProject.name)
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class CloudflareInterceptor(
|
|||||||
companion object {
|
companion object {
|
||||||
private val ERROR_CODES = listOf(403, 503)
|
private val ERROR_CODES = listOf(403, 503)
|
||||||
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
|
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
|
||||||
val COOKIE_NAMES = listOf("cf_clearance")
|
private val COOKIE_NAMES = listOf("cf_clearance")
|
||||||
private val CHROME_IMAGE_TEMPLATE_REGEX = Regex("""<title>(.*?) \(\d+×\d+\)</title>""")
|
private val CHROME_IMAGE_TEMPLATE_REGEX = Regex("""<title>(.*?) \(\d+×\d+\)</title>""")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,12 +205,9 @@ object CFClearance {
|
|||||||
session = serverConfig.flareSolverrSessionName.value,
|
session = serverConfig.flareSolverrSessionName.value,
|
||||||
sessionTtlMinutes = serverConfig.flareSolverrSessionTtl.value,
|
sessionTtlMinutes = serverConfig.flareSolverrSessionTtl.value,
|
||||||
cookies =
|
cookies =
|
||||||
network.cookieStore
|
network.cookieStore.get(originalRequest.url).map {
|
||||||
.get(originalRequest.url)
|
FlareSolverCookie(it.name, it.value)
|
||||||
.filter { it.name !in CloudflareInterceptor.COOKIE_NAMES }
|
},
|
||||||
.map { cookie ->
|
|
||||||
FlareSolverCookie(cookie.name, cookie.value)
|
|
||||||
},
|
|
||||||
returnOnlyCookies = onlyCookies,
|
returnOnlyCookies = onlyCookies,
|
||||||
maxTimeout = timeout.inWholeMilliseconds.toInt(),
|
maxTimeout = timeout.inWholeMilliseconds.toInt(),
|
||||||
postData =
|
postData =
|
||||||
|
|||||||
@@ -27,10 +27,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.future.future
|
import kotlinx.coroutines.future.future
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
|
||||||
import org.eclipse.jetty.server.ServerConnector
|
import org.eclipse.jetty.server.ServerConnector
|
||||||
import suwayomi.tachidesk.global.GlobalAPI
|
import suwayomi.tachidesk.global.GlobalAPI
|
||||||
import suwayomi.tachidesk.graphql.GraphQL
|
import suwayomi.tachidesk.graphql.GraphQL
|
||||||
@@ -54,7 +51,6 @@ import java.util.concurrent.CompletableFuture
|
|||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
import kotlin.text.get
|
import kotlin.text.get
|
||||||
import kotlin.time.Duration.Companion.days
|
import kotlin.time.Duration.Companion.days
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
|
|
||||||
object JavalinSetup {
|
object JavalinSetup {
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
@@ -136,12 +132,7 @@ object JavalinSetup {
|
|||||||
|
|
||||||
config.events.serverStarted {
|
config.events.serverStarted {
|
||||||
if (serverConfig.initialOpenInBrowserEnabled.value) {
|
if (serverConfig.initialOpenInBrowserEnabled.value) {
|
||||||
scope.launch {
|
Browser.openInBrowser()
|
||||||
withTimeoutOrNull(10.seconds) {
|
|
||||||
WebInterfaceManager.isSetupComplete.first { it }
|
|
||||||
}
|
|
||||||
Browser.openInBrowser()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import kotlinx.coroutines.GlobalScope
|
|||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.sample
|
import kotlinx.coroutines.flow.sample
|
||||||
@@ -91,7 +90,7 @@ object WebInterfaceManager {
|
|||||||
private val preferences = Injekt.get<Application>().getSharedPreferences("server_util", Context.MODE_PRIVATE)
|
private val preferences = Injekt.get<Application>().getSharedPreferences("server_util", Context.MODE_PRIVATE)
|
||||||
private var currentUpdateTaskId: String = ""
|
private var currentUpdateTaskId: String = ""
|
||||||
|
|
||||||
val isSetupComplete = MutableStateFlow(false)
|
private var isSetupComplete = false
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
private val network: NetworkHelper by injectLazy()
|
private val network: NetworkHelper by injectLazy()
|
||||||
@@ -197,7 +196,7 @@ object WebInterfaceManager {
|
|||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
GlobalScope.launchIO {
|
GlobalScope.launchIO {
|
||||||
setupWebUI()
|
setupWebUI()
|
||||||
isSetupComplete.value = true
|
isSetupComplete = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,7 +260,7 @@ object WebInterfaceManager {
|
|||||||
val lastAutomatedUpdate = preferences.getLong(LAST_WEBUI_UPDATE_CHECK_KEY, System.currentTimeMillis())
|
val lastAutomatedUpdate = preferences.getLong(LAST_WEBUI_UPDATE_CHECK_KEY, System.currentTimeMillis())
|
||||||
|
|
||||||
val task = {
|
val task = {
|
||||||
if (isSetupComplete.value) {
|
if (isSetupComplete) {
|
||||||
val log =
|
val log =
|
||||||
KotlinLogging.logger(
|
KotlinLogging.logger(
|
||||||
"${logger.name}::scheduleWebUIUpdateCheck(" +
|
"${logger.name}::scheduleWebUIUpdateCheck(" +
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
package suwayomi.tachidesk
|
|
||||||
|
|
||||||
import xyz.nulldev.androidcompat.util.SafePath
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
class SafePathTest {
|
|
||||||
@Test
|
|
||||||
fun invalidCharactersAreReplacedAndEdgesAreTrimmed() {
|
|
||||||
val input = " .a:b*c?d<e>f|g\\h/i. "
|
|
||||||
|
|
||||||
val result = SafePath.buildValidFilename(input)
|
|
||||||
|
|
||||||
assertEquals("a_b_c_d_e_f_g_h_i", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun emptyAfterTrimReturnsInvalidMarker() {
|
|
||||||
assertEquals("(invalid)", SafePath.buildValidFilename(" ... . "))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun resultIsTruncatedTo240Characters() {
|
|
||||||
val input = "a".repeat(300)
|
|
||||||
|
|
||||||
val result = SafePath.buildValidFilename(input)
|
|
||||||
|
|
||||||
assertEquals(240, result.length)
|
|
||||||
assertEquals("a".repeat(240), result)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun mixed256CharactersCanExceed255Utf8Bytes() {
|
|
||||||
val mixed256 =
|
|
||||||
buildString {
|
|
||||||
repeat(128) {
|
|
||||||
append('a')
|
|
||||||
append('你')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = SafePath.buildValidFilename(mixed256)
|
|
||||||
|
|
||||||
assertEquals(120, result.length)
|
|
||||||
assertEquals(mixed256.take(120), result)
|
|
||||||
assertEquals(240, result.toByteArray(Charsets.UTF_8).size)
|
|
||||||
assertTrue(result.toByteArray(Charsets.UTF_8).size == 240)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user