diff --git a/client/components/modals/ShareModal.vue b/client/components/modals/ShareModal.vue index 2009c41d6..5b3798847 100644 --- a/client/components/modals/ShareModal.vue +++ b/client/components/modals/ShareModal.vue @@ -19,12 +19,13 @@
-

{{ $getString('MessageShareExpiresIn', [currentShareTimeRemaining]) }}

+

{{ $strings.LabelDownloadable }}

+

{{ $getString('MessageShareExpiresIn', [currentShareTimeRemaining]) }}

{{ $strings.LabelPermanent }}

diff --git a/server/controllers/ShareController.js b/server/controllers/ShareController.js index 129d7da50..93c6e9fbc 100644 --- a/server/controllers/ShareController.js +++ b/server/controllers/ShareController.js @@ -212,7 +212,7 @@ class ShareController { } /** - * Public route + * Public route - requires share_session_id cookie * * GET: /api/share/:slug/download * Downloads media item share @@ -221,62 +221,52 @@ class ShareController { * @param {Response} res */ async downloadMediaItemShare(req, res) { + if (!req.cookies.share_session_id) { + return res.status(404).send('Share session not set') + } + const { slug } = req.params + const mediaItemShare = ShareManager.findBySlug(slug) + if (!mediaItemShare) { + return res.status(404) + } + if (!mediaItemShare.isDownloadable) { + return res.status(403).send('Download is not allowed for this item') + } - // Find matching MediaItemShare based on slug - const mediaItemShare = await ShareManager.findBySlug(slug) - // If the file isDownloadable, download the file - if (mediaItemShare.isDownloadable) { - // Get mediaItemId and type - const { mediaItemId, mediaItemType } = mediaItemShare + const playbackSession = ShareManager.findPlaybackSessionBySessionId(req.cookies.share_session_id) + if (!playbackSession || playbackSession.mediaItemShareId !== mediaItemShare.id) { + return res.status(404).send('Share session not found') + } - // Get the library item from the mediaItemId - const libraryItem = await Database.libraryItemModel.findOne({ - where: { - mediaId: mediaItemId + const libraryItem = await Database.libraryItemModel.findByPk(playbackSession.libraryItemId, { + attributes: ['id', 'path', 'relPath', 'isFile'] + }) + if (!libraryItem) { + return res.status(404).send('Library item not found') + } + + const itemPath = libraryItem.path + const itemTitle = playbackSession.displayTitle + + Logger.info(`[ShareController] Requested download for book "${itemTitle}" at "${itemPath}"`) + + try { + if (libraryItem.isFile) { + const audioMimeType = getAudioMimeTypeFromExtname(Path.extname(itemPath)) + if (audioMimeType) { + res.setHeader('Content-Type', audioMimeType) } - }) - - const itemPath = libraryItem.path - const itemRelPath = libraryItem.relPath - let itemTitle - - // Get the title based on the mediaItemType - if (mediaItemType === 'podcastEpisode') { - const podcastEpisode = await Database.podcastEpisodeModel.findOne({ - where: { - id: mediaItemId - } - }) - itemTitle = podcastEpisode.title - } else if (mediaItemType === 'book') { - const book = await Database.bookModel.findOne({ - where: { - id: mediaItemId - } - }) - itemTitle = book.title + await new Promise((resolve, reject) => res.download(itemPath, libraryItem.relPath, (error) => (error ? reject(error) : resolve()))) + } else { + const filename = `${itemTitle}.zip` + await zipHelpers.zipDirectoryPipe(itemPath, filename, res) } - Logger.info(`[ShareController] Requested download for book "${itemTitle}" at "${itemPath}"`) - - try { - if (libraryItem.isFile) { - const audioMimeType = getAudioMimeTypeFromExtname(Path.extname(itemPath)) - if (audioMimeType) { - res.setHeader('Content-Type', audioMimeType) - } - await new Promise((resolve, reject) => res.download(itemPath, itemRelPath, (error) => (error ? reject(error) : resolve()))) - } else { - const filename = `${itemTitle}.zip` - await zipHelpers.zipDirectoryPipe(itemPath, filename, res) - } - - Logger.info(`[ShareController] Downloaded item "${itemTitle}" at "${itemPath}"`) - } catch (error) { - Logger.error(`[ShareController] Download failed for item "${itemTitle}" at "${itemPath}"`, error) - res.status(500).send('Failed to download the item') - } + Logger.info(`[ShareController] Downloaded item "${itemTitle}" at "${itemPath}"`) + } catch (error) { + Logger.error(`[ShareController] Download failed for item "${itemTitle}" at "${itemPath}"`, error) + res.status(500).send('Failed to download the item') } } diff --git a/server/models/MediaItemShare.js b/server/models/MediaItemShare.js index 065dff10c..2d7b3896a 100644 --- a/server/models/MediaItemShare.js +++ b/server/models/MediaItemShare.js @@ -32,6 +32,34 @@ const { DataTypes, Model } = require('sequelize') class MediaItemShare extends Model { constructor(values, options) { super(values, options) + + /** @type {UUIDV4} */ + this.id + /** @type {UUIDV4} */ + this.mediaItemId + /** @type {string} */ + this.mediaItemType + /** @type {string} */ + this.slug + /** @type {string} */ + this.pash + /** @type {UUIDV4} */ + this.userId + /** @type {Date} */ + this.expiresAt + /** @type {Object} */ + this.extraData + /** @type {Date} */ + this.createdAt + /** @type {Date} */ + this.updatedAt + /** @type {boolean} */ + this.isDownloadable + + // Expanded properties + + /** @type {import('./Book')|import('./PodcastEpisode')} */ + this.mediaItem } toJSONForClient() {