diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/BookController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/BookController.kt index fac0fb0c94..d14851fb72 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/BookController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/BookController.kt @@ -81,6 +81,7 @@ import org.springframework.web.server.ResponseStatusException import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody import java.io.FileNotFoundException import java.io.OutputStream +import java.nio.charset.StandardCharsets.UTF_8 import java.nio.file.NoSuchFileException import java.time.LocalDate import java.time.ZoneOffset @@ -386,7 +387,7 @@ class BookController( .headers( HttpHeaders().apply { contentDisposition = ContentDisposition.builder("attachment") - .filename(book.path.name) + .filename(book.path.name, UTF_8) .build() }, ) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReadListController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReadListController.kt index 5ca0fe8a2a..8ee9cec543 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReadListController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReadListController.kt @@ -73,6 +73,7 @@ import org.springframework.web.multipart.MultipartFile import org.springframework.web.server.ResponseStatusException import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody import java.io.OutputStream +import java.nio.charset.StandardCharsets.UTF_8 import java.util.concurrent.TimeUnit import java.util.zip.Deflater import javax.validation.Valid @@ -422,7 +423,7 @@ class ReadListController( .headers( HttpHeaders().apply { contentDisposition = ContentDisposition.builder("attachment") - .filename(readList.name + ".zip") + .filename(readList.name + ".zip", UTF_8) .build() }, ) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SeriesController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SeriesController.kt index 38092e4390..4f0c3e0f96 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SeriesController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SeriesController.kt @@ -88,6 +88,7 @@ import org.springframework.web.multipart.MultipartFile import org.springframework.web.server.ResponseStatusException import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody import java.io.OutputStream +import java.nio.charset.StandardCharsets.UTF_8 import java.util.zip.Deflater import javax.validation.Valid @@ -672,7 +673,7 @@ class SeriesController( .headers( HttpHeaders().apply { contentDisposition = ContentDisposition.builder("attachment") - .filename(seriesMetadataRepository.findById(seriesId).title + ".zip") + .filename(seriesMetadataRepository.findById(seriesId).title + ".zip", UTF_8) .build() }, ) diff --git a/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/BookControllerTest.kt b/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/BookControllerTest.kt index 429bcd61c6..03131ee29b 100644 --- a/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/BookControllerTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/BookControllerTest.kt @@ -25,6 +25,7 @@ import org.gotson.komga.domain.service.LibraryLifecycle import org.gotson.komga.domain.service.SeriesLifecycle import org.gotson.komga.infrastructure.security.KomgaPrincipal import org.hamcrest.Matchers +import org.hamcrest.Matchers.containsString import org.hamcrest.core.IsNull import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.AfterEach @@ -49,6 +50,9 @@ import org.springframework.test.web.servlet.get import org.springframework.test.web.servlet.patch import org.springframework.test.web.servlet.request.MockMvcRequestBuilders import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import java.net.URLEncoder +import java.nio.charset.StandardCharsets +import java.nio.file.Files import java.time.LocalDate import kotlin.random.Random @@ -1346,4 +1350,26 @@ class BookControllerTest( jsonPath("$.totalElements").value(2), ) } + + @Test + @WithMockCustomUser + fun `given book with Unicode name when getting book file then attachment name is correct`() { + val bookName = "アキラ" + val tempFile = Files.createTempFile(bookName, ".cbz") + .also { it.toFile().deleteOnExit() } + makeSeries(name = "series", libraryId = library.id).let { series -> + seriesLifecycle.createSeries(series).let { created -> + val books = listOf(makeBook(bookName, libraryId = library.id, url = tempFile.toUri().toURL())) + seriesLifecycle.addBooks(created, books) + } + } + + val book = bookRepository.findAll().first() + + mockMvc.get("/api/v1/books/${book.id}/file") + .andExpect { + status { isOk() } + header { string("Content-Disposition", containsString(URLEncoder.encode(bookName, StandardCharsets.UTF_8.name()))) } + } + } } diff --git a/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/ReadListControllerTest.kt b/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/ReadListControllerTest.kt index 320ff6de5a..032f6572db 100644 --- a/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/ReadListControllerTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/ReadListControllerTest.kt @@ -13,6 +13,7 @@ import org.gotson.komga.domain.service.LibraryLifecycle import org.gotson.komga.domain.service.ReadListLifecycle import org.gotson.komga.domain.service.SeriesLifecycle import org.gotson.komga.language.toIndexedMap +import org.hamcrest.Matchers import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeAll @@ -29,6 +30,9 @@ import org.springframework.test.web.servlet.delete import org.springframework.test.web.servlet.get import org.springframework.test.web.servlet.patch import org.springframework.test.web.servlet.post +import java.net.URLEncoder +import java.nio.charset.StandardCharsets +import java.nio.file.Files @ExtendWith(SpringExtension::class) @SpringBootTest @@ -979,4 +983,32 @@ class ReadListControllerTest( } } } + + @Test + @WithMockCustomUser + fun `given readlist with Unicode name when getting readlist file then attachment name is correct`() { + val name = "アキラ" + val tempFile = Files.createTempFile(name, ".cbz") + .also { it.toFile().deleteOnExit() } + val book = makeBook(name, libraryId = library1.id, url = tempFile.toUri().toURL()) + makeSeries(name = "series", libraryId = library1.id).let { series -> + seriesLifecycle.createSeries(series).let { created -> + val books = listOf(book) + seriesLifecycle.addBooks(created, books) + } + } + + val readlist = readListLifecycle.addReadList( + ReadList( + name = name, + bookIds = listOf(book.id).toIndexedMap(), + ), + ) + + mockMvc.get("/api/v1/readlists/${readlist.id}/file") + .andExpect { + status { isOk() } + header { string("Content-Disposition", Matchers.containsString(URLEncoder.encode(name, StandardCharsets.UTF_8.name()))) } + } + } } diff --git a/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/SeriesControllerTest.kt b/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/SeriesControllerTest.kt index 1879fa6cf4..b85cc02089 100644 --- a/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/SeriesControllerTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/SeriesControllerTest.kt @@ -44,6 +44,9 @@ import org.springframework.test.web.servlet.delete import org.springframework.test.web.servlet.get import org.springframework.test.web.servlet.patch import org.springframework.test.web.servlet.post +import java.net.URLEncoder +import java.nio.charset.StandardCharsets +import java.nio.file.Files import kotlin.random.Random @ExtendWith(SpringExtension::class) @@ -1075,4 +1078,25 @@ class SeriesControllerTest( } } } + + @Test + @WithMockCustomUser + fun `given series with Unicode name when getting series file then attachment name is correct`() { + val name = "アキラ" + val tempFile = Files.createTempFile(name, ".cbz") + .also { it.toFile().deleteOnExit() } + val series = makeSeries(name = name, libraryId = library.id).let { series -> + seriesLifecycle.createSeries(series).let { created -> + val books = listOf(makeBook(name, libraryId = library.id, url = tempFile.toUri().toURL())) + seriesLifecycle.addBooks(created, books) + } + series + } + + mockMvc.get("/api/v1/series/${series.id}/file") + .andExpect { + status { isOk() } + header { string("Content-Disposition", Matchers.containsString(URLEncoder.encode(name, StandardCharsets.UTF_8.name()))) } + } + } }