mirror of
https://github.com/gotson/komga.git
synced 2025-01-08 11:47:47 +08:00
support for multiple libraries
if there are some existing series, a library will be created and existing series attached to it first version of a /libraries endpoint to add/remove/list libraries some komga properties have been renamed or deprecated
This commit is contained in:
parent
eae425e6d3
commit
a9ff90596c
4
gradlew
vendored
4
gradlew
vendored
@ -125,8 +125,8 @@ 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"`
|
||||
|
@ -19,7 +19,7 @@ plugins {
|
||||
}
|
||||
|
||||
group = "org.gotson"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
|
||||
val developmentOnly = configurations.create("developmentOnly")
|
||||
configurations.runtimeClasspath.get().extendsFrom(developmentOnly)
|
||||
|
@ -0,0 +1,47 @@
|
||||
package db.migration
|
||||
|
||||
import org.flywaydb.core.api.migration.BaseJavaMigration
|
||||
import org.flywaydb.core.api.migration.Context
|
||||
import org.springframework.jdbc.core.JdbcTemplate
|
||||
import org.springframework.jdbc.datasource.SingleConnectionDataSource
|
||||
import java.net.URI
|
||||
import java.nio.file.Paths
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
class V20190926114415__create_library_from_series : BaseJavaMigration() {
|
||||
override fun migrate(context: Context) {
|
||||
val jdbcTemplate = JdbcTemplate(SingleConnectionDataSource(context.connection, true))
|
||||
|
||||
val urls = jdbcTemplate.queryForList("SELECT url FROM serie", String::class.java)
|
||||
|
||||
if (urls.isNotEmpty()) {
|
||||
val rootFolder = findCommonDirPath(urls, '/')
|
||||
|
||||
val libraryId = jdbcTemplate.queryForObject("SELECT NEXTVAL('hibernate_sequence')", Int::class.java)
|
||||
val libraryName = Paths.get(URI(rootFolder)).fileName
|
||||
|
||||
val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"))
|
||||
|
||||
jdbcTemplate.execute("INSERT INTO library (ID, CREATED_DATE, LAST_MODIFIED_DATE, NAME, ROOT) VALUES ($libraryId, '$now', '$now', '$libraryName', '$rootFolder')")
|
||||
jdbcTemplate.execute("UPDATE serie SET library_id = $libraryId")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// version 1.1.51 - https://www.rosettacode.org/wiki/Find_common_directory_path#Kotlin
|
||||
fun findCommonDirPath(paths: List<String>, separator: Char): String {
|
||||
if (paths.isEmpty()) return ""
|
||||
if (paths.size == 1) return paths[0]
|
||||
val splits = paths[0].split(separator)
|
||||
val n = splits.size
|
||||
val paths2 = paths.drop(1)
|
||||
var k = 0
|
||||
var common = ""
|
||||
while (true) {
|
||||
val prevCommon = common
|
||||
common += if (k == 0) splits[0] else separator + splits[k]
|
||||
if (!paths2.all { it.startsWith(common + separator) || it == common }) return prevCommon
|
||||
if (++k == n) return common
|
||||
}
|
||||
}
|
@ -1,4 +1,7 @@
|
||||
package org.gotson.komga.domain.model
|
||||
|
||||
class MetadataNotReadyException : Exception()
|
||||
class UnsupportedMediaTypeException(msg: String, val mediaType: String) : Exception(msg)
|
||||
class UnsupportedMediaTypeException(message: String, val mediaType: String) : Exception(message)
|
||||
class DirectoryNotFoundException(message: String) : Exception(message)
|
||||
class DuplicateNameException(message: String) : Exception(message)
|
||||
class PathContainedInPath(message: String) : Exception(message)
|
@ -1,10 +1,32 @@
|
||||
package org.gotson.komga.domain.model
|
||||
|
||||
import java.nio.file.FileSystem
|
||||
import java.nio.file.FileSystems
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.GeneratedValue
|
||||
import javax.persistence.Id
|
||||
import javax.persistence.Table
|
||||
import javax.validation.constraints.NotBlank
|
||||
|
||||
data class Library(
|
||||
@Entity
|
||||
@Table(name = "library")
|
||||
class Library(
|
||||
@NotBlank
|
||||
@Column(name = "name", nullable = false, unique = true)
|
||||
val name: String,
|
||||
val root: String,
|
||||
val fileSystem: FileSystem = FileSystems.getDefault()
|
||||
)
|
||||
|
||||
@NotBlank
|
||||
@Column(name = "root", nullable = false)
|
||||
val root: URL
|
||||
) : AuditableEntity() {
|
||||
@Id
|
||||
@GeneratedValue
|
||||
@Column(name = "id", nullable = false, unique = true)
|
||||
var id: Long = 0
|
||||
|
||||
constructor(name: String, root: String) : this(name, Paths.get(root).toUri().toURL())
|
||||
}
|
||||
|
||||
fun Library.path(): Path = Paths.get(this.root.toURI())
|
@ -10,10 +10,13 @@ import javax.persistence.Entity
|
||||
import javax.persistence.FetchType
|
||||
import javax.persistence.GeneratedValue
|
||||
import javax.persistence.Id
|
||||
import javax.persistence.JoinColumn
|
||||
import javax.persistence.ManyToOne
|
||||
import javax.persistence.OneToMany
|
||||
import javax.persistence.OrderColumn
|
||||
import javax.persistence.Table
|
||||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.NotNull
|
||||
|
||||
private val natSortComparator: Comparator<String> = CaseInsensitiveSimpleNaturalComparator.getInstance()
|
||||
|
||||
@ -38,6 +41,11 @@ class Serie(
|
||||
@Column(name = "id", nullable = false, unique = true)
|
||||
var id: Long = 0
|
||||
|
||||
@NotNull
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "library_id", nullable = false)
|
||||
lateinit var library: Library
|
||||
|
||||
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY, orphanRemoval = true, mappedBy = "serie")
|
||||
@OrderColumn(name = "index")
|
||||
private var _books: MutableList<Book> = mutableListOf()
|
||||
|
@ -0,0 +1,10 @@
|
||||
package org.gotson.komga.domain.persistence
|
||||
|
||||
import org.gotson.komga.domain.model.Library
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface LibraryRepository : JpaRepository<Library, Long> {
|
||||
fun existsByName(name: String): Boolean
|
||||
}
|
@ -8,6 +8,7 @@ import java.net.URL
|
||||
|
||||
@Repository
|
||||
interface SerieRepository : JpaRepository<Serie, Long>, JpaSpecificationExecutor<Serie> {
|
||||
fun findByUrlNotIn(urls: Iterable<URL>): List<Serie>
|
||||
fun findByUrl(url: URL): Serie?
|
||||
fun findByLibraryIdAndUrlNotIn(libraryId: Long, urls: Iterable<URL>): List<Serie>
|
||||
fun findByLibraryIdAndUrl(libraryId: Long, url: URL): Serie?
|
||||
fun deleteByLibraryId(libraryId: Long)
|
||||
}
|
@ -3,8 +3,8 @@ package org.gotson.komga.domain.service
|
||||
import mu.KotlinLogging
|
||||
import org.apache.commons.lang3.time.DurationFormatUtils
|
||||
import org.gotson.komga.domain.model.Book
|
||||
import org.gotson.komga.domain.model.Library
|
||||
import org.gotson.komga.domain.persistence.BookRepository
|
||||
import org.gotson.komga.domain.persistence.LibraryRepository
|
||||
import org.springframework.scheduling.annotation.Async
|
||||
import org.springframework.stereotype.Service
|
||||
import kotlin.system.measureTimeMillis
|
||||
@ -13,19 +13,27 @@ private val logger = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
class AsyncOrchestrator(
|
||||
private val libraryManager: LibraryManager,
|
||||
private val libraryScanner: LibraryScanner,
|
||||
private val libraryRepository: LibraryRepository,
|
||||
private val bookRepository: BookRepository,
|
||||
private val bookManager: BookManager
|
||||
private val bookLifecyle: BookLifecyle
|
||||
) {
|
||||
|
||||
|
||||
@Async("periodicScanTaskExecutor")
|
||||
fun scanAndParse(library: Library) {
|
||||
logger.info { "Starting periodic library scan" }
|
||||
libraryManager.scanRootFolder(library)
|
||||
fun scanAndParse() {
|
||||
logger.info { "Starting periodic libraries scan" }
|
||||
val libraries = libraryRepository.findAll()
|
||||
|
||||
logger.info { "Starting periodic book parsing" }
|
||||
libraryManager.parseUnparsedBooks()
|
||||
if (libraries.isEmpty()) {
|
||||
logger.info { "No libraries defined, nothing to scan" }
|
||||
} else {
|
||||
libraries.forEach {
|
||||
libraryScanner.scanRootFolder(it)
|
||||
}
|
||||
|
||||
logger.info { "Starting periodic book parsing" }
|
||||
libraryScanner.parseUnparsedBooks()
|
||||
}
|
||||
}
|
||||
|
||||
@Async("regenerateThumbnailsTaskExecutor")
|
||||
@ -44,7 +52,7 @@ class AsyncOrchestrator(
|
||||
var sumOfTasksTime = 0L
|
||||
measureTimeMillis {
|
||||
sumOfTasksTime = books
|
||||
.map { bookManager.regenerateThumbnailAndPersist(it) }
|
||||
.map { bookLifecyle.regenerateThumbnailAndPersist(it) }
|
||||
.map {
|
||||
try {
|
||||
it.get()
|
||||
|
@ -20,7 +20,7 @@ import kotlin.system.measureTimeMillis
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
class BookManager(
|
||||
class BookLifecyle(
|
||||
private val bookRepository: BookRepository,
|
||||
private val bookParser: BookParser,
|
||||
private val imageConverter: ImageConverter
|
@ -0,0 +1,70 @@
|
||||
package org.gotson.komga.domain.service
|
||||
|
||||
import mu.KotlinLogging
|
||||
import org.gotson.komga.domain.model.DirectoryNotFoundException
|
||||
import org.gotson.komga.domain.model.DuplicateNameException
|
||||
import org.gotson.komga.domain.model.Library
|
||||
import org.gotson.komga.domain.model.PathContainedInPath
|
||||
import org.gotson.komga.domain.model.path
|
||||
import org.gotson.komga.domain.persistence.LibraryRepository
|
||||
import org.gotson.komga.domain.persistence.SerieRepository
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.io.FileNotFoundException
|
||||
import java.nio.file.Files
|
||||
import java.util.concurrent.RejectedExecutionException
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
class LibraryLifecycle(
|
||||
private val libraryRepository: LibraryRepository,
|
||||
private val serieRepository: SerieRepository,
|
||||
private val asyncOrchestrator: AsyncOrchestrator
|
||||
) {
|
||||
|
||||
@Throws(
|
||||
FileNotFoundException::class,
|
||||
DirectoryNotFoundException::class,
|
||||
DuplicateNameException::class,
|
||||
PathContainedInPath::class
|
||||
)
|
||||
fun addLibrary(library: Library): Library {
|
||||
logger.info { "Adding new library: ${library.name} with root folder: ${library.root}" }
|
||||
|
||||
if (!Files.exists(library.path()))
|
||||
throw FileNotFoundException("Library root folder does not exist: ${library.root}")
|
||||
|
||||
if (!Files.isDirectory(library.path()))
|
||||
throw DirectoryNotFoundException("Library root folder is not a folder: ${library.root}")
|
||||
|
||||
if (libraryRepository.existsByName(library.name))
|
||||
throw DuplicateNameException("Library name already exists")
|
||||
|
||||
libraryRepository.findAll().forEach {
|
||||
if (library.path().startsWith(it.path()))
|
||||
throw PathContainedInPath("Library path ${library.path()} is a child of existing library ${it.name}: ${it.path()}")
|
||||
if (it.path().startsWith(library.path()))
|
||||
throw PathContainedInPath("Library path ${library.path()} is a parent of existing library ${it.name}: ${it.path()}")
|
||||
}
|
||||
|
||||
|
||||
libraryRepository.save(library)
|
||||
|
||||
logger.info { "Trying to launch a scan for the newly added library: ${library.name}" }
|
||||
try {
|
||||
asyncOrchestrator.scanAndParse()
|
||||
} catch (e: RejectedExecutionException) {
|
||||
logger.warn { "Another scan is already running, skipping" }
|
||||
}
|
||||
|
||||
return library
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun deleteLibrary(library: Library) {
|
||||
logger.info { "Deleting library: ${library.name} with root folder: ${library.root}" }
|
||||
serieRepository.deleteByLibraryId(library.id)
|
||||
libraryRepository.delete(library)
|
||||
}
|
||||
}
|
@ -8,31 +8,32 @@ import org.gotson.komga.domain.persistence.BookRepository
|
||||
import org.gotson.komga.domain.persistence.SerieRepository
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.nio.file.Paths
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
class LibraryManager(
|
||||
class LibraryScanner(
|
||||
private val fileSystemScanner: FileSystemScanner,
|
||||
private val serieRepository: SerieRepository,
|
||||
private val bookRepository: BookRepository,
|
||||
private val bookManager: BookManager
|
||||
private val bookLifecyle: BookLifecyle
|
||||
) {
|
||||
|
||||
@Transactional
|
||||
fun scanRootFolder(library: Library) {
|
||||
logger.info { "Updating library: ${library.name}, root folder: ${library.root}" }
|
||||
measureTimeMillis {
|
||||
val series = fileSystemScanner.scanRootFolder(library.fileSystem.getPath(library.root))
|
||||
val series = fileSystemScanner.scanRootFolder(Paths.get(library.root.toURI()))
|
||||
|
||||
// delete series that don't exist anymore
|
||||
if (series.isEmpty()) {
|
||||
logger.info { "Scan returned no series, deleting all existing series" }
|
||||
serieRepository.deleteAll()
|
||||
serieRepository.deleteByLibraryId(library.id)
|
||||
} else {
|
||||
series.map { it.url }.let { urls ->
|
||||
serieRepository.findByUrlNotIn(urls).forEach {
|
||||
serieRepository.findByLibraryIdAndUrlNotIn(library.id, urls).forEach {
|
||||
logger.info { "Deleting serie not on disk anymore: $it" }
|
||||
serieRepository.delete(it)
|
||||
}
|
||||
@ -40,12 +41,12 @@ class LibraryManager(
|
||||
}
|
||||
|
||||
series.forEach { newSerie ->
|
||||
val existingSerie = serieRepository.findByUrl(newSerie.url)
|
||||
val existingSerie = serieRepository.findByLibraryIdAndUrl(library.id, newSerie.url)
|
||||
|
||||
// if serie does not exist, save it
|
||||
if (existingSerie == null) {
|
||||
logger.info { "Adding new serie: $newSerie" }
|
||||
serieRepository.save(newSerie)
|
||||
serieRepository.save(newSerie.also { it.library = library })
|
||||
} else {
|
||||
// if serie already exists, update it
|
||||
if (newSerie.fileLastModified != existingSerie.fileLastModified) {
|
||||
@ -80,7 +81,7 @@ class LibraryManager(
|
||||
var sumOfTasksTime = 0L
|
||||
measureTimeMillis {
|
||||
sumOfTasksTime = booksToParse
|
||||
.map { bookManager.parseAndPersist(it) }
|
||||
.map { bookLifecyle.parseAndPersist(it) }
|
||||
.map {
|
||||
try {
|
||||
it.get()
|
@ -1,6 +1,7 @@
|
||||
package org.gotson.komga.infrastructure.configuration
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.validation.annotation.Validated
|
||||
import javax.validation.constraints.Min
|
||||
@ -10,9 +11,21 @@ import javax.validation.constraints.NotBlank
|
||||
@ConfigurationProperties(prefix = "komga")
|
||||
@Validated
|
||||
class KomgaProperties {
|
||||
@get:DeprecatedConfigurationProperty(reason = "As of v0.5.0 Komga supports multiple libraries, which must be created via the API")
|
||||
var rootFolder: String = ""
|
||||
|
||||
@get:DeprecatedConfigurationProperty(
|
||||
reason = "As of v0.5.0 Komga supports multiple libraries, which must be created via the API",
|
||||
replacement = "komga.libraries-scan-cron"
|
||||
)
|
||||
var rootFolderScanCron: String = ""
|
||||
|
||||
var librariesScanCron: String = ""
|
||||
get() {
|
||||
if (field.isBlank()) return rootFolderScanCron
|
||||
return field
|
||||
}
|
||||
|
||||
@NotBlank
|
||||
var userPassword: String = "user"
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
package org.gotson.komga.interfaces.scheduler
|
||||
|
||||
import mu.KotlinLogging
|
||||
import org.gotson.komga.domain.model.Library
|
||||
import org.gotson.komga.domain.service.AsyncOrchestrator
|
||||
import org.gotson.komga.infrastructure.configuration.KomgaProperties
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.context.event.EventListener
|
||||
@ -15,16 +13,15 @@ private val logger = KotlinLogging.logger {}
|
||||
|
||||
@Profile("dev", "prod")
|
||||
@Controller
|
||||
class RootScannerController(
|
||||
private val asyncOrchestrator: AsyncOrchestrator,
|
||||
private val komgaProperties: KomgaProperties
|
||||
class PeriodicScannerController(
|
||||
private val asyncOrchestrator: AsyncOrchestrator
|
||||
) {
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
@Scheduled(cron = "#{@komgaProperties.rootFolderScanCron ?: '-'}")
|
||||
@Scheduled(cron = "#{@komgaProperties.librariesScanCron ?: '-'}")
|
||||
fun scanRootFolder() {
|
||||
try {
|
||||
asyncOrchestrator.scanAndParse(Library("default", komgaProperties.rootFolder))
|
||||
asyncOrchestrator.scanAndParse()
|
||||
} catch (e: RejectedExecutionException) {
|
||||
logger.warn { "Another scan is already running, skipping" }
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
package org.gotson.komga.interfaces.web.rest
|
||||
|
||||
import mu.KotlinLogging
|
||||
import org.gotson.komga.domain.model.DirectoryNotFoundException
|
||||
import org.gotson.komga.domain.model.DuplicateNameException
|
||||
import org.gotson.komga.domain.model.Library
|
||||
import org.gotson.komga.domain.model.PathContainedInPath
|
||||
import org.gotson.komga.domain.persistence.LibraryRepository
|
||||
import org.gotson.komga.domain.service.LibraryLifecycle
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.DeleteMapping
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.ResponseStatus
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import java.io.FileNotFoundException
|
||||
import javax.validation.Valid
|
||||
import javax.validation.constraints.NotBlank
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@RestController
|
||||
@RequestMapping("api/v1/libraries", produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||
class LibraryController(
|
||||
private val libraryLifecycle: LibraryLifecycle,
|
||||
private val libraryRepository: LibraryRepository
|
||||
) {
|
||||
|
||||
@GetMapping
|
||||
fun getAll() =
|
||||
libraryRepository.findAll().map { it.toDto() }
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
fun addOne(
|
||||
@Valid @RequestBody library: LibraryCreationDto
|
||||
): LibraryDto =
|
||||
try {
|
||||
libraryLifecycle.addLibrary(Library(library.name, library.root)).toDto()
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is FileNotFoundException,
|
||||
is DirectoryNotFoundException,
|
||||
is DuplicateNameException,
|
||||
is PathContainedInPath ->
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, e.message)
|
||||
else -> throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
fun deleteOne(@PathVariable id: Long) {
|
||||
libraryRepository.findByIdOrNull(id)?.let {
|
||||
libraryLifecycle.deleteLibrary(it)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
}
|
||||
|
||||
data class LibraryCreationDto(
|
||||
@get:NotBlank val name: String,
|
||||
@get:NotBlank val root: String
|
||||
)
|
||||
|
||||
data class LibraryDto(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val root: String
|
||||
)
|
||||
|
||||
fun Library.toDto() = LibraryDto(
|
||||
id = id,
|
||||
name = name,
|
||||
root = root.toString()
|
||||
)
|
@ -11,7 +11,7 @@ import org.gotson.komga.domain.model.Status
|
||||
import org.gotson.komga.domain.model.UnsupportedMediaTypeException
|
||||
import org.gotson.komga.domain.persistence.BookRepository
|
||||
import org.gotson.komga.domain.persistence.SerieRepository
|
||||
import org.gotson.komga.domain.service.BookManager
|
||||
import org.gotson.komga.domain.service.BookLifecyle
|
||||
import org.gotson.komga.infrastructure.image.ImageType
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.PageRequest
|
||||
@ -43,7 +43,7 @@ private val logger = KotlinLogging.logger {}
|
||||
class SerieController(
|
||||
private val serieRepository: SerieRepository,
|
||||
private val bookRepository: BookRepository,
|
||||
private val bookManager: BookManager
|
||||
private val bookLifecyle: BookLifecyle
|
||||
) {
|
||||
|
||||
@GetMapping
|
||||
@ -189,7 +189,7 @@ class SerieController(
|
||||
val pageNum = if (zeroBasedIndex) pageNumber + 1 else pageNumber
|
||||
|
||||
val pageContent = try {
|
||||
bookManager.getBookPage(book, pageNum, convertFormat)
|
||||
bookLifecyle.getBookPage(book, pageNum, convertFormat)
|
||||
} catch (e: UnsupportedMediaTypeException) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, e.message)
|
||||
} catch (e: Exception) {
|
||||
|
@ -2,10 +2,9 @@ logging.level:
|
||||
web: DEBUG
|
||||
# org.springframework.security.web.FilterChainProxy: DEBUG
|
||||
komga:
|
||||
root-folder: D:\\files\\pdf
|
||||
threads:
|
||||
parse: 1
|
||||
# root-folder-scan-cron: "*/5 * * * * ?"
|
||||
# libraries-scan-cron: "*/5 * * * * ?"
|
||||
spring:
|
||||
profiles:
|
||||
include: flyway
|
||||
|
@ -4,5 +4,4 @@ spring:
|
||||
datasource:
|
||||
url: jdbc:h2:/config/database.h2;DB_CLOSE_DELAY=-1
|
||||
komga:
|
||||
root-folder: /books
|
||||
root-folder-scan-cron: "0 */15 * * * ?"
|
||||
libraries-scan-cron: "0 */15 * * * ?"
|
||||
|
@ -0,0 +1,18 @@
|
||||
create table library
|
||||
(
|
||||
id bigint not null,
|
||||
created_date timestamp not null,
|
||||
last_modified_date timestamp not null,
|
||||
name varchar not null,
|
||||
root varchar not null,
|
||||
primary key (id)
|
||||
);
|
||||
|
||||
alter table library
|
||||
add constraint uk_library_name unique (name);
|
||||
|
||||
alter table serie
|
||||
add (library_id bigint);
|
||||
|
||||
alter table serie
|
||||
add constraint fk_serie_library_library_id foreign key (library_id) references library (id);
|
@ -0,0 +1,2 @@
|
||||
alter table serie
|
||||
alter column library_id set not null;
|
@ -13,5 +13,9 @@ fun makeSerie(name: String, url: String = "file:/$name", books: List<Book> = lis
|
||||
return Serie(name = name, url = URL(url), fileLastModified = LocalDateTime.now(), books = books.toMutableList())
|
||||
}
|
||||
|
||||
fun makeLibrary(name: String = "default", url: String = "file:/$name"): Library {
|
||||
return Library(name, URL(url))
|
||||
}
|
||||
|
||||
fun makeBookPage(name: String) =
|
||||
BookPage(name, "image/png")
|
@ -2,13 +2,15 @@ package org.gotson.komga.domain.persistence
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.gotson.komga.domain.model.makeBook
|
||||
import org.gotson.komga.domain.model.makeLibrary
|
||||
import org.gotson.komga.domain.model.makeSerie
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.time.LocalDateTime
|
||||
@ -18,18 +20,30 @@ import java.time.LocalDateTime
|
||||
@Transactional
|
||||
class AuditableEntityTest(
|
||||
@Autowired private val serieRepository: SerieRepository,
|
||||
@Autowired private val entityManager: TestEntityManager
|
||||
@Autowired private val libraryRepository: LibraryRepository
|
||||
) {
|
||||
|
||||
private val library = makeLibrary()
|
||||
|
||||
@BeforeAll
|
||||
fun `setup library`() {
|
||||
libraryRepository.save(library)
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun `teardown library`() {
|
||||
libraryRepository.deleteAll()
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun `clear repository`() {
|
||||
entityManager.clear()
|
||||
serieRepository.deleteAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given serie with book when saving then created and modified date is also saved`() {
|
||||
// given
|
||||
val serie = makeSerie(name = "serie", books = listOf(makeBook("book1")))
|
||||
val serie = makeSerie(name = "serie", books = listOf(makeBook("book1"))).also { it.library = library }
|
||||
|
||||
// when
|
||||
serieRepository.save(serie)
|
||||
@ -44,7 +58,7 @@ class AuditableEntityTest(
|
||||
@Test
|
||||
fun `given existing serie with book when updating serie only then created date is kept and modified date is changed for serie only`() {
|
||||
// given
|
||||
val serie = makeSerie(name = "serie", books = listOf(makeBook("book1")))
|
||||
val serie = makeSerie(name = "serie", books = listOf(makeBook("book1"))).also { it.library = library }
|
||||
|
||||
serieRepository.save(serie)
|
||||
|
||||
@ -74,7 +88,7 @@ class AuditableEntityTest(
|
||||
@Test
|
||||
fun `given existing serie with book when updating book only then created date is kept and modified date is changed for book only`() {
|
||||
// given
|
||||
val serie = makeSerie(name = "serie", books = listOf(makeBook("book1")))
|
||||
val serie = makeSerie(name = "serie", books = listOf(makeBook("book1"))).also { it.library = library }
|
||||
|
||||
serieRepository.save(serie)
|
||||
|
||||
|
@ -4,13 +4,15 @@ import org.assertj.core.api.Assertions.assertThat
|
||||
import org.gotson.komga.domain.model.BookMetadata
|
||||
import org.gotson.komga.domain.model.Status
|
||||
import org.gotson.komga.domain.model.makeBook
|
||||
import org.gotson.komga.domain.model.makeLibrary
|
||||
import org.gotson.komga.domain.model.makeSerie
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager
|
||||
import org.springframework.data.domain.PageRequest
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
@ -21,12 +23,24 @@ import org.springframework.transaction.annotation.Transactional
|
||||
class BookRepositoryTest(
|
||||
@Autowired private val serieRepository: SerieRepository,
|
||||
@Autowired private val bookRepository: BookRepository,
|
||||
@Autowired private val entityManager: TestEntityManager
|
||||
@Autowired private val libraryRepository: LibraryRepository
|
||||
) {
|
||||
|
||||
private val library = makeLibrary()
|
||||
|
||||
@BeforeAll
|
||||
fun `setup library`() {
|
||||
libraryRepository.save(library)
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun `teardown library`() {
|
||||
libraryRepository.deleteAll()
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun `clear repository`() {
|
||||
entityManager.clear()
|
||||
serieRepository.deleteAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -34,7 +48,7 @@ class BookRepositoryTest(
|
||||
val serie = makeSerie(
|
||||
name = "serie",
|
||||
books = (1..100 step 2).map { makeBook("$it") }
|
||||
)
|
||||
).also { it.library = library }
|
||||
serieRepository.save(serie)
|
||||
|
||||
serie.books = serie.books.toMutableList().also { it.add(makeBook("2")) }
|
||||
@ -56,7 +70,7 @@ class BookRepositoryTest(
|
||||
val serie = makeSerie(
|
||||
name = "serie",
|
||||
books = (1..100 step 2).map { makeBook("$it") }
|
||||
)
|
||||
).also { it.library = library }
|
||||
serieRepository.save(serie)
|
||||
|
||||
serie.books = serie.books.toMutableList().also { it.add(makeBook("2")) }
|
||||
|
@ -5,13 +5,15 @@ import org.gotson.komga.domain.model.BookMetadata
|
||||
import org.gotson.komga.domain.model.Status
|
||||
import org.gotson.komga.domain.model.makeBook
|
||||
import org.gotson.komga.domain.model.makeBookPage
|
||||
import org.gotson.komga.domain.model.makeLibrary
|
||||
import org.gotson.komga.domain.model.makeSerie
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
@ -22,18 +24,30 @@ class PersistenceTest(
|
||||
@Autowired private val serieRepository: SerieRepository,
|
||||
@Autowired private val bookRepository: BookRepository,
|
||||
@Autowired private val bookMetadataRepository: BookMetadataRepository,
|
||||
@Autowired private val entityManager: TestEntityManager
|
||||
@Autowired private val libraryRepository: LibraryRepository
|
||||
) {
|
||||
|
||||
private val library = makeLibrary()
|
||||
|
||||
@BeforeAll
|
||||
fun `setup library`() {
|
||||
libraryRepository.save(library)
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun `teardown library`() {
|
||||
libraryRepository.deleteAll()
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun `clear repository`() {
|
||||
entityManager.clear()
|
||||
serieRepository.deleteAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given serie with book when saving then metadata is also saved`() {
|
||||
// given
|
||||
val serie = makeSerie(name = "serie", books = listOf(makeBook("book1")))
|
||||
val serie = makeSerie(name = "serie", books = listOf(makeBook("book1"))).also { it.library = library }
|
||||
|
||||
// when
|
||||
serieRepository.save(serie)
|
||||
@ -52,7 +66,7 @@ class PersistenceTest(
|
||||
makeBook("book 05"),
|
||||
makeBook("book 6"),
|
||||
makeBook("book 002")
|
||||
))
|
||||
)).also { it.library = library }
|
||||
|
||||
// when
|
||||
serieRepository.save(serie)
|
||||
@ -67,7 +81,7 @@ class PersistenceTest(
|
||||
@Test
|
||||
fun `given existing book when updating metadata then new metadata is saved`() {
|
||||
// given
|
||||
val serie = makeSerie(name = "serie", books = listOf(makeBook("book1")))
|
||||
val serie = makeSerie(name = "serie", books = listOf(makeBook("book1"))).also { it.library = library }
|
||||
serieRepository.save(serie)
|
||||
|
||||
// when
|
||||
@ -91,7 +105,7 @@ class PersistenceTest(
|
||||
@Test
|
||||
fun `given book pages unordered when saving then pages are ordered with natural sort`() {
|
||||
// given
|
||||
val serie = makeSerie(name = "serie", books = listOf(makeBook("book1")))
|
||||
val serie = makeSerie(name = "serie", books = listOf(makeBook("book1"))).also { it.library = library }
|
||||
serieRepository.save(serie)
|
||||
|
||||
// when
|
||||
|
@ -0,0 +1,103 @@
|
||||
package org.gotson.komga.domain.service
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.catchThrowable
|
||||
import org.gotson.komga.domain.model.DirectoryNotFoundException
|
||||
import org.gotson.komga.domain.model.DuplicateNameException
|
||||
import org.gotson.komga.domain.model.Library
|
||||
import org.gotson.komga.domain.model.PathContainedInPath
|
||||
import org.gotson.komga.domain.persistence.LibraryRepository
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||
import java.io.FileNotFoundException
|
||||
import java.nio.file.Files
|
||||
|
||||
@ExtendWith(SpringExtension::class)
|
||||
@SpringBootTest
|
||||
@AutoConfigureTestDatabase
|
||||
class LibraryLifecycleTest(
|
||||
@Autowired private val libraryRepository: LibraryRepository,
|
||||
@Autowired private val libraryLifecycle: LibraryLifecycle
|
||||
) {
|
||||
|
||||
@AfterEach
|
||||
fun `clear repositories`() {
|
||||
libraryRepository.deleteAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when adding library with non-existent root folder then exception is thrown`() {
|
||||
// when
|
||||
val thrown = catchThrowable { libraryLifecycle.addLibrary(Library("test", "/non-existent")) }
|
||||
|
||||
// then
|
||||
assertThat(thrown).isInstanceOf(FileNotFoundException::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when adding library with non-directory root folder then exception is thrown`() {
|
||||
// when
|
||||
val thrown = catchThrowable {
|
||||
libraryLifecycle.addLibrary(Library("test", Files.createTempFile(null, null).toUri().toURL()))
|
||||
}
|
||||
|
||||
// then
|
||||
assertThat(thrown).isInstanceOf(DirectoryNotFoundException::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given existing library when adding library with same name then exception is thrown`() {
|
||||
// given
|
||||
libraryLifecycle.addLibrary(Library("test", Files.createTempDirectory(null).toUri().toURL()))
|
||||
|
||||
// when
|
||||
val thrown = catchThrowable {
|
||||
libraryLifecycle.addLibrary(Library("test", Files.createTempDirectory(null).toUri().toURL()))
|
||||
}
|
||||
|
||||
// then
|
||||
assertThat(thrown).isInstanceOf(DuplicateNameException::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given existing library when adding library with root folder as child of existing library then exception is thrown`() {
|
||||
// given
|
||||
val parent = Files.createTempDirectory(null)
|
||||
libraryLifecycle.addLibrary(Library("parent", parent.toUri().toURL()))
|
||||
|
||||
// when
|
||||
val child = Files.createTempDirectory(parent, "")
|
||||
val thrown = catchThrowable {
|
||||
libraryLifecycle.addLibrary(Library("child", child.toUri().toURL()))
|
||||
}
|
||||
|
||||
// then
|
||||
assertThat(thrown)
|
||||
.isInstanceOf(PathContainedInPath::class.java)
|
||||
.hasMessageContaining("child")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given existing library when adding library with root folder as parent of existing library then exception is thrown`() {
|
||||
// given
|
||||
val parent = Files.createTempDirectory(null)
|
||||
val child = Files.createTempDirectory(parent, null)
|
||||
libraryLifecycle.addLibrary(Library("child", child.toUri().toURL()))
|
||||
|
||||
// when
|
||||
val thrown = catchThrowable {
|
||||
libraryLifecycle.addLibrary(Library("parent", parent.toUri().toURL()))
|
||||
}
|
||||
|
||||
// then
|
||||
assertThat(thrown)
|
||||
.isInstanceOf(PathContainedInPath::class.java)
|
||||
.hasMessageContaining("parent")
|
||||
}
|
||||
|
||||
}
|
@ -5,12 +5,13 @@ import io.mockk.every
|
||||
import io.mockk.verify
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.gotson.komga.domain.model.BookMetadata
|
||||
import org.gotson.komga.domain.model.Library
|
||||
import org.gotson.komga.domain.model.Status
|
||||
import org.gotson.komga.domain.model.makeBook
|
||||
import org.gotson.komga.domain.model.makeBookPage
|
||||
import org.gotson.komga.domain.model.makeLibrary
|
||||
import org.gotson.komga.domain.model.makeSerie
|
||||
import org.gotson.komga.domain.persistence.BookRepository
|
||||
import org.gotson.komga.domain.persistence.LibraryRepository
|
||||
import org.gotson.komga.domain.persistence.SerieRepository
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
@ -20,17 +21,17 @@ import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabas
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import javax.persistence.EntityManager
|
||||
import java.nio.file.Paths
|
||||
|
||||
@ExtendWith(SpringExtension::class)
|
||||
@SpringBootTest
|
||||
@AutoConfigureTestDatabase
|
||||
class LibraryManagerTest(
|
||||
class LibraryScannerTest(
|
||||
@Autowired private val serieRepository: SerieRepository,
|
||||
@Autowired private val libraryRepository: LibraryRepository,
|
||||
@Autowired private val bookRepository: BookRepository,
|
||||
@Autowired private val libraryManager: LibraryManager,
|
||||
@Autowired private val bookManager: BookManager,
|
||||
@Autowired private val entityManager: EntityManager
|
||||
@Autowired private val libraryScanner: LibraryScanner,
|
||||
@Autowired private val bookLifecyle: BookLifecyle
|
||||
) {
|
||||
|
||||
@MockkBean
|
||||
@ -39,17 +40,18 @@ class LibraryManagerTest(
|
||||
@MockkBean
|
||||
private lateinit var mockParser: BookParser
|
||||
|
||||
private val library = Library(name = "test", root = "/root")
|
||||
|
||||
@AfterEach
|
||||
fun `clear repositories`() {
|
||||
entityManager.clear()
|
||||
serieRepository.deleteAll()
|
||||
libraryRepository.deleteAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Transactional
|
||||
fun `given existing Serie when adding files and scanning then only updated Books are persisted`() {
|
||||
//given
|
||||
// given
|
||||
val library = libraryRepository.save(makeLibrary())
|
||||
|
||||
val serie = makeSerie(name = "serie", books = listOf(makeBook("book1")))
|
||||
val serieWithMoreBooks = makeSerie(name = "serie", books = listOf(makeBook("book1"), makeBook("book2")))
|
||||
|
||||
@ -57,10 +59,10 @@ class LibraryManagerTest(
|
||||
listOf(serie),
|
||||
listOf(serieWithMoreBooks)
|
||||
)
|
||||
libraryManager.scanRootFolder(library)
|
||||
libraryScanner.scanRootFolder(library)
|
||||
|
||||
// when
|
||||
libraryManager.scanRootFolder(library)
|
||||
libraryScanner.scanRootFolder(library)
|
||||
|
||||
// then
|
||||
val series = serieRepository.findAll()
|
||||
@ -75,7 +77,9 @@ class LibraryManagerTest(
|
||||
@Test
|
||||
@Transactional
|
||||
fun `given existing Serie when removing files and scanning then only updated Books are persisted`() {
|
||||
//given
|
||||
// given
|
||||
val library = libraryRepository.save(makeLibrary())
|
||||
|
||||
val serie = makeSerie(name = "serie", books = listOf(makeBook("book1"), makeBook("book2")))
|
||||
val serieWithLessBooks = makeSerie(name = "serie", books = listOf(makeBook("book1")))
|
||||
|
||||
@ -84,10 +88,10 @@ class LibraryManagerTest(
|
||||
listOf(serie),
|
||||
listOf(serieWithLessBooks)
|
||||
)
|
||||
libraryManager.scanRootFolder(library)
|
||||
libraryScanner.scanRootFolder(library)
|
||||
|
||||
// when
|
||||
libraryManager.scanRootFolder(library)
|
||||
libraryScanner.scanRootFolder(library)
|
||||
|
||||
// then
|
||||
val series = serieRepository.findAll()
|
||||
@ -103,7 +107,9 @@ class LibraryManagerTest(
|
||||
@Test
|
||||
@Transactional
|
||||
fun `given existing Serie when updating files and scanning then Books are updated`() {
|
||||
//given
|
||||
// given
|
||||
val library = libraryRepository.save(makeLibrary())
|
||||
|
||||
val serie = makeSerie(name = "serie", books = listOf(makeBook("book1")))
|
||||
val serieWithUpdatedBooks = makeSerie(name = "serie", books = listOf(makeBook("book1updated", "file:/book1")))
|
||||
|
||||
@ -112,10 +118,10 @@ class LibraryManagerTest(
|
||||
listOf(serie),
|
||||
listOf(serieWithUpdatedBooks)
|
||||
)
|
||||
libraryManager.scanRootFolder(library)
|
||||
libraryScanner.scanRootFolder(library)
|
||||
|
||||
// when
|
||||
libraryManager.scanRootFolder(library)
|
||||
libraryScanner.scanRootFolder(library)
|
||||
|
||||
// then
|
||||
val series = serieRepository.findAll()
|
||||
@ -131,18 +137,18 @@ class LibraryManagerTest(
|
||||
|
||||
@Test
|
||||
fun `given existing Serie when deleting all books and scanning then Series and Books are removed`() {
|
||||
//given
|
||||
val serie = makeSerie(name = "serie", books = listOf(makeBook("book1")))
|
||||
// given
|
||||
val library = libraryRepository.save(makeLibrary())
|
||||
|
||||
every { mockScanner.scanRootFolder(any()) }
|
||||
.returnsMany(
|
||||
listOf(serie),
|
||||
listOf(makeSerie(name = "serie", books = listOf(makeBook("book1")))),
|
||||
emptyList()
|
||||
)
|
||||
libraryManager.scanRootFolder(library)
|
||||
libraryScanner.scanRootFolder(library)
|
||||
|
||||
// when
|
||||
libraryManager.scanRootFolder(library)
|
||||
libraryScanner.scanRootFolder(library)
|
||||
|
||||
// then
|
||||
verify(exactly = 2) { mockScanner.scanRootFolder(any()) }
|
||||
@ -151,22 +157,46 @@ class LibraryManagerTest(
|
||||
assertThat(bookRepository.count()).describedAs("Book repository should be empty").isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given existing Series when deleting all books of one serie and scanning then Serie and its Books are removed`() {
|
||||
// given
|
||||
val library = libraryRepository.save(makeLibrary())
|
||||
|
||||
every { mockScanner.scanRootFolder(any()) }
|
||||
.returnsMany(
|
||||
listOf(makeSerie(name = "serie", books = listOf(makeBook("book1"))), makeSerie(name = "serie2", books = listOf(makeBook("book2")))),
|
||||
listOf(makeSerie(name = "serie", books = listOf(makeBook("book1"))))
|
||||
)
|
||||
libraryScanner.scanRootFolder(library)
|
||||
|
||||
// when
|
||||
libraryScanner.scanRootFolder(library)
|
||||
|
||||
// then
|
||||
verify(exactly = 2) { mockScanner.scanRootFolder(any()) }
|
||||
|
||||
assertThat(serieRepository.count()).describedAs("Serie repository should be empty").isEqualTo(1)
|
||||
assertThat(bookRepository.count()).describedAs("Book repository should be empty").isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given existing Book with metadata when rescanning then metadata is kept intact`() {
|
||||
//given
|
||||
// given
|
||||
val library = libraryRepository.save(makeLibrary())
|
||||
|
||||
val book1 = makeBook("book1")
|
||||
every { mockScanner.scanRootFolder(any()) }
|
||||
.returnsMany(
|
||||
listOf(makeSerie(name = "serie", books = listOf(book1))),
|
||||
listOf(makeSerie(name = "serie", books = listOf(makeBook(name = "book1", fileLastModified = book1.fileLastModified))))
|
||||
)
|
||||
libraryManager.scanRootFolder(library)
|
||||
libraryScanner.scanRootFolder(library)
|
||||
|
||||
every { mockParser.parse(any()) } returns BookMetadata(status = Status.READY, mediaType = "application/zip", pages = mutableListOf(makeBookPage("1.jpg"), makeBookPage("2.jpg")))
|
||||
bookRepository.findAll().map { bookManager.parseAndPersist(it) }.map { it.get() }
|
||||
bookRepository.findAll().map { bookLifecyle.parseAndPersist(it) }.map { it.get() }
|
||||
|
||||
// when
|
||||
libraryManager.scanRootFolder(library)
|
||||
libraryScanner.scanRootFolder(library)
|
||||
|
||||
// then
|
||||
verify(exactly = 2) { mockScanner.scanRootFolder(any()) }
|
||||
@ -179,4 +209,35 @@ class LibraryManagerTest(
|
||||
assertThat(book.metadata.pages.map { it.fileName }).containsExactly("1.jpg", "2.jpg")
|
||||
assertThat(book.lastModifiedDate).isNotEqualTo(book.createdDate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given 2 libraries when deleting all books of one and scanning then the other library is kept intact`() {
|
||||
// given
|
||||
val library1 = libraryRepository.save(makeLibrary(name = "library1"))
|
||||
val library2 = libraryRepository.save(makeLibrary(name = "library2"))
|
||||
|
||||
every { mockScanner.scanRootFolder(Paths.get(library1.root.toURI())) } returns
|
||||
listOf(makeSerie(name = "serie1", books = listOf(makeBook("book1"))))
|
||||
|
||||
every { mockScanner.scanRootFolder(Paths.get(library2.root.toURI())) }.returnsMany(
|
||||
listOf(makeSerie(name = "serie2", books = listOf(makeBook("book2")))),
|
||||
emptyList()
|
||||
)
|
||||
|
||||
libraryScanner.scanRootFolder(library1)
|
||||
libraryScanner.scanRootFolder(library2)
|
||||
|
||||
assertThat(serieRepository.count()).describedAs("Serie repository should be empty").isEqualTo(2)
|
||||
assertThat(bookRepository.count()).describedAs("Book repository should be empty").isEqualTo(2)
|
||||
|
||||
// when
|
||||
libraryScanner.scanRootFolder(library2)
|
||||
|
||||
// then
|
||||
verify(exactly = 1) { mockScanner.scanRootFolder(Paths.get(library1.root.toURI())) }
|
||||
verify(exactly = 2) { mockScanner.scanRootFolder(Paths.get(library2.root.toURI())) }
|
||||
|
||||
assertThat(serieRepository.count()).describedAs("Serie repository should be empty").isEqualTo(1)
|
||||
assertThat(bookRepository.count()).describedAs("Book repository should be empty").isEqualTo(1)
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package org.gotson.komga.interfaces.web.rest
|
||||
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.security.test.context.support.WithAnonymousUser
|
||||
import org.springframework.security.test.context.support.WithMockUser
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
|
||||
|
||||
@ExtendWith(SpringExtension::class)
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc(printOnlyOnFailure = false)
|
||||
class LibraryControllerTest(
|
||||
@Autowired private val mockMvc: MockMvc
|
||||
) {
|
||||
private val route = "/api/v1/libraries"
|
||||
|
||||
@Nested
|
||||
inner class AnonymousUser {
|
||||
@Test
|
||||
@WithAnonymousUser
|
||||
fun `given anonymous user when getAll then return unauthorized`() {
|
||||
mockMvc.perform(MockMvcRequestBuilders.get(route))
|
||||
.andExpect(MockMvcResultMatchers.status().isUnauthorized)
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithAnonymousUser
|
||||
fun `given anonymous user when addOne then return unauthorized`() {
|
||||
val jsonString = """{"name":"test", "root": "C:\\Temp"}"""
|
||||
|
||||
mockMvc.perform(MockMvcRequestBuilders.post(route)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(jsonString))
|
||||
.andExpect(MockMvcResultMatchers.status().isUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class UserRole {
|
||||
@Test
|
||||
@WithMockUser(roles = ["USER"])
|
||||
fun `given user with USER role when getAll then return ok`() {
|
||||
mockMvc.perform(MockMvcRequestBuilders.get(route))
|
||||
.andExpect(MockMvcResultMatchers.status().isOk)
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = ["USER"])
|
||||
fun `given user with USER role when addOne then return forbidden`() {
|
||||
val jsonString = """{"name":"test", "root": "C:\\Temp"}"""
|
||||
|
||||
mockMvc.perform(MockMvcRequestBuilders.post(route)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(jsonString))
|
||||
.andExpect(MockMvcResultMatchers.status().isForbidden)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +1,16 @@
|
||||
package org.gotson.komga.interfaces.web
|
||||
package org.gotson.komga.interfaces.web.rest
|
||||
|
||||
import org.gotson.komga.domain.model.BookMetadata
|
||||
import org.gotson.komga.domain.model.Status
|
||||
import org.gotson.komga.domain.model.makeBook
|
||||
import org.gotson.komga.domain.model.makeLibrary
|
||||
import org.gotson.komga.domain.model.makeSerie
|
||||
import org.gotson.komga.domain.persistence.BookRepository
|
||||
import org.gotson.komga.domain.persistence.LibraryRepository
|
||||
import org.gotson.komga.domain.persistence.SerieRepository
|
||||
import org.gotson.komga.domain.service.BookManager
|
||||
import org.gotson.komga.domain.service.LibraryManager
|
||||
import org.hamcrest.CoreMatchers.equalTo
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
@ -21,7 +22,6 @@ import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@ExtendWith(SpringExtension::class)
|
||||
@SpringBootTest
|
||||
@ -29,17 +29,26 @@ import javax.persistence.EntityManager
|
||||
@AutoConfigureMockMvc(printOnlyOnFailure = false)
|
||||
class SerieControllerTest(
|
||||
@Autowired private val serieRepository: SerieRepository,
|
||||
@Autowired private val bookRepository: BookRepository,
|
||||
@Autowired private val libraryManager: LibraryManager,
|
||||
@Autowired private val bookManager: BookManager,
|
||||
@Autowired private val entityManager: EntityManager,
|
||||
@Autowired private val libraryRepository: LibraryRepository,
|
||||
@Autowired private val mockMvc: MockMvc
|
||||
|
||||
) {
|
||||
|
||||
private val library = makeLibrary()
|
||||
|
||||
@BeforeAll
|
||||
fun `setup library`() {
|
||||
libraryRepository.save(library)
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun `teardown library`() {
|
||||
libraryRepository.deleteAll()
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun `clear repositories`() {
|
||||
entityManager.clear()
|
||||
fun `clear repository`() {
|
||||
serieRepository.deleteAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -48,7 +57,7 @@ class SerieControllerTest(
|
||||
val serie = makeSerie(
|
||||
name = "serie",
|
||||
books = listOf(makeBook("1"), makeBook("3"))
|
||||
)
|
||||
).also { it.library = library }
|
||||
serieRepository.save(serie)
|
||||
|
||||
serie.books = serie.books.toMutableList().also { it.add(makeBook("2")) }
|
||||
@ -67,7 +76,7 @@ class SerieControllerTest(
|
||||
val serie = makeSerie(
|
||||
name = "serie",
|
||||
books = (1..100 step 2).map { makeBook("$it") }
|
||||
)
|
||||
).also { it.library = library }
|
||||
serieRepository.save(serie)
|
||||
|
||||
serie.books = serie.books.toMutableList().also { it.add(makeBook("2")) }
|
||||
@ -90,7 +99,7 @@ class SerieControllerTest(
|
||||
val serie = makeSerie(
|
||||
name = "serie",
|
||||
books = (1..100 step 2).map { makeBook("$it") }
|
||||
)
|
||||
).also { it.library = library }
|
||||
serieRepository.save(serie)
|
||||
|
||||
serie.books = serie.books.toMutableList().also { it.add(makeBook("2")) }
|
Loading…
Reference in New Issue
Block a user