Add ability to remove hls/webtorrent files

pull/4548/head
Chocobozzz 2021-11-17 16:04:53 +01:00
parent 3cfa817672
commit b46cf4b920
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
20 changed files with 497 additions and 37 deletions

View File

@ -57,7 +57,7 @@
<td class="action-cell"> <td class="action-cell">
<my-video-actions-dropdown <my-video-actions-dropdown
placement="bottom auto" buttonDirection="horizontal" [buttonStyled]="true" [video]="video" placement="bottom auto" buttonDirection="horizontal" [buttonStyled]="true" [video]="video"
[displayOptions]="videoActionsOptions" (videoRemoved)="onVideoRemoved()" [displayOptions]="videoActionsOptions" (videoRemoved)="reloadData()" (videoFilesRemoved)="reloadData()"
></my-video-actions-dropdown> ></my-video-actions-dropdown>
</td> </td>
@ -127,4 +127,4 @@
</ng-template> </ng-template>
</p-table> </p-table>
<my-video-block #videoBlockModal (videoBlocked)="onVideoBlocked()"></my-video-block> <my-video-block #videoBlockModal (videoBlocked)="reloadData()"></my-video-block>

View File

@ -39,7 +39,8 @@ export class VideoListComponent extends RestTable implements OnInit {
report: false, report: false,
duplicate: true, duplicate: true,
mute: true, mute: true,
liveInfo: false liveInfo: false,
removeFiles: true
} }
loading = true loading = true
@ -71,17 +72,34 @@ export class VideoListComponent extends RestTable implements OnInit {
{ {
label: $localize`Delete`, label: $localize`Delete`,
handler: videos => this.removeVideos(videos), handler: videos => this.removeVideos(videos),
isDisplayed: () => this.authUser.hasRight(UserRight.REMOVE_ANY_VIDEO) isDisplayed: () => this.authUser.hasRight(UserRight.REMOVE_ANY_VIDEO),
iconName: 'delete'
}, },
{ {
label: $localize`Block`, label: $localize`Block`,
handler: videos => this.videoBlockModal.show(videos), handler: videos => this.videoBlockModal.show(videos),
isDisplayed: videos => this.authUser.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) && videos.every(v => !v.blacklisted) isDisplayed: videos => this.authUser.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) && videos.every(v => !v.blacklisted),
iconName: 'no'
}, },
{ {
label: $localize`Unblock`, label: $localize`Unblock`,
handler: videos => this.unblockVideos(videos), handler: videos => this.unblockVideos(videos),
isDisplayed: videos => this.authUser.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) && videos.every(v => v.blacklisted) isDisplayed: videos => this.authUser.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) && videos.every(v => v.blacklisted),
iconName: 'undo'
}
],
[
{
label: $localize`Delete HLS files`,
handler: videos => this.removeVideoFiles(videos, 'hls'),
isDisplayed: videos => this.authUser.hasRight(UserRight.MANAGE_VIDEO_FILES) && videos.every(v => v.hasHLS() && v.hasWebTorrent()),
iconName: 'delete'
},
{
label: $localize`Delete WebTorrent files`,
handler: videos => this.removeVideoFiles(videos, 'webtorrent'),
isDisplayed: videos => this.authUser.hasRight(UserRight.MANAGE_VIDEO_FILES) && videos.every(v => v.hasHLS() && v.hasWebTorrent()),
iconName: 'delete'
} }
] ]
] ]
@ -95,10 +113,6 @@ export class VideoListComponent extends RestTable implements OnInit {
return this.selectedVideos.length !== 0 return this.selectedVideos.length !== 0
} }
onVideoRemoved () {
this.reloadData()
}
getPrivacyBadgeClass (video: Video) { getPrivacyBadgeClass (video: Video) {
if (video.privacy.id === VideoPrivacy.PUBLIC) return 'badge-green' if (video.privacy.id === VideoPrivacy.PUBLIC) return 'badge-green'
@ -146,11 +160,7 @@ export class VideoListComponent extends RestTable implements OnInit {
return files.reduce((p, f) => p += f.size, 0) return files.reduce((p, f) => p += f.size, 0)
} }
onVideoBlocked () { reloadData () {
this.reloadData()
}
protected reloadData () {
this.selectedVideos = [] this.selectedVideos = []
this.loading = true this.loading = true
@ -197,4 +207,23 @@ export class VideoListComponent extends RestTable implements OnInit {
error: err => this.notifier.error(err.message) error: err => this.notifier.error(err.message)
}) })
} }
private async removeVideoFiles (videos: Video[], type: 'hls' | 'webtorrent') {
const message = type === 'hls'
? $localize`Are you sure you want to delete ${videos.length} HLS streaming playlists?`
: $localize`Are you sure you want to delete WebTorrent files of ${videos.length} videos?`
const res = await this.confirmService.confirm(message, $localize`Delete`)
if (res === false) return
this.videoService.removeVideoFiles(videos.map(v => v.id), type)
.subscribe({
next: () => {
this.notifier.success($localize`Files were removed.`)
this.reloadData()
},
error: err => this.notifier.error(err.message)
})
}
} }

View File

@ -14,7 +14,8 @@ import {
VideoPrivacy, VideoPrivacy,
VideoScheduleUpdate, VideoScheduleUpdate,
VideoState, VideoState,
VideoStreamingPlaylist VideoStreamingPlaylist,
VideoStreamingPlaylistType
} from '@shared/models' } from '@shared/models'
export class Video implements VideoServerModel { export class Video implements VideoServerModel {
@ -219,6 +220,14 @@ export class Video implements VideoServerModel {
return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO)) return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO))
} }
hasHLS () {
return this.streamingPlaylists?.some(p => p.type === VideoStreamingPlaylistType.HLS)
}
hasWebTorrent () {
return this.files && this.files.length !== 0
}
isLiveInfoAvailableBy (user: AuthUser) { isLiveInfoAvailableBy (user: AuthUser) {
return this.isLive && return this.isLive &&
user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.GET_ANY_LIVE)) user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.GET_ANY_LIVE))

View File

@ -299,6 +299,15 @@ export class VideoService {
) )
} }
removeVideoFiles (videoIds: (number | string)[], type: 'hls' | 'webtorrent') {
return from(videoIds)
.pipe(
concatMap(id => this.authHttp.delete(VideoService.BASE_VIDEO_URL + '/' + id + '/' + type)),
toArray(),
catchError(err => this.restExtractor.handleError(err))
)
}
loadCompleteDescription (descriptionPath: string) { loadCompleteDescription (descriptionPath: string) {
return this.authHttp return this.authHttp
.get<{ description: string }>(environment.apiUrl + descriptionPath) .get<{ description: string }>(environment.apiUrl + descriptionPath)

View File

@ -2,7 +2,7 @@ import { Component, EventEmitter, Input, OnChanges, Output, ViewChild } from '@a
import { AuthService, ConfirmService, Notifier, ScreenService } from '@app/core' import { AuthService, ConfirmService, Notifier, ScreenService } from '@app/core'
import { BlocklistService, VideoBlockComponent, VideoBlockService, VideoReportComponent } from '@app/shared/shared-moderation' import { BlocklistService, VideoBlockComponent, VideoBlockService, VideoReportComponent } from '@app/shared/shared-moderation'
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
import { VideoCaption } from '@shared/models' import { UserRight, VideoCaption } from '@shared/models'
import { import {
Actor, Actor,
DropdownAction, DropdownAction,
@ -27,6 +27,7 @@ export type VideoActionsDisplayType = {
duplicate?: boolean duplicate?: boolean
mute?: boolean mute?: boolean
liveInfo?: boolean liveInfo?: boolean
removeFiles?: boolean
} }
@Component({ @Component({
@ -65,6 +66,7 @@ export class VideoActionsDropdownComponent implements OnChanges {
@Input() buttonSize: DropdownButtonSize = 'normal' @Input() buttonSize: DropdownButtonSize = 'normal'
@Input() buttonDirection: DropdownDirection = 'vertical' @Input() buttonDirection: DropdownDirection = 'vertical'
@Output() videoFilesRemoved = new EventEmitter()
@Output() videoRemoved = new EventEmitter() @Output() videoRemoved = new EventEmitter()
@Output() videoUnblocked = new EventEmitter() @Output() videoUnblocked = new EventEmitter()
@Output() videoBlocked = new EventEmitter() @Output() videoBlocked = new EventEmitter()
@ -174,6 +176,10 @@ export class VideoActionsDropdownComponent implements OnChanges {
return this.video.account.id !== this.user.account.id return this.video.account.id !== this.user.account.id
} }
canRemoveVideoFiles () {
return this.user.hasRight(UserRight.MANAGE_VIDEO_FILES) && this.video.hasHLS() && this.video.hasWebTorrent()
}
/* Action handlers */ /* Action handlers */
async unblockVideo () { async unblockVideo () {
@ -245,6 +251,23 @@ export class VideoActionsDropdownComponent implements OnChanges {
}) })
} }
async removeVideoFiles (video: Video, type: 'hls' | 'webtorrent') {
const confirmMessage = $localize`Do you really want to remove "${this.video.name}" files?`
const res = await this.confirmService.confirm(confirmMessage, $localize`Remove "${this.video.name}" files`)
if (res === false) return
this.videoService.removeVideoFiles([ video.id ], type)
.subscribe({
next: () => {
this.notifier.success($localize`Removed files of ${video.name}.`)
this.videoFilesRemoved.emit()
},
error: err => this.notifier.error(err.message)
})
}
onVideoBlocked () { onVideoBlocked () {
this.videoBlocked.emit() this.videoBlocked.emit()
} }
@ -317,6 +340,20 @@ export class VideoActionsDropdownComponent implements OnChanges {
iconName: 'flag' iconName: 'flag'
} }
], ],
[
{
label: $localize`Delete HLS files`,
handler: ({ video }) => this.removeVideoFiles(video, 'hls'),
isDisplayed: () => this.displayOptions.removeFiles && this.canRemoveVideoFiles(),
iconName: 'delete'
},
{
label: $localize`Delete WebTorrent files`,
handler: ({ video }) => this.removeVideoFiles(video, 'webtorrent'),
isDisplayed: () => this.displayOptions.removeFiles && this.canRemoveVideoFiles(),
iconName: 'delete'
}
],
[ // actions regarding the account/its server [ // actions regarding the account/its server
{ {
label: $localize`Mute account`, label: $localize`Mute account`,

View File

@ -0,0 +1,79 @@
import express from 'express'
import toInt from 'validator/lib/toInt'
import { logger, loggerTagsFactory } from '@server/helpers/logger'
import { federateVideoIfNeeded } from '@server/lib/activitypub/videos'
import { VideoFileModel } from '@server/models/video/video-file'
import { HttpStatusCode } from '@shared/models'
import {
asyncMiddleware,
authenticate,
videoFileMetadataGetValidator,
videoFilesDeleteHLSValidator,
videoFilesDeleteWebTorrentValidator
} from '../../../middlewares'
const lTags = loggerTagsFactory('api', 'video')
const filesRouter = express.Router()
filesRouter.get('/:id/metadata/:videoFileId',
asyncMiddleware(videoFileMetadataGetValidator),
asyncMiddleware(getVideoFileMetadata)
)
filesRouter.delete('/:id/hls',
authenticate,
asyncMiddleware(videoFilesDeleteHLSValidator),
asyncMiddleware(removeHLSPlaylist)
)
filesRouter.delete('/:id/webtorrent',
authenticate,
asyncMiddleware(videoFilesDeleteWebTorrentValidator),
asyncMiddleware(removeWebTorrentFiles)
)
// ---------------------------------------------------------------------------
export {
filesRouter
}
// ---------------------------------------------------------------------------
async function getVideoFileMetadata (req: express.Request, res: express.Response) {
const videoFile = await VideoFileModel.loadWithMetadata(toInt(req.params.videoFileId))
return res.json(videoFile.metadata)
}
async function removeHLSPlaylist (req: express.Request, res: express.Response) {
const video = res.locals.videoAll
logger.info('Deleting HLS playlist of %s.', video.url, lTags(video.uuid))
const hls = video.getHLSPlaylist()
await video.removeStreamingPlaylistFiles(hls)
await hls.destroy()
video.VideoStreamingPlaylists = video.VideoStreamingPlaylists.filter(p => p.id !== hls.id)
await federateVideoIfNeeded(video, false, undefined)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function removeWebTorrentFiles (req: express.Request, res: express.Response) {
const video = res.locals.videoAll
logger.info('Deleting WebTorrent files of %s.', video.url, lTags(video.uuid))
for (const file of video.VideoFiles) {
await video.removeWebTorrentFileAndTorrent(file)
await file.destroy()
}
video.VideoFiles = []
await federateVideoIfNeeded(video, false, undefined)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}

View File

@ -1,5 +1,4 @@
import express from 'express' import express from 'express'
import toInt from 'validator/lib/toInt'
import { pickCommonVideoQuery } from '@server/helpers/query' import { pickCommonVideoQuery } from '@server/helpers/query'
import { doJSONRequest } from '@server/helpers/requests' import { doJSONRequest } from '@server/helpers/requests'
import { VideoViews } from '@server/lib/video-views' import { VideoViews } from '@server/lib/video-views'
@ -27,17 +26,16 @@ import {
paginationValidator, paginationValidator,
setDefaultPagination, setDefaultPagination,
setDefaultVideosSort, setDefaultVideosSort,
videoFileMetadataGetValidator,
videosCustomGetValidator, videosCustomGetValidator,
videosGetValidator, videosGetValidator,
videosRemoveValidator, videosRemoveValidator,
videosSortValidator videosSortValidator
} from '../../../middlewares' } from '../../../middlewares'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { VideoFileModel } from '../../../models/video/video-file'
import { blacklistRouter } from './blacklist' import { blacklistRouter } from './blacklist'
import { videoCaptionsRouter } from './captions' import { videoCaptionsRouter } from './captions'
import { videoCommentRouter } from './comment' import { videoCommentRouter } from './comment'
import { filesRouter } from './files'
import { videoImportsRouter } from './import' import { videoImportsRouter } from './import'
import { liveRouter } from './live' import { liveRouter } from './live'
import { ownershipVideoRouter } from './ownership' import { ownershipVideoRouter } from './ownership'
@ -59,6 +57,7 @@ videosRouter.use('/', watchingRouter)
videosRouter.use('/', liveRouter) videosRouter.use('/', liveRouter)
videosRouter.use('/', uploadRouter) videosRouter.use('/', uploadRouter)
videosRouter.use('/', updateRouter) videosRouter.use('/', updateRouter)
videosRouter.use('/', filesRouter)
videosRouter.get('/categories', videosRouter.get('/categories',
openapiOperationDoc({ operationId: 'getCategories' }), openapiOperationDoc({ operationId: 'getCategories' }),
@ -93,10 +92,6 @@ videosRouter.get('/:id/description',
asyncMiddleware(videosGetValidator), asyncMiddleware(videosGetValidator),
asyncMiddleware(getVideoDescription) asyncMiddleware(getVideoDescription)
) )
videosRouter.get('/:id/metadata/:videoFileId',
asyncMiddleware(videoFileMetadataGetValidator),
asyncMiddleware(getVideoFileMetadata)
)
videosRouter.get('/:id', videosRouter.get('/:id',
openapiOperationDoc({ operationId: 'getVideo' }), openapiOperationDoc({ operationId: 'getVideo' }),
optionalAuthenticate, optionalAuthenticate,
@ -177,12 +172,6 @@ async function getVideoDescription (req: express.Request, res: express.Response)
return res.json({ description }) return res.json({ description })
} }
async function getVideoFileMetadata (req: express.Request, res: express.Response) {
const videoFile = await VideoFileModel.loadWithMetadata(toInt(req.params.videoFileId))
return res.json(videoFile.metadata)
}
async function listVideos (req: express.Request, res: express.Response) { async function listVideos (req: express.Request, res: express.Response) {
const serverActor = await getServerActor() const serverActor = await getServerActor()

View File

@ -51,7 +51,7 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export async function updateVideo (req: express.Request, res: express.Response) { async function updateVideo (req: express.Request, res: express.Response) {
const videoFromReq = res.locals.videoAll const videoFromReq = res.locals.videoAll
const videoFieldsSave = videoFromReq.toJSON() const videoFieldsSave = videoFromReq.toJSON()
const oldVideoAuditView = new VideoAuditView(videoFromReq.toFormattedDetailsJSON()) const oldVideoAuditView = new VideoAuditView(videoFromReq.toFormattedDetailsJSON())

View File

@ -55,7 +55,7 @@ async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) {
if (currentVideoFile) { if (currentVideoFile) {
// Remove old file and old torrent // Remove old file and old torrent
await video.removeFileAndTorrent(currentVideoFile) await video.removeWebTorrentFileAndTorrent(currentVideoFile)
// Remove the old video file from the array // Remove the old video file from the array
video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile) video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile)

View File

@ -138,7 +138,7 @@ async function onHlsPlaylistGeneration (video: MVideoFullLight, user: MUser, pay
if (payload.isMaxQuality && CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) { if (payload.isMaxQuality && CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) {
// Remove webtorrent files if not enabled // Remove webtorrent files if not enabled
for (const file of video.VideoFiles) { for (const file of video.VideoFiles) {
await video.removeFileAndTorrent(file) await video.removeWebTorrentFileAndTorrent(file)
await file.destroy() await file.destroy()
} }

View File

@ -2,6 +2,7 @@ export * from './video-blacklist'
export * from './video-captions' export * from './video-captions'
export * from './video-channels' export * from './video-channels'
export * from './video-comments' export * from './video-comments'
export * from './video-files'
export * from './video-imports' export * from './video-imports'
export * from './video-live' export * from './video-live'
export * from './video-ownership-changes' export * from './video-ownership-changes'

View File

@ -0,0 +1,104 @@
import express from 'express'
import { MUser, MVideo } from '@server/types/models'
import { HttpStatusCode, UserRight } from '../../../../shared'
import { logger } from '../../../helpers/logger'
import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared'
const videoFilesDeleteWebTorrentValidator = [
isValidVideoIdParam('id'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoFilesDeleteWebTorrent parameters', { parameters: req.params })
if (areValidationErrors(req, res)) return
if (!await doesVideoExist(req.params.id, res)) return
const video = res.locals.videoAll
const user = res.locals.oauth.token.User
if (!checkUserCanDeleteFiles(user, res)) return
if (!checkLocalVideo(video, res)) return
if (!video.hasWebTorrentFiles()) {
return res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
message: 'This video does not have WebTorrent files'
})
}
if (!video.getHLSPlaylist()) {
return res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
message: 'Cannot delete WebTorrent files since this video does not have HLS playlist'
})
}
return next()
}
]
const videoFilesDeleteHLSValidator = [
isValidVideoIdParam('id'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoFilesDeleteHLS parameters', { parameters: req.params })
if (areValidationErrors(req, res)) return
if (!await doesVideoExist(req.params.id, res)) return
const video = res.locals.videoAll
const user = res.locals.oauth.token.User
if (!checkUserCanDeleteFiles(user, res)) return
if (!checkLocalVideo(video, res)) return
if (!video.getHLSPlaylist()) {
return res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
message: 'This video does not have HLS files'
})
}
if (!video.hasWebTorrentFiles()) {
return res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
message: 'Cannot delete HLS playlist since this video does not have WebTorrent files'
})
}
return next()
}
]
export {
videoFilesDeleteWebTorrentValidator,
videoFilesDeleteHLSValidator
}
// ---------------------------------------------------------------------------
function checkLocalVideo (video: MVideo, res: express.Response) {
if (video.remote) {
res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
message: 'Cannot delete files of remote video'
})
return false
}
return true
}
function checkUserCanDeleteFiles (user: MUser, res: express.Response) {
if (user.hasRight(UserRight.MANAGE_VIDEO_FILES) !== true) {
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'User cannot update video files'
})
return false
}
return true
}

View File

@ -160,7 +160,7 @@ export class VideoRedundancyModel extends Model<Partial<AttributesOnly<VideoRedu
const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}`
logger.info('Removing duplicated video file %s.', logIdentifier) logger.info('Removing duplicated video file %s.', logIdentifier)
videoFile.Video.removeFileAndTorrent(videoFile, true) videoFile.Video.removeWebTorrentFileAndTorrent(videoFile, true)
.catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err }))
} }

View File

@ -746,7 +746,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
// Remove physical files and torrents // Remove physical files and torrents
instance.VideoFiles.forEach(file => { instance.VideoFiles.forEach(file => {
tasks.push(instance.removeFileAndTorrent(file)) tasks.push(instance.removeWebTorrentFileAndTorrent(file))
}) })
// Remove playlists file // Remove playlists file
@ -1706,7 +1706,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
.concat(toAdd) .concat(toAdd)
} }
removeFileAndTorrent (videoFile: MVideoFile, isRedundancy = false) { removeWebTorrentFileAndTorrent (videoFile: MVideoFile, isRedundancy = false) {
const filePath = isRedundancy const filePath = isRedundancy
? VideoPathManager.Instance.getFSRedundancyVideoFilePath(this, videoFile) ? VideoPathManager.Instance.getFSRedundancyVideoFilePath(this, videoFile)
: VideoPathManager.Instance.getFSVideoFileOutputPath(this, videoFile) : VideoPathManager.Instance.getFSVideoFileOutputPath(this, videoFile)

View File

@ -28,5 +28,6 @@ import './video-imports'
import './video-playlists' import './video-playlists'
import './videos' import './videos'
import './videos-common-filters' import './videos-common-filters'
import './video-files'
import './videos-history' import './videos-history'
import './videos-overviews' import './videos-overviews'

View File

@ -0,0 +1,99 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import 'mocha'
import { cleanupTests, createMultipleServers, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/extra-utils'
import { HttpStatusCode, UserRole } from '@shared/models'
describe('Test videos files', function () {
let servers: PeerTubeServer[]
let webtorrentId: string
let hlsId: string
let remoteId: string
let userToken: string
let moderatorToken: string
let validId1: string
let validId2: string
// ---------------------------------------------------------------
before(async function () {
this.timeout(150_000)
servers = await createMultipleServers(2)
await setAccessTokensToServers(servers)
userToken = await servers[0].users.generateUserAndToken('user', UserRole.USER)
moderatorToken = await servers[0].users.generateUserAndToken('moderator', UserRole.MODERATOR)
{
await servers[0].config.enableTranscoding(true, true)
{
const { uuid } = await servers[0].videos.quickUpload({ name: 'both 1' })
validId1 = uuid
}
{
const { uuid } = await servers[0].videos.quickUpload({ name: 'both 2' })
validId2 = uuid
}
}
await waitJobs(servers)
{
await servers[0].config.enableTranscoding(false, true)
const { uuid } = await servers[0].videos.quickUpload({ name: 'hls' })
hlsId = uuid
}
await waitJobs(servers)
{
await servers[0].config.enableTranscoding(false, true)
const { uuid } = await servers[0].videos.quickUpload({ name: 'webtorrent' })
webtorrentId = uuid
}
await waitJobs(servers)
})
it('Should not delete files of a remote video', async function () {
await servers[0].videos.removeHLSFiles({ videoId: remoteId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
await servers[0].videos.removeWebTorrentFiles({ videoId: remoteId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
})
it('Should not delete files by a non admin user', async function () {
const expectedStatus = HttpStatusCode.FORBIDDEN_403
await servers[0].videos.removeHLSFiles({ videoId: validId1, token: userToken, expectedStatus })
await servers[0].videos.removeHLSFiles({ videoId: validId1, token: moderatorToken, expectedStatus })
await servers[0].videos.removeWebTorrentFiles({ videoId: validId1, token: userToken, expectedStatus })
await servers[0].videos.removeWebTorrentFiles({ videoId: validId1, token: moderatorToken, expectedStatus })
})
it('Should not delete files if the files are not available', async function () {
await servers[0].videos.removeHLSFiles({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
await servers[0].videos.removeWebTorrentFiles({ videoId: webtorrentId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
})
it('Should not delete files if no both versions are available', async function () {
await servers[0].videos.removeHLSFiles({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
await servers[0].videos.removeWebTorrentFiles({ videoId: webtorrentId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
})
it('Should not delete files if no both versions are available', async function () {
await servers[0].videos.removeHLSFiles({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
await servers[0].videos.removeWebTorrentFiles({ videoId: webtorrentId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
})
it('Should delete files if both versions are available', async function () {
await servers[0].videos.removeHLSFiles({ videoId: validId1 })
await servers[0].videos.removeWebTorrentFiles({ videoId: validId2 })
})
after(async function () {
await cleanupTests(servers)
})
})

View File

@ -7,6 +7,7 @@ import './video-change-ownership'
import './video-channels' import './video-channels'
import './video-comments' import './video-comments'
import './video-description' import './video-description'
import './video-files'
import './video-hls' import './video-hls'
import './video-imports' import './video-imports'
import './video-nsfw' import './video-nsfw'

View File

@ -0,0 +1,70 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import 'mocha'
import { expect } from 'chai'
import { cleanupTests, createMultipleServers, doubleFollow, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/extra-utils'
describe('Test videos files', function () {
let servers: PeerTubeServer[]
let validId1: string
let validId2: string
// ---------------------------------------------------------------
before(async function () {
this.timeout(150_000)
servers = await createMultipleServers(2)
await setAccessTokensToServers(servers)
await doubleFollow(servers[0], servers[1])
await servers[0].config.enableTranscoding(true, true)
{
const { uuid } = await servers[0].videos.quickUpload({ name: 'video 1' })
validId1 = uuid
}
{
const { uuid } = await servers[0].videos.quickUpload({ name: 'video 2' })
validId2 = uuid
}
await waitJobs(servers)
})
it('Should delete webtorrent files', async function () {
this.timeout(30_000)
await servers[0].videos.removeWebTorrentFiles({ videoId: validId1 })
await waitJobs(servers)
for (const server of servers) {
const video = await server.videos.get({ id: validId1 })
expect(video.files).to.have.lengthOf(0)
expect(video.streamingPlaylists).to.have.lengthOf(1)
}
})
it('Should delete HLS files', async function () {
this.timeout(30_000)
await servers[0].videos.removeHLSFiles({ videoId: validId2 })
await waitJobs(servers)
for (const server of servers) {
const video = await server.videos.get({ id: validId2 })
expect(video.files).to.have.length.above(0)
expect(video.streamingPlaylists).to.have.lengthOf(0)
}
})
after(async function () {
await cleanupTests(servers)
})
})

View File

@ -602,6 +602,36 @@ export class VideosCommand extends AbstractCommand {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
removeHLSFiles (options: OverrideCommandOptions & {
videoId: number | string
}) {
const path = '/api/v1/videos/' + options.videoId + '/hls'
return this.deleteRequest({
...options,
path,
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
})
}
removeWebTorrentFiles (options: OverrideCommandOptions & {
videoId: number | string
}) {
const path = '/api/v1/videos/' + options.videoId + '/webtorrent'
return this.deleteRequest({
...options,
path,
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
})
}
// ---------------------------------------------------------------------------
private buildListQuery (options: VideosCommonQuery) { private buildListQuery (options: VideosCommonQuery) {
return pick(options, [ return pick(options, [
'start', 'start',

View File

@ -38,5 +38,7 @@ export const enum UserRight {
MANAGE_PLUGINS, MANAGE_PLUGINS,
MANAGE_VIDEOS_REDUNDANCIES MANAGE_VIDEOS_REDUNDANCIES,
MANAGE_VIDEO_FILES
} }