mirror of
https://github.com/gotson/komga.git
synced 2025-01-09 04:08:00 +08:00
feat: series and book files deletion
closes #731 Co-authored-by: Gauthier Roebroeck <gauthier.roebroeck@gmail.com>
This commit is contained in:
parent
31ad351144
commit
e626ff850f
@ -70,6 +70,26 @@
|
||||
:series="updateSeries"
|
||||
/>
|
||||
|
||||
<confirmation-dialog
|
||||
v-model="deleteSeriesDialog"
|
||||
:title="$t('dialog.delete_series.dialog_title')"
|
||||
:body-html="seriesToDeleteSingle ? $t('dialog.delete_series.warning_html', {name: seriesToDelete.name}) : $t('dialog.delete_series.warning_multiple_html', {count: seriesToDelete.length})"
|
||||
:confirm-text="seriesToDeleteSingle ? $t('dialog.delete_series.confirm_delete', {name: seriesToDelete.name}) : $t('dialog.delete_series.confirm_delete_multiple', {count: seriesToDelete.length})"
|
||||
:button-confirm="$t('dialog.delete_series.button_confirm')"
|
||||
button-confirm-color="error"
|
||||
@confirm="deleteSeries"
|
||||
/>
|
||||
|
||||
<confirmation-dialog
|
||||
v-model="deleteBookDialog"
|
||||
:title="booksToDeleteSingle ? $t('dialog.delete_book.dialog_title') : $t('dialog.delete_book.dialog_title_multiple')"
|
||||
:body-html="booksToDeleteSingle ? $t('dialog.delete_book.warning_html', {name: booksToDelete.name}) : $t('dialog.delete_book.warning_multiple_html', {count: booksToDelete.length})"
|
||||
:confirm-text="booksToDeleteSingle ? $t('dialog.delete_book.confirm_delete', {name: booksToDelete.name}) : $t('dialog.delete_book.confirm_delete_multiple', {count: booksToDelete.length})"
|
||||
:button-confirm="$t('dialog.delete_book.button_confirm')"
|
||||
button-confirm-color="error"
|
||||
@confirm="deleteBooks"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -224,6 +244,20 @@ export default Vue.extend({
|
||||
updateBulkBooks(): BookDto[] {
|
||||
return this.$store.state.updateBulkBooks
|
||||
},
|
||||
deleteBookDialog: {
|
||||
get(): boolean {
|
||||
return this.$store.state.deleteBookDialog
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('dialogDeleteBookDisplay', val)
|
||||
},
|
||||
},
|
||||
booksToDelete(): BookDto | BookDto[] {
|
||||
return this.$store.state.deleteBooks
|
||||
},
|
||||
booksToDeleteSingle(): boolean {
|
||||
return !Array.isArray(this.booksToDelete)
|
||||
},
|
||||
// series
|
||||
updateSeriesDialog: {
|
||||
get(): boolean {
|
||||
@ -236,6 +270,20 @@ export default Vue.extend({
|
||||
updateSeries(): SeriesDto | SeriesDto[] {
|
||||
return this.$store.state.updateSeries
|
||||
},
|
||||
deleteSeriesDialog: {
|
||||
get(): boolean {
|
||||
return this.$store.state.deleteSeriesDialog
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('dialogDeleteSeriesDisplay', val)
|
||||
},
|
||||
},
|
||||
seriesToDelete(): SeriesDto | SeriesDto[] {
|
||||
return this.$store.state.deleteSeries
|
||||
},
|
||||
seriesToDeleteSingle(): boolean {
|
||||
return !Array.isArray(this.seriesToDelete)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async deleteLibrary() {
|
||||
@ -265,6 +313,26 @@ export default Vue.extend({
|
||||
}
|
||||
}
|
||||
},
|
||||
async deleteSeries() {
|
||||
const toUpdate = (this.seriesToDeleteSingle ? [this.seriesToDelete] : this.seriesToDelete) as SeriesDto[]
|
||||
for (const b of toUpdate) {
|
||||
try {
|
||||
await this.$komgaSeries.deleteSeries(b.id)
|
||||
} catch (e) {
|
||||
this.$eventHub.$emit(ERROR, {message: e.message} as ErrorEvent)
|
||||
}
|
||||
}
|
||||
},
|
||||
async deleteBooks() {
|
||||
const toUpdate = (this.booksToDeleteSingle ? [this.booksToDelete] : this.booksToDelete) as BookDto[]
|
||||
for (const b of toUpdate) {
|
||||
try {
|
||||
await this.$komgaBooks.deleteBook(b.id)
|
||||
} catch (e) {
|
||||
this.$eventHub.$emit(ERROR, {message: e.message} as ErrorEvent)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
@ -79,7 +79,7 @@
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon @click="doDelete" v-if="isAdmin && (kind === 'collections' || kind === 'readlists')">
|
||||
<v-btn icon @click="doDelete" v-if="isAdmin">
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon v-on="on">mdi-delete</v-icon>
|
||||
|
@ -22,6 +22,9 @@
|
||||
<v-list-item @click="markUnread" v-if="!isUnread">
|
||||
<v-list-item-title>{{ $t('menu.mark_unread') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="promptDeleteBook" class="list-warning" v-if="isAdmin">
|
||||
<v-list-item-title>{{ $t('menu.delete') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
@ -82,6 +85,9 @@ export default Vue.extend({
|
||||
async markUnread () {
|
||||
await this.$komgaBooks.deleteReadProgress(this.book.id)
|
||||
},
|
||||
promptDeleteBook () {
|
||||
this.$store.dispatch('dialogDeleteBook', this.book)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
@ -22,6 +22,9 @@
|
||||
<v-list-item @click="markUnread" v-if="!isUnread">
|
||||
<v-list-item-title>{{ $t('menu.mark_unread') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="promptDeleteSeries" class="list-warning" v-if="isAdmin">
|
||||
<v-list-item-title>{{ $t('menu.delete') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
@ -81,6 +84,9 @@ export default Vue.extend({
|
||||
await this.$komgaSeries.markAsUnread(this.series.id)
|
||||
// this.$eventHub.$emit(SERIES_CHANGED, seriesToEventSeriesChanged(this.series))
|
||||
},
|
||||
promptDeleteSeries () {
|
||||
this.$store.dispatch('dialogDeleteSeries', this.series)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
@ -272,6 +272,15 @@
|
||||
"button_confirm": "Analyze",
|
||||
"title": "Analyze library"
|
||||
},
|
||||
"delete_book": {
|
||||
"button_confirm": "Delete",
|
||||
"confirm_delete": "Yes, delete book \"{name}\" and its files",
|
||||
"confirm_delete_multiple": "Yes, delete {count} books and their files",
|
||||
"dialog_title": "Delete Book",
|
||||
"dialog_title_multiple": "Delete Books",
|
||||
"warning_html": "The book <b>{name}</b> will be removed from this server alongside with stored media files. This <b>cannot</b> be undone. Continue?",
|
||||
"warning_multiple_html": "{count} books will be removed from this server alongside with stored media files. This <b>cannot</b> be undone. Continue?"
|
||||
},
|
||||
"delete_collection": {
|
||||
"button_confirm": "Delete",
|
||||
"confirm_delete": "Yes, delete the collection \"{name}\"",
|
||||
@ -302,6 +311,14 @@
|
||||
"dialog_title": "Delete User",
|
||||
"warning_html": "The user <b>{name}</b> will be deleted from this server. This <b>cannot</b> be undone. Continue?"
|
||||
},
|
||||
"delete_series": {
|
||||
"button_confirm": "Delete",
|
||||
"confirm_delete": "Yes, delete series \"{name}\" and its files",
|
||||
"confirm_delete_multiple": "Yes, delete {count} series and their files",
|
||||
"dialog_title": "Delete Series",
|
||||
"warning_html": "The Series <b>{name}</b> will be removed from this server alongside with stored media files. This <b>cannot</b> be undone. Continue?",
|
||||
"warning_multiple_html": "{count} series will be removed from this server alongside with stored media files. This <b>cannot</b> be undone. Continue?"
|
||||
},
|
||||
"edit_books": {
|
||||
"authors_notice_multiple_edit": "You are editing authors for multiple books. This will override existing authors of each book.",
|
||||
"button_cancel": "Cancel",
|
||||
|
@ -212,4 +212,16 @@ export default class KomgaBooksService {
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async deleteBook(bookId: string) {
|
||||
try {
|
||||
await this.http.delete(`${API_BOOKS}/${bookId}/file`)
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to delete book'
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -265,4 +265,16 @@ export default class KomgaSeriesService {
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async deleteSeries(seriesId: string) {
|
||||
try {
|
||||
await this.http.delete(`${API_SERIES}/${seriesId}/file`)
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying delete series '${seriesId}'`
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,8 @@ export default new Vuex.Store({
|
||||
// books
|
||||
updateBooks: {} as BookDto | BookDto[],
|
||||
updateBooksDialog: false,
|
||||
deleteBooks: {} as BookDto | BookDto[],
|
||||
deleteBookDialog: false,
|
||||
// books bulk
|
||||
updateBulkBooks: [] as BookDto[],
|
||||
updateBulkBooksDialog: false,
|
||||
@ -43,6 +45,8 @@ export default new Vuex.Store({
|
||||
// series
|
||||
updateSeries: {} as SeriesDto | SeriesDto[],
|
||||
updateSeriesDialog: false,
|
||||
deleteSeries: {} as SeriesDto | SeriesDto[],
|
||||
deleteSeriesDialog: false,
|
||||
|
||||
booksToCheck: 0,
|
||||
},
|
||||
@ -105,6 +109,12 @@ export default new Vuex.Store({
|
||||
setUpdateBooksDialog(state, dialog) {
|
||||
state.updateBooksDialog = dialog
|
||||
},
|
||||
setDeleteBooks(state, books) {
|
||||
state.deleteBooks = books
|
||||
},
|
||||
setDeleteBookDialog(state, dialog) {
|
||||
state.deleteBookDialog = dialog
|
||||
},
|
||||
// Books bulk
|
||||
setUpdateBulkBooks(state, books) {
|
||||
state.updateBulkBooks = books
|
||||
@ -122,6 +132,12 @@ export default new Vuex.Store({
|
||||
setBooksToCheck(state, count) {
|
||||
state.booksToCheck = count
|
||||
},
|
||||
setDeleteSeries(state, series) {
|
||||
state.deleteSeries = series
|
||||
},
|
||||
setDeleteSeriesDialog(state, dialog) {
|
||||
state.deleteSeriesDialog = dialog
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
// collections
|
||||
@ -195,6 +211,13 @@ export default new Vuex.Store({
|
||||
dialogUpdateBooksDisplay({commit}, value) {
|
||||
commit('setUpdateBooksDialog', value)
|
||||
},
|
||||
dialogDeleteBook({commit}, books) {
|
||||
commit('setDeleteBooks', books)
|
||||
commit('setDeleteBookDialog', true)
|
||||
},
|
||||
dialogDeleteBookDisplay({commit}, value) {
|
||||
commit('setDeleteBookDialog', value)
|
||||
},
|
||||
// books bulk
|
||||
dialogUpdateBulkBooks({commit}, books) {
|
||||
commit('setUpdateBulkBooks', books)
|
||||
@ -211,6 +234,13 @@ export default new Vuex.Store({
|
||||
dialogUpdateSeriesDisplay({commit}, value) {
|
||||
commit('setUpdateSeriesDialog', value)
|
||||
},
|
||||
dialogDeleteSeries({commit}, series) {
|
||||
commit('setDeleteSeries', series)
|
||||
commit('setDeleteSeriesDialog', true)
|
||||
},
|
||||
dialogDeleteSeriesDisplay({commit}, value) {
|
||||
commit('setDeleteSeriesDialog', value)
|
||||
},
|
||||
},
|
||||
modules: {
|
||||
persistedState: persistedModule,
|
||||
|
@ -35,6 +35,7 @@
|
||||
@mark-unread="markSelectedUnread"
|
||||
@add-to-collection="addToCollection"
|
||||
@edit="editMultipleSeries"
|
||||
@delete="deleteSeries"
|
||||
/>
|
||||
|
||||
<library-navigation v-if="$vuetify.breakpoint.name === 'xs'" :libraryId="libraryId" bottom-navigation/>
|
||||
@ -554,6 +555,9 @@ export default Vue.extend({
|
||||
editMultipleSeries() {
|
||||
this.$store.dispatch('dialogUpdateSeries', this.selectedSeries)
|
||||
},
|
||||
deleteSeries() {
|
||||
this.$store.dispatch('dialogDeleteSeries', this.selectedSeries)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
@ -48,6 +48,7 @@
|
||||
@add-to-readlist="addToReadList"
|
||||
@bulk-edit="bulkEditMultipleBooks"
|
||||
@edit="editMultipleBooks"
|
||||
@delete="deleteBooks"
|
||||
/>
|
||||
|
||||
<filter-drawer
|
||||
@ -832,6 +833,9 @@ export default Vue.extend({
|
||||
))
|
||||
this.selectedBooks = []
|
||||
},
|
||||
deleteBooks() {
|
||||
this.$store.dispatch('dialogDeleteBook', this.selectedBooks)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
@ -13,8 +13,9 @@ sealed class Task(priority: Int = DEFAULT_PRIORITY) : Serializable {
|
||||
abstract fun uniqueId(): String
|
||||
val priority = priority.coerceIn(0, 9)
|
||||
|
||||
data class ScanLibrary(val libraryId: String) : Task() {
|
||||
class ScanLibrary(val libraryId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
|
||||
override fun uniqueId() = "SCAN_LIBRARY_$libraryId"
|
||||
override fun toString(): String = "ScanLibrary(libraryId='$libraryId', priority='$priority')"
|
||||
}
|
||||
|
||||
class EmptyTrash(val libraryId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
|
||||
@ -82,4 +83,14 @@ sealed class Task(priority: Int = DEFAULT_PRIORITY) : Serializable {
|
||||
override fun uniqueId() = "REBUILD_INDEX"
|
||||
override fun toString(): String = "RebuildIndex(priority='$priority')"
|
||||
}
|
||||
|
||||
class DeleteBook(val bookId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
|
||||
override fun uniqueId() = "DELETE_BOOK_$bookId"
|
||||
override fun toString(): String = "DeleteBook(bookId='$bookId', priority='$priority')"
|
||||
}
|
||||
|
||||
class DeleteSeries(val seriesId: String, priority: Int = DEFAULT_PRIORITY) : Task(priority) {
|
||||
override fun uniqueId() = "DELETE_SERIES_$seriesId"
|
||||
override fun toString(): String = "DeleteSeries(seriesId='$seriesId', priority='$priority')"
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import org.gotson.komga.domain.service.BookLifecycle
|
||||
import org.gotson.komga.domain.service.BookMetadataLifecycle
|
||||
import org.gotson.komga.domain.service.LibraryContentLifecycle
|
||||
import org.gotson.komga.domain.service.LocalArtworkLifecycle
|
||||
import org.gotson.komga.domain.service.SeriesLifecycle
|
||||
import org.gotson.komga.domain.service.SeriesMetadataLifecycle
|
||||
import org.gotson.komga.infrastructure.jms.QUEUE_FACTORY
|
||||
import org.gotson.komga.infrastructure.jms.QUEUE_TASKS
|
||||
@ -31,6 +32,7 @@ class TaskHandler(
|
||||
private val libraryContentLifecycle: LibraryContentLifecycle,
|
||||
private val bookLifecycle: BookLifecycle,
|
||||
private val bookMetadataLifecycle: BookMetadataLifecycle,
|
||||
private val seriesLifecycle: SeriesLifecycle,
|
||||
private val seriesMetadataLifecycle: SeriesMetadataLifecycle,
|
||||
private val localArtworkLifecycle: LocalArtworkLifecycle,
|
||||
private val bookImporter: BookImporter,
|
||||
@ -121,6 +123,20 @@ class TaskHandler(
|
||||
} ?: logger.warn { "Cannot execute task $task: Book does not exist" }
|
||||
|
||||
is Task.RebuildIndex -> searchIndexLifecycle.rebuildIndex()
|
||||
|
||||
is Task.DeleteBook -> {
|
||||
bookRepository.findByIdOrNull(task.bookId)?.let { book ->
|
||||
bookLifecycle.deleteBookFiles(book)
|
||||
taskReceiver.scanLibrary(book.libraryId, task.priority)
|
||||
}
|
||||
}
|
||||
|
||||
is Task.DeleteSeries -> {
|
||||
seriesRepository.findByIdOrNull(task.seriesId)?.let { series ->
|
||||
seriesLifecycle.deleteSeriesFiles(series)
|
||||
taskReceiver.scanLibrary(series.libraryId, task.priority)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.also {
|
||||
logger.info { "Task $task executed in $it" }
|
||||
|
@ -42,8 +42,8 @@ class TaskReceiver(
|
||||
libraryRepository.findAll().forEach { scanLibrary(it.id) }
|
||||
}
|
||||
|
||||
fun scanLibrary(libraryId: String) {
|
||||
submitTask(Task.ScanLibrary(libraryId))
|
||||
fun scanLibrary(libraryId: String, priority: Int = DEFAULT_PRIORITY) {
|
||||
submitTask(Task.ScanLibrary(libraryId, priority))
|
||||
}
|
||||
|
||||
fun emptyTrash(libraryId: String, priority: Int = DEFAULT_PRIORITY) {
|
||||
@ -121,6 +121,14 @@ class TaskReceiver(
|
||||
submitTask(Task.RebuildIndex(priority))
|
||||
}
|
||||
|
||||
fun deleteBook(bookId: String, priority: Int = DEFAULT_PRIORITY) {
|
||||
submitTask(Task.DeleteBook(bookId, priority))
|
||||
}
|
||||
|
||||
fun deleteSeries(seriesId: String, priority: Int = DEFAULT_PRIORITY) {
|
||||
submitTask(Task.DeleteSeries(seriesId, priority))
|
||||
}
|
||||
|
||||
private fun submitTask(task: Task) {
|
||||
logger.info { "Sending task: $task" }
|
||||
jmsTemplates[task.priority]!!.convertAndSend(QUEUE_TASKS, task) {
|
||||
|
@ -8,6 +8,7 @@ interface ThumbnailSeriesRepository {
|
||||
fun findSelectedBySeriesIdOrNull(seriesId: String): ThumbnailSeries?
|
||||
|
||||
fun findAllBySeriesId(seriesId: String): Collection<ThumbnailSeries>
|
||||
fun findAllBySeriesIdIdAndType(seriesId: String, type: ThumbnailSeries.Type): Collection<ThumbnailSeries>
|
||||
|
||||
fun insert(thumbnail: ThumbnailSeries)
|
||||
fun markSelected(thumbnail: ThumbnailSeries)
|
||||
|
@ -13,6 +13,7 @@ import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.MediaNotReadyException
|
||||
import org.gotson.komga.domain.model.ReadProgress
|
||||
import org.gotson.komga.domain.model.ThumbnailBook
|
||||
import org.gotson.komga.domain.model.withCode
|
||||
import org.gotson.komga.domain.persistence.BookMetadataRepository
|
||||
import org.gotson.komga.domain.persistence.BookRepository
|
||||
import org.gotson.komga.domain.persistence.MediaRepository
|
||||
@ -26,7 +27,15 @@ import org.gotson.komga.infrastructure.image.ImageType
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.support.TransactionTemplate
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.io.path.deleteExisting
|
||||
import kotlin.io.path.deleteIfExists
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.isWritable
|
||||
import kotlin.io.path.listDirectoryEntries
|
||||
import kotlin.io.path.notExists
|
||||
import kotlin.io.path.toPath
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@ -307,4 +316,19 @@ class BookLifecycle(
|
||||
eventPublisher.publishEvent(DomainEvent.ReadProgressDeleted(progress))
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteBookFiles(book: Book) {
|
||||
if (book.path.notExists() || !book.path.isWritable())
|
||||
throw FileNotFoundException("File is not accessible : ${book.path}").withCode("ERR_1018")
|
||||
|
||||
val thumbnails = thumbnailBookRepository.findAllByBookIdAndType(book.id, ThumbnailBook.Type.SIDECAR)
|
||||
.mapNotNull { it.url?.toURI()?.toPath() }
|
||||
.filter { it.exists() && it.isWritable() }
|
||||
|
||||
book.path.deleteIfExists()
|
||||
thumbnails.forEach { it.deleteIfExists() }
|
||||
|
||||
if (book.path.parent.listDirectoryEntries().isEmpty())
|
||||
book.path.parent.deleteExisting()
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import org.gotson.komga.domain.model.ReadProgress
|
||||
import org.gotson.komga.domain.model.Series
|
||||
import org.gotson.komga.domain.model.SeriesMetadata
|
||||
import org.gotson.komga.domain.model.ThumbnailSeries
|
||||
import org.gotson.komga.domain.model.withCode
|
||||
import org.gotson.komga.domain.persistence.BookMetadataAggregationRepository
|
||||
import org.gotson.komga.domain.persistence.BookMetadataRepository
|
||||
import org.gotson.komga.domain.persistence.BookRepository
|
||||
@ -32,7 +33,15 @@ import org.gotson.komga.infrastructure.language.stripAccents
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.support.TransactionTemplate
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.nio.file.Path
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.io.path.deleteIfExists
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.isWritable
|
||||
import kotlin.io.path.listDirectoryEntries
|
||||
import kotlin.io.path.notExists
|
||||
import kotlin.io.path.toPath
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
private val natSortComparator: Comparator<String> = CaseInsensitiveSimpleNaturalComparator.getInstance()
|
||||
@ -279,6 +288,22 @@ class SeriesLifecycle(
|
||||
eventPublisher.publishEvent(DomainEvent.ThumbnailSeriesDeleted(thumbnail))
|
||||
}
|
||||
|
||||
fun deleteSeriesFiles(series: Series) {
|
||||
if (series.path.notExists() || !series.path.isWritable())
|
||||
throw FileNotFoundException("File is not accessible : ${series.path}").withCode("ERR_1018")
|
||||
|
||||
val thumbnails = thumbnailsSeriesRepository.findAllBySeriesIdIdAndType(series.id, ThumbnailSeries.Type.SIDECAR)
|
||||
.mapNotNull { it.url?.toURI()?.toPath() }
|
||||
.filter { it.exists() && it.isWritable() }
|
||||
|
||||
bookRepository.findAllBySeriesId(series.id)
|
||||
.forEach { bookLifecycle.deleteBookFiles(it) }
|
||||
thumbnails.forEach(Path::deleteIfExists)
|
||||
|
||||
if (series.path.exists() && series.path.listDirectoryEntries().isEmpty())
|
||||
series.path.deleteIfExists()
|
||||
}
|
||||
|
||||
private fun thumbnailsHouseKeeping(seriesId: String) {
|
||||
logger.info { "House keeping thumbnails for series: $seriesId" }
|
||||
val all = thumbnailsSeriesRepository.findAllBySeriesId(seriesId)
|
||||
|
@ -29,6 +29,13 @@ class ThumbnailSeriesDao(
|
||||
.fetchInto(ts)
|
||||
.map { it.toDomain() }
|
||||
|
||||
override fun findAllBySeriesIdIdAndType(seriesId: String, type: ThumbnailSeries.Type): Collection<ThumbnailSeries> =
|
||||
dsl.selectFrom(ts)
|
||||
.where(ts.SERIES_ID.eq(seriesId))
|
||||
.and(ts.TYPE.eq(type.toString()))
|
||||
.fetchInto(ts)
|
||||
.map { it.toDomain() }
|
||||
|
||||
override fun findSelectedBySeriesIdOrNull(seriesId: String): ThumbnailSeries? =
|
||||
dsl.selectFrom(ts)
|
||||
.where(ts.SERIES_ID.eq(seriesId))
|
||||
|
@ -643,6 +643,18 @@ class BookController(
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("api/v1/books/{bookId}/file")
|
||||
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||
@ResponseStatus(HttpStatus.ACCEPTED)
|
||||
fun deleteBook(
|
||||
@PathVariable bookId: String
|
||||
) {
|
||||
taskReceiver.deleteBook(
|
||||
bookId = bookId,
|
||||
priority = HIGHEST_PRIORITY,
|
||||
)
|
||||
}
|
||||
|
||||
private fun ResponseEntity.BodyBuilder.setNotModified(media: Media) =
|
||||
this.setCachePrivate().lastModified(getBookLastModified(media))
|
||||
|
||||
|
@ -12,6 +12,7 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.gotson.komga.application.events.EventPublisher
|
||||
import org.gotson.komga.application.tasks.HIGHEST_PRIORITY
|
||||
import org.gotson.komga.application.tasks.HIGH_PRIORITY
|
||||
import org.gotson.komga.application.tasks.TaskReceiver
|
||||
import org.gotson.komga.domain.model.Author
|
||||
@ -682,4 +683,16 @@ class SeriesController(
|
||||
.contentType(MediaType.parseMediaType("application/zip"))
|
||||
.body(streamingResponse)
|
||||
}
|
||||
|
||||
@DeleteMapping("v1/series/{seriesId}/file")
|
||||
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||
@ResponseStatus(HttpStatus.ACCEPTED)
|
||||
fun deleteSeries(
|
||||
@PathVariable seriesId: String
|
||||
) {
|
||||
taskReceiver.deleteSeries(
|
||||
seriesId = seriesId,
|
||||
priority = HIGHEST_PRIORITY,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,15 @@
|
||||
package org.gotson.komga.domain.service
|
||||
|
||||
import com.google.common.jimfs.Configuration
|
||||
import com.google.common.jimfs.Jimfs
|
||||
import com.ninjasquad.springmockk.MockkBean
|
||||
import io.mockk.every
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.catchThrowable
|
||||
import org.gotson.komga.domain.model.BookPage
|
||||
import org.gotson.komga.domain.model.KomgaUser
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.ThumbnailBook
|
||||
import org.gotson.komga.domain.model.makeBook
|
||||
import org.gotson.komga.domain.model.makeBookPage
|
||||
import org.gotson.komga.domain.model.makeLibrary
|
||||
@ -16,6 +20,7 @@ import org.gotson.komga.domain.persistence.LibraryRepository
|
||||
import org.gotson.komga.domain.persistence.MediaRepository
|
||||
import org.gotson.komga.domain.persistence.ReadProgressRepository
|
||||
import org.gotson.komga.domain.persistence.SeriesRepository
|
||||
import org.gotson.komga.domain.persistence.ThumbnailBookRepository
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
@ -24,6 +29,9 @@ import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||
import java.io.FileNotFoundException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
|
||||
@ExtendWith(SpringExtension::class)
|
||||
@SpringBootTest
|
||||
@ -36,6 +44,7 @@ class BookLifecycleTest(
|
||||
@Autowired private val readProgressRepository: ReadProgressRepository,
|
||||
@Autowired private val mediaRepository: MediaRepository,
|
||||
@Autowired private val userRepository: KomgaUserRepository,
|
||||
@Autowired private val thumbnailBookRepository: ThumbnailBookRepository,
|
||||
) {
|
||||
|
||||
@MockkBean
|
||||
@ -130,4 +139,133 @@ class BookLifecycleTest(
|
||||
// then
|
||||
assertThat(readProgressRepository.findAll()).hasSize(2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a book with a sidecar when deleting book then all book files should be deleted`() {
|
||||
Jimfs.newFileSystem(Configuration.unix()).use { fs ->
|
||||
// given
|
||||
val root = fs.getPath("/root")
|
||||
Files.createDirectory(root)
|
||||
val seriesPath = root.resolve("series")
|
||||
Files.createDirectory(seriesPath)
|
||||
val bookPath = seriesPath.resolve("book1.cbz")
|
||||
Files.createFile(bookPath)
|
||||
val sidecarPath = seriesPath.resolve("sidecar1.png")
|
||||
Files.createFile(sidecarPath)
|
||||
|
||||
val series = makeSeries(name = "series", libraryId = library.id, url = seriesPath.toUri().toURL())
|
||||
val book = makeBook("1", libraryId = library.id, url = bookPath.toUri().toURL())
|
||||
val sidecar = ThumbnailBook(bookId = book.id, type = ThumbnailBook.Type.SIDECAR, url = sidecarPath.toUri().toURL())
|
||||
|
||||
seriesLifecycle.createSeries(series)
|
||||
seriesLifecycle.addBooks(series, listOf(book))
|
||||
thumbnailBookRepository.insert(sidecar)
|
||||
|
||||
// when
|
||||
bookLifecycle.deleteBookFiles(book)
|
||||
|
||||
// then
|
||||
assertThat(Files.notExists(bookPath))
|
||||
assertThat(Files.notExists(sidecarPath))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a non-existent book file when deleting book then exception is thrown`() {
|
||||
// given
|
||||
val bookPath = Paths.get("/non-existent")
|
||||
val book = makeBook("1", libraryId = library.id, url = bookPath.toUri().toURL())
|
||||
|
||||
// when
|
||||
val thrown = catchThrowable { bookLifecycle.deleteBookFiles(book) }
|
||||
|
||||
// then
|
||||
assertThat(thrown).hasCauseInstanceOf(FileNotFoundException::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a book and a non-existent sidecar file when deleting book then book should be deleted`() {
|
||||
Jimfs.newFileSystem(Configuration.unix()).use { fs ->
|
||||
// given
|
||||
val root = fs.getPath("/root")
|
||||
Files.createDirectory(root)
|
||||
val seriesPath = root.resolve("series")
|
||||
Files.createDirectory(seriesPath)
|
||||
val bookPath = seriesPath.resolve("book1.cbz")
|
||||
Files.createFile(bookPath)
|
||||
val sidecar1Path = seriesPath.resolve("sidecar1.png")
|
||||
Files.createFile(sidecar1Path)
|
||||
val sidecar2Path = seriesPath.resolve("sidecar2.png")
|
||||
|
||||
val series = makeSeries(name = "series", libraryId = library.id, url = seriesPath.toUri().toURL())
|
||||
val book = makeBook("1", libraryId = library.id, url = bookPath.toUri().toURL())
|
||||
val sidecar1 = ThumbnailBook(bookId = book.id, type = ThumbnailBook.Type.SIDECAR, url = sidecar1Path.toUri().toURL())
|
||||
val sidecar2 = ThumbnailBook(bookId = book.id, type = ThumbnailBook.Type.SIDECAR, url = sidecar2Path.toUri().toURL())
|
||||
|
||||
seriesLifecycle.createSeries(series)
|
||||
seriesLifecycle.addBooks(series, listOf(book))
|
||||
thumbnailBookRepository.insert(sidecar1)
|
||||
thumbnailBookRepository.insert(sidecar2)
|
||||
|
||||
// when
|
||||
bookLifecycle.deleteBookFiles(book)
|
||||
|
||||
// then
|
||||
assertThat(Files.notExists(seriesPath))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a single book file when deleting book then parent directory should be deleted`() {
|
||||
Jimfs.newFileSystem(Configuration.unix()).use { fs ->
|
||||
// given
|
||||
val root = fs.getPath("/root")
|
||||
Files.createDirectory(root)
|
||||
val seriesPath = root.resolve("series")
|
||||
Files.createDirectory(seriesPath)
|
||||
val bookPath = seriesPath.resolve("book1.cbz")
|
||||
Files.createFile(bookPath)
|
||||
|
||||
val series = makeSeries(name = "series", libraryId = library.id, url = seriesPath.toUri().toURL())
|
||||
val book = makeBook("1", libraryId = library.id, url = bookPath.toUri().toURL())
|
||||
|
||||
seriesLifecycle.createSeries(series)
|
||||
seriesLifecycle.addBooks(series, listOf(book))
|
||||
|
||||
// when
|
||||
bookLifecycle.deleteBookFiles(book)
|
||||
|
||||
// then
|
||||
assertThat(Files.notExists(seriesPath))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a single book file with unrelated files in directory when deleting book then parent directory should not be deleted`() {
|
||||
Jimfs.newFileSystem(Configuration.unix()).use { fs ->
|
||||
// given
|
||||
val root = fs.getPath("/root")
|
||||
Files.createDirectory(root)
|
||||
val seriesPath = root.resolve("series")
|
||||
Files.createDirectory(seriesPath)
|
||||
val bookPath = seriesPath.resolve("book1.cbz")
|
||||
Files.createFile(bookPath)
|
||||
val filePath = seriesPath.resolve("file.txt")
|
||||
Files.createFile(filePath)
|
||||
|
||||
val series = makeSeries(name = "series", libraryId = library.id, url = seriesPath.toUri().toURL())
|
||||
val book = makeBook("1", libraryId = library.id, url = bookPath.toUri().toURL())
|
||||
|
||||
seriesLifecycle.createSeries(series)
|
||||
seriesLifecycle.addBooks(series, listOf(book))
|
||||
|
||||
// when
|
||||
bookLifecycle.deleteBookFiles(book)
|
||||
|
||||
// then
|
||||
assertThat(Files.exists(seriesPath))
|
||||
assertThat(Files.exists(filePath))
|
||||
assertThat(Files.notExists(bookPath))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,14 @@
|
||||
package org.gotson.komga.domain.service
|
||||
|
||||
import com.google.common.jimfs.Configuration
|
||||
import com.google.common.jimfs.Jimfs
|
||||
import com.ninjasquad.springmockk.SpykBean
|
||||
import io.mockk.every
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.catchThrowable
|
||||
import org.gotson.komga.domain.model.BookMetadata
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.ThumbnailBook
|
||||
import org.gotson.komga.domain.model.ThumbnailSeries
|
||||
import org.gotson.komga.domain.model.makeBook
|
||||
import org.gotson.komga.domain.model.makeLibrary
|
||||
@ -17,6 +20,8 @@ import org.gotson.komga.domain.persistence.LibraryRepository
|
||||
import org.gotson.komga.domain.persistence.MediaRepository
|
||||
import org.gotson.komga.domain.persistence.SeriesMetadataRepository
|
||||
import org.gotson.komga.domain.persistence.SeriesRepository
|
||||
import org.gotson.komga.domain.persistence.ThumbnailBookRepository
|
||||
import org.gotson.komga.domain.persistence.ThumbnailSeriesRepository
|
||||
import org.jooq.exception.DataAccessException
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
@ -27,6 +32,9 @@ import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||
import java.io.FileNotFoundException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
|
||||
@ExtendWith(SpringExtension::class)
|
||||
@SpringBootTest
|
||||
@ -35,7 +43,9 @@ class SeriesLifecycleTest(
|
||||
@Autowired private val bookLifecycle: BookLifecycle,
|
||||
@Autowired private val seriesRepository: SeriesRepository,
|
||||
@Autowired private val bookRepository: BookRepository,
|
||||
@Autowired private val libraryRepository: LibraryRepository
|
||||
@Autowired private val libraryRepository: LibraryRepository,
|
||||
@Autowired private val thumbnailSeriesRepository: ThumbnailSeriesRepository,
|
||||
@Autowired private val thumbnailBookRepository: ThumbnailBookRepository
|
||||
) {
|
||||
|
||||
@SpykBean
|
||||
@ -265,4 +275,171 @@ class SeriesLifecycleTest(
|
||||
|
||||
assertThat(thrown).isInstanceOf(IllegalArgumentException::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a series when deleting series then series directory is deleted`() {
|
||||
Jimfs.newFileSystem(Configuration.unix()).use { fs ->
|
||||
// given
|
||||
val root = fs.getPath("/root")
|
||||
Files.createDirectory(root)
|
||||
val seriesPath = root.resolve("series")
|
||||
Files.createDirectory(seriesPath)
|
||||
val book1Path = seriesPath.resolve("book1.cbz")
|
||||
Files.createFile(book1Path)
|
||||
val book2Path = seriesPath.resolve("book2.cbz")
|
||||
Files.createFile(book2Path)
|
||||
val bookSidecarPath = seriesPath.resolve("sidecar1.png")
|
||||
Files.createFile(bookSidecarPath)
|
||||
|
||||
val series = makeSeries(name = "series", libraryId = library.id, url = seriesPath.toUri().toURL())
|
||||
val books = listOf(
|
||||
makeBook("1", libraryId = library.id, url = book1Path.toUri().toURL()),
|
||||
makeBook("2", libraryId = library.id, url = book2Path.toUri().toURL()),
|
||||
)
|
||||
val bookSidecar = ThumbnailBook(bookId = books[0].id, type = ThumbnailBook.Type.SIDECAR, url = bookSidecarPath.toUri().toURL())
|
||||
|
||||
seriesLifecycle.createSeries(series)
|
||||
seriesLifecycle.addBooks(series, books)
|
||||
thumbnailBookRepository.insert(bookSidecar)
|
||||
|
||||
// when
|
||||
seriesLifecycle.deleteSeriesFiles(series)
|
||||
|
||||
// then
|
||||
assertThat(Files.notExists(seriesPath))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a series with a series sidecar when deleting series then series directory is deleted`() {
|
||||
Jimfs.newFileSystem(Configuration.unix()).use { fs ->
|
||||
// given
|
||||
val root = fs.getPath("/root")
|
||||
Files.createDirectory(root)
|
||||
val seriesPath = root.resolve("series")
|
||||
Files.createDirectory(seriesPath)
|
||||
val book1Path = seriesPath.resolve("book1.cbz")
|
||||
Files.createFile(book1Path)
|
||||
val book2Path = seriesPath.resolve("book2.cbz")
|
||||
Files.createFile(book2Path)
|
||||
val bookSidecarPath = seriesPath.resolve("sidecar1.png")
|
||||
Files.createFile(bookSidecarPath)
|
||||
val seriesSidecarPath = seriesPath.resolve("cover.png")
|
||||
Files.createFile(seriesSidecarPath)
|
||||
|
||||
val series = makeSeries(name = "series", libraryId = library.id, url = seriesPath.toUri().toURL())
|
||||
val books = listOf(
|
||||
makeBook("1", libraryId = library.id, url = book1Path.toUri().toURL()),
|
||||
makeBook("2", libraryId = library.id, url = book2Path.toUri().toURL()),
|
||||
)
|
||||
val bookSidecar = ThumbnailBook(bookId = books[0].id, type = ThumbnailBook.Type.SIDECAR, url = bookSidecarPath.toUri().toURL())
|
||||
val seriesSidecar = ThumbnailSeries(seriesId = series.id, type = ThumbnailSeries.Type.SIDECAR, url = seriesSidecarPath.toUri().toURL())
|
||||
|
||||
seriesLifecycle.createSeries(series)
|
||||
seriesLifecycle.addBooks(series, books)
|
||||
thumbnailBookRepository.insert(bookSidecar)
|
||||
thumbnailSeriesRepository.insert(seriesSidecar)
|
||||
|
||||
// when
|
||||
seriesLifecycle.deleteSeriesFiles(series)
|
||||
|
||||
// then
|
||||
assertThat(Files.notExists(seriesPath))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a series directory with unrelated files when deleting series then series directory should not be deleted`() {
|
||||
Jimfs.newFileSystem(Configuration.unix()).use { fs ->
|
||||
// given
|
||||
val root = fs.getPath("/root")
|
||||
Files.createDirectory(root)
|
||||
val seriesPath = root.resolve("series")
|
||||
Files.createDirectory(seriesPath)
|
||||
val book1Path = seriesPath.resolve("book1.cbz")
|
||||
Files.createFile(book1Path)
|
||||
val book2Path = seriesPath.resolve("book2.cbz")
|
||||
Files.createFile(book2Path)
|
||||
val filePath = seriesPath.resolve("file.txt")
|
||||
Files.createFile(filePath)
|
||||
val bookSidecarPath = seriesPath.resolve("sidecar1.png")
|
||||
Files.createFile(bookSidecarPath)
|
||||
val seriesSidecarPath = seriesPath.resolve("cover.png")
|
||||
Files.createFile(seriesSidecarPath)
|
||||
|
||||
val series = makeSeries(name = "series", libraryId = library.id, url = seriesPath.toUri().toURL())
|
||||
val books = listOf(
|
||||
makeBook("1", libraryId = library.id, url = book1Path.toUri().toURL()),
|
||||
makeBook("2", libraryId = library.id, url = book2Path.toUri().toURL()),
|
||||
)
|
||||
val bookSidecar = ThumbnailBook(bookId = books[0].id, type = ThumbnailBook.Type.SIDECAR, url = bookSidecarPath.toUri().toURL())
|
||||
val seriesSidecar = ThumbnailSeries(seriesId = series.id, type = ThumbnailSeries.Type.SIDECAR, url = seriesSidecarPath.toUri().toURL())
|
||||
|
||||
seriesLifecycle.createSeries(series)
|
||||
seriesLifecycle.addBooks(series, books)
|
||||
thumbnailBookRepository.insert(bookSidecar)
|
||||
thumbnailSeriesRepository.insert(seriesSidecar)
|
||||
|
||||
// when
|
||||
seriesLifecycle.deleteSeriesFiles(series)
|
||||
|
||||
// then
|
||||
assertThat(Files.exists(seriesPath))
|
||||
assertThat(Files.exists(filePath))
|
||||
assertThat(Files.notExists(book1Path))
|
||||
assertThat(Files.notExists(book2Path))
|
||||
assertThat(Files.notExists(bookSidecarPath))
|
||||
assertThat(Files.notExists(seriesSidecarPath))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a non-existent series directory when deleting series then exception is thrown`() {
|
||||
// given
|
||||
val seriesPath = Paths.get("/non-existent")
|
||||
val series = makeSeries(name = "series", libraryId = library.id, url = seriesPath.toUri().toURL())
|
||||
|
||||
// when
|
||||
val thrown = catchThrowable { seriesLifecycle.deleteSeriesFiles(series) }
|
||||
|
||||
// then
|
||||
assertThat(thrown).hasCauseInstanceOf(FileNotFoundException::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a series and a non-existent sidecar file when deleting series then series should be deleted`() {
|
||||
Jimfs.newFileSystem(Configuration.unix()).use { fs ->
|
||||
// given
|
||||
val root = fs.getPath("/root")
|
||||
Files.createDirectory(root)
|
||||
val seriesPath = root.resolve("series")
|
||||
Files.createDirectory(seriesPath)
|
||||
val book1Path = seriesPath.resolve("book1.cbz")
|
||||
Files.createFile(book1Path)
|
||||
val book2Path = seriesPath.resolve("book2.cbz")
|
||||
Files.createFile(book2Path)
|
||||
val bookSidecarPath = seriesPath.resolve("sidecar1.png")
|
||||
Files.createFile(bookSidecarPath)
|
||||
val seriesSidecarPath = seriesPath.resolve("cover.png")
|
||||
|
||||
val series = makeSeries(name = "series", libraryId = library.id, url = seriesPath.toUri().toURL())
|
||||
val books = listOf(
|
||||
makeBook("1", libraryId = library.id, url = book1Path.toUri().toURL()),
|
||||
makeBook("2", libraryId = library.id, url = book2Path.toUri().toURL()),
|
||||
)
|
||||
val bookSidecar = ThumbnailBook(bookId = books[0].id, type = ThumbnailBook.Type.SIDECAR, url = bookSidecarPath.toUri().toURL())
|
||||
val seriesSidecar = ThumbnailSeries(seriesId = series.id, type = ThumbnailSeries.Type.SIDECAR, url = seriesSidecarPath.toUri().toURL())
|
||||
|
||||
seriesLifecycle.createSeries(series)
|
||||
seriesLifecycle.addBooks(series, books)
|
||||
thumbnailBookRepository.insert(bookSidecar)
|
||||
thumbnailSeriesRepository.insert(seriesSidecar)
|
||||
|
||||
// when
|
||||
seriesLifecycle.deleteSeriesFiles(series)
|
||||
|
||||
// then
|
||||
assertThat(Files.notExists(seriesPath))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user