+
+
@@ -169,8 +169,8 @@ export default {
return this.libraryFiles
.filter((f) => f.fileType === 'image')
.map((file) => {
- var _file = { ...file }
- _file.localPath = `${process.env.serverUrl}/s/item/${this.libraryItemId}/${this.$encodeUriPath(file.metadata.relPath).replace(/^\//, '')}`
+ const _file = { ...file }
+ _file.localPath = `${process.env.serverUrl}/api/items/${this.libraryItemId}/file/${file.ino}?token=${this.userToken}`
return _file
})
}
diff --git a/client/components/tables/AudioTracksTableRow.vue b/client/components/tables/AudioTracksTableRow.vue
index 837aaa1a..1b1919d7 100644
--- a/client/components/tables/AudioTracksTableRow.vue
+++ b/client/components/tables/AudioTracksTableRow.vue
@@ -73,7 +73,7 @@ export default {
return items
},
downloadUrl() {
- return `${process.env.serverUrl}/s/item/${this.libraryItemId}/${this.$encodeUriPath(this.track.metadata.relPath).replace(/^\//, '')}?token=${this.userToken}`
+ return `${process.env.serverUrl}/api/items/${this.libraryItemId}/file/${this.track.audioFile.ino}/download?token=${this.userToken}`
}
},
methods: {
diff --git a/client/components/tables/LibraryFilesTableRow.vue b/client/components/tables/LibraryFilesTableRow.vue
index 38bc885d..d4154a93 100644
--- a/client/components/tables/LibraryFilesTableRow.vue
+++ b/client/components/tables/LibraryFilesTableRow.vue
@@ -45,7 +45,7 @@ export default {
return this.$store.getters['user/getIsAdminOrUp']
},
downloadUrl() {
- return `${process.env.serverUrl}/s/item/${this.libraryItemId}/${this.$encodeUriPath(this.file.metadata.relPath).replace(/^\//, '')}?token=${this.userToken}`
+ return `${process.env.serverUrl}/api/items/${this.libraryItemId}/file/${this.file.ino}/download?token=${this.userToken}`
},
contextMenuItems() {
const items = []
diff --git a/server/Server.js b/server/Server.js
index 86559149..2b5b6cf0 100644
--- a/server/Server.js
+++ b/server/Server.js
@@ -162,6 +162,8 @@ class Server {
router.use('/api', this.authMiddleware.bind(this), this.apiRouter.router)
router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router)
+
+ // TODO: Deprecated as of 2.2.21 edge
router.use('/s', this.authMiddleware.bind(this), this.staticRouter.router)
// EBook static file routes
diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js
index fd01a806..c041fae7 100644
--- a/server/controllers/LibraryItemController.js
+++ b/server/controllers/LibraryItemController.js
@@ -6,6 +6,7 @@ const SocketAuthority = require('../SocketAuthority')
const zipHelpers = require('../utils/zipHelpers')
const { reqSupportsWebp, isNullOrNaN } = require('../utils/index')
const { ScanResult } = require('../utils/constants')
+const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils')
class LibraryItemController {
constructor() { }
@@ -529,19 +530,45 @@ class LibraryItemController {
res.json(toneData)
}
- async deleteLibraryFile(req, res) {
- const libraryFile = req.libraryItem.libraryFiles.find(lf => lf.ino === req.params.ino)
- if (!libraryFile) {
- Logger.error(`[LibraryItemController] Unable to delete library file. Not found. "${req.params.ino}"`)
- return res.sendStatus(404)
+ /**
+ * GET api/items/:id/file/:fileid
+ *
+ * @param {express.Request} req
+ * @param {express.Response} res
+ */
+ async getLibraryFile(req, res) {
+ const libraryFile = req.libraryFile
+
+ if (global.XAccel) {
+ Logger.debug(`Use X-Accel to serve static file ${libraryFile.metadata.path}`)
+ return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + libraryFile.metadata.path }).send()
}
+ // Express does not set the correct mimetype for m4b files so use our defined mimetypes if available
+ const audioMimeType = getAudioMimeTypeFromExtname(Path.extname(libraryFile.metadata.path))
+ if (audioMimeType) {
+ res.setHeader('Content-Type', audioMimeType)
+ }
+ res.sendFile(libraryFile.metadata.path)
+ }
+
+ /**
+ * DELETE api/items/:id/file/:fileid
+ *
+ * @param {express.Request} req
+ * @param {express.Response} res
+ */
+ async deleteLibraryFile(req, res) {
+ const libraryFile = req.libraryFile
+
+ Logger.info(`[LibraryItemController] User "${req.user.username}" requested file delete at "${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)
+ req.libraryItem.removeLibraryFile(req.params.fileid)
- if (req.libraryItem.media.removeFileWithInode(req.params.ino)) {
+ if (req.libraryItem.media.removeFileWithInode(req.params.fileid)) {
// If book has no more media files then mark it as missing
if (req.libraryItem.mediaType === 'book' && !req.libraryItem.media.hasMediaEntities) {
req.libraryItem.setMissing()
@@ -553,6 +580,42 @@ class LibraryItemController {
res.sendStatus(200)
}
+ /**
+ * GET api/items/:id/file/:fileid/download
+ * Same as GET api/items/:id/file/:fileid but allows logging and restricting downloads
+ * @param {express.Request} req
+ * @param {express.Response} res
+ */
+ async downloadLibraryFile(req, res) {
+ const libraryFile = req.libraryFile
+
+ if (!req.user.canDownload) {
+ Logger.error(`[LibraryItemController] User without download permission attempted to download file "${libraryFile.metadata.path}"`, req.user)
+ return res.sendStatus(403)
+ }
+
+ Logger.info(`[LibraryItemController] User "${req.user.username}" requested file download at "${libraryFile.metadata.path}"`)
+
+ if (global.XAccel) {
+ Logger.debug(`Use X-Accel to serve static file ${libraryFile.metadata.path}`)
+ return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + libraryFile.metadata.path }).send()
+ }
+
+ // Express does not set the correct mimetype for m4b files so use our defined mimetypes if available
+ const audioMimeType = getAudioMimeTypeFromExtname(Path.extname(libraryFile.metadata.path))
+ if (audioMimeType) {
+ res.setHeader('Content-Type', audioMimeType)
+ }
+
+ res.download(libraryFile.metadata.path, libraryFile.metadata.filename)
+ }
+
+ /**
+ * GET api/items/:id/ebook
+ *
+ * @param {express.Request} req
+ * @param {express.Response} res
+ */
async getEBookFile(req, res) {
const ebookFile = req.libraryItem.media.ebookFile
if (!ebookFile) {
@@ -560,18 +623,33 @@ class LibraryItemController {
return res.sendStatus(404)
}
const ebookFilePath = ebookFile.metadata.path
+
+ if (global.XAccel) {
+ Logger.debug(`Use X-Accel to serve static file ${ebookFilePath}`)
+ return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + ebookFilePath }).send()
+ }
+
res.sendFile(ebookFilePath)
}
middleware(req, res, next) {
- const item = this.db.libraryItems.find(li => li.id === req.params.id)
- if (!item || !item.media) return res.sendStatus(404)
+ req.libraryItem = this.db.libraryItems.find(li => li.id === req.params.id)
+ if (!req.libraryItem?.media) return res.sendStatus(404)
// Check user can access this library item
- if (!req.user.checkCanAccessLibraryItem(item)) {
+ if (!req.user.checkCanAccessLibraryItem(req.libraryItem)) {
return res.sendStatus(403)
}
+ // For library file routes, get the library file
+ if (req.params.fileid) {
+ req.libraryFile = req.libraryItem.libraryFiles.find(lf => lf.ino === req.params.fileid)
+ if (!req.libraryFile) {
+ Logger.error(`[LibraryItemController] Library file "${req.params.fileid}" does not exist for library item`)
+ return res.sendStatus(404)
+ }
+ }
+
if (req.path.includes('/play')) {
// allow POST requests using /play and /play/:episodeId
} else if (req.method == 'DELETE' && !req.user.canDelete) {
@@ -582,7 +660,6 @@ class LibraryItemController {
return res.sendStatus(403)
}
- req.libraryItem = item
next()
}
}
diff --git a/server/objects/files/AudioTrack.js b/server/objects/files/AudioTrack.js
index 7f2cc73b..c4cd91c6 100644
--- a/server/objects/files/AudioTrack.js
+++ b/server/objects/files/AudioTrack.js
@@ -31,6 +31,7 @@ class AudioTrack {
this.startOffset = startOffset
this.duration = audioFile.duration
this.title = audioFile.metadata.filename || ''
+ // TODO: Switch to /api/items/:id/file/:fileid
this.contentUrl = Path.join(`${global.RouterBasePath}/s/item/${itemId}`, encodeUriPath(audioFile.metadata.relPath))
this.mimeType = audioFile.mimeType
this.codec = audioFile.codec || null
diff --git a/server/objects/files/VideoTrack.js b/server/objects/files/VideoTrack.js
index 800ab784..a993c77a 100644
--- a/server/objects/files/VideoTrack.js
+++ b/server/objects/files/VideoTrack.js
@@ -28,6 +28,7 @@ class VideoTrack {
this.index = videoFile.index
this.duration = videoFile.duration
this.title = videoFile.metadata.filename || ''
+ // TODO: Switch to /api/items/:id/file/:fileid
this.contentUrl = Path.join(`${global.RouterBasePath}/s/item/${itemId}`, encodeUriPath(videoFile.metadata.relPath))
this.mimeType = videoFile.mimeType
this.codec = videoFile.codec
diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js
index aec30aa4..f853219e 100644
--- a/server/routers/ApiRouter.js
+++ b/server/routers/ApiRouter.js
@@ -120,7 +120,9 @@ class ApiRouter {
this.router.get('/items/:id/tone-object', LibraryItemController.middleware.bind(this), LibraryItemController.getToneMetadataObject.bind(this))
this.router.post('/items/:id/chapters', LibraryItemController.middleware.bind(this), LibraryItemController.updateMediaChapters.bind(this))
this.router.post('/items/:id/tone-scan/:index?', LibraryItemController.middleware.bind(this), LibraryItemController.toneScan.bind(this))
- this.router.delete('/items/:id/file/:ino', LibraryItemController.middleware.bind(this), LibraryItemController.deleteLibraryFile.bind(this))
+ this.router.get('/items/:id/file/:fileid', LibraryItemController.middleware.bind(this), LibraryItemController.getLibraryFile.bind(this))
+ this.router.delete('/items/:id/file/:fileid', LibraryItemController.middleware.bind(this), LibraryItemController.deleteLibraryFile.bind(this))
+ this.router.get('/items/:id/file/:fileid/download', LibraryItemController.middleware.bind(this), LibraryItemController.downloadLibraryFile.bind(this))
this.router.get('/items/:id/ebook', LibraryItemController.middleware.bind(this), LibraryItemController.getEBookFile.bind(this))
//
diff --git a/server/routers/StaticRouter.js b/server/routers/StaticRouter.js
index eb8b424f..bd935ba3 100644
--- a/server/routers/StaticRouter.js
+++ b/server/routers/StaticRouter.js
@@ -3,6 +3,7 @@ const Path = require('path')
const Logger = require('../Logger')
const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils')
+// TODO: Deprecated as of 2.2.21 edge
class StaticRouter {
constructor(db) {
this.db = db