mirror of https://github.com/Chocobozzz/PeerTube
Add ability to remove hls/webtorrent files
parent
3cfa817672
commit
b46cf4b920
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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`,
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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 }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
|
@ -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'
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
|
@ -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',
|
||||||
|
|
|
@ -38,5 +38,7 @@ export const enum UserRight {
|
||||||
|
|
||||||
MANAGE_PLUGINS,
|
MANAGE_PLUGINS,
|
||||||
|
|
||||||
MANAGE_VIDEOS_REDUNDANCIES
|
MANAGE_VIDEOS_REDUNDANCIES,
|
||||||
|
|
||||||
|
MANAGE_VIDEO_FILES
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue