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