mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-02-06 00:16:02 +01:00
Feat/download via share link (#3666)
* Adds share download endpoint * Adds Downloadable toggle to share modal --------- Co-authored-by: advplyr <advplyr@protonmail.com>
This commit is contained in:
parent
e0c674d9a9
commit
4cdc2a8c28
@ -19,12 +19,13 @@
|
|||||||
<ui-text-input v-model="currentShareUrl" show-copy readonly class="text-base h-10" />
|
<ui-text-input v-model="currentShareUrl" show-copy readonly class="text-base h-10" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full py-2 px-1">
|
<div class="w-full py-2 px-1">
|
||||||
<p v-if="currentShare.expiresAt" class="text-base">{{ $getString('MessageShareExpiresIn', [currentShareTimeRemaining]) }}</p>
|
<p v-if="currentShare.isDownloadable" class="text-sm mb-2">{{ $strings.LabelDownloadable }}</p>
|
||||||
|
<p v-if="currentShare.expiresAt">{{ $getString('MessageShareExpiresIn', [currentShareTimeRemaining]) }}</p>
|
||||||
<p v-else>{{ $strings.LabelPermanent }}</p>
|
<p v-else>{{ $strings.LabelPermanent }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="flex flex-col sm:flex-row items-center justify-between space-y-4 sm:space-y-0 sm:space-x-4 mb-4">
|
<div class="flex flex-col sm:flex-row items-center justify-between space-y-4 sm:space-y-0 sm:space-x-4 mb-2">
|
||||||
<div class="w-full sm:w-48">
|
<div class="w-full sm:w-48">
|
||||||
<label class="px-1 text-sm font-semibold block">{{ $strings.LabelSlug }}</label>
|
<label class="px-1 text-sm font-semibold block">{{ $strings.LabelSlug }}</label>
|
||||||
<ui-text-input v-model="newShareSlug" class="text-base h-10" />
|
<ui-text-input v-model="newShareSlug" class="text-base h-10" />
|
||||||
@ -46,6 +47,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center w-full md:w-1/2 mb-4">
|
||||||
|
<p class="text-sm text-gray-300 py-1 px-1">{{ $strings.LabelDownloadable }}</p>
|
||||||
|
<ui-toggle-switch size="sm" v-model="isDownloadable" />
|
||||||
|
<ui-tooltip :text="$strings.LabelShareDownloadableHelp">
|
||||||
|
<p class="pl-4 text-sm">
|
||||||
|
<span class="material-symbols icon-text text-sm">info</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
<p class="text-sm text-gray-300 py-1 px-1" v-html="$getString('MessageShareURLWillBe', [demoShareUrl])" />
|
<p class="text-sm text-gray-300 py-1 px-1" v-html="$getString('MessageShareURLWillBe', [demoShareUrl])" />
|
||||||
<p class="text-sm text-gray-300 py-1 px-1" v-html="$getString('MessageShareExpirationWillBe', [expirationDateString])" />
|
<p class="text-sm text-gray-300 py-1 px-1" v-html="$getString('MessageShareExpirationWillBe', [expirationDateString])" />
|
||||||
</template>
|
</template>
|
||||||
@ -81,7 +91,8 @@ export default {
|
|||||||
text: this.$strings.LabelDays,
|
text: this.$strings.LabelDays,
|
||||||
value: 'days'
|
value: 'days'
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
isDownloadable: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@ -172,7 +183,8 @@ export default {
|
|||||||
slug: this.newShareSlug,
|
slug: this.newShareSlug,
|
||||||
mediaItemType: 'book',
|
mediaItemType: 'book',
|
||||||
mediaItemId: this.libraryItem.media.id,
|
mediaItemId: this.libraryItem.media.id,
|
||||||
expiresAt: this.expireDurationSeconds ? Date.now() + this.expireDurationSeconds * 1000 : 0
|
expiresAt: this.expireDurationSeconds ? Date.now() + this.expireDurationSeconds * 1000 : 0,
|
||||||
|
isDownloadable: this.isDownloadable
|
||||||
}
|
}
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
|
@ -12,6 +12,10 @@
|
|||||||
<div class="w-full pt-16">
|
<div class="w-full pt-16">
|
||||||
<player-ui ref="audioPlayer" :chapters="chapters" :current-chapter="currentChapter" :paused="isPaused" :loading="!hasLoaded" :is-podcast="false" hide-bookmarks hide-sleep-timer @playPause="playPause" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setVolume="setVolume" @setPlaybackRate="setPlaybackRate" @seek="seek" />
|
<player-ui ref="audioPlayer" :chapters="chapters" :current-chapter="currentChapter" :paused="isPaused" :loading="!hasLoaded" :is-podcast="false" hide-bookmarks hide-sleep-timer @playPause="playPause" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setVolume="setVolume" @setPlaybackRate="setPlaybackRate" @seek="seek" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ui-tooltip v-if="mediaItemShare.isDownloadable" direction="bottom" :text="$strings.LabelDownload" class="absolute top-0 left-0 m-4">
|
||||||
|
<button aria-label="Download" class="text-gray-300 hover:text-white" @click="downloadShareItem"><span class="material-symbols text-2xl sm:text-3xl">download</span></button>
|
||||||
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -63,6 +67,9 @@ export default {
|
|||||||
if (!this.playbackSession.coverPath) return `${this.$config.routerBasePath}/book_placeholder.jpg`
|
if (!this.playbackSession.coverPath) return `${this.$config.routerBasePath}/book_placeholder.jpg`
|
||||||
return `${this.$config.routerBasePath}/public/share/${this.mediaItemShare.slug}/cover`
|
return `${this.$config.routerBasePath}/public/share/${this.mediaItemShare.slug}/cover`
|
||||||
},
|
},
|
||||||
|
downloadUrl() {
|
||||||
|
return `${process.env.serverUrl}/public/share/${this.mediaItemShare.slug}/download`
|
||||||
|
},
|
||||||
audioTracks() {
|
audioTracks() {
|
||||||
return (this.playbackSession.audioTracks || []).map((track) => {
|
return (this.playbackSession.audioTracks || []).map((track) => {
|
||||||
track.relativeContentUrl = track.contentUrl
|
track.relativeContentUrl = track.contentUrl
|
||||||
@ -247,6 +254,9 @@ export default {
|
|||||||
},
|
},
|
||||||
playerFinished() {
|
playerFinished() {
|
||||||
console.log('Player finished')
|
console.log('Player finished')
|
||||||
|
},
|
||||||
|
downloadShareItem() {
|
||||||
|
this.$downloadFile(this.downloadUrl)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -300,6 +300,7 @@
|
|||||||
"LabelDiscover": "Discover",
|
"LabelDiscover": "Discover",
|
||||||
"LabelDownload": "Download",
|
"LabelDownload": "Download",
|
||||||
"LabelDownloadNEpisodes": "Download {0} episodes",
|
"LabelDownloadNEpisodes": "Download {0} episodes",
|
||||||
|
"LabelDownloadable": "Downloadable",
|
||||||
"LabelDuration": "Duration",
|
"LabelDuration": "Duration",
|
||||||
"LabelDurationComparisonExactMatch": "(exact match)",
|
"LabelDurationComparisonExactMatch": "(exact match)",
|
||||||
"LabelDurationComparisonLonger": "({0} longer)",
|
"LabelDurationComparisonLonger": "({0} longer)",
|
||||||
@ -588,6 +589,7 @@
|
|||||||
"LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders",
|
"LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders",
|
||||||
"LabelSettingsTimeFormat": "Time Format",
|
"LabelSettingsTimeFormat": "Time Format",
|
||||||
"LabelShare": "Share",
|
"LabelShare": "Share",
|
||||||
|
"LabelShareDownloadableHelp": "Allows users with the share link to download a zip file of the library item.",
|
||||||
"LabelShareOpen": "Share Open",
|
"LabelShareOpen": "Share Open",
|
||||||
"LabelShareURL": "Share URL",
|
"LabelShareURL": "Share URL",
|
||||||
"LabelShowAll": "Show All",
|
"LabelShowAll": "Show All",
|
||||||
|
@ -7,6 +7,7 @@ const Database = require('../Database')
|
|||||||
|
|
||||||
const { PlayMethod } = require('../utils/constants')
|
const { PlayMethod } = require('../utils/constants')
|
||||||
const { getAudioMimeTypeFromExtname, encodeUriPath } = require('../utils/fileUtils')
|
const { getAudioMimeTypeFromExtname, encodeUriPath } = require('../utils/fileUtils')
|
||||||
|
const zipHelpers = require('../utils/zipHelpers')
|
||||||
|
|
||||||
const PlaybackSession = require('../objects/PlaybackSession')
|
const PlaybackSession = require('../objects/PlaybackSession')
|
||||||
const ShareManager = require('../managers/ShareManager')
|
const ShareManager = require('../managers/ShareManager')
|
||||||
@ -210,6 +211,65 @@ class ShareController {
|
|||||||
res.sendFile(audioTrackPath)
|
res.sendFile(audioTrackPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public route - requires share_session_id cookie
|
||||||
|
*
|
||||||
|
* GET: /api/share/:slug/download
|
||||||
|
* Downloads media item share
|
||||||
|
*
|
||||||
|
* @param {Request} req
|
||||||
|
* @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')
|
||||||
|
}
|
||||||
|
|
||||||
|
const playbackSession = ShareManager.findPlaybackSessionBySessionId(req.cookies.share_session_id)
|
||||||
|
if (!playbackSession || playbackSession.mediaItemShareId !== mediaItemShare.id) {
|
||||||
|
return res.status(404).send('Share session not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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] 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')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public route - requires share_session_id cookie
|
* Public route - requires share_session_id cookie
|
||||||
*
|
*
|
||||||
@ -259,7 +319,7 @@ class ShareController {
|
|||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { slug, expiresAt, mediaItemType, mediaItemId } = req.body
|
const { slug, expiresAt, mediaItemType, mediaItemId, isDownloadable } = req.body
|
||||||
|
|
||||||
if (!slug?.trim?.() || typeof mediaItemType !== 'string' || typeof mediaItemId !== 'string') {
|
if (!slug?.trim?.() || typeof mediaItemType !== 'string' || typeof mediaItemId !== 'string') {
|
||||||
return res.status(400).send('Missing or invalid required fields')
|
return res.status(400).send('Missing or invalid required fields')
|
||||||
@ -298,7 +358,8 @@ class ShareController {
|
|||||||
expiresAt: expiresAt || null,
|
expiresAt: expiresAt || null,
|
||||||
mediaItemId,
|
mediaItemId,
|
||||||
mediaItemType,
|
mediaItemType,
|
||||||
userId: req.user.id
|
userId: req.user.id,
|
||||||
|
isDownloadable
|
||||||
})
|
})
|
||||||
|
|
||||||
ShareManager.openMediaItemShare(mediaItemShare)
|
ShareManager.openMediaItemShare(mediaItemShare)
|
||||||
|
@ -11,3 +11,4 @@ Please add a record of every database migration that you create to this file. Th
|
|||||||
| v2.17.3 | v2.17.3-fk-constraints | Changes the foreign key constraints for tables due to sequelize bug dropping constraints in v2.17.0 migration |
|
| v2.17.3 | v2.17.3-fk-constraints | Changes the foreign key constraints for tables due to sequelize bug dropping constraints in v2.17.0 migration |
|
||||||
| v2.17.4 | v2.17.4-use-subfolder-for-oidc-redirect-uris | Save subfolder to OIDC redirect URIs to support existing installations |
|
| v2.17.4 | v2.17.4-use-subfolder-for-oidc-redirect-uris | Save subfolder to OIDC redirect URIs to support existing installations |
|
||||||
| v2.17.5 | v2.17.5-remove-host-from-feed-urls | removes the host (serverAddress) from URL columns in the feeds and feedEpisodes tables |
|
| v2.17.5 | v2.17.5-remove-host-from-feed-urls | removes the host (serverAddress) from URL columns in the feeds and feedEpisodes tables |
|
||||||
|
| v2.17.6 | v2.17.6-share-add-isdownloadable | Adds the isDownloadable column to the mediaItemShares table |
|
||||||
|
68
server/migrations/v2.17.6-share-add-isdownloadable.js
Normal file
68
server/migrations/v2.17.6-share-add-isdownloadable.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* @typedef MigrationContext
|
||||||
|
* @property {import('sequelize').QueryInterface} queryInterface - a Sequelize QueryInterface object.
|
||||||
|
* @property {import('../Logger')} logger - a Logger object.
|
||||||
|
*
|
||||||
|
* @typedef MigrationOptions
|
||||||
|
* @property {MigrationContext} context - an object containing the migration context.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const migrationVersion = '2.17.6'
|
||||||
|
const migrationName = `${migrationVersion}-share-add-isdownloadable`
|
||||||
|
const loggerPrefix = `[${migrationVersion} migration]`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This migration script adds the isDownloadable column to the mediaItemShares table.
|
||||||
|
*
|
||||||
|
* @param {MigrationOptions} options - an object containing the migration context.
|
||||||
|
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||||
|
*/
|
||||||
|
async function up({ context: { queryInterface, logger } }) {
|
||||||
|
logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`)
|
||||||
|
|
||||||
|
if (await queryInterface.tableExists('mediaItemShares')) {
|
||||||
|
const tableDescription = await queryInterface.describeTable('mediaItemShares')
|
||||||
|
if (!tableDescription.isDownloadable) {
|
||||||
|
logger.info(`${loggerPrefix} Adding isDownloadable column to mediaItemShares table`)
|
||||||
|
await queryInterface.addColumn('mediaItemShares', 'isDownloadable', {
|
||||||
|
type: queryInterface.sequelize.Sequelize.DataTypes.BOOLEAN,
|
||||||
|
defaultValue: false,
|
||||||
|
allowNull: false
|
||||||
|
})
|
||||||
|
logger.info(`${loggerPrefix} Added isDownloadable column to mediaItemShares table`)
|
||||||
|
} else {
|
||||||
|
logger.info(`${loggerPrefix} isDownloadable column already exists in mediaItemShares table`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info(`${loggerPrefix} mediaItemShares table does not exist`)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This migration script removes the isDownloadable column from the mediaItemShares table.
|
||||||
|
*
|
||||||
|
* @param {MigrationOptions} options - an object containing the migration context.
|
||||||
|
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||||
|
*/
|
||||||
|
async function down({ context: { queryInterface, logger } }) {
|
||||||
|
logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`)
|
||||||
|
|
||||||
|
if (await queryInterface.tableExists('mediaItemShares')) {
|
||||||
|
const tableDescription = await queryInterface.describeTable('mediaItemShares')
|
||||||
|
if (tableDescription.isDownloadable) {
|
||||||
|
logger.info(`${loggerPrefix} Removing isDownloadable column from mediaItemShares table`)
|
||||||
|
await queryInterface.removeColumn('mediaItemShares', 'isDownloadable')
|
||||||
|
logger.info(`${loggerPrefix} Removed isDownloadable column from mediaItemShares table`)
|
||||||
|
} else {
|
||||||
|
logger.info(`${loggerPrefix} isDownloadable column does not exist in mediaItemShares table`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info(`${loggerPrefix} mediaItemShares table does not exist`)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { up, down }
|
@ -12,6 +12,7 @@ const { DataTypes, Model } = require('sequelize')
|
|||||||
* @property {Object} extraData
|
* @property {Object} extraData
|
||||||
* @property {Date} createdAt
|
* @property {Date} createdAt
|
||||||
* @property {Date} updatedAt
|
* @property {Date} updatedAt
|
||||||
|
* @property {boolean} isDownloadable
|
||||||
*
|
*
|
||||||
* @typedef {MediaItemShareObject & MediaItemShare} MediaItemShareModel
|
* @typedef {MediaItemShareObject & MediaItemShare} MediaItemShareModel
|
||||||
*/
|
*/
|
||||||
@ -25,11 +26,40 @@ const { DataTypes, Model } = require('sequelize')
|
|||||||
* @property {Date} expiresAt
|
* @property {Date} expiresAt
|
||||||
* @property {Date} createdAt
|
* @property {Date} createdAt
|
||||||
* @property {Date} updatedAt
|
* @property {Date} updatedAt
|
||||||
|
* @property {boolean} isDownloadable
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class MediaItemShare extends Model {
|
class MediaItemShare extends Model {
|
||||||
constructor(values, options) {
|
constructor(values, options) {
|
||||||
super(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() {
|
toJSONForClient() {
|
||||||
@ -40,7 +70,8 @@ class MediaItemShare extends Model {
|
|||||||
slug: this.slug,
|
slug: this.slug,
|
||||||
expiresAt: this.expiresAt,
|
expiresAt: this.expiresAt,
|
||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
updatedAt: this.updatedAt
|
updatedAt: this.updatedAt,
|
||||||
|
isDownloadable: this.isDownloadable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +145,8 @@ class MediaItemShare extends Model {
|
|||||||
slug: DataTypes.STRING,
|
slug: DataTypes.STRING,
|
||||||
pash: DataTypes.STRING,
|
pash: DataTypes.STRING,
|
||||||
expiresAt: DataTypes.DATE,
|
expiresAt: DataTypes.DATE,
|
||||||
extraData: DataTypes.JSON
|
extraData: DataTypes.JSON,
|
||||||
|
isDownloadable: DataTypes.BOOLEAN
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sequelize,
|
sequelize,
|
||||||
|
@ -15,6 +15,7 @@ class PublicRouter {
|
|||||||
this.router.get('/share/:slug', ShareController.getMediaItemShareBySlug.bind(this))
|
this.router.get('/share/:slug', ShareController.getMediaItemShareBySlug.bind(this))
|
||||||
this.router.get('/share/:slug/track/:index', ShareController.getMediaItemShareAudioTrack.bind(this))
|
this.router.get('/share/:slug/track/:index', ShareController.getMediaItemShareAudioTrack.bind(this))
|
||||||
this.router.get('/share/:slug/cover', ShareController.getMediaItemShareCoverImage.bind(this))
|
this.router.get('/share/:slug/cover', ShareController.getMediaItemShareCoverImage.bind(this))
|
||||||
|
this.router.get('/share/:slug/download', ShareController.downloadMediaItemShare.bind(this))
|
||||||
this.router.patch('/share/:slug/progress', ShareController.updateMediaItemShareProgress.bind(this))
|
this.router.patch('/share/:slug/progress', ShareController.updateMediaItemShareProgress.bind(this))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,68 @@
|
|||||||
|
const chai = require('chai')
|
||||||
|
const sinon = require('sinon')
|
||||||
|
const { expect } = chai
|
||||||
|
|
||||||
|
const { DataTypes } = require('sequelize')
|
||||||
|
|
||||||
|
const { up, down } = require('../../../server/migrations/v2.17.6-share-add-isdownloadable')
|
||||||
|
|
||||||
|
describe('Migration v2.17.6-share-add-isDownloadable', () => {
|
||||||
|
let queryInterface, logger
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
queryInterface = {
|
||||||
|
addColumn: sinon.stub().resolves(),
|
||||||
|
removeColumn: sinon.stub().resolves(),
|
||||||
|
tableExists: sinon.stub().resolves(true),
|
||||||
|
describeTable: sinon.stub().resolves({ isDownloadable: undefined }),
|
||||||
|
sequelize: {
|
||||||
|
Sequelize: {
|
||||||
|
DataTypes: {
|
||||||
|
BOOLEAN: DataTypes.BOOLEAN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger = {
|
||||||
|
info: sinon.stub(),
|
||||||
|
error: sinon.stub()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('up', () => {
|
||||||
|
it('should add the isDownloadable column to mediaItemShares table', async () => {
|
||||||
|
await up({ context: { queryInterface, logger } })
|
||||||
|
|
||||||
|
expect(queryInterface.addColumn.calledOnce).to.be.true
|
||||||
|
expect(
|
||||||
|
queryInterface.addColumn.calledWith('mediaItemShares', 'isDownloadable', {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: false,
|
||||||
|
allowNull: false
|
||||||
|
})
|
||||||
|
).to.be.true
|
||||||
|
|
||||||
|
expect(logger.info.calledWith('[2.17.6 migration] UPGRADE BEGIN: 2.17.6-share-add-isdownloadable')).to.be.true
|
||||||
|
expect(logger.info.calledWith('[2.17.6 migration] Adding isDownloadable column to mediaItemShares table')).to.be.true
|
||||||
|
expect(logger.info.calledWith('[2.17.6 migration] Added isDownloadable column to mediaItemShares table')).to.be.true
|
||||||
|
expect(logger.info.calledWith('[2.17.6 migration] UPGRADE END: 2.17.6-share-add-isdownloadable')).to.be.true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('down', () => {
|
||||||
|
it('should remove the isDownloadable column from mediaItemShares table', async () => {
|
||||||
|
queryInterface.describeTable.resolves({ isDownloadable: true })
|
||||||
|
|
||||||
|
await down({ context: { queryInterface, logger } })
|
||||||
|
|
||||||
|
expect(queryInterface.removeColumn.calledOnce).to.be.true
|
||||||
|
expect(queryInterface.removeColumn.calledWith('mediaItemShares', 'isDownloadable')).to.be.true
|
||||||
|
|
||||||
|
expect(logger.info.calledWith('[2.17.6 migration] DOWNGRADE BEGIN: 2.17.6-share-add-isdownloadable')).to.be.true
|
||||||
|
expect(logger.info.calledWith('[2.17.6 migration] Removing isDownloadable column from mediaItemShares table')).to.be.true
|
||||||
|
expect(logger.info.calledWith('[2.17.6 migration] Removed isDownloadable column from mediaItemShares table')).to.be.true
|
||||||
|
expect(logger.info.calledWith('[2.17.6 migration] DOWNGRADE END: 2.17.6-share-add-isdownloadable')).to.be.true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user