From 179f11f55dffaf47a1f215461084360b591e358e Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 14 Apr 2023 16:44:41 -0500 Subject: [PATCH] Add:Delete library items from file system #1439 --- client/components/app/Appbar.vue | 49 ++++++++++++------- client/components/cards/LazyBookCard.vue | 37 ++++++++++++++ .../components/modals/item/tabs/Details.vue | 27 +--------- client/components/prompt/Confirm.vue | 25 ++++++++-- client/pages/item/_id/index.vue | 24 +++++---- server/controllers/LibraryItemController.js | 24 +++++++-- 6 files changed, 125 insertions(+), 61 deletions(-) diff --git a/client/components/app/Appbar.vue b/client/components/app/Appbar.vue index 382f5cf3..d625dac7 100644 --- a/client/components/app/Appbar.vue +++ b/client/components/app/Appbar.vue @@ -287,26 +287,37 @@ export default { }) }, batchDeleteClick() { - const audiobookText = this.numMediaItemsSelected > 1 ? `these ${this.numMediaItemsSelected} items` : 'this item' - const confirmMsg = `Are you sure you want to remove ${audiobookText}?\n\n*Does not delete your files, only removes the items from Audiobookshelf` - if (confirm(confirmMsg)) { - this.$store.commit('setProcessingBatch', true) - this.$axios - .$post(`/api/items/batch/delete`, { - libraryItemIds: this.selectedMediaItems.map((i) => i.id) - }) - .then(() => { - this.$toast.success('Batch delete success!') - this.$store.commit('setProcessingBatch', false) - this.$store.commit('globals/resetSelectedMediaItems', []) - this.$eventBus.$emit('bookshelf_clear_selection') - }) - .catch((error) => { - this.$toast.error('Batch delete failed') - console.error('Failed to batch delete', error) - this.$store.commit('setProcessingBatch', false) - }) + const payload = { + message: `This will delete ${this.numMediaItemsSelected} library items from the database and your file system. Are you sure?`, + checkboxLabel: 'Delete from file system. Uncheck to only remove from database.', + yesButtonText: this.$strings.ButtonDelete, + yesButtonColor: 'error', + checkboxDefaultValue: true, + callback: (confirmed, hardDelete) => { + if (confirmed) { + this.$store.commit('setProcessingBatch', true) + + this.$axios + .$post(`/api/items/batch/delete?hard=${hardDelete ? 1 : 0}`, { + libraryItemIds: this.selectedMediaItems.map((i) => i.id) + }) + .then(() => { + this.$toast.success('Batch delete success') + this.$store.commit('globals/resetSelectedMediaItems', []) + this.$eventBus.$emit('bookshelf_clear_selection') + }) + .catch((error) => { + console.error('Batch delete failed', error) + this.$toast.error('Batch delete failed') + }) + .finally(() => { + this.$store.commit('setProcessingBatch', false) + }) + } + }, + type: 'yesNo' } + this.$store.commit('globals/setConfirmPrompt', payload) }, batchEditClick() { this.$router.push('/batch') diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index c4d3e967..42a3be4e 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -526,6 +526,14 @@ export default { } } } + + if (this.userCanDelete) { + items.push({ + func: 'deleteLibraryItem', + text: this.$strings.ButtonDelete + }) + } + return items }, _socket() { @@ -777,6 +785,35 @@ export default { this.store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode: this.recentEpisode }]) this.store.commit('globals/setShowPlaylistsModal', true) }, + deleteLibraryItem() { + const payload = { + message: 'This will delete the library item from the database and your file system. Are you sure?', + checkboxLabel: 'Delete from file system. Uncheck to only remove from database.', + yesButtonText: this.$strings.ButtonDelete, + yesButtonColor: 'error', + checkboxDefaultValue: true, + callback: (confirmed, hardDelete) => { + if (confirmed) { + this.processing = true + const axios = this.$axios || this.$nuxt.$axios + axios + .$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`) + .then(() => { + this.$toast.success('Item deleted') + }) + .catch((error) => { + console.error('Failed to delete item', error) + this.$toast.error('Failed to delete item') + }) + .finally(() => { + this.processing = false + }) + } + }, + type: 'yesNo' + } + this.store.commit('globals/setConfirmPrompt', payload) + }, createMoreMenu() { if (!this.$refs.moreIcon) return diff --git a/client/components/modals/item/tabs/Details.vue b/client/components/modals/item/tabs/Details.vue index e95b8d57..14fe68a7 100644 --- a/client/components/modals/item/tabs/Details.vue +++ b/client/components/modals/item/tabs/Details.vue @@ -7,11 +7,6 @@
- - - -
- {{ $strings.ButtonQuickMatch }} @@ -20,6 +15,8 @@ {{ $strings.ButtonReScan }} +
+ @@ -77,9 +74,6 @@ export default { mediaMetadata() { return this.media.metadata || {} }, - userCanDelete() { - return this.$store.getters['user/getUserCanDelete'] - }, libraryId() { return this.libraryItem ? this.libraryItem.libraryId : null }, @@ -184,23 +178,6 @@ export default { } return false }, - removeItem() { - if (confirm(`Are you sure you want to remove this item?\n\n*Does not delete your files, only removes the item from audiobookshelf`)) { - this.isProcessing = true - this.$axios - .$delete(`/api/items/${this.libraryItemId}`) - .then(() => { - console.log('Item removed') - this.$toast.success('Item Removed') - this.$emit('close') - this.isProcessing = false - }) - .catch((error) => { - console.error('Remove item failed', error) - this.isProcessing = false - }) - } - }, checkIsScrollable() { this.$nextTick(() => { var formWrapper = document.getElementById('formWrapper') diff --git a/client/components/prompt/Confirm.vue b/client/components/prompt/Confirm.vue index 00e38e08..b09bf55d 100644 --- a/client/components/prompt/Confirm.vue +++ b/client/components/prompt/Confirm.vue @@ -3,11 +3,14 @@
-

+

+ + +

{{ $strings.ButtonCancel }}
- {{ $strings.ButtonYes }} + {{ yesButtonText }} {{ $strings.ButtonOk }}
@@ -21,7 +24,8 @@ export default { data() { return { el: null, - content: null + content: null, + checkboxValue: false } }, watch: { @@ -57,6 +61,18 @@ export default { persistent() { return !!this.confirmPromptOptions.persistent }, + checkboxLabel() { + return this.confirmPromptOptions.checkboxLabel + }, + yesButtonText() { + return this.confirmPromptOptions.yesButtonText || this.$strings.ButtonYes + }, + yesButtonColor() { + return this.confirmPromptOptions.yesButtonColor || 'success' + }, + checkboxDefaultValue() { + return !!this.confirmPromptOptions.checkboxDefaultValue + }, isYesNo() { return this.type === 'yesNo' }, @@ -84,10 +100,11 @@ export default { this.show = false }, confirm() { - if (this.callback) this.callback(true) + if (this.callback) this.callback(true, this.checkboxValue) this.show = false }, setShow() { + this.checkboxValue = this.checkboxDefaultValue this.$eventBus.$emit('showing-prompt', true) document.body.appendChild(this.el) setTimeout(() => { diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 2d376646..88b15fc1 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -562,12 +562,12 @@ export default { }) } - // if (this.userCanDelete) { - // items.push({ - // text: this.$strings.ButtonDelete, - // action: 'delete' - // }) - // } + if (this.userCanDelete) { + items.push({ + text: this.$strings.ButtonDelete, + action: 'delete' + }) + } return items } @@ -818,14 +818,18 @@ export default { }, deleteLibraryItem() { const payload = { - message: 'This will delete the library item files from your file system. Are you sure?', - callback: (confirmed) => { + message: 'This will delete the library item from the database and your file system. Are you sure?', + checkboxLabel: 'Delete from file system. Uncheck to only remove from database.', + yesButtonText: this.$strings.ButtonDelete, + yesButtonColor: 'error', + checkboxDefaultValue: true, + callback: (confirmed, hardDelete) => { if (confirmed) { this.$axios - .$delete(`/api/items/${this.libraryItemId}?hard=1`) + .$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`) .then(() => { this.$toast.success('Item deleted') - this.$router.replace(`/library/${this.libraryId}/bookshelf`) + this.$router.replace(`/library/${this.libraryId}`) }) .catch((error) => { console.error('Failed to delete item', error) diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 842db177..19f287f0 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -66,7 +66,15 @@ class LibraryItemController { } async delete(req, res) { + const hardDelete = req.query.hard == 1 // Delete from file system + const libraryItemPath = req.libraryItem.path await this.handleDeleteLibraryItem(req.libraryItem) + if (hardDelete) { + Logger.info(`[LibraryItemController] Deleting library item from file system at "${libraryItemPath}"`) + await fs.remove(libraryItemPath).catch((error) => { + Logger.error(`[LibraryItemController] Failed to delete library item from file system at "${libraryItemPath}"`, error) + }) + } res.sendStatus(200) } @@ -292,19 +300,27 @@ class LibraryItemController { Logger.warn(`[LibraryItemController] User attempted to delete without permission`, req.user) return res.sendStatus(403) } + const hardDelete = req.query.hard == 1 // Delete files from filesystem - var { libraryItemIds } = req.body + const { libraryItemIds } = req.body if (!libraryItemIds || !libraryItemIds.length) { return res.sendStatus(500) } - var itemsToDelete = this.db.libraryItems.filter(li => libraryItemIds.includes(li.id)) + const itemsToDelete = this.db.libraryItems.filter(li => libraryItemIds.includes(li.id)) if (!itemsToDelete.length) { return res.sendStatus(404) } for (let i = 0; i < itemsToDelete.length; i++) { + const libraryItemPath = itemsToDelete[i].path Logger.info(`[LibraryItemController] Deleting Library Item "${itemsToDelete[i].media.metadata.title}"`) await this.handleDeleteLibraryItem(itemsToDelete[i]) + if (hardDelete) { + Logger.info(`[LibraryItemController] Deleting library item from file system at "${libraryItemPath}"`) + await fs.remove(libraryItemPath).catch((error) => { + Logger.error(`[LibraryItemController] Failed to delete library item from file system at "${libraryItemPath}"`, error) + }) + } } res.sendStatus(200) } @@ -489,7 +505,9 @@ class LibraryItemController { return res.sendStatus(404) } - await fs.remove(libraryFile.metadata.path) + await fs.remove(libraryFile.metadata.path).catch((error) => { + Logger.error(`[LibraryItemController] Failed to delete library file at "${libraryFile.metadata.path}"`, error) + }) req.libraryItem.removeLibraryFile(req.params.ino) if (req.libraryItem.media.removeFileWithInode(req.params.ino)) {