From 718890cfadcd8731166b4676275d4d1191613bf2 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 9 Apr 2023 17:05:35 -0500 Subject: [PATCH] Add:Download button to download full library item #580 --- client/pages/item/_id/index.vue | 13 ++++++ server/controllers/LibraryItemController.js | 12 +++++ server/routers/ApiRouter.js | 1 + server/utils/zipHelpers.js | 52 +++++++++++++++++++++ 4 files changed, 78 insertions(+) create mode 100644 server/utils/zipHelpers.js diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 105682f2..dce62789 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -222,6 +222,13 @@ + + + + + download + +
@@ -284,6 +291,12 @@ export default { } }, computed: { + userToken() { + return this.$store.getters['user/getToken'] + }, + downloadPath() { + return `${process.env.serverUrl}/api/items/${this.libraryItemId}/download?token=${this.userToken}` + }, dateFormat() { return this.$store.state.serverSettings.dateFormat }, diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 55bbe72b..b1b8cc6a 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -2,6 +2,7 @@ const fs = require('../libs/fsExtra') const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') +const zipHelpers = require('../utils/zipHelpers') const { reqSupportsWebp, isNullOrNaN } = require('../utils/index') const { ScanResult } = require('../utils/constants') @@ -69,6 +70,17 @@ class LibraryItemController { res.sendStatus(200) } + download(req, res) { + if (!req.user.canDownload) { + Logger.warn('User attempted to download without permission', req.user) + return res.sendStatus(403) + } + + const libraryItemPath = req.libraryItem.path + const filename = `${req.libraryItem.media.metadata.title}.zip` + zipHelpers.zipDirectoryPipe(libraryItemPath, filename, res) + } + // // PATCH: will create new authors & series if in payload // diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 2264e175..7850adcb 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -98,6 +98,7 @@ class ApiRouter { this.router.get('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.findOne.bind(this)) this.router.patch('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.update.bind(this)) this.router.delete('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.delete.bind(this)) + this.router.get('/items/:id/download', LibraryItemController.middleware.bind(this), LibraryItemController.download.bind(this)) this.router.patch('/items/:id/media', LibraryItemController.middleware.bind(this), LibraryItemController.updateMedia.bind(this)) this.router.get('/items/:id/cover', LibraryItemController.middleware.bind(this), LibraryItemController.getCover.bind(this)) this.router.post('/items/:id/cover', LibraryItemController.middleware.bind(this), LibraryItemController.uploadCover.bind(this)) diff --git a/server/utils/zipHelpers.js b/server/utils/zipHelpers.js new file mode 100644 index 00000000..c1617272 --- /dev/null +++ b/server/utils/zipHelpers.js @@ -0,0 +1,52 @@ +const Logger = require('../Logger') +const archiver = require('../libs/archiver') + +module.exports.zipDirectoryPipe = (path, filename, res) => { + return new Promise((resolve, reject) => { + // create a file to stream archive data to + res.attachment(filename) + + const archive = archiver('zip', { + zlib: { level: 9 } // Sets the compression level. + }) + + // listen for all archive data to be written + // 'close' event is fired only when a file descriptor is involved + res.on('close', () => { + Logger.info(archive.pointer() + ' total bytes') + Logger.debug('archiver has been finalized and the output file descriptor has closed.') + resolve() + }) + + // This event is fired when the data source is drained no matter what was the data source. + // It is not part of this library but rather from the NodeJS Stream API. + // @see: https://nodejs.org/api/stream.html#stream_event_end + res.on('end', () => { + Logger.debug('Data has been drained') + }) + + // good practice to catch warnings (ie stat failures and other non-blocking errors) + archive.on('warning', function (err) { + if (err.code === 'ENOENT') { + // log warning + Logger.warn(`[DownloadManager] Archiver warning: ${err.message}`) + } else { + // throw error + Logger.error(`[DownloadManager] Archiver error: ${err.message}`) + // throw err + reject(err) + } + }) + archive.on('error', function (err) { + Logger.error(`[DownloadManager] Archiver error: ${err.message}`) + reject(err) + }) + + // pipe archive data to the file + archive.pipe(res) + + archive.directory(path, false) + + archive.finalize() + }) +} \ No newline at end of file