]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add ability to remove hls/webtorrent files
authorChocobozzz <me@florianbigard.com>
Wed, 17 Nov 2021 15:04:53 +0000 (16:04 +0100)
committerChocobozzz <me@florianbigard.com>
Thu, 18 Nov 2021 08:04:30 +0000 (09:04 +0100)
20 files changed:
client/src/app/+admin/overview/videos/video-list.component.html
client/src/app/+admin/overview/videos/video-list.component.ts
client/src/app/shared/shared-main/video/video.model.ts
client/src/app/shared/shared-main/video/video.service.ts
client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts
server/controllers/api/videos/files.ts [new file with mode: 0644]
server/controllers/api/videos/index.ts
server/controllers/api/videos/update.ts
server/lib/job-queue/handlers/video-file-import.ts
server/lib/job-queue/handlers/video-transcoding.ts
server/middlewares/validators/videos/index.ts
server/middlewares/validators/videos/video-files.ts [new file with mode: 0644]
server/models/redundancy/video-redundancy.ts
server/models/video/video.ts
server/tests/api/check-params/index.ts
server/tests/api/check-params/video-files.ts [new file with mode: 0644]
server/tests/api/videos/index.ts
server/tests/api/videos/video-files.ts [new file with mode: 0644]
shared/extra-utils/videos/videos-command.ts
shared/models/users/user-right.enum.ts

index 9b536ec11a5b269fc7d53c23ba1e98608cafed73..6e4fb4c6fbe7798466d0cf6aa78924891138aa17 100644 (file)
@@ -57,7 +57,7 @@
       <td class="action-cell">
         <my-video-actions-dropdown
           placement="bottom auto" buttonDirection="horizontal" [buttonStyled]="true" [video]="video"
-          [displayOptions]="videoActionsOptions" (videoRemoved)="onVideoRemoved()"
+          [displayOptions]="videoActionsOptions" (videoRemoved)="reloadData()" (videoFilesRemoved)="reloadData()"
         ></my-video-actions-dropdown>
       </td>
 
   </ng-template>
 </p-table>
 
-<my-video-block #videoBlockModal (videoBlocked)="onVideoBlocked()"></my-video-block>
+<my-video-block #videoBlockModal (videoBlocked)="reloadData()"></my-video-block>
index 7f268bb23c2952a4fcac0e8b5e247af86a8c1ff2..3c21adb44c8dd8bc346aff8b4cce4cd1ec7927cc 100644 (file)
@@ -39,7 +39,8 @@ export class VideoListComponent extends RestTable implements OnInit {
     report: false,
     duplicate: true,
     mute: true,
-    liveInfo: false
+    liveInfo: false,
+    removeFiles: true
   }
 
   loading = true
@@ -71,17 +72,34 @@ export class VideoListComponent extends RestTable implements OnInit {
         {
           label: $localize`Delete`,
           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`,
           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`,
           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
   }
 
-  onVideoRemoved () {
-    this.reloadData()
-  }
-
   getPrivacyBadgeClass (video: Video) {
     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)
   }
 
-  onVideoBlocked () {
-    this.reloadData()
-  }
-
-  protected reloadData () {
+  reloadData () {
     this.selectedVideos = []
 
     this.loading = true
@@ -197,4 +207,23 @@ export class VideoListComponent extends RestTable implements OnInit {
         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)
+      })
+  }
 }
index 472a8c8108b360b1ffdfa91b04229ce3b9630b52..4203ff1c07db8b744dd1046e4dbb970bbd23397c 100644 (file)
@@ -14,7 +14,8 @@ import {
   VideoPrivacy,
   VideoScheduleUpdate,
   VideoState,
-  VideoStreamingPlaylist
+  VideoStreamingPlaylist,
+  VideoStreamingPlaylistType
 } from '@shared/models'
 
 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))
   }
 
+  hasHLS () {
+    return this.streamingPlaylists?.some(p => p.type === VideoStreamingPlaylistType.HLS)
+  }
+
+  hasWebTorrent () {
+    return this.files && this.files.length !== 0
+  }
+
   isLiveInfoAvailableBy (user: AuthUser) {
     return this.isLive &&
       user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.GET_ANY_LIVE))
index 570e8e3beca1747330cd3447f5dd5f84a4892306..d135a27dc8ded93d0f525539a328fc004076d420 100644 (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) {
     return this.authHttp
                .get<{ description: string }>(environment.apiUrl + descriptionPath)
index eff56b40e49802f788998812033e9ea7a1f4dda5..82c0847915c6ce30441017ce9ff855c9d03462b5 100644 (file)
@@ -2,7 +2,7 @@ import { Component, EventEmitter, Input, OnChanges, Output, ViewChild } from '@a
 import { AuthService, ConfirmService, Notifier, ScreenService } from '@app/core'
 import { BlocklistService, VideoBlockComponent, VideoBlockService, VideoReportComponent } from '@app/shared/shared-moderation'
 import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
-import { VideoCaption } from '@shared/models'
+import { UserRight, VideoCaption } from '@shared/models'
 import {
   Actor,
   DropdownAction,
@@ -27,6 +27,7 @@ export type VideoActionsDisplayType = {
   duplicate?: boolean
   mute?: boolean
   liveInfo?: boolean
+  removeFiles?: boolean
 }
 
 @Component({
@@ -65,6 +66,7 @@ export class VideoActionsDropdownComponent implements OnChanges {
   @Input() buttonSize: DropdownButtonSize = 'normal'
   @Input() buttonDirection: DropdownDirection = 'vertical'
 
+  @Output() videoFilesRemoved = new EventEmitter()
   @Output() videoRemoved = new EventEmitter()
   @Output() videoUnblocked = new EventEmitter()
   @Output() videoBlocked = new EventEmitter()
@@ -174,6 +176,10 @@ export class VideoActionsDropdownComponent implements OnChanges {
     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 */
 
   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 () {
     this.videoBlocked.emit()
   }
@@ -317,6 +340,20 @@ export class VideoActionsDropdownComponent implements OnChanges {
           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
         {
           label: $localize`Mute account`,
diff --git a/server/controllers/api/videos/files.ts b/server/controllers/api/videos/files.ts
new file mode 100644 (file)
index 0000000..2fe4b5a
--- /dev/null
@@ -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)
+}
index 72b382595865356697030c6421d97977e6100e39..2d088a73e2c6f13b4b76dbc6c7e1a02f623adc4d 100644 (file)
@@ -1,5 +1,4 @@
 import express from 'express'
-import toInt from 'validator/lib/toInt'
 import { pickCommonVideoQuery } from '@server/helpers/query'
 import { doJSONRequest } from '@server/helpers/requests'
 import { VideoViews } from '@server/lib/video-views'
@@ -27,17 +26,16 @@ import {
   paginationValidator,
   setDefaultPagination,
   setDefaultVideosSort,
-  videoFileMetadataGetValidator,
   videosCustomGetValidator,
   videosGetValidator,
   videosRemoveValidator,
   videosSortValidator
 } from '../../../middlewares'
 import { VideoModel } from '../../../models/video/video'
-import { VideoFileModel } from '../../../models/video/video-file'
 import { blacklistRouter } from './blacklist'
 import { videoCaptionsRouter } from './captions'
 import { videoCommentRouter } from './comment'
+import { filesRouter } from './files'
 import { videoImportsRouter } from './import'
 import { liveRouter } from './live'
 import { ownershipVideoRouter } from './ownership'
@@ -59,6 +57,7 @@ videosRouter.use('/', watchingRouter)
 videosRouter.use('/', liveRouter)
 videosRouter.use('/', uploadRouter)
 videosRouter.use('/', updateRouter)
+videosRouter.use('/', filesRouter)
 
 videosRouter.get('/categories',
   openapiOperationDoc({ operationId: 'getCategories' }),
@@ -93,10 +92,6 @@ videosRouter.get('/:id/description',
   asyncMiddleware(videosGetValidator),
   asyncMiddleware(getVideoDescription)
 )
-videosRouter.get('/:id/metadata/:videoFileId',
-  asyncMiddleware(videoFileMetadataGetValidator),
-  asyncMiddleware(getVideoFileMetadata)
-)
 videosRouter.get('/:id',
   openapiOperationDoc({ operationId: 'getVideo' }),
   optionalAuthenticate,
@@ -177,12 +172,6 @@ async function getVideoDescription (req: express.Request, res: express.Response)
   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) {
   const serverActor = await getServerActor()
 
index a0aa13d71cd87d4d0070a71496709be0b7cddefc..de5d94d5524132d12790071caec97c550bb7541a 100644 (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 videoFieldsSave = videoFromReq.toJSON()
   const oldVideoAuditView = new VideoAuditView(videoFromReq.toFormattedDetailsJSON())
index 47ae10a6680fb44a43c4a1ef83d44985ef5640f2..a91c2ef8020f2ce62b5d74a4d1c8b3aed9ca03d7 100644 (file)
@@ -55,7 +55,7 @@ async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) {
 
   if (currentVideoFile) {
     // Remove old file and old torrent
-    await video.removeFileAndTorrent(currentVideoFile)
+    await video.removeWebTorrentFileAndTorrent(currentVideoFile)
     // Remove the old video file from the array
     video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile)
 
index 0143cd02a36e0ac9ef0754109b956112a640a203..904ef2e3cf714ee796638071a60a975928143dc2 100644 (file)
@@ -138,7 +138,7 @@ async function onHlsPlaylistGeneration (video: MVideoFullLight, user: MUser, pay
   if (payload.isMaxQuality && CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) {
     // Remove webtorrent files if not enabled
     for (const file of video.VideoFiles) {
-      await video.removeFileAndTorrent(file)
+      await video.removeWebTorrentFileAndTorrent(file)
       await file.destroy()
     }
 
index 369c2c9b63e1c15fc4c4f2caabc2110ba07bf221..fd1d5809358b6c55dc3a5d855f5bcbafdf95838b 100644 (file)
@@ -2,6 +2,7 @@ export * from './video-blacklist'
 export * from './video-captions'
 export * from './video-channels'
 export * from './video-comments'
+export * from './video-files'
 export * from './video-imports'
 export * from './video-live'
 export * from './video-ownership-changes'
diff --git a/server/middlewares/validators/videos/video-files.ts b/server/middlewares/validators/videos/video-files.ts
new file mode 100644 (file)
index 0000000..282594a
--- /dev/null
@@ -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
+}
index 52997792420b6062e96c93803f6078bfe2a874d5..e8d79a3ab23dc218a5795751a22c76cdb72ee5eb 100644 (file)
@@ -160,7 +160,7 @@ export class VideoRedundancyModel extends Model<Partial<AttributesOnly<VideoRedu
       const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}`
       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 }))
     }
 
index 69d009e046c9b0f5a9674f1590ea348769bcc150..6eeb6b3126eec5c066a0b3fcf1747149c150e66e 100644 (file)
@@ -746,7 +746,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
 
       // Remove physical files and torrents
       instance.VideoFiles.forEach(file => {
-        tasks.push(instance.removeFileAndTorrent(file))
+        tasks.push(instance.removeWebTorrentFileAndTorrent(file))
       })
 
       // Remove playlists file
@@ -1706,7 +1706,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
                                        .concat(toAdd)
   }
 
-  removeFileAndTorrent (videoFile: MVideoFile, isRedundancy = false) {
+  removeWebTorrentFileAndTorrent (videoFile: MVideoFile, isRedundancy = false) {
     const filePath = isRedundancy
       ? VideoPathManager.Instance.getFSRedundancyVideoFilePath(this, videoFile)
       : VideoPathManager.Instance.getFSVideoFileOutputPath(this, videoFile)
index 0882f817624a465ea48e78d1137ccd80e714023f..ff7dc4abb6cf27d40c4bb9f929a25aeb51db9b63 100644 (file)
@@ -28,5 +28,6 @@ import './video-imports'
 import './video-playlists'
 import './videos'
 import './videos-common-filters'
+import './video-files'
 import './videos-history'
 import './videos-overviews'
diff --git a/server/tests/api/check-params/video-files.ts b/server/tests/api/check-params/video-files.ts
new file mode 100644 (file)
index 0000000..48b10d2
--- /dev/null
@@ -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)
+  })
+})
index c9c678e9d6799dfaa55b4f2b6f38d1db041e5bf2..f92e339e7a8b03d99f8c6e898a110bb4ca89f977 100644 (file)
@@ -7,6 +7,7 @@ import './video-change-ownership'
 import './video-channels'
 import './video-comments'
 import './video-description'
+import './video-files'
 import './video-hls'
 import './video-imports'
 import './video-nsfw'
diff --git a/server/tests/api/videos/video-files.ts b/server/tests/api/videos/video-files.ts
new file mode 100644 (file)
index 0000000..fcb2ca2
--- /dev/null
@@ -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)
+  })
+})
index 167fae22d031a4324991a9c2eda3b1835ebbea91..13a7d0e1ca9527ad53a8f72c16d78abc91d9e6a2 100644 (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) {
     return pick(options, [
       'start',
index 950b22bad14adb69aefbc44020b11e88fb10569c..96bccaf2f49aace3e975b94b9ac9a4cbdb6d6f88 100644 (file)
@@ -38,5 +38,7 @@ export const enum UserRight {
 
   MANAGE_PLUGINS,
 
-  MANAGE_VIDEOS_REDUNDANCIES
+  MANAGE_VIDEOS_REDUNDANCIES,
+
+  MANAGE_VIDEO_FILES
 }