mirror of
https://github.com/gotson/komga.git
synced 2026-05-08 04:22:28 +02: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
21 changed files with 596 additions and 5 deletions
|
|
@ -70,6 +70,26 @@
|
||||||
:series="updateSeries"
|
: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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -224,6 +244,20 @@ export default Vue.extend({
|
||||||
updateBulkBooks(): BookDto[] {
|
updateBulkBooks(): BookDto[] {
|
||||||
return this.$store.state.updateBulkBooks
|
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
|
// series
|
||||||
updateSeriesDialog: {
|
updateSeriesDialog: {
|
||||||
get(): boolean {
|
get(): boolean {
|
||||||
|
|
@ -236,6 +270,20 @@ export default Vue.extend({
|
||||||
updateSeries(): SeriesDto | SeriesDto[] {
|
updateSeries(): SeriesDto | SeriesDto[] {
|
||||||
return this.$store.state.updateSeries
|
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: {
|
methods: {
|
||||||
async deleteLibrary() {
|
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>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
</v-btn>
|
</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>
|
<v-tooltip bottom>
|
||||||
<template v-slot:activator="{ on }">
|
<template v-slot:activator="{ on }">
|
||||||
<v-icon v-on="on">mdi-delete</v-icon>
|
<v-icon v-on="on">mdi-delete</v-icon>
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@
|
||||||
<v-list-item @click="markUnread" v-if="!isUnread">
|
<v-list-item @click="markUnread" v-if="!isUnread">
|
||||||
<v-list-item-title>{{ $t('menu.mark_unread') }}</v-list-item-title>
|
<v-list-item-title>{{ $t('menu.mark_unread') }}</v-list-item-title>
|
||||||
</v-list-item>
|
</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-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -82,6 +85,9 @@ export default Vue.extend({
|
||||||
async markUnread () {
|
async markUnread () {
|
||||||
await this.$komgaBooks.deleteReadProgress(this.book.id)
|
await this.$komgaBooks.deleteReadProgress(this.book.id)
|
||||||
},
|
},
|
||||||
|
promptDeleteBook () {
|
||||||
|
this.$store.dispatch('dialogDeleteBook', this.book)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@
|
||||||
<v-list-item @click="markUnread" v-if="!isUnread">
|
<v-list-item @click="markUnread" v-if="!isUnread">
|
||||||
<v-list-item-title>{{ $t('menu.mark_unread') }}</v-list-item-title>
|
<v-list-item-title>{{ $t('menu.mark_unread') }}</v-list-item-title>
|
||||||
</v-list-item>
|
</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-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -81,6 +84,9 @@ export default Vue.extend({
|
||||||
await this.$komgaSeries.markAsUnread(this.series.id)
|
await this.$komgaSeries.markAsUnread(this.series.id)
|
||||||
// this.$eventHub.$emit(SERIES_CHANGED, seriesToEventSeriesChanged(this.series))
|
// this.$eventHub.$emit(SERIES_CHANGED, seriesToEventSeriesChanged(this.series))
|
||||||
},
|
},
|
||||||
|
promptDeleteSeries () {
|
||||||
|
this.$store.dispatch('dialogDeleteSeries', this.series)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -272,6 +272,15 @@
|
||||||
"button_confirm": "Analyze",
|
"button_confirm": "Analyze",
|
||||||
"title": "Analyze library"
|
"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": {
|
"delete_collection": {
|
||||||
"button_confirm": "Delete",
|
"button_confirm": "Delete",
|
||||||
"confirm_delete": "Yes, delete the collection \"{name}\"",
|
"confirm_delete": "Yes, delete the collection \"{name}\"",
|
||||||
|
|
@ -302,6 +311,14 @@
|
||||||
"dialog_title": "Delete User",
|
"dialog_title": "Delete User",
|
||||||
"warning_html": "The user <b>{name}</b> will be deleted from this server. This <b>cannot</b> be undone. Continue?"
|
"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": {
|
"edit_books": {
|
||||||
"authors_notice_multiple_edit": "You are editing authors for multiple books. This will override existing authors of each book.",
|
"authors_notice_multiple_edit": "You are editing authors for multiple books. This will override existing authors of each book.",
|
||||||
"button_cancel": "Cancel",
|
"button_cancel": "Cancel",
|
||||||
|
|
|
||||||
|
|
@ -212,4 +212,16 @@ export default class KomgaBooksService {
|
||||||
throw new Error(msg)
|
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)
|
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
|
// books
|
||||||
updateBooks: {} as BookDto | BookDto[],
|
updateBooks: {} as BookDto | BookDto[],
|
||||||
updateBooksDialog: false,
|
updateBooksDialog: false,
|
||||||
|
deleteBooks: {} as BookDto | BookDto[],
|
||||||
|
deleteBookDialog: false,
|
||||||
// books bulk
|
// books bulk
|
||||||
updateBulkBooks: [] as BookDto[],
|
updateBulkBooks: [] as BookDto[],
|
||||||
updateBulkBooksDialog: false,
|
updateBulkBooksDialog: false,
|
||||||
|
|
@ -43,6 +45,8 @@ export default new Vuex.Store({
|
||||||
// series
|
// series
|
||||||
updateSeries: {} as SeriesDto | SeriesDto[],
|
updateSeries: {} as SeriesDto | SeriesDto[],
|
||||||
updateSeriesDialog: false,
|
updateSeriesDialog: false,
|
||||||
|
deleteSeries: {} as SeriesDto | SeriesDto[],
|
||||||
|
deleteSeriesDialog: false,
|
||||||
|
|
||||||
booksToCheck: 0,
|
booksToCheck: 0,
|
||||||
},
|
},
|
||||||
|
|
@ -105,6 +109,12 @@ export default new Vuex.Store({
|
||||||
setUpdateBooksDialog(state, dialog) {
|
setUpdateBooksDialog(state, dialog) {
|
||||||
state.updateBooksDialog = dialog
|
state.updateBooksDialog = dialog
|
||||||
},
|
},
|
||||||
|
setDeleteBooks(state, books) {
|
||||||
|
state.deleteBooks = books
|
||||||
|
},
|
||||||
|
setDeleteBookDialog(state, dialog) {
|
||||||
|
state.deleteBookDialog = dialog
|
||||||
|
},
|
||||||
// Books bulk
|
// Books bulk
|
||||||
setUpdateBulkBooks(state, books) {
|
setUpdateBulkBooks(state, books) {
|
||||||
state.updateBulkBooks = books
|
state.updateBulkBooks = books
|
||||||
|
|
@ -122,6 +132,12 @@ export default new Vuex.Store({
|
||||||
setBooksToCheck(state, count) {
|
setBooksToCheck(state, count) {
|
||||||
state.booksToCheck = count
|
state.booksToCheck = count
|
||||||
},
|
},
|
||||||
|
setDeleteSeries(state, series) {
|
||||||
|
state.deleteSeries = series
|
||||||
|
},
|
||||||
|
setDeleteSeriesDialog(state, dialog) {
|
||||||
|
state.deleteSeriesDialog = dialog
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
// collections
|
// collections
|
||||||
|
|
@ -195,6 +211,13 @@ export default new Vuex.Store({
|
||||||
dialogUpdateBooksDisplay({commit}, value) {
|
dialogUpdateBooksDisplay({commit}, value) {
|
||||||
commit('setUpdateBooksDialog', value)
|
commit('setUpdateBooksDialog', value)
|
||||||
},
|
},
|
||||||
|
dialogDeleteBook({commit}, books) {
|
||||||
|
commit('setDeleteBooks', books)
|
||||||
|
commit('setDeleteBookDialog', true)
|
||||||
|
},
|
||||||
|
dialogDeleteBookDisplay({commit}, value) {
|
||||||
|
commit('setDeleteBookDialog', value)
|
||||||
|
},
|
||||||
// books bulk
|
// books bulk
|
||||||
dialogUpdateBulkBooks({commit}, books) {
|
dialogUpdateBulkBooks({commit}, books) {
|
||||||
commit('setUpdateBulkBooks', books)
|
commit('setUpdateBulkBooks', books)
|
||||||
|
|
@ -211,6 +234,13 @@ export default new Vuex.Store({
|
||||||
dialogUpdateSeriesDisplay({commit}, value) {
|
dialogUpdateSeriesDisplay({commit}, value) {
|
||||||
commit('setUpdateSeriesDialog', value)
|
commit('setUpdateSeriesDialog', value)
|
||||||
},
|
},
|
||||||
|
dialogDeleteSeries({commit}, series) {
|
||||||
|
commit('setDeleteSeries', series)
|
||||||
|
commit('setDeleteSeriesDialog', true)
|
||||||
|
},
|
||||||
|
dialogDeleteSeriesDisplay({commit}, value) {
|
||||||
|
commit('setDeleteSeriesDialog', value)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
modules: {
|
modules: {
|
||||||
persistedState: persistedModule,
|
persistedState: persistedModule,
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
@mark-unread="markSelectedUnread"
|
@mark-unread="markSelectedUnread"
|
||||||
@add-to-collection="addToCollection"
|
@add-to-collection="addToCollection"
|
||||||
@edit="editMultipleSeries"
|
@edit="editMultipleSeries"
|
||||||
|
@delete="deleteSeries"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<library-navigation v-if="$vuetify.breakpoint.name === 'xs'" :libraryId="libraryId" bottom-navigation/>
|
<library-navigation v-if="$vuetify.breakpoint.name === 'xs'" :libraryId="libraryId" bottom-navigation/>
|
||||||
|
|
@ -554,6 +555,9 @@ export default Vue.extend({
|
||||||
editMultipleSeries() {
|
editMultipleSeries() {
|
||||||
this.$store.dispatch('dialogUpdateSeries', this.selectedSeries)
|
this.$store.dispatch('dialogUpdateSeries', this.selectedSeries)
|
||||||
},
|
},
|
||||||
|
deleteSeries() {
|
||||||
|
this.$store.dispatch('dialogDeleteSeries', this.selectedSeries)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@
|
||||||
@add-to-readlist="addToReadList"
|
@add-to-readlist="addToReadList"
|
||||||
@bulk-edit="bulkEditMultipleBooks"
|
@bulk-edit="bulkEditMultipleBooks"
|
||||||
@edit="editMultipleBooks"
|
@edit="editMultipleBooks"
|
||||||
|
@delete="deleteBooks"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<filter-drawer
|
<filter-drawer
|
||||||
|
|
@ -832,6 +833,9 @@ export default Vue.extend({
|
||||||
))
|
))
|
||||||
this.selectedBooks = []
|
this.selectedBooks = []
|
||||||
},
|
},
|
||||||
|
deleteBooks() {
|
||||||
|
this.$store.dispatch('dialogDeleteBook', this.selectedBooks)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,9 @@ sealed class Task(priority: Int = DEFAULT_PRIORITY) : Serializable {
|
||||||
abstract fun uniqueId(): String
|
abstract fun uniqueId(): String
|
||||||
val priority = priority.coerceIn(0, 9)
|
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 uniqueId() = "SCAN_LIBRARY_$libraryId"
|
||||||
|
override fun toString(): String = "ScanLibrary(libraryId='$libraryId', priority='$priority')"
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmptyTrash(val libraryId: String, priority: Int = DEFAULT_PRIORITY) : Task(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 uniqueId() = "REBUILD_INDEX"
|
||||||
override fun toString(): String = "RebuildIndex(priority='$priority')"
|
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.BookMetadataLifecycle
|
||||||
import org.gotson.komga.domain.service.LibraryContentLifecycle
|
import org.gotson.komga.domain.service.LibraryContentLifecycle
|
||||||
import org.gotson.komga.domain.service.LocalArtworkLifecycle
|
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.domain.service.SeriesMetadataLifecycle
|
||||||
import org.gotson.komga.infrastructure.jms.QUEUE_FACTORY
|
import org.gotson.komga.infrastructure.jms.QUEUE_FACTORY
|
||||||
import org.gotson.komga.infrastructure.jms.QUEUE_TASKS
|
import org.gotson.komga.infrastructure.jms.QUEUE_TASKS
|
||||||
|
|
@ -31,6 +32,7 @@ class TaskHandler(
|
||||||
private val libraryContentLifecycle: LibraryContentLifecycle,
|
private val libraryContentLifecycle: LibraryContentLifecycle,
|
||||||
private val bookLifecycle: BookLifecycle,
|
private val bookLifecycle: BookLifecycle,
|
||||||
private val bookMetadataLifecycle: BookMetadataLifecycle,
|
private val bookMetadataLifecycle: BookMetadataLifecycle,
|
||||||
|
private val seriesLifecycle: SeriesLifecycle,
|
||||||
private val seriesMetadataLifecycle: SeriesMetadataLifecycle,
|
private val seriesMetadataLifecycle: SeriesMetadataLifecycle,
|
||||||
private val localArtworkLifecycle: LocalArtworkLifecycle,
|
private val localArtworkLifecycle: LocalArtworkLifecycle,
|
||||||
private val bookImporter: BookImporter,
|
private val bookImporter: BookImporter,
|
||||||
|
|
@ -121,6 +123,20 @@ class TaskHandler(
|
||||||
} ?: logger.warn { "Cannot execute task $task: Book does not exist" }
|
} ?: logger.warn { "Cannot execute task $task: Book does not exist" }
|
||||||
|
|
||||||
is Task.RebuildIndex -> searchIndexLifecycle.rebuildIndex()
|
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 {
|
}.also {
|
||||||
logger.info { "Task $task executed in $it" }
|
logger.info { "Task $task executed in $it" }
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,8 @@ class TaskReceiver(
|
||||||
libraryRepository.findAll().forEach { scanLibrary(it.id) }
|
libraryRepository.findAll().forEach { scanLibrary(it.id) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun scanLibrary(libraryId: String) {
|
fun scanLibrary(libraryId: String, priority: Int = DEFAULT_PRIORITY) {
|
||||||
submitTask(Task.ScanLibrary(libraryId))
|
submitTask(Task.ScanLibrary(libraryId, priority))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun emptyTrash(libraryId: String, priority: Int = DEFAULT_PRIORITY) {
|
fun emptyTrash(libraryId: String, priority: Int = DEFAULT_PRIORITY) {
|
||||||
|
|
@ -121,6 +121,14 @@ class TaskReceiver(
|
||||||
submitTask(Task.RebuildIndex(priority))
|
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) {
|
private fun submitTask(task: Task) {
|
||||||
logger.info { "Sending task: $task" }
|
logger.info { "Sending task: $task" }
|
||||||
jmsTemplates[task.priority]!!.convertAndSend(QUEUE_TASKS, task) {
|
jmsTemplates[task.priority]!!.convertAndSend(QUEUE_TASKS, task) {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ interface ThumbnailSeriesRepository {
|
||||||
fun findSelectedBySeriesIdOrNull(seriesId: String): ThumbnailSeries?
|
fun findSelectedBySeriesIdOrNull(seriesId: String): ThumbnailSeries?
|
||||||
|
|
||||||
fun findAllBySeriesId(seriesId: String): Collection<ThumbnailSeries>
|
fun findAllBySeriesId(seriesId: String): Collection<ThumbnailSeries>
|
||||||
|
fun findAllBySeriesIdIdAndType(seriesId: String, type: ThumbnailSeries.Type): Collection<ThumbnailSeries>
|
||||||
|
|
||||||
fun insert(thumbnail: ThumbnailSeries)
|
fun insert(thumbnail: ThumbnailSeries)
|
||||||
fun markSelected(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.MediaNotReadyException
|
||||||
import org.gotson.komga.domain.model.ReadProgress
|
import org.gotson.komga.domain.model.ReadProgress
|
||||||
import org.gotson.komga.domain.model.ThumbnailBook
|
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.BookMetadataRepository
|
||||||
import org.gotson.komga.domain.persistence.BookRepository
|
import org.gotson.komga.domain.persistence.BookRepository
|
||||||
import org.gotson.komga.domain.persistence.MediaRepository
|
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.stereotype.Service
|
||||||
import org.springframework.transaction.support.TransactionTemplate
|
import org.springframework.transaction.support.TransactionTemplate
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileNotFoundException
|
||||||
import java.time.LocalDateTime
|
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 {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
|
@ -307,4 +316,19 @@ class BookLifecycle(
|
||||||
eventPublisher.publishEvent(DomainEvent.ReadProgressDeleted(progress))
|
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.Series
|
||||||
import org.gotson.komga.domain.model.SeriesMetadata
|
import org.gotson.komga.domain.model.SeriesMetadata
|
||||||
import org.gotson.komga.domain.model.ThumbnailSeries
|
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.BookMetadataAggregationRepository
|
||||||
import org.gotson.komga.domain.persistence.BookMetadataRepository
|
import org.gotson.komga.domain.persistence.BookMetadataRepository
|
||||||
import org.gotson.komga.domain.persistence.BookRepository
|
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.stereotype.Service
|
||||||
import org.springframework.transaction.support.TransactionTemplate
|
import org.springframework.transaction.support.TransactionTemplate
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.nio.file.Path
|
||||||
import java.time.LocalDateTime
|
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 logger = KotlinLogging.logger {}
|
||||||
private val natSortComparator: Comparator<String> = CaseInsensitiveSimpleNaturalComparator.getInstance()
|
private val natSortComparator: Comparator<String> = CaseInsensitiveSimpleNaturalComparator.getInstance()
|
||||||
|
|
@ -279,6 +288,22 @@ class SeriesLifecycle(
|
||||||
eventPublisher.publishEvent(DomainEvent.ThumbnailSeriesDeleted(thumbnail))
|
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) {
|
private fun thumbnailsHouseKeeping(seriesId: String) {
|
||||||
logger.info { "House keeping thumbnails for series: $seriesId" }
|
logger.info { "House keeping thumbnails for series: $seriesId" }
|
||||||
val all = thumbnailsSeriesRepository.findAllBySeriesId(seriesId)
|
val all = thumbnailsSeriesRepository.findAllBySeriesId(seriesId)
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,13 @@ class ThumbnailSeriesDao(
|
||||||
.fetchInto(ts)
|
.fetchInto(ts)
|
||||||
.map { it.toDomain() }
|
.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? =
|
override fun findSelectedBySeriesIdOrNull(seriesId: String): ThumbnailSeries? =
|
||||||
dsl.selectFrom(ts)
|
dsl.selectFrom(ts)
|
||||||
.where(ts.SERIES_ID.eq(seriesId))
|
.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) =
|
private fun ResponseEntity.BodyBuilder.setNotModified(media: Media) =
|
||||||
this.setCachePrivate().lastModified(getBookLastModified(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.compress.archivers.zip.ZipArchiveOutputStream
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
import org.gotson.komga.application.events.EventPublisher
|
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.HIGH_PRIORITY
|
||||||
import org.gotson.komga.application.tasks.TaskReceiver
|
import org.gotson.komga.application.tasks.TaskReceiver
|
||||||
import org.gotson.komga.domain.model.Author
|
import org.gotson.komga.domain.model.Author
|
||||||
|
|
@ -682,4 +683,16 @@ class SeriesController(
|
||||||
.contentType(MediaType.parseMediaType("application/zip"))
|
.contentType(MediaType.parseMediaType("application/zip"))
|
||||||
.body(streamingResponse)
|
.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
|
package org.gotson.komga.domain.service
|
||||||
|
|
||||||
|
import com.google.common.jimfs.Configuration
|
||||||
|
import com.google.common.jimfs.Jimfs
|
||||||
import com.ninjasquad.springmockk.MockkBean
|
import com.ninjasquad.springmockk.MockkBean
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
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.BookPage
|
||||||
import org.gotson.komga.domain.model.KomgaUser
|
import org.gotson.komga.domain.model.KomgaUser
|
||||||
import org.gotson.komga.domain.model.Media
|
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.makeBook
|
||||||
import org.gotson.komga.domain.model.makeBookPage
|
import org.gotson.komga.domain.model.makeBookPage
|
||||||
import org.gotson.komga.domain.model.makeLibrary
|
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.MediaRepository
|
||||||
import org.gotson.komga.domain.persistence.ReadProgressRepository
|
import org.gotson.komga.domain.persistence.ReadProgressRepository
|
||||||
import org.gotson.komga.domain.persistence.SeriesRepository
|
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.AfterAll
|
||||||
import org.junit.jupiter.api.AfterEach
|
import org.junit.jupiter.api.AfterEach
|
||||||
import org.junit.jupiter.api.BeforeAll
|
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.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.context.SpringBootTest
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
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)
|
@ExtendWith(SpringExtension::class)
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
|
|
@ -36,6 +44,7 @@ class BookLifecycleTest(
|
||||||
@Autowired private val readProgressRepository: ReadProgressRepository,
|
@Autowired private val readProgressRepository: ReadProgressRepository,
|
||||||
@Autowired private val mediaRepository: MediaRepository,
|
@Autowired private val mediaRepository: MediaRepository,
|
||||||
@Autowired private val userRepository: KomgaUserRepository,
|
@Autowired private val userRepository: KomgaUserRepository,
|
||||||
|
@Autowired private val thumbnailBookRepository: ThumbnailBookRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@MockkBean
|
@MockkBean
|
||||||
|
|
@ -130,4 +139,133 @@ class BookLifecycleTest(
|
||||||
// then
|
// then
|
||||||
assertThat(readProgressRepository.findAll()).hasSize(2)
|
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
|
package org.gotson.komga.domain.service
|
||||||
|
|
||||||
|
import com.google.common.jimfs.Configuration
|
||||||
|
import com.google.common.jimfs.Jimfs
|
||||||
import com.ninjasquad.springmockk.SpykBean
|
import com.ninjasquad.springmockk.SpykBean
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.assertj.core.api.Assertions.catchThrowable
|
import org.assertj.core.api.Assertions.catchThrowable
|
||||||
import org.gotson.komga.domain.model.BookMetadata
|
import org.gotson.komga.domain.model.BookMetadata
|
||||||
import org.gotson.komga.domain.model.Media
|
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.ThumbnailSeries
|
||||||
import org.gotson.komga.domain.model.makeBook
|
import org.gotson.komga.domain.model.makeBook
|
||||||
import org.gotson.komga.domain.model.makeLibrary
|
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.MediaRepository
|
||||||
import org.gotson.komga.domain.persistence.SeriesMetadataRepository
|
import org.gotson.komga.domain.persistence.SeriesMetadataRepository
|
||||||
import org.gotson.komga.domain.persistence.SeriesRepository
|
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.jooq.exception.DataAccessException
|
||||||
import org.junit.jupiter.api.AfterAll
|
import org.junit.jupiter.api.AfterAll
|
||||||
import org.junit.jupiter.api.AfterEach
|
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.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.context.SpringBootTest
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
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)
|
@ExtendWith(SpringExtension::class)
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
|
|
@ -35,7 +43,9 @@ class SeriesLifecycleTest(
|
||||||
@Autowired private val bookLifecycle: BookLifecycle,
|
@Autowired private val bookLifecycle: BookLifecycle,
|
||||||
@Autowired private val seriesRepository: SeriesRepository,
|
@Autowired private val seriesRepository: SeriesRepository,
|
||||||
@Autowired private val bookRepository: BookRepository,
|
@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
|
@SpykBean
|
||||||
|
|
@ -265,4 +275,171 @@ class SeriesLifecycleTest(
|
||||||
|
|
||||||
assertThat(thrown).isInstanceOf(IllegalArgumentException::class.java)
|
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 a new issue