Resumable video uploads (#3933)
* WIP: resumable video uploads relates to #324 * fix review comments * video upload: error handling * fix audio upload * fixes after self review * Update server/controllers/api/videos/index.ts Co-authored-by: Rigel Kent <par@rigelk.eu> * Update server/middlewares/validators/videos/videos.ts Co-authored-by: Rigel Kent <par@rigelk.eu> * Update server/controllers/api/videos/index.ts Co-authored-by: Rigel Kent <par@rigelk.eu> * update after code review * refactor upload route - restore multipart upload route - move resumable to dedicated upload-resumable route - move checks to middleware - do not leak internal fs structure in response * fix yarn.lock upon rebase * factorize addVideo for reuse in both endpoints * add resumable upload API to openapi spec * add initial test and test helper for resumable upload * typings for videoAddResumable middleware * avoid including aws and google packages via node-uploadx, by only including uploadx/core * rename ex-isAudioBg to more explicit name mentioning it is a preview file for audio * add video-upload-tmp-folder-cleaner job * stronger typing of video upload middleware * reduce dependency to @uploadx/core * add audio upload test * refactor resumable uploads cleanup from job to scheduler * refactor resumable uploads scheduler to compare to last execution time * make resumable upload validator to always cleanup on failure * move legacy upload request building outside of uploadVideo test helper * filter upload-resumable middlewares down to POST, PUT, DELETE also begin to type metadata * merge add duration functions * stronger typings and documentation for uploadx behaviour, move init validator up * refactor(client/video-edit): options > uploadxOptions * refactor(client/video-edit): remove obsolete else * scheduler/remove-dangling-resum: rename tag * refactor(server/video): add UploadVideoFiles type * refactor(mw/validators): restructure eslint disable * refactor(mw/validators/videos): rename import * refactor(client/vid-upload): rename html elem id * refactor(sched/remove-dangl): move fn to method * refactor(mw/async): add method typing * refactor(mw/vali/video): double quote > single * refactor(server/upload-resum): express use > all * proper http methud enum server/middlewares/async.ts * properly type http methods * factorize common video upload validation steps * add check for maximum partially uploaded file size * fix audioBg use * fix extname(filename) in addVideo * document parameters for uploadx's resumable protocol * clear META files in scheduler * last audio refactor before cramming preview in the initial POST form data * refactor as mulitpart/form-data initial post request this allows preview/thumbnail uploads alongside the initial request, and cleans up the upload form * Add more tests for resumable uploads * Refactor remove dangling resumable uploads * Prepare changelog * Add more resumable upload tests * Remove user quota check for resumable uploads * Fix upload error handler * Update nginx template for upload-resumable * Cleanup comment * Remove unused express methods * Prefer to use got instead of raw http * Don't retry on error 500 Co-authored-by: Rigel Kent <par@rigelk.eu> Co-authored-by: Rigel Kent <sendmemail@rigelk.eu> Co-authored-by: Chocobozzz <me@florianbigard.com>
This commit is contained in:
		
							parent
							
								
									d29ced1a85
								
							
						
					
					
						commit
						f6d6e7f861
					
				
					 46 changed files with 2454 additions and 1293 deletions
				
			
		| 
						 | 
				
			
			@ -96,6 +96,7 @@
 | 
			
		|||
    "lodash-es": "^4.17.4",
 | 
			
		||||
    "markdown-it": "12.0.4",
 | 
			
		||||
    "mini-css-extract-plugin": "^1.3.1",
 | 
			
		||||
    "ngx-uploadx": "^4.1.0",
 | 
			
		||||
    "p2p-media-loader-hlsjs": "^0.6.2",
 | 
			
		||||
    "path-browserify": "^1.0.0",
 | 
			
		||||
    "primeng": "^11.0.0-rc.1",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@ import { ViewportScroller } from '@angular/common'
 | 
			
		|||
import { HttpErrorResponse } from '@angular/common/http'
 | 
			
		||||
import { AfterViewChecked, Component, OnInit } from '@angular/core'
 | 
			
		||||
import { AuthService, Notifier, User, UserService } from '@app/core'
 | 
			
		||||
import { uploadErrorHandler } from '@app/helpers'
 | 
			
		||||
import { genericUploadErrorHandler } from '@app/helpers'
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'my-account-settings',
 | 
			
		||||
| 
						 | 
				
			
			@ -46,7 +46,7 @@ export class MyAccountSettingsComponent implements OnInit, AfterViewChecked {
 | 
			
		|||
          this.user.updateAccountAvatar(data.avatar)
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        (err: HttpErrorResponse) => uploadErrorHandler({
 | 
			
		||||
        (err: HttpErrorResponse) => genericUploadErrorHandler({
 | 
			
		||||
          err,
 | 
			
		||||
          name: $localize`avatar`,
 | 
			
		||||
          notifier: this.notifier
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@ import { HttpErrorResponse } from '@angular/common/http'
 | 
			
		|||
import { Component, OnDestroy, OnInit } from '@angular/core'
 | 
			
		||||
import { ActivatedRoute, Router } from '@angular/router'
 | 
			
		||||
import { AuthService, Notifier, ServerService } from '@app/core'
 | 
			
		||||
import { uploadErrorHandler } from '@app/helpers'
 | 
			
		||||
import { genericUploadErrorHandler } from '@app/helpers'
 | 
			
		||||
import {
 | 
			
		||||
  VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
 | 
			
		||||
  VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
 | 
			
		||||
| 
						 | 
				
			
			@ -109,7 +109,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
 | 
			
		|||
            this.videoChannel.updateAvatar(data.avatar)
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          (err: HttpErrorResponse) => uploadErrorHandler({
 | 
			
		||||
          (err: HttpErrorResponse) => genericUploadErrorHandler({
 | 
			
		||||
            err,
 | 
			
		||||
            name: $localize`avatar`,
 | 
			
		||||
            notifier: this.notifier
 | 
			
		||||
| 
						 | 
				
			
			@ -139,7 +139,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
 | 
			
		|||
            this.videoChannel.updateBanner(data.banner)
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          (err: HttpErrorResponse) => uploadErrorHandler({
 | 
			
		||||
          (err: HttpErrorResponse) => genericUploadErrorHandler({
 | 
			
		||||
            err,
 | 
			
		||||
            name: $localize`banner`,
 | 
			
		||||
            notifier: this.notifier
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,48 @@
 | 
			
		|||
import { objectToFormData } from '@app/helpers'
 | 
			
		||||
import { resolveUrl, UploaderX } from 'ngx-uploadx'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * multipart/form-data uploader extending the UploaderX implementation of Google Resumable
 | 
			
		||||
 * for use with multer
 | 
			
		||||
 *
 | 
			
		||||
 * @see https://github.com/kukhariev/ngx-uploadx/blob/637e258fe366b8095203f387a6101a230ee4f8e6/src/uploadx/lib/uploaderx.ts
 | 
			
		||||
 * @example
 | 
			
		||||
 *
 | 
			
		||||
 *   options: UploadxOptions = {
 | 
			
		||||
 *     uploaderClass: UploaderXFormData
 | 
			
		||||
 *   };
 | 
			
		||||
 */
 | 
			
		||||
export class UploaderXFormData extends UploaderX {
 | 
			
		||||
 | 
			
		||||
  async getFileUrl (): Promise<string> {
 | 
			
		||||
    const headers = {
 | 
			
		||||
      'X-Upload-Content-Length': this.size.toString(),
 | 
			
		||||
      'X-Upload-Content-Type': this.file.type || 'application/octet-stream'
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const previewfile = this.metadata.previewfile as any as File
 | 
			
		||||
    delete this.metadata.previewfile
 | 
			
		||||
 | 
			
		||||
    const data = objectToFormData(this.metadata)
 | 
			
		||||
    if (previewfile !== undefined) {
 | 
			
		||||
      data.append('previewfile', previewfile, previewfile.name)
 | 
			
		||||
      data.append('thumbnailfile', previewfile, previewfile.name)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await this.request({
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      body: data,
 | 
			
		||||
      url: this.endpoint,
 | 
			
		||||
      headers
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const location = this.getValueFromResponse('location')
 | 
			
		||||
    if (!location) {
 | 
			
		||||
      throw new Error('Invalid or missing Location header')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.offset = this.responseStatus === 201 ? 0 : undefined
 | 
			
		||||
 | 
			
		||||
    return resolveUrl(location, this.endpoint)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,12 +1,17 @@
 | 
			
		|||
<div *ngIf="!isUploadingVideo" class="upload-video-container" dragDrop (fileDropped)="setVideoFile($event)">
 | 
			
		||||
<div *ngIf="!isUploadingVideo" class="upload-video-container" dragDrop (fileDropped)="onFileDropped($event)">
 | 
			
		||||
  <div class="first-step-block">
 | 
			
		||||
    <my-global-icon class="upload-icon" iconName="upload" aria-hidden="true"></my-global-icon>
 | 
			
		||||
 | 
			
		||||
    <div class="button-file form-control" [ngbTooltip]="'(extensions: ' + videoExtensions + ')'">
 | 
			
		||||
      <span i18n>Select the file to upload</span>
 | 
			
		||||
      <input
 | 
			
		||||
        aria-label="Select the file to upload" i18n-aria-label
 | 
			
		||||
        #videofileInput type="file" name="videofile" id="videofile" [accept]="videoExtensions" (change)="fileChange()" autofocus
 | 
			
		||||
        aria-label="Select the file to upload"
 | 
			
		||||
        i18n-aria-label
 | 
			
		||||
        #videofileInput
 | 
			
		||||
        [accept]="videoExtensions"
 | 
			
		||||
        (change)="onFileChange($event)"
 | 
			
		||||
        id="videofile"
 | 
			
		||||
        type="file"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -41,7 +46,13 @@
 | 
			
		|||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="form-group upload-audio-button">
 | 
			
		||||
        <my-button className="orange-button" i18n-label [label]="getAudioUploadLabel()" icon="upload" (click)="uploadFirstStep(true)"></my-button>
 | 
			
		||||
        <my-button
 | 
			
		||||
          className="orange-button"
 | 
			
		||||
          [label]="getAudioUploadLabel()"
 | 
			
		||||
          icon="upload"
 | 
			
		||||
          (click)="uploadAudio()"
 | 
			
		||||
        >
 | 
			
		||||
        </my-button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
  </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -64,6 +75,7 @@
 | 
			
		|||
      <span>{{ error }}</span>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="btn-group" role="group">
 | 
			
		||||
    <input type="button" class="btn" i18n-value="Retry failed upload of a video" value="Retry" (click)="retryUpload()" />
 | 
			
		||||
    <input type="button" class="btn" i18n-value="Cancel ongoing upload of a video" value="Cancel" (click)="cancelUpload()" />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -47,8 +47,4 @@
 | 
			
		|||
 | 
			
		||||
    margin-left: 10px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .btn-group > input:not(:first-child) {
 | 
			
		||||
    margin-left: 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,15 +1,16 @@
 | 
			
		|||
import { Subscription } from 'rxjs'
 | 
			
		||||
import { HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http'
 | 
			
		||||
import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
 | 
			
		||||
import { Router } from '@angular/router'
 | 
			
		||||
import { UploadxOptions, UploadState, UploadxService } from 'ngx-uploadx'
 | 
			
		||||
import { UploaderXFormData } from './uploaderx-form-data'
 | 
			
		||||
import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService, UserService } from '@app/core'
 | 
			
		||||
import { scrollToTop, uploadErrorHandler } from '@app/helpers'
 | 
			
		||||
import { scrollToTop, genericUploadErrorHandler } from '@app/helpers'
 | 
			
		||||
import { FormValidatorService } from '@app/shared/shared-forms'
 | 
			
		||||
import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
 | 
			
		||||
import { LoadingBarService } from '@ngx-loading-bar/core'
 | 
			
		||||
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
 | 
			
		||||
import { VideoPrivacy } from '@shared/models'
 | 
			
		||||
import { VideoSend } from './video-send'
 | 
			
		||||
import { HttpErrorResponse, HttpEventType, HttpHeaders } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'my-video-upload',
 | 
			
		||||
| 
						 | 
				
			
			@ -20,23 +21,18 @@ import { VideoSend } from './video-send'
 | 
			
		|||
    './video-send.scss'
 | 
			
		||||
  ]
 | 
			
		||||
})
 | 
			
		||||
export class VideoUploadComponent extends VideoSend implements OnInit, AfterViewInit, OnDestroy, CanComponentDeactivate {
 | 
			
		||||
export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, AfterViewInit, CanComponentDeactivate {
 | 
			
		||||
  @Output() firstStepDone = new EventEmitter<string>()
 | 
			
		||||
  @Output() firstStepError = new EventEmitter<void>()
 | 
			
		||||
  @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement>
 | 
			
		||||
 | 
			
		||||
  // So that it can be accessed in the template
 | 
			
		||||
  readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
 | 
			
		||||
 | 
			
		||||
  userVideoQuotaUsed = 0
 | 
			
		||||
  userVideoQuotaUsedDaily = 0
 | 
			
		||||
 | 
			
		||||
  isUploadingAudioFile = false
 | 
			
		||||
  isUploadingVideo = false
 | 
			
		||||
  isUpdatingVideo = false
 | 
			
		||||
 | 
			
		||||
  videoUploaded = false
 | 
			
		||||
  videoUploadObservable: Subscription = null
 | 
			
		||||
  videoUploadPercents = 0
 | 
			
		||||
  videoUploadedIds = {
 | 
			
		||||
    id: 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -49,7 +45,13 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView
 | 
			
		|||
  error: string
 | 
			
		||||
  enableRetryAfterError: boolean
 | 
			
		||||
 | 
			
		||||
  // So that it can be accessed in the template
 | 
			
		||||
  protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
 | 
			
		||||
  protected readonly BASE_VIDEO_UPLOAD_URL = VideoService.BASE_VIDEO_URL + 'upload-resumable'
 | 
			
		||||
 | 
			
		||||
  private uploadxOptions: UploadxOptions
 | 
			
		||||
  private isUpdatingVideo = false
 | 
			
		||||
  private fileToUpload: File
 | 
			
		||||
 | 
			
		||||
  constructor (
 | 
			
		||||
    protected formValidatorService: FormValidatorService,
 | 
			
		||||
| 
						 | 
				
			
			@ -61,15 +63,77 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView
 | 
			
		|||
    protected videoCaptionService: VideoCaptionService,
 | 
			
		||||
    private userService: UserService,
 | 
			
		||||
    private router: Router,
 | 
			
		||||
    private hooks: HooksService
 | 
			
		||||
    ) {
 | 
			
		||||
    private hooks: HooksService,
 | 
			
		||||
    private resumableUploadService: UploadxService
 | 
			
		||||
  ) {
 | 
			
		||||
    super()
 | 
			
		||||
 | 
			
		||||
    this.uploadxOptions = {
 | 
			
		||||
      endpoint: this.BASE_VIDEO_UPLOAD_URL,
 | 
			
		||||
      multiple: false,
 | 
			
		||||
      token: this.authService.getAccessToken(),
 | 
			
		||||
      uploaderClass: UploaderXFormData,
 | 
			
		||||
      retryConfig: {
 | 
			
		||||
        maxAttempts: 6,
 | 
			
		||||
        shouldRetry: (code: number) => {
 | 
			
		||||
          return code < 400 || code >= 501
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get videoExtensions () {
 | 
			
		||||
    return this.serverConfig.video.file.extensions.join(', ')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onUploadVideoOngoing (state: UploadState) {
 | 
			
		||||
    switch (state.status) {
 | 
			
		||||
      case 'error':
 | 
			
		||||
        const error = state.response?.error || 'Unknow error'
 | 
			
		||||
 | 
			
		||||
        this.handleUploadError({
 | 
			
		||||
          error: new Error(error),
 | 
			
		||||
          name: 'HttpErrorResponse',
 | 
			
		||||
          message: error,
 | 
			
		||||
          ok: false,
 | 
			
		||||
          headers: new HttpHeaders(state.responseHeaders),
 | 
			
		||||
          status: +state.responseStatus,
 | 
			
		||||
          statusText: error,
 | 
			
		||||
          type: HttpEventType.Response,
 | 
			
		||||
          url: state.url
 | 
			
		||||
        })
 | 
			
		||||
        break
 | 
			
		||||
 | 
			
		||||
      case 'cancelled':
 | 
			
		||||
        this.isUploadingVideo = false
 | 
			
		||||
        this.videoUploadPercents = 0
 | 
			
		||||
 | 
			
		||||
        this.firstStepError.emit()
 | 
			
		||||
        this.enableRetryAfterError = false
 | 
			
		||||
        this.error = ''
 | 
			
		||||
        break
 | 
			
		||||
 | 
			
		||||
      case 'queue':
 | 
			
		||||
        this.closeFirstStep(state.name)
 | 
			
		||||
        break
 | 
			
		||||
 | 
			
		||||
      case 'uploading':
 | 
			
		||||
        this.videoUploadPercents = state.progress
 | 
			
		||||
        break
 | 
			
		||||
 | 
			
		||||
      case 'paused':
 | 
			
		||||
        this.notifier.info($localize`Upload cancelled`)
 | 
			
		||||
        break
 | 
			
		||||
 | 
			
		||||
      case 'complete':
 | 
			
		||||
        this.videoUploaded = true
 | 
			
		||||
        this.videoUploadPercents = 100
 | 
			
		||||
 | 
			
		||||
        this.videoUploadedIds = state?.response.video
 | 
			
		||||
        break
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit () {
 | 
			
		||||
    super.ngOnInit()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -78,6 +142,9 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView
 | 
			
		|||
          this.userVideoQuotaUsed = data.videoQuotaUsed
 | 
			
		||||
          this.userVideoQuotaUsedDaily = data.videoQuotaUsedDaily
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    this.resumableUploadService.events
 | 
			
		||||
      .subscribe(state => this.onUploadVideoOngoing(state))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngAfterViewInit () {
 | 
			
		||||
| 
						 | 
				
			
			@ -85,7 +152,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy () {
 | 
			
		||||
    if (this.videoUploadObservable) this.videoUploadObservable.unsubscribe()
 | 
			
		||||
    this.cancelUpload()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  canDeactivate () {
 | 
			
		||||
| 
						 | 
				
			
			@ -105,137 +172,43 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getVideoFile () {
 | 
			
		||||
    return this.videofileInput.nativeElement.files[0]
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setVideoFile (files: FileList) {
 | 
			
		||||
  onFileDropped (files: FileList) {
 | 
			
		||||
    this.videofileInput.nativeElement.files = files
 | 
			
		||||
    this.fileChange()
 | 
			
		||||
 | 
			
		||||
    this.onFileChange({ target: this.videofileInput.nativeElement })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getAudioUploadLabel () {
 | 
			
		||||
    const videofile = this.getVideoFile()
 | 
			
		||||
    if (!videofile) return $localize`Upload`
 | 
			
		||||
  onFileChange (event: Event | { target: HTMLInputElement }) {
 | 
			
		||||
    const file = (event.target as HTMLInputElement).files[0]
 | 
			
		||||
 | 
			
		||||
    return $localize`Upload ${videofile.name}`
 | 
			
		||||
    if (!file) return
 | 
			
		||||
 | 
			
		||||
    if (!this.checkGlobalUserQuota(file)) return
 | 
			
		||||
    if (!this.checkDailyUserQuota(file)) return
 | 
			
		||||
 | 
			
		||||
    if (this.isAudioFile(file.name)) {
 | 
			
		||||
      this.isUploadingAudioFile = true
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.isUploadingVideo = true
 | 
			
		||||
    this.fileToUpload = file
 | 
			
		||||
 | 
			
		||||
    this.uploadFile(file)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  fileChange () {
 | 
			
		||||
    this.uploadFirstStep()
 | 
			
		||||
  uploadAudio () {
 | 
			
		||||
    this.uploadFile(this.getInputVideoFile(), this.previewfileUpload)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  retryUpload () {
 | 
			
		||||
    this.enableRetryAfterError = false
 | 
			
		||||
    this.error = ''
 | 
			
		||||
    this.uploadVideo()
 | 
			
		||||
    this.uploadFile(this.fileToUpload)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  cancelUpload () {
 | 
			
		||||
    if (this.videoUploadObservable !== null) {
 | 
			
		||||
      this.videoUploadObservable.unsubscribe()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.isUploadingVideo = false
 | 
			
		||||
    this.videoUploadPercents = 0
 | 
			
		||||
    this.videoUploadObservable = null
 | 
			
		||||
 | 
			
		||||
    this.firstStepError.emit()
 | 
			
		||||
    this.enableRetryAfterError = false
 | 
			
		||||
    this.error = ''
 | 
			
		||||
 | 
			
		||||
    this.notifier.info($localize`Upload cancelled`)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uploadFirstStep (clickedOnButton = false) {
 | 
			
		||||
    const videofile = this.getVideoFile()
 | 
			
		||||
    if (!videofile) return
 | 
			
		||||
 | 
			
		||||
    if (!this.checkGlobalUserQuota(videofile)) return
 | 
			
		||||
    if (!this.checkDailyUserQuota(videofile)) return
 | 
			
		||||
 | 
			
		||||
    if (clickedOnButton === false && this.isAudioFile(videofile.name)) {
 | 
			
		||||
      this.isUploadingAudioFile = true
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Build name field
 | 
			
		||||
    const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '')
 | 
			
		||||
    let name: string
 | 
			
		||||
 | 
			
		||||
    // If the name of the file is very small, keep the extension
 | 
			
		||||
    if (nameWithoutExtension.length < 3) name = videofile.name
 | 
			
		||||
    else name = nameWithoutExtension
 | 
			
		||||
 | 
			
		||||
    const nsfw = this.serverConfig.instance.isNSFW
 | 
			
		||||
    const waitTranscoding = true
 | 
			
		||||
    const commentsEnabled = true
 | 
			
		||||
    const downloadEnabled = true
 | 
			
		||||
    const channelId = this.firstStepChannelId.toString()
 | 
			
		||||
 | 
			
		||||
    this.formData = new FormData()
 | 
			
		||||
    this.formData.append('name', name)
 | 
			
		||||
    // Put the video "private" -> we are waiting the user validation of the second step
 | 
			
		||||
    this.formData.append('privacy', VideoPrivacy.PRIVATE.toString())
 | 
			
		||||
    this.formData.append('nsfw', '' + nsfw)
 | 
			
		||||
    this.formData.append('commentsEnabled', '' + commentsEnabled)
 | 
			
		||||
    this.formData.append('downloadEnabled', '' + downloadEnabled)
 | 
			
		||||
    this.formData.append('waitTranscoding', '' + waitTranscoding)
 | 
			
		||||
    this.formData.append('channelId', '' + channelId)
 | 
			
		||||
    this.formData.append('videofile', videofile)
 | 
			
		||||
 | 
			
		||||
    if (this.previewfileUpload) {
 | 
			
		||||
      this.formData.append('previewfile', this.previewfileUpload)
 | 
			
		||||
      this.formData.append('thumbnailfile', this.previewfileUpload)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.isUploadingVideo = true
 | 
			
		||||
    this.firstStepDone.emit(name)
 | 
			
		||||
 | 
			
		||||
    this.form.patchValue({
 | 
			
		||||
      name,
 | 
			
		||||
      privacy: this.firstStepPrivacyId,
 | 
			
		||||
      nsfw,
 | 
			
		||||
      channelId: this.firstStepChannelId,
 | 
			
		||||
      previewfile: this.previewfileUpload
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    this.uploadVideo()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uploadVideo () {
 | 
			
		||||
    this.videoUploadObservable = this.videoService.uploadVideo(this.formData).subscribe(
 | 
			
		||||
      event => {
 | 
			
		||||
        if (event.type === HttpEventType.UploadProgress) {
 | 
			
		||||
          this.videoUploadPercents = Math.round(100 * event.loaded / event.total)
 | 
			
		||||
        } else if (event instanceof HttpResponse) {
 | 
			
		||||
          this.videoUploaded = true
 | 
			
		||||
 | 
			
		||||
          this.videoUploadedIds = event.body.video
 | 
			
		||||
 | 
			
		||||
          this.videoUploadObservable = null
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      (err: HttpErrorResponse) => {
 | 
			
		||||
        // Reset progress (but keep isUploadingVideo true)
 | 
			
		||||
        this.videoUploadPercents = 0
 | 
			
		||||
        this.videoUploadObservable = null
 | 
			
		||||
        this.enableRetryAfterError = true
 | 
			
		||||
 | 
			
		||||
        this.error = uploadErrorHandler({
 | 
			
		||||
          err,
 | 
			
		||||
          name: $localize`video`,
 | 
			
		||||
          notifier: this.notifier,
 | 
			
		||||
          sticky: false
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413 ||
 | 
			
		||||
            err.status === HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) {
 | 
			
		||||
          this.cancelUpload()
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
    this.resumableUploadService.control({ action: 'cancel' })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isPublishingButtonDisabled () {
 | 
			
		||||
| 
						 | 
				
			
			@ -245,6 +218,13 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView
 | 
			
		|||
      !this.videoUploadedIds.id
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getAudioUploadLabel () {
 | 
			
		||||
    const videofile = this.getInputVideoFile()
 | 
			
		||||
    if (!videofile) return $localize`Upload`
 | 
			
		||||
 | 
			
		||||
    return $localize`Upload ${videofile.name}`
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateSecondStep () {
 | 
			
		||||
    if (this.isPublishingButtonDisabled() || !this.checkForm()) {
 | 
			
		||||
      return
 | 
			
		||||
| 
						 | 
				
			
			@ -275,6 +255,62 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView
 | 
			
		|||
        )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getInputVideoFile () {
 | 
			
		||||
    return this.videofileInput.nativeElement.files[0]
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private uploadFile (file: File, previewfile?: File) {
 | 
			
		||||
    const metadata = {
 | 
			
		||||
      waitTranscoding: true,
 | 
			
		||||
      commentsEnabled: true,
 | 
			
		||||
      downloadEnabled: true,
 | 
			
		||||
      channelId: this.firstStepChannelId,
 | 
			
		||||
      nsfw: this.serverConfig.instance.isNSFW,
 | 
			
		||||
      privacy: VideoPrivacy.PRIVATE.toString(),
 | 
			
		||||
      filename: file.name,
 | 
			
		||||
      previewfile: previewfile as any
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.resumableUploadService.handleFiles(file, {
 | 
			
		||||
      ...this.uploadxOptions,
 | 
			
		||||
      metadata
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    this.isUploadingVideo = true
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private handleUploadError (err: HttpErrorResponse) {
 | 
			
		||||
    // Reset progress (but keep isUploadingVideo true)
 | 
			
		||||
    this.videoUploadPercents = 0
 | 
			
		||||
    this.enableRetryAfterError = true
 | 
			
		||||
 | 
			
		||||
    this.error = genericUploadErrorHandler({
 | 
			
		||||
      err,
 | 
			
		||||
      name: $localize`video`,
 | 
			
		||||
      notifier: this.notifier,
 | 
			
		||||
      sticky: false
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    if (err.status === HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) {
 | 
			
		||||
      this.cancelUpload()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private closeFirstStep (filename: string) {
 | 
			
		||||
    const nameWithoutExtension = filename.replace(/\.[^/.]+$/, '')
 | 
			
		||||
    const name = nameWithoutExtension.length < 3 ? filename : nameWithoutExtension
 | 
			
		||||
 | 
			
		||||
    this.form.patchValue({
 | 
			
		||||
      name,
 | 
			
		||||
      privacy: this.firstStepPrivacyId,
 | 
			
		||||
      nsfw: this.serverConfig.instance.isNSFW,
 | 
			
		||||
      channelId: this.firstStepChannelId,
 | 
			
		||||
      previewfile: this.previewfileUpload
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    this.firstStepDone.emit(name)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private checkGlobalUserQuota (videofile: File) {
 | 
			
		||||
    const bytePipes = new BytesPipe()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -285,8 +321,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView
 | 
			
		|||
      const videoQuotaUsedBytes = bytePipes.transform(this.userVideoQuotaUsed, 0)
 | 
			
		||||
      const videoQuotaBytes = bytePipes.transform(videoQuota, 0)
 | 
			
		||||
 | 
			
		||||
      const msg = $localize`Your video quota is exceeded with this video (
 | 
			
		||||
video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuotaBytes})`
 | 
			
		||||
      const msg = $localize`Your video quota is exceeded with this video (video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuotaBytes})`
 | 
			
		||||
      this.notifier.error(msg)
 | 
			
		||||
 | 
			
		||||
      return false
 | 
			
		||||
| 
						 | 
				
			
			@ -304,9 +339,7 @@ video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuota
 | 
			
		|||
      const videoSizeBytes = bytePipes.transform(videofile.size, 0)
 | 
			
		||||
      const quotaUsedDailyBytes = bytePipes.transform(this.userVideoQuotaUsedDaily, 0)
 | 
			
		||||
      const quotaDailyBytes = bytePipes.transform(videoQuotaDaily, 0)
 | 
			
		||||
 | 
			
		||||
      const msg = $localize`Your daily video quota is exceeded with this video (
 | 
			
		||||
video size: ${videoSizeBytes}, used: ${quotaUsedDailyBytes}, quota: ${quotaDailyBytes})`
 | 
			
		||||
      const msg = $localize`Your daily video quota is exceeded with this video (video size: ${videoSizeBytes}, used: ${quotaUsedDailyBytes}, quota: ${quotaDailyBytes})`
 | 
			
		||||
      this.notifier.error(msg)
 | 
			
		||||
 | 
			
		||||
      return false
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
import { NgModule } from '@angular/core'
 | 
			
		||||
import { CanDeactivateGuard } from '@app/core'
 | 
			
		||||
import { UploadxModule } from 'ngx-uploadx'
 | 
			
		||||
import { VideoEditModule } from './shared/video-edit.module'
 | 
			
		||||
import { DragDropDirective } from './video-add-components/drag-drop.directive'
 | 
			
		||||
import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component'
 | 
			
		||||
| 
						 | 
				
			
			@ -13,7 +14,9 @@ import { VideoAddComponent } from './video-add.component'
 | 
			
		|||
  imports: [
 | 
			
		||||
    VideoAddRoutingModule,
 | 
			
		||||
 | 
			
		||||
    VideoEditModule
 | 
			
		||||
    VideoEditModule,
 | 
			
		||||
 | 
			
		||||
    UploadxModule
 | 
			
		||||
  ],
 | 
			
		||||
 | 
			
		||||
  declarations: [
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -173,8 +173,8 @@ function isXPercentInViewport (el: HTMLElement, percentVisible: number) {
 | 
			
		|||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function uploadErrorHandler (parameters: {
 | 
			
		||||
  err: HttpErrorResponse
 | 
			
		||||
function genericUploadErrorHandler (parameters: {
 | 
			
		||||
  err: Pick<HttpErrorResponse, 'message' | 'status' | 'headers'>
 | 
			
		||||
  name: string
 | 
			
		||||
  notifier: Notifier
 | 
			
		||||
  sticky?: boolean
 | 
			
		||||
| 
						 | 
				
			
			@ -186,6 +186,9 @@ function uploadErrorHandler (parameters: {
 | 
			
		|||
  if (err instanceof ErrorEvent) { // network error
 | 
			
		||||
    message = $localize`The connection was interrupted`
 | 
			
		||||
    notifier.error(message, title, null, sticky)
 | 
			
		||||
  } else if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) {
 | 
			
		||||
    message = $localize`The server encountered an error`
 | 
			
		||||
    notifier.error(message, title, null, sticky)
 | 
			
		||||
  } else if (err.status === HttpStatusCode.REQUEST_TIMEOUT_408) {
 | 
			
		||||
    message = $localize`Your ${name} file couldn't be transferred before the set timeout (usually 10min)`
 | 
			
		||||
    notifier.error(message, title, null, sticky)
 | 
			
		||||
| 
						 | 
				
			
			@ -216,5 +219,5 @@ export {
 | 
			
		|||
  isInViewport,
 | 
			
		||||
  isXPercentInViewport,
 | 
			
		||||
  listUserChannels,
 | 
			
		||||
  uploadErrorHandler
 | 
			
		||||
  genericUploadErrorHandler
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7793,6 +7793,13 @@ next-tick@~1.0.0:
 | 
			
		|||
  resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
 | 
			
		||||
  integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
 | 
			
		||||
 | 
			
		||||
ngx-uploadx@^4.1.0:
 | 
			
		||||
  version "4.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/ngx-uploadx/-/ngx-uploadx-4.1.0.tgz#b3ed4566a2505239026bbdc10c2345aae28d67df"
 | 
			
		||||
  integrity sha512-KCG0NT4SBc/5MRl8aR6joHHg+WeTdrkhLeC1DrNgVxrTBuuenlEwOVDpkLJMPX/8HE6Bq33rx1U2NNZYVl9NMQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    tslib "^1.9.0"
 | 
			
		||||
 | 
			
		||||
nice-try@^1.0.4:
 | 
			
		||||
  version "1.0.5"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -73,6 +73,7 @@
 | 
			
		|||
    "swagger-cli": "swagger-cli"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@uploadx/core": "^4.4.0",
 | 
			
		||||
    "apicache": "1.6.2",
 | 
			
		||||
    "async": "^3.0.1",
 | 
			
		||||
    "async-lru": "^1.1.1",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -116,6 +116,7 @@ import { YoutubeDlUpdateScheduler } from './server/lib/schedulers/youtube-dl-upd
 | 
			
		|||
import { VideosRedundancyScheduler } from './server/lib/schedulers/videos-redundancy-scheduler'
 | 
			
		||||
import { RemoveOldHistoryScheduler } from './server/lib/schedulers/remove-old-history-scheduler'
 | 
			
		||||
import { AutoFollowIndexInstances } from './server/lib/schedulers/auto-follow-index-instances'
 | 
			
		||||
import { RemoveDanglingResumableUploadsScheduler } from './server/lib/schedulers/remove-dangling-resumable-uploads-scheduler'
 | 
			
		||||
import { isHTTPSignatureDigestValid } from './server/helpers/peertube-crypto'
 | 
			
		||||
import { PeerTubeSocket } from './server/lib/peertube-socket'
 | 
			
		||||
import { updateStreamingPlaylistsInfohashesIfNeeded } from './server/lib/hls'
 | 
			
		||||
| 
						 | 
				
			
			@ -280,6 +281,7 @@ async function startApplication () {
 | 
			
		|||
  PluginsCheckScheduler.Instance.enable()
 | 
			
		||||
  PeerTubeVersionCheckScheduler.Instance.enable()
 | 
			
		||||
  AutoFollowIndexInstances.Instance.enable()
 | 
			
		||||
  RemoveDanglingResumableUploadsScheduler.Instance.enable()
 | 
			
		||||
 | 
			
		||||
  // Redis initialization
 | 
			
		||||
  Redis.Instance.init()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,6 @@
 | 
			
		|||
import { InboxManager } from '@server/lib/activitypub/inbox-manager'
 | 
			
		||||
import { RemoveDanglingResumableUploadsScheduler } from '@server/lib/schedulers/remove-dangling-resumable-uploads-scheduler'
 | 
			
		||||
import { SendDebugCommand } from '@shared/models'
 | 
			
		||||
import * as express from 'express'
 | 
			
		||||
import { UserRight } from '../../../../shared/models/users'
 | 
			
		||||
import { authenticate, ensureUserHasRight } from '../../../middlewares'
 | 
			
		||||
| 
						 | 
				
			
			@ -11,6 +13,12 @@ debugRouter.get('/debug',
 | 
			
		|||
  getDebug
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
debugRouter.post('/debug/run-command',
 | 
			
		||||
  authenticate,
 | 
			
		||||
  ensureUserHasRight(UserRight.MANAGE_DEBUG),
 | 
			
		||||
  runCommand
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
| 
						 | 
				
			
			@ -25,3 +33,13 @@ function getDebug (req: express.Request, res: express.Response) {
 | 
			
		|||
    activityPubMessagesWaiting: InboxManager.Instance.getActivityPubMessagesWaiting()
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function runCommand (req: express.Request, res: express.Response) {
 | 
			
		||||
  const body: SendDebugCommand = req.body
 | 
			
		||||
 | 
			
		||||
  if (body.command === 'remove-dandling-resumable-uploads') {
 | 
			
		||||
    await RemoveDanglingResumableUploadsScheduler.Instance.execute()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return res.sendStatus(204)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@ import * as express from 'express'
 | 
			
		|||
import { move } from 'fs-extra'
 | 
			
		||||
import { extname } from 'path'
 | 
			
		||||
import toInt from 'validator/lib/toInt'
 | 
			
		||||
import { deleteResumableUploadMetaFile, getResumableUploadPath } from '@server/helpers/upload'
 | 
			
		||||
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
 | 
			
		||||
import { changeVideoChannelShare } from '@server/lib/activitypub/share'
 | 
			
		||||
import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
 | 
			
		||||
| 
						 | 
				
			
			@ -10,8 +11,9 @@ import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnail
 | 
			
		|||
import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
 | 
			
		||||
import { getServerActor } from '@server/models/application/application'
 | 
			
		||||
import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
 | 
			
		||||
import { uploadx } from '@uploadx/core'
 | 
			
		||||
import { VideoCreate, VideosCommonQuery, VideoState, VideoUpdate } from '../../../../shared'
 | 
			
		||||
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 | 
			
		||||
import { HttpStatusCode } from '../../../../shared/core-utils/miscs'
 | 
			
		||||
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
 | 
			
		||||
import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils'
 | 
			
		||||
import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils'
 | 
			
		||||
| 
						 | 
				
			
			@ -47,7 +49,9 @@ import {
 | 
			
		|||
  setDefaultPagination,
 | 
			
		||||
  setDefaultVideosSort,
 | 
			
		||||
  videoFileMetadataGetValidator,
 | 
			
		||||
  videosAddValidator,
 | 
			
		||||
  videosAddLegacyValidator,
 | 
			
		||||
  videosAddResumableInitValidator,
 | 
			
		||||
  videosAddResumableValidator,
 | 
			
		||||
  videosCustomGetValidator,
 | 
			
		||||
  videosGetValidator,
 | 
			
		||||
  videosRemoveValidator,
 | 
			
		||||
| 
						 | 
				
			
			@ -69,6 +73,7 @@ import { watchingRouter } from './watching'
 | 
			
		|||
const lTags = loggerTagsFactory('api', 'video')
 | 
			
		||||
const auditLogger = auditLoggerFactory('videos')
 | 
			
		||||
const videosRouter = express.Router()
 | 
			
		||||
const uploadxMiddleware = uploadx.upload({ directory: getResumableUploadPath() })
 | 
			
		||||
 | 
			
		||||
const reqVideoFileAdd = createReqFiles(
 | 
			
		||||
  [ 'videofile', 'thumbnailfile', 'previewfile' ],
 | 
			
		||||
| 
						 | 
				
			
			@ -79,6 +84,16 @@ const reqVideoFileAdd = createReqFiles(
 | 
			
		|||
    previewfile: CONFIG.STORAGE.TMP_DIR
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const reqVideoFileAddResumable = createReqFiles(
 | 
			
		||||
  [ 'thumbnailfile', 'previewfile' ],
 | 
			
		||||
  MIMETYPES.IMAGE.MIMETYPE_EXT,
 | 
			
		||||
  {
 | 
			
		||||
    thumbnailfile: getResumableUploadPath(),
 | 
			
		||||
    previewfile: getResumableUploadPath()
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const reqVideoFileUpdate = createReqFiles(
 | 
			
		||||
  [ 'thumbnailfile', 'previewfile' ],
 | 
			
		||||
  MIMETYPES.IMAGE.MIMETYPE_EXT,
 | 
			
		||||
| 
						 | 
				
			
			@ -111,18 +126,39 @@ videosRouter.get('/',
 | 
			
		|||
  commonVideosFiltersValidator,
 | 
			
		||||
  asyncMiddleware(listVideos)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
videosRouter.post('/upload',
 | 
			
		||||
  authenticate,
 | 
			
		||||
  reqVideoFileAdd,
 | 
			
		||||
  asyncMiddleware(videosAddLegacyValidator),
 | 
			
		||||
  asyncRetryTransactionMiddleware(addVideoLegacy)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
videosRouter.post('/upload-resumable',
 | 
			
		||||
  authenticate,
 | 
			
		||||
  reqVideoFileAddResumable,
 | 
			
		||||
  asyncMiddleware(videosAddResumableInitValidator),
 | 
			
		||||
  uploadxMiddleware
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
videosRouter.delete('/upload-resumable',
 | 
			
		||||
  authenticate,
 | 
			
		||||
  uploadxMiddleware
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
videosRouter.put('/upload-resumable',
 | 
			
		||||
  authenticate,
 | 
			
		||||
  uploadxMiddleware, // uploadx doesn't use call next() before the file upload completes
 | 
			
		||||
  asyncMiddleware(videosAddResumableValidator),
 | 
			
		||||
  asyncMiddleware(addVideoResumable)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
videosRouter.put('/:id',
 | 
			
		||||
  authenticate,
 | 
			
		||||
  reqVideoFileUpdate,
 | 
			
		||||
  asyncMiddleware(videosUpdateValidator),
 | 
			
		||||
  asyncRetryTransactionMiddleware(updateVideo)
 | 
			
		||||
)
 | 
			
		||||
videosRouter.post('/upload',
 | 
			
		||||
  authenticate,
 | 
			
		||||
  reqVideoFileAdd,
 | 
			
		||||
  asyncMiddleware(videosAddValidator),
 | 
			
		||||
  asyncRetryTransactionMiddleware(addVideo)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
videosRouter.get('/:id/description',
 | 
			
		||||
  asyncMiddleware(videosGetValidator),
 | 
			
		||||
| 
						 | 
				
			
			@ -157,23 +193,23 @@ export {
 | 
			
		|||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
function listVideoCategories (req: express.Request, res: express.Response) {
 | 
			
		||||
function listVideoCategories (_req: express.Request, res: express.Response) {
 | 
			
		||||
  res.json(VIDEO_CATEGORIES)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function listVideoLicences (req: express.Request, res: express.Response) {
 | 
			
		||||
function listVideoLicences (_req: express.Request, res: express.Response) {
 | 
			
		||||
  res.json(VIDEO_LICENCES)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function listVideoLanguages (req: express.Request, res: express.Response) {
 | 
			
		||||
function listVideoLanguages (_req: express.Request, res: express.Response) {
 | 
			
		||||
  res.json(VIDEO_LANGUAGES)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function listVideoPrivacies (req: express.Request, res: express.Response) {
 | 
			
		||||
function listVideoPrivacies (_req: express.Request, res: express.Response) {
 | 
			
		||||
  res.json(VIDEO_PRIVACIES)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function addVideo (req: express.Request, res: express.Response) {
 | 
			
		||||
async function addVideoLegacy (req: express.Request, res: express.Response) {
 | 
			
		||||
  // Uploading the video could be long
 | 
			
		||||
  // Set timeout to 10 minutes, as Express's default is 2 minutes
 | 
			
		||||
  req.setTimeout(1000 * 60 * 10, () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -183,13 +219,42 @@ async function addVideo (req: express.Request, res: express.Response) {
 | 
			
		|||
 | 
			
		||||
  const videoPhysicalFile = req.files['videofile'][0]
 | 
			
		||||
  const videoInfo: VideoCreate = req.body
 | 
			
		||||
  const files = req.files
 | 
			
		||||
 | 
			
		||||
  const videoData = buildLocalVideoFromReq(videoInfo, res.locals.videoChannel.id)
 | 
			
		||||
  videoData.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED
 | 
			
		||||
  videoData.duration = videoPhysicalFile['duration'] // duration was added by a previous middleware
 | 
			
		||||
  return addVideo({ res, videoPhysicalFile, videoInfo, files })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function addVideoResumable (_req: express.Request, res: express.Response) {
 | 
			
		||||
  const videoPhysicalFile = res.locals.videoFileResumable
 | 
			
		||||
  const videoInfo = videoPhysicalFile.metadata
 | 
			
		||||
  const files = { previewfile: videoInfo.previewfile }
 | 
			
		||||
 | 
			
		||||
  // Don't need the meta file anymore
 | 
			
		||||
  await deleteResumableUploadMetaFile(videoPhysicalFile.path)
 | 
			
		||||
 | 
			
		||||
  return addVideo({ res, videoPhysicalFile, videoInfo, files })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function addVideo (options: {
 | 
			
		||||
  res: express.Response
 | 
			
		||||
  videoPhysicalFile: express.VideoUploadFile
 | 
			
		||||
  videoInfo: VideoCreate
 | 
			
		||||
  files: express.UploadFiles
 | 
			
		||||
}) {
 | 
			
		||||
  const { res, videoPhysicalFile, videoInfo, files } = options
 | 
			
		||||
  const videoChannel = res.locals.videoChannel
 | 
			
		||||
  const user = res.locals.oauth.token.User
 | 
			
		||||
 | 
			
		||||
  const videoData = buildLocalVideoFromReq(videoInfo, videoChannel.id)
 | 
			
		||||
 | 
			
		||||
  videoData.state = CONFIG.TRANSCODING.ENABLED
 | 
			
		||||
    ? VideoState.TO_TRANSCODE
 | 
			
		||||
    : VideoState.PUBLISHED
 | 
			
		||||
 | 
			
		||||
  videoData.duration = videoPhysicalFile.duration // duration was added by a previous middleware
 | 
			
		||||
 | 
			
		||||
  const video = new VideoModel(videoData) as MVideoFullLight
 | 
			
		||||
  video.VideoChannel = res.locals.videoChannel
 | 
			
		||||
  video.VideoChannel = videoChannel
 | 
			
		||||
  video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
 | 
			
		||||
 | 
			
		||||
  const videoFile = new VideoFileModel({
 | 
			
		||||
| 
						 | 
				
			
			@ -217,7 +282,7 @@ async function addVideo (req: express.Request, res: express.Response) {
 | 
			
		|||
 | 
			
		||||
  const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({
 | 
			
		||||
    video,
 | 
			
		||||
    files: req.files,
 | 
			
		||||
    files,
 | 
			
		||||
    fallback: type => generateVideoMiniature({ video, videoFile, type })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -253,7 +318,7 @@ async function addVideo (req: express.Request, res: express.Response) {
 | 
			
		|||
 | 
			
		||||
    await autoBlacklistVideoIfNeeded({
 | 
			
		||||
      video,
 | 
			
		||||
      user: res.locals.oauth.token.User,
 | 
			
		||||
      user,
 | 
			
		||||
      isRemote: false,
 | 
			
		||||
      isNew: true,
 | 
			
		||||
      transaction: t
 | 
			
		||||
| 
						 | 
				
			
			@ -282,7 +347,7 @@ async function addVideo (req: express.Request, res: express.Response) {
 | 
			
		|||
    .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) }))
 | 
			
		||||
 | 
			
		||||
  if (video.state === VideoState.TO_TRANSCODE) {
 | 
			
		||||
    await addOptimizeOrMergeAudioJob(videoCreated, videoFile, res.locals.oauth.token.User)
 | 
			
		||||
    await addOptimizeOrMergeAudioJob(videoCreated, videoFile, user)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Hooks.runAction('action:api.video.uploaded', { video: videoCreated })
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
import 'multer'
 | 
			
		||||
import validator from 'validator'
 | 
			
		||||
import { UploadFilesForCheck } from 'express'
 | 
			
		||||
import { sep } from 'path'
 | 
			
		||||
import validator from 'validator'
 | 
			
		||||
 | 
			
		||||
function exists (value: any) {
 | 
			
		||||
  return value !== undefined && value !== null
 | 
			
		||||
| 
						 | 
				
			
			@ -108,7 +109,7 @@ function isFileFieldValid (
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
function isFileMimeTypeValid (
 | 
			
		||||
  files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[],
 | 
			
		||||
  files: UploadFilesForCheck,
 | 
			
		||||
  mimeTypeRegex: string,
 | 
			
		||||
  field: string,
 | 
			
		||||
  optional = false
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,6 @@
 | 
			
		|||
import { UploadFilesForCheck } from 'express'
 | 
			
		||||
import { values } from 'lodash'
 | 
			
		||||
import * as magnetUtil from 'magnet-uri'
 | 
			
		||||
import validator from 'validator'
 | 
			
		||||
import { VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared'
 | 
			
		||||
import {
 | 
			
		||||
| 
						 | 
				
			
			@ -6,13 +8,12 @@ import {
 | 
			
		|||
  MIMETYPES,
 | 
			
		||||
  VIDEO_CATEGORIES,
 | 
			
		||||
  VIDEO_LICENCES,
 | 
			
		||||
  VIDEO_LIVE,
 | 
			
		||||
  VIDEO_PRIVACIES,
 | 
			
		||||
  VIDEO_RATE_TYPES,
 | 
			
		||||
  VIDEO_STATES,
 | 
			
		||||
  VIDEO_LIVE
 | 
			
		||||
  VIDEO_STATES
 | 
			
		||||
} from '../../initializers/constants'
 | 
			
		||||
import { exists, isArray, isDateValid, isFileMimeTypeValid, isFileValid } from './misc'
 | 
			
		||||
import * as magnetUtil from 'magnet-uri'
 | 
			
		||||
 | 
			
		||||
const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -81,7 +82,7 @@ function isVideoFileExtnameValid (value: string) {
 | 
			
		|||
  return exists(value) && (value === VIDEO_LIVE.EXTENSION || MIMETYPES.VIDEO.EXT_MIMETYPE[value] !== undefined)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isVideoFileMimeTypeValid (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
 | 
			
		||||
function isVideoFileMimeTypeValid (files: UploadFilesForCheck) {
 | 
			
		||||
  return isFileMimeTypeValid(files, MIMETYPES.VIDEO.MIMETYPES_REGEX, 'videofile')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@ import * as express from 'express'
 | 
			
		|||
import * as multer from 'multer'
 | 
			
		||||
import { REMOTE_SCHEME } from '../initializers/constants'
 | 
			
		||||
import { logger } from './logger'
 | 
			
		||||
import { deleteFileAsync, generateRandomString } from './utils'
 | 
			
		||||
import { deleteFileAndCatch, generateRandomString } from './utils'
 | 
			
		||||
import { extname } from 'path'
 | 
			
		||||
import { isArray } from './custom-validators/misc'
 | 
			
		||||
import { CONFIG } from '../initializers/config'
 | 
			
		||||
| 
						 | 
				
			
			@ -36,15 +36,15 @@ function cleanUpReqFiles (req: { files: { [fieldname: string]: Express.Multer.Fi
 | 
			
		|||
  if (!files) return
 | 
			
		||||
 | 
			
		||||
  if (isArray(files)) {
 | 
			
		||||
    (files as Express.Multer.File[]).forEach(f => deleteFileAsync(f.path))
 | 
			
		||||
    (files as Express.Multer.File[]).forEach(f => deleteFileAndCatch(f.path))
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (const key of Object.keys(files)) {
 | 
			
		||||
    const file = files[key]
 | 
			
		||||
 | 
			
		||||
    if (isArray(file)) file.forEach(f => deleteFileAsync(f.path))
 | 
			
		||||
    else deleteFileAsync(file.path)
 | 
			
		||||
    if (isArray(file)) file.forEach(f => deleteFileAndCatch(f.path))
 | 
			
		||||
    else deleteFileAndCatch(file.path)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										21
									
								
								server/helpers/upload.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								server/helpers/upload.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
import { METAFILE_EXTNAME } from '@uploadx/core'
 | 
			
		||||
import { remove } from 'fs-extra'
 | 
			
		||||
import { join } from 'path'
 | 
			
		||||
import { RESUMABLE_UPLOAD_DIRECTORY } from '../initializers/constants'
 | 
			
		||||
 | 
			
		||||
function getResumableUploadPath (filename?: string) {
 | 
			
		||||
  if (filename) return join(RESUMABLE_UPLOAD_DIRECTORY, filename)
 | 
			
		||||
 | 
			
		||||
  return RESUMABLE_UPLOAD_DIRECTORY
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function deleteResumableUploadMetaFile (filepath: string) {
 | 
			
		||||
  return remove(filepath + METAFILE_EXTNAME)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  getResumableUploadPath,
 | 
			
		||||
  deleteResumableUploadMetaFile
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -6,7 +6,7 @@ import { CONFIG } from '../initializers/config'
 | 
			
		|||
import { execPromise, execPromise2, randomBytesPromise, sha256 } from './core-utils'
 | 
			
		||||
import { logger } from './logger'
 | 
			
		||||
 | 
			
		||||
function deleteFileAsync (path: string) {
 | 
			
		||||
function deleteFileAndCatch (path: string) {
 | 
			
		||||
  remove(path)
 | 
			
		||||
    .catch(err => logger.error('Cannot delete the file %s asynchronously.', path, { err }))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -83,7 +83,7 @@ function getUUIDFromFilename (filename: string) {
 | 
			
		|||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  deleteFileAsync,
 | 
			
		||||
  deleteFileAndCatch,
 | 
			
		||||
  generateRandomString,
 | 
			
		||||
  getFormattedObjects,
 | 
			
		||||
  getSecureTorrentName,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -208,7 +208,8 @@ const SCHEDULER_INTERVALS_MS = {
 | 
			
		|||
  autoFollowIndexInstances: 60000 * 60 * 24, // 1 day
 | 
			
		||||
  removeOldViews: 60000 * 60 * 24, // 1 day
 | 
			
		||||
  removeOldHistory: 60000 * 60 * 24, // 1 day
 | 
			
		||||
  updateInboxStats: 1000 * 60// 1 minute
 | 
			
		||||
  updateInboxStats: 1000 * 60, // 1 minute
 | 
			
		||||
  removeDanglingResumableUploads: 60000 * 60 * 16 // 16 hours
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
| 
						 | 
				
			
			@ -285,6 +286,7 @@ const CONSTRAINTS_FIELDS = {
 | 
			
		|||
    LIKES: { min: 0 },
 | 
			
		||||
    DISLIKES: { min: 0 },
 | 
			
		||||
    FILE_SIZE: { min: -1 },
 | 
			
		||||
    PARTIAL_UPLOAD_SIZE: { max: 50 * 1024 * 1024 * 1024 }, // 50GB
 | 
			
		||||
    URL: { min: 3, max: 2000 } // Length
 | 
			
		||||
  },
 | 
			
		||||
  VIDEO_PLAYLISTS: {
 | 
			
		||||
| 
						 | 
				
			
			@ -645,6 +647,7 @@ const LRU_CACHE = {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const RESUMABLE_UPLOAD_DIRECTORY = join(CONFIG.STORAGE.TMP_DIR, 'resumable-uploads')
 | 
			
		||||
const HLS_STREAMING_PLAYLIST_DIRECTORY = join(CONFIG.STORAGE.STREAMING_PLAYLISTS_DIR, 'hls')
 | 
			
		||||
const HLS_REDUNDANCY_DIRECTORY = join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls')
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -819,6 +822,7 @@ export {
 | 
			
		|||
  PEERTUBE_VERSION,
 | 
			
		||||
  LAZY_STATIC_PATHS,
 | 
			
		||||
  SEARCH_INDEX,
 | 
			
		||||
  RESUMABLE_UPLOAD_DIRECTORY,
 | 
			
		||||
  HLS_REDUNDANCY_DIRECTORY,
 | 
			
		||||
  P2P_MEDIA_LOADER_PEER_VERSION,
 | 
			
		||||
  ACTOR_IMAGES_SIZE,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@ import { UserModel } from '../models/account/user'
 | 
			
		|||
import { ApplicationModel } from '../models/application/application'
 | 
			
		||||
import { OAuthClientModel } from '../models/oauth/oauth-client'
 | 
			
		||||
import { applicationExist, clientsExist, usersExist } from './checker-after-init'
 | 
			
		||||
import { FILES_CACHE, HLS_STREAMING_PLAYLIST_DIRECTORY, LAST_MIGRATION_VERSION } from './constants'
 | 
			
		||||
import { FILES_CACHE, HLS_STREAMING_PLAYLIST_DIRECTORY, LAST_MIGRATION_VERSION, RESUMABLE_UPLOAD_DIRECTORY } from './constants'
 | 
			
		||||
import { sequelizeTypescript } from './database'
 | 
			
		||||
import { ensureDir, remove } from 'fs-extra'
 | 
			
		||||
import { CONFIG } from './config'
 | 
			
		||||
| 
						 | 
				
			
			@ -79,6 +79,9 @@ function createDirectoriesIfNotExist () {
 | 
			
		|||
  // Playlist directories
 | 
			
		||||
  tasks.push(ensureDir(HLS_STREAMING_PLAYLIST_DIRECTORY))
 | 
			
		||||
 | 
			
		||||
  // Resumable upload directory
 | 
			
		||||
  tasks.push(ensureDir(RESUMABLE_UPLOAD_DIRECTORY))
 | 
			
		||||
 | 
			
		||||
  return Promise.all(tasks)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,8 @@
 | 
			
		|||
import { VideoUploadFile } from 'express'
 | 
			
		||||
import { PathLike } from 'fs-extra'
 | 
			
		||||
import { Transaction } from 'sequelize/types'
 | 
			
		||||
import { AbuseAuditView, auditLoggerFactory } from '@server/helpers/audit-logger'
 | 
			
		||||
import { afterCommitIfTransaction } from '@server/helpers/database-utils'
 | 
			
		||||
import { logger } from '@server/helpers/logger'
 | 
			
		||||
import { AbuseModel } from '@server/models/abuse/abuse'
 | 
			
		||||
import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
 | 
			
		||||
| 
						 | 
				
			
			@ -28,7 +30,6 @@ import { VideoModel } from '../models/video/video'
 | 
			
		|||
import { VideoCommentModel } from '../models/video/video-comment'
 | 
			
		||||
import { sendAbuse } from './activitypub/send/send-flag'
 | 
			
		||||
import { Notifier } from './notifier'
 | 
			
		||||
import { afterCommitIfTransaction } from '@server/helpers/database-utils'
 | 
			
		||||
 | 
			
		||||
export type AcceptResult = {
 | 
			
		||||
  accepted: boolean
 | 
			
		||||
| 
						 | 
				
			
			@ -38,7 +39,7 @@ export type AcceptResult = {
 | 
			
		|||
// Can be filtered by plugins
 | 
			
		||||
function isLocalVideoAccepted (object: {
 | 
			
		||||
  videoBody: VideoCreate
 | 
			
		||||
  videoFile: Express.Multer.File & { duration?: number }
 | 
			
		||||
  videoFile: VideoUploadFile
 | 
			
		||||
  user: UserModel
 | 
			
		||||
}): AcceptResult {
 | 
			
		||||
  return { accepted: true }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,61 @@
 | 
			
		|||
import * as bluebird from 'bluebird'
 | 
			
		||||
import { readdir, remove, stat } from 'fs-extra'
 | 
			
		||||
import { logger, loggerTagsFactory } from '@server/helpers/logger'
 | 
			
		||||
import { getResumableUploadPath } from '@server/helpers/upload'
 | 
			
		||||
import { SCHEDULER_INTERVALS_MS } from '@server/initializers/constants'
 | 
			
		||||
import { METAFILE_EXTNAME } from '@uploadx/core'
 | 
			
		||||
import { AbstractScheduler } from './abstract-scheduler'
 | 
			
		||||
 | 
			
		||||
const lTags = loggerTagsFactory('scheduler', 'resumable-upload', 'cleaner')
 | 
			
		||||
 | 
			
		||||
export class RemoveDanglingResumableUploadsScheduler extends AbstractScheduler {
 | 
			
		||||
 | 
			
		||||
  private static instance: AbstractScheduler
 | 
			
		||||
  private lastExecutionTimeMs: number
 | 
			
		||||
 | 
			
		||||
  protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.removeDanglingResumableUploads
 | 
			
		||||
 | 
			
		||||
  private constructor () {
 | 
			
		||||
    super()
 | 
			
		||||
 | 
			
		||||
    this.lastExecutionTimeMs = new Date().getTime()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected async internalExecute () {
 | 
			
		||||
    const path = getResumableUploadPath()
 | 
			
		||||
    const files = await readdir(path)
 | 
			
		||||
 | 
			
		||||
    const metafiles = files.filter(f => f.endsWith(METAFILE_EXTNAME))
 | 
			
		||||
 | 
			
		||||
    if (metafiles.length === 0) return
 | 
			
		||||
 | 
			
		||||
    logger.debug('Reading resumable video upload folder %s with %d files', path, metafiles.length, lTags())
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      await bluebird.map(metafiles, metafile => {
 | 
			
		||||
        return this.deleteIfOlderThan(metafile, this.lastExecutionTimeMs)
 | 
			
		||||
      }, { concurrency: 5 })
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      logger.error('Failed to handle file during resumable video upload folder cleanup', { error, ...lTags() })
 | 
			
		||||
    } finally {
 | 
			
		||||
      this.lastExecutionTimeMs = new Date().getTime()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async deleteIfOlderThan (metafile: string, olderThan: number) {
 | 
			
		||||
    const metafilePath = getResumableUploadPath(metafile)
 | 
			
		||||
    const statResult = await stat(metafilePath)
 | 
			
		||||
 | 
			
		||||
    // Delete uploads that started since a long time
 | 
			
		||||
    if (statResult.ctimeMs < olderThan) {
 | 
			
		||||
      await remove(metafilePath)
 | 
			
		||||
 | 
			
		||||
      const datafile = metafilePath.replace(new RegExp(`${METAFILE_EXTNAME}$`), '')
 | 
			
		||||
      await remove(datafile)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static get Instance () {
 | 
			
		||||
    return this.instance || (this.instance = new this())
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +1,4 @@
 | 
			
		|||
import { UploadFiles } from 'express'
 | 
			
		||||
import { Transaction } from 'sequelize/types'
 | 
			
		||||
import { DEFAULT_AUDIO_RESOLUTION, JOB_PRIORITY } from '@server/initializers/constants'
 | 
			
		||||
import { sequelizeTypescript } from '@server/initializers/database'
 | 
			
		||||
| 
						 | 
				
			
			@ -32,7 +33,7 @@ function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): Fil
 | 
			
		|||
 | 
			
		||||
async function buildVideoThumbnailsFromReq (options: {
 | 
			
		||||
  video: MVideoThumbnail
 | 
			
		||||
  files: { [fieldname: string]: Express.Multer.File[] } | Express.Multer.File[]
 | 
			
		||||
  files: UploadFiles
 | 
			
		||||
  fallback: (type: ThumbnailType) => Promise<MThumbnail>
 | 
			
		||||
  automaticallyGenerated?: boolean
 | 
			
		||||
}) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@ import { NextFunction, Request, RequestHandler, Response } from 'express'
 | 
			
		|||
import { ValidationChain } from 'express-validator'
 | 
			
		||||
import { ExpressPromiseHandler } from '@server/types/express'
 | 
			
		||||
import { retryTransactionWrapper } from '../helpers/database-utils'
 | 
			
		||||
import { HttpMethod, HttpStatusCode } from '@shared/core-utils'
 | 
			
		||||
 | 
			
		||||
// Syntactic sugar to avoid try/catch in express controllers
 | 
			
		||||
// Thanks: https://medium.com/@Abazhenov/using-async-await-in-express-with-node-8-b8af872c0016
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,10 @@
 | 
			
		|||
import * as express from 'express'
 | 
			
		||||
import { body, param, query, ValidationChain } from 'express-validator'
 | 
			
		||||
import { body, header, param, query, ValidationChain } from 'express-validator'
 | 
			
		||||
import { getResumableUploadPath } from '@server/helpers/upload'
 | 
			
		||||
import { isAbleToUploadVideo } from '@server/lib/user'
 | 
			
		||||
import { getServerActor } from '@server/models/application/application'
 | 
			
		||||
import { ExpressPromiseHandler } from '@server/types/express'
 | 
			
		||||
import { MVideoWithRights } from '@server/types/models'
 | 
			
		||||
import { MUserAccountId, MVideoWithRights } from '@server/types/models'
 | 
			
		||||
import { ServerErrorCode, UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
 | 
			
		||||
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 | 
			
		||||
import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
 | 
			
		||||
| 
						 | 
				
			
			@ -47,6 +48,7 @@ import {
 | 
			
		|||
  doesVideoExist,
 | 
			
		||||
  doesVideoFileOfVideoExist
 | 
			
		||||
} from '../../../helpers/middlewares'
 | 
			
		||||
import { deleteFileAndCatch } from '../../../helpers/utils'
 | 
			
		||||
import { getVideoWithAttributes } from '../../../helpers/video'
 | 
			
		||||
import { CONFIG } from '../../../initializers/config'
 | 
			
		||||
import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants'
 | 
			
		||||
| 
						 | 
				
			
			@ -57,7 +59,7 @@ import { VideoModel } from '../../../models/video/video'
 | 
			
		|||
import { authenticatePromiseIfNeeded } from '../../auth'
 | 
			
		||||
import { areValidationErrors } from '../utils'
 | 
			
		||||
 | 
			
		||||
const videosAddValidator = getCommonVideoEditAttributes().concat([
 | 
			
		||||
const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
 | 
			
		||||
  body('videofile')
 | 
			
		||||
    .custom((value, { req }) => isFileFieldValid(req.files, 'videofile'))
 | 
			
		||||
    .withMessage('Should have a file'),
 | 
			
		||||
| 
						 | 
				
			
			@ -73,59 +75,122 @@ const videosAddValidator = getCommonVideoEditAttributes().concat([
 | 
			
		|||
    logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
 | 
			
		||||
 | 
			
		||||
    if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
 | 
			
		||||
    if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
 | 
			
		||||
 | 
			
		||||
    const videoFile: Express.Multer.File & { duration?: number } = req.files['videofile'][0]
 | 
			
		||||
    const videoFile: express.VideoUploadFile = req.files['videofile'][0]
 | 
			
		||||
    const user = res.locals.oauth.token.User
 | 
			
		||||
 | 
			
		||||
    if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
 | 
			
		||||
 | 
			
		||||
    if (!isVideoFileMimeTypeValid(req.files)) {
 | 
			
		||||
      res.status(HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415)
 | 
			
		||||
         .json({
 | 
			
		||||
           error: 'This file is not supported. Please, make sure it is of the following type: ' +
 | 
			
		||||
                  CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
 | 
			
		||||
         })
 | 
			
		||||
 | 
			
		||||
    if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFile.size, files: req.files })) {
 | 
			
		||||
      return cleanUpReqFiles(req)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!isVideoFileSizeValid(videoFile.size.toString())) {
 | 
			
		||||
      res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413)
 | 
			
		||||
         .json({
 | 
			
		||||
           error: 'This file is too large.'
 | 
			
		||||
         })
 | 
			
		||||
 | 
			
		||||
      return cleanUpReqFiles(req)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (await isAbleToUploadVideo(user.id, videoFile.size) === false) {
 | 
			
		||||
      res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413)
 | 
			
		||||
         .json({ error: 'The user video quota is exceeded with this video.' })
 | 
			
		||||
 | 
			
		||||
      return cleanUpReqFiles(req)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let duration: number
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      duration = await getDurationFromVideoFile(videoFile.path)
 | 
			
		||||
      if (!videoFile.duration) await addDurationToVideo(videoFile)
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      logger.error('Invalid input file in videosAddValidator.', { err })
 | 
			
		||||
      logger.error('Invalid input file in videosAddLegacyValidator.', { err })
 | 
			
		||||
      res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422)
 | 
			
		||||
         .json({ error: 'Video file unreadable.' })
 | 
			
		||||
 | 
			
		||||
      return cleanUpReqFiles(req)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    videoFile.duration = duration
 | 
			
		||||
 | 
			
		||||
    if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req)
 | 
			
		||||
 | 
			
		||||
    return next()
 | 
			
		||||
  }
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Gets called after the last PUT request
 | 
			
		||||
 */
 | 
			
		||||
const videosAddResumableValidator = [
 | 
			
		||||
  async (req: express.Request, res: express.Response, next: express.NextFunction) => {
 | 
			
		||||
    const user = res.locals.oauth.token.User
 | 
			
		||||
 | 
			
		||||
    const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body
 | 
			
		||||
    const file = { ...body, duration: undefined, path: getResumableUploadPath(body.id), filename: body.metadata.filename }
 | 
			
		||||
 | 
			
		||||
    const cleanup = () => deleteFileAndCatch(file.path)
 | 
			
		||||
 | 
			
		||||
    if (!await doesVideoChannelOfAccountExist(file.metadata.channelId, user, res)) return cleanup()
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      if (!file.duration) await addDurationToVideo(file)
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      logger.error('Invalid input file in videosAddResumableValidator.', { err })
 | 
			
		||||
      res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422)
 | 
			
		||||
         .json({ error: 'Video file unreadable.' })
 | 
			
		||||
 | 
			
		||||
      return cleanup()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!await isVideoAccepted(req, res, file)) return cleanup()
 | 
			
		||||
 | 
			
		||||
    res.locals.videoFileResumable = file
 | 
			
		||||
 | 
			
		||||
    return next()
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * File is created in POST initialisation, and its body is saved as a 'metadata' field is saved by uploadx for later use.
 | 
			
		||||
 * see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/uploadx.ts
 | 
			
		||||
 *
 | 
			
		||||
 * Uploadx doesn't use next() until the upload completes, so this middleware has to be placed before uploadx
 | 
			
		||||
 * see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/base-handler.ts
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([
 | 
			
		||||
  body('filename')
 | 
			
		||||
    .isString()
 | 
			
		||||
    .exists()
 | 
			
		||||
    .withMessage('Should have a valid filename'),
 | 
			
		||||
  body('name')
 | 
			
		||||
    .trim()
 | 
			
		||||
    .custom(isVideoNameValid)
 | 
			
		||||
    .withMessage('Should have a valid name'),
 | 
			
		||||
  body('channelId')
 | 
			
		||||
    .customSanitizer(toIntOrNull)
 | 
			
		||||
    .custom(isIdValid).withMessage('Should have correct video channel id'),
 | 
			
		||||
 | 
			
		||||
  header('x-upload-content-length')
 | 
			
		||||
    .isNumeric()
 | 
			
		||||
    .exists()
 | 
			
		||||
    .withMessage('Should specify the file length'),
 | 
			
		||||
  header('x-upload-content-type')
 | 
			
		||||
    .isString()
 | 
			
		||||
    .exists()
 | 
			
		||||
    .withMessage('Should specify the file mimetype'),
 | 
			
		||||
 | 
			
		||||
  async (req: express.Request, res: express.Response, next: express.NextFunction) => {
 | 
			
		||||
    const videoFileMetadata = {
 | 
			
		||||
      mimetype: req.headers['x-upload-content-type'] as string,
 | 
			
		||||
      size: +req.headers['x-upload-content-length'],
 | 
			
		||||
      originalname: req.body.name
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const user = res.locals.oauth.token.User
 | 
			
		||||
    const cleanup = () => cleanUpReqFiles(req)
 | 
			
		||||
 | 
			
		||||
    logger.debug('Checking videosAddResumableInitValidator parameters and headers', {
 | 
			
		||||
      parameters: req.body,
 | 
			
		||||
      headers: req.headers,
 | 
			
		||||
      files: req.files
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    if (areValidationErrors(req, res)) return cleanup()
 | 
			
		||||
 | 
			
		||||
    const files = { videofile: [ videoFileMetadata ] }
 | 
			
		||||
    if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFileMetadata.size, files })) return cleanup()
 | 
			
		||||
 | 
			
		||||
    // multer required unsetting the Content-Type, now we can set it for node-uploadx
 | 
			
		||||
    req.headers['content-type'] = 'application/json; charset=utf-8'
 | 
			
		||||
    // place previewfile in metadata so that uploadx saves it in .META
 | 
			
		||||
    if (req.files['previewfile']) req.body.previewfile = req.files['previewfile']
 | 
			
		||||
 | 
			
		||||
    return next()
 | 
			
		||||
  }
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
const videosUpdateValidator = getCommonVideoEditAttributes().concat([
 | 
			
		||||
  param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
 | 
			
		||||
  body('name')
 | 
			
		||||
| 
						 | 
				
			
			@ -478,7 +543,10 @@ const commonVideosFiltersValidator = [
 | 
			
		|||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  videosAddValidator,
 | 
			
		||||
  videosAddLegacyValidator,
 | 
			
		||||
  videosAddResumableValidator,
 | 
			
		||||
  videosAddResumableInitValidator,
 | 
			
		||||
 | 
			
		||||
  videosUpdateValidator,
 | 
			
		||||
  videosGetValidator,
 | 
			
		||||
  videoFileMetadataGetValidator,
 | 
			
		||||
| 
						 | 
				
			
			@ -515,7 +583,51 @@ function areErrorsInScheduleUpdate (req: express.Request, res: express.Response)
 | 
			
		|||
  return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function isVideoAccepted (req: express.Request, res: express.Response, videoFile: Express.Multer.File & { duration?: number }) {
 | 
			
		||||
async function commonVideoChecksPass (parameters: {
 | 
			
		||||
  req: express.Request
 | 
			
		||||
  res: express.Response
 | 
			
		||||
  user: MUserAccountId
 | 
			
		||||
  videoFileSize: number
 | 
			
		||||
  files: express.UploadFilesForCheck
 | 
			
		||||
}): Promise<boolean> {
 | 
			
		||||
  const { req, res, user, videoFileSize, files } = parameters
 | 
			
		||||
 | 
			
		||||
  if (areErrorsInScheduleUpdate(req, res)) return false
 | 
			
		||||
 | 
			
		||||
  if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false
 | 
			
		||||
 | 
			
		||||
  if (!isVideoFileMimeTypeValid(files)) {
 | 
			
		||||
    res.status(HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415)
 | 
			
		||||
        .json({
 | 
			
		||||
          error: 'This file is not supported. Please, make sure it is of the following type: ' +
 | 
			
		||||
                CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!isVideoFileSizeValid(videoFileSize.toString())) {
 | 
			
		||||
    res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413)
 | 
			
		||||
        .json({ error: 'This file is too large. It exceeds the maximum file size authorized.' })
 | 
			
		||||
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (await isAbleToUploadVideo(user.id, videoFileSize) === false) {
 | 
			
		||||
    res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413)
 | 
			
		||||
        .json({ error: 'The user video quota is exceeded with this video.' })
 | 
			
		||||
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function isVideoAccepted (
 | 
			
		||||
  req: express.Request,
 | 
			
		||||
  res: express.Response,
 | 
			
		||||
  videoFile: express.VideoUploadFile
 | 
			
		||||
) {
 | 
			
		||||
  // Check we accept this video
 | 
			
		||||
  const acceptParameters = {
 | 
			
		||||
    videoBody: req.body,
 | 
			
		||||
| 
						 | 
				
			
			@ -538,3 +650,11 @@ async function isVideoAccepted (req: express.Request, res: express.Response, vid
 | 
			
		|||
 | 
			
		||||
  return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function addDurationToVideo (videoFile: { path: string, duration?: number }) {
 | 
			
		||||
  const duration: number = await getDurationFromVideoFile(videoFile.path)
 | 
			
		||||
 | 
			
		||||
  if (isNaN(duration)) throw new Error(`Couldn't get video duration`)
 | 
			
		||||
 | 
			
		||||
  videoFile.duration = duration
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,6 +13,7 @@ import './plugins'
 | 
			
		|||
import './redundancy'
 | 
			
		||||
import './search'
 | 
			
		||||
import './services'
 | 
			
		||||
import './upload-quota'
 | 
			
		||||
import './user-notifications'
 | 
			
		||||
import './user-subscriptions'
 | 
			
		||||
import './users'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										152
									
								
								server/tests/api/check-params/upload-quota.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								server/tests/api/check-params/upload-quota.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,152 @@
 | 
			
		|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 | 
			
		||||
 | 
			
		||||
import 'mocha'
 | 
			
		||||
import { expect } from 'chai'
 | 
			
		||||
import { HttpStatusCode, randomInt } from '@shared/core-utils'
 | 
			
		||||
import { getGoodVideoUrl, getMagnetURI, getMyVideoImports, importVideo } from '@shared/extra-utils/videos/video-imports'
 | 
			
		||||
import { MyUser, VideoImport, VideoImportState, VideoPrivacy } from '@shared/models'
 | 
			
		||||
import {
 | 
			
		||||
  cleanupTests,
 | 
			
		||||
  flushAndRunServer,
 | 
			
		||||
  getMyUserInformation,
 | 
			
		||||
  immutableAssign,
 | 
			
		||||
  registerUser,
 | 
			
		||||
  ServerInfo,
 | 
			
		||||
  setAccessTokensToServers,
 | 
			
		||||
  setDefaultVideoChannel,
 | 
			
		||||
  updateUser,
 | 
			
		||||
  uploadVideo,
 | 
			
		||||
  userLogin,
 | 
			
		||||
  waitJobs
 | 
			
		||||
} from '../../../../shared/extra-utils'
 | 
			
		||||
 | 
			
		||||
describe('Test upload quota', function () {
 | 
			
		||||
  let server: ServerInfo
 | 
			
		||||
  let rootId: number
 | 
			
		||||
 | 
			
		||||
  // ---------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
  before(async function () {
 | 
			
		||||
    this.timeout(30000)
 | 
			
		||||
 | 
			
		||||
    server = await flushAndRunServer(1)
 | 
			
		||||
    await setAccessTokensToServers([ server ])
 | 
			
		||||
    await setDefaultVideoChannel([ server ])
 | 
			
		||||
 | 
			
		||||
    const res = await getMyUserInformation(server.url, server.accessToken)
 | 
			
		||||
    rootId = (res.body as MyUser).id
 | 
			
		||||
 | 
			
		||||
    await updateUser({
 | 
			
		||||
      url: server.url,
 | 
			
		||||
      userId: rootId,
 | 
			
		||||
      accessToken: server.accessToken,
 | 
			
		||||
      videoQuota: 42
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe('When having a video quota', function () {
 | 
			
		||||
 | 
			
		||||
    it('Should fail with a registered user having too many videos with legacy upload', async function () {
 | 
			
		||||
      this.timeout(30000)
 | 
			
		||||
 | 
			
		||||
      const user = { username: 'registered' + randomInt(1, 1500), password: 'password' }
 | 
			
		||||
      await registerUser(server.url, user.username, user.password)
 | 
			
		||||
      const userAccessToken = await userLogin(server, user)
 | 
			
		||||
 | 
			
		||||
      const videoAttributes = { fixture: 'video_short2.webm' }
 | 
			
		||||
      for (let i = 0; i < 5; i++) {
 | 
			
		||||
        await uploadVideo(server.url, userAccessToken, videoAttributes)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await uploadVideo(server.url, userAccessToken, videoAttributes, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'legacy')
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail with a registered user having too many videos with resumable upload', async function () {
 | 
			
		||||
      this.timeout(30000)
 | 
			
		||||
 | 
			
		||||
      const user = { username: 'registered' + randomInt(1, 1500), password: 'password' }
 | 
			
		||||
      await registerUser(server.url, user.username, user.password)
 | 
			
		||||
      const userAccessToken = await userLogin(server, user)
 | 
			
		||||
 | 
			
		||||
      const videoAttributes = { fixture: 'video_short2.webm' }
 | 
			
		||||
      for (let i = 0; i < 5; i++) {
 | 
			
		||||
        await uploadVideo(server.url, userAccessToken, videoAttributes)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await uploadVideo(server.url, userAccessToken, videoAttributes, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'resumable')
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail to import with HTTP/Torrent/magnet', async function () {
 | 
			
		||||
      this.timeout(120000)
 | 
			
		||||
 | 
			
		||||
      const baseAttributes = {
 | 
			
		||||
        channelId: server.videoChannel.id,
 | 
			
		||||
        privacy: VideoPrivacy.PUBLIC
 | 
			
		||||
      }
 | 
			
		||||
      await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { targetUrl: getGoodVideoUrl() }))
 | 
			
		||||
      await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { magnetUri: getMagnetURI() }))
 | 
			
		||||
      await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { torrentfile: 'video-720p.torrent' as any }))
 | 
			
		||||
 | 
			
		||||
      await waitJobs([ server ])
 | 
			
		||||
 | 
			
		||||
      const res = await getMyVideoImports(server.url, server.accessToken)
 | 
			
		||||
 | 
			
		||||
      expect(res.body.total).to.equal(3)
 | 
			
		||||
      const videoImports: VideoImport[] = res.body.data
 | 
			
		||||
      expect(videoImports).to.have.lengthOf(3)
 | 
			
		||||
 | 
			
		||||
      for (const videoImport of videoImports) {
 | 
			
		||||
        expect(videoImport.state.id).to.equal(VideoImportState.FAILED)
 | 
			
		||||
        expect(videoImport.error).not.to.be.undefined
 | 
			
		||||
        expect(videoImport.error).to.contain('user video quota is exceeded')
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe('When having a daily video quota', function () {
 | 
			
		||||
 | 
			
		||||
    it('Should fail with a user having too many videos daily', async function () {
 | 
			
		||||
      await updateUser({
 | 
			
		||||
        url: server.url,
 | 
			
		||||
        userId: rootId,
 | 
			
		||||
        accessToken: server.accessToken,
 | 
			
		||||
        videoQuotaDaily: 42
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'legacy')
 | 
			
		||||
      await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'resumable')
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe('When having an absolute and daily video quota', function () {
 | 
			
		||||
    it('Should fail if exceeding total quota', async function () {
 | 
			
		||||
      await updateUser({
 | 
			
		||||
        url: server.url,
 | 
			
		||||
        userId: rootId,
 | 
			
		||||
        accessToken: server.accessToken,
 | 
			
		||||
        videoQuota: 42,
 | 
			
		||||
        videoQuotaDaily: 1024 * 1024 * 1024
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'legacy')
 | 
			
		||||
      await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'resumable')
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail if exceeding daily quota', async function () {
 | 
			
		||||
      await updateUser({
 | 
			
		||||
        url: server.url,
 | 
			
		||||
        userId: rootId,
 | 
			
		||||
        accessToken: server.accessToken,
 | 
			
		||||
        videoQuota: 1024 * 1024 * 1024,
 | 
			
		||||
        videoQuotaDaily: 42
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'legacy')
 | 
			
		||||
      await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'resumable')
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  after(async function () {
 | 
			
		||||
    await cleanupTests([ server ])
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -1,10 +1,10 @@
 | 
			
		|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 | 
			
		||||
 | 
			
		||||
import 'mocha'
 | 
			
		||||
import { expect } from 'chai'
 | 
			
		||||
import { omit } from 'lodash'
 | 
			
		||||
import { join } from 'path'
 | 
			
		||||
import { User, UserRole, VideoImport, VideoImportState } from '../../../../shared'
 | 
			
		||||
import { User, UserRole } from '../../../../shared'
 | 
			
		||||
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 | 
			
		||||
import {
 | 
			
		||||
  addVideoChannel,
 | 
			
		||||
  blockUser,
 | 
			
		||||
| 
						 | 
				
			
			@ -29,7 +29,6 @@ import {
 | 
			
		|||
  ServerInfo,
 | 
			
		||||
  setAccessTokensToServers,
 | 
			
		||||
  unblockUser,
 | 
			
		||||
  updateUser,
 | 
			
		||||
  uploadVideo,
 | 
			
		||||
  userLogin
 | 
			
		||||
} from '../../../../shared/extra-utils'
 | 
			
		||||
| 
						 | 
				
			
			@ -39,11 +38,7 @@ import {
 | 
			
		|||
  checkBadSortPagination,
 | 
			
		||||
  checkBadStartPagination
 | 
			
		||||
} from '../../../../shared/extra-utils/requests/check-api-params'
 | 
			
		||||
import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
 | 
			
		||||
import { getGoodVideoUrl, getMagnetURI, getMyVideoImports, importVideo } from '../../../../shared/extra-utils/videos/video-imports'
 | 
			
		||||
import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
 | 
			
		||||
import { VideoPrivacy } from '../../../../shared/models/videos'
 | 
			
		||||
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 | 
			
		||||
 | 
			
		||||
describe('Test users API validators', function () {
 | 
			
		||||
  const path = '/api/v1/users/'
 | 
			
		||||
| 
						 | 
				
			
			@ -1093,102 +1088,6 @@ describe('Test users API validators', function () {
 | 
			
		|||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe('When having a video quota', function () {
 | 
			
		||||
    it('Should fail with a user having too many videos', async function () {
 | 
			
		||||
      await updateUser({
 | 
			
		||||
        url: server.url,
 | 
			
		||||
        userId: rootId,
 | 
			
		||||
        accessToken: server.accessToken,
 | 
			
		||||
        videoQuota: 42
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail with a registered user having too many videos', async function () {
 | 
			
		||||
      this.timeout(30000)
 | 
			
		||||
 | 
			
		||||
      const user = {
 | 
			
		||||
        username: 'user3',
 | 
			
		||||
        password: 'my super password'
 | 
			
		||||
      }
 | 
			
		||||
      userAccessToken = await userLogin(server, user)
 | 
			
		||||
 | 
			
		||||
      const videoAttributes = { fixture: 'video_short2.webm' }
 | 
			
		||||
      await uploadVideo(server.url, userAccessToken, videoAttributes)
 | 
			
		||||
      await uploadVideo(server.url, userAccessToken, videoAttributes)
 | 
			
		||||
      await uploadVideo(server.url, userAccessToken, videoAttributes)
 | 
			
		||||
      await uploadVideo(server.url, userAccessToken, videoAttributes)
 | 
			
		||||
      await uploadVideo(server.url, userAccessToken, videoAttributes)
 | 
			
		||||
      await uploadVideo(server.url, userAccessToken, videoAttributes, HttpStatusCode.PAYLOAD_TOO_LARGE_413)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail to import with HTTP/Torrent/magnet', async function () {
 | 
			
		||||
      this.timeout(120000)
 | 
			
		||||
 | 
			
		||||
      const baseAttributes = {
 | 
			
		||||
        channelId: 1,
 | 
			
		||||
        privacy: VideoPrivacy.PUBLIC
 | 
			
		||||
      }
 | 
			
		||||
      await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { targetUrl: getGoodVideoUrl() }))
 | 
			
		||||
      await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { magnetUri: getMagnetURI() }))
 | 
			
		||||
      await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { torrentfile: 'video-720p.torrent' as any }))
 | 
			
		||||
 | 
			
		||||
      await waitJobs([ server ])
 | 
			
		||||
 | 
			
		||||
      const res = await getMyVideoImports(server.url, server.accessToken)
 | 
			
		||||
 | 
			
		||||
      expect(res.body.total).to.equal(3)
 | 
			
		||||
      const videoImports: VideoImport[] = res.body.data
 | 
			
		||||
      expect(videoImports).to.have.lengthOf(3)
 | 
			
		||||
 | 
			
		||||
      for (const videoImport of videoImports) {
 | 
			
		||||
        expect(videoImport.state.id).to.equal(VideoImportState.FAILED)
 | 
			
		||||
        expect(videoImport.error).not.to.be.undefined
 | 
			
		||||
        expect(videoImport.error).to.contain('user video quota is exceeded')
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe('When having a daily video quota', function () {
 | 
			
		||||
    it('Should fail with a user having too many videos daily', async function () {
 | 
			
		||||
      await updateUser({
 | 
			
		||||
        url: server.url,
 | 
			
		||||
        userId: rootId,
 | 
			
		||||
        accessToken: server.accessToken,
 | 
			
		||||
        videoQuotaDaily: 42
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413)
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe('When having an absolute and daily video quota', function () {
 | 
			
		||||
    it('Should fail if exceeding total quota', async function () {
 | 
			
		||||
      await updateUser({
 | 
			
		||||
        url: server.url,
 | 
			
		||||
        userId: rootId,
 | 
			
		||||
        accessToken: server.accessToken,
 | 
			
		||||
        videoQuota: 42,
 | 
			
		||||
        videoQuotaDaily: 1024 * 1024 * 1024
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail if exceeding daily quota', async function () {
 | 
			
		||||
      await updateUser({
 | 
			
		||||
        url: server.url,
 | 
			
		||||
        userId: rootId,
 | 
			
		||||
        accessToken: server.accessToken,
 | 
			
		||||
        videoQuota: 1024 * 1024 * 1024,
 | 
			
		||||
        videoQuotaDaily: 42
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413)
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe('When asking a password reset', function () {
 | 
			
		||||
    const path = '/api/v1/users/ask-reset-password'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,12 @@
 | 
			
		|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 | 
			
		||||
 | 
			
		||||
import 'mocha'
 | 
			
		||||
import * as chai from 'chai'
 | 
			
		||||
import { omit } from 'lodash'
 | 
			
		||||
import 'mocha'
 | 
			
		||||
import { join } from 'path'
 | 
			
		||||
import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum'
 | 
			
		||||
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 | 
			
		||||
import {
 | 
			
		||||
  checkUploadVideoParam,
 | 
			
		||||
  cleanupTests,
 | 
			
		||||
  createUser,
 | 
			
		||||
  flushAndRunServer,
 | 
			
		||||
| 
						 | 
				
			
			@ -18,17 +19,18 @@ import {
 | 
			
		|||
  makePutBodyRequest,
 | 
			
		||||
  makeUploadRequest,
 | 
			
		||||
  removeVideo,
 | 
			
		||||
  root,
 | 
			
		||||
  ServerInfo,
 | 
			
		||||
  setAccessTokensToServers,
 | 
			
		||||
  userLogin,
 | 
			
		||||
  root
 | 
			
		||||
  userLogin
 | 
			
		||||
} from '../../../../shared/extra-utils'
 | 
			
		||||
import {
 | 
			
		||||
  checkBadCountPagination,
 | 
			
		||||
  checkBadSortPagination,
 | 
			
		||||
  checkBadStartPagination
 | 
			
		||||
} from '../../../../shared/extra-utils/requests/check-api-params'
 | 
			
		||||
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 | 
			
		||||
import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum'
 | 
			
		||||
import { randomInt } from '@shared/core-utils'
 | 
			
		||||
 | 
			
		||||
const expect = chai.expect
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -183,7 +185,7 @@ describe('Test videos API validator', function () {
 | 
			
		|||
  describe('When adding a video', function () {
 | 
			
		||||
    let baseCorrectParams
 | 
			
		||||
    const baseCorrectAttaches = {
 | 
			
		||||
      videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.webm')
 | 
			
		||||
      fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.webm')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    before(function () {
 | 
			
		||||
| 
						 | 
				
			
			@ -206,256 +208,243 @@ describe('Test videos API validator', function () {
 | 
			
		|||
      }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail with nothing', async function () {
 | 
			
		||||
      const fields = {}
 | 
			
		||||
      const attaches = {}
 | 
			
		||||
      await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
 | 
			
		||||
    })
 | 
			
		||||
    function runSuite (mode: 'legacy' | 'resumable') {
 | 
			
		||||
 | 
			
		||||
    it('Should fail without name', async function () {
 | 
			
		||||
      const fields = omit(baseCorrectParams, 'name')
 | 
			
		||||
      const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
      await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail with a long name', async function () {
 | 
			
		||||
      const fields = immutableAssign(baseCorrectParams, { name: 'super'.repeat(65) })
 | 
			
		||||
      const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
      await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail with a bad category', async function () {
 | 
			
		||||
      const fields = immutableAssign(baseCorrectParams, { category: 125 })
 | 
			
		||||
      const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
      await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail with a bad licence', async function () {
 | 
			
		||||
      const fields = immutableAssign(baseCorrectParams, { licence: 125 })
 | 
			
		||||
      const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
      await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail with a bad language', async function () {
 | 
			
		||||
      const fields = immutableAssign(baseCorrectParams, { language: 'a'.repeat(15) })
 | 
			
		||||
      const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
      await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail with a long description', async function () {
 | 
			
		||||
      const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(2500) })
 | 
			
		||||
      const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
      await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail with a long support text', async function () {
 | 
			
		||||
      const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) })
 | 
			
		||||
      const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
      await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail without a channel', async function () {
 | 
			
		||||
      const fields = omit(baseCorrectParams, 'channelId')
 | 
			
		||||
      const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
      await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail with a bad channel', async function () {
 | 
			
		||||
      const fields = immutableAssign(baseCorrectParams, { channelId: 545454 })
 | 
			
		||||
      const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
      await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail with another user channel', async function () {
 | 
			
		||||
      const user = {
 | 
			
		||||
        username: 'fake',
 | 
			
		||||
        password: 'fake_password'
 | 
			
		||||
      }
 | 
			
		||||
      await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
 | 
			
		||||
 | 
			
		||||
      const accessTokenUser = await userLogin(server, user)
 | 
			
		||||
      const res = await getMyUserInformation(server.url, accessTokenUser)
 | 
			
		||||
      const customChannelId = res.body.videoChannels[0].id
 | 
			
		||||
 | 
			
		||||
      const fields = immutableAssign(baseCorrectParams, { channelId: customChannelId })
 | 
			
		||||
      const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
      await makeUploadRequest({ url: server.url, path: path + '/upload', token: userAccessToken, fields, attaches })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail with too many tags', async function () {
 | 
			
		||||
      const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] })
 | 
			
		||||
      const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
      await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail with a tag length too low', async function () {
 | 
			
		||||
      const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 't' ] })
 | 
			
		||||
      const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
      await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail with a tag length too big', async function () {
 | 
			
		||||
      const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] })
 | 
			
		||||
      const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
      await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail with a bad schedule update (miss updateAt)', async function () {
 | 
			
		||||
      const fields = immutableAssign(baseCorrectParams, { 'scheduleUpdate[privacy]': VideoPrivacy.PUBLIC })
 | 
			
		||||
      const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
      await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail with a bad schedule update (wrong updateAt)', async function () {
 | 
			
		||||
      const fields = immutableAssign(baseCorrectParams, {
 | 
			
		||||
        'scheduleUpdate[privacy]': VideoPrivacy.PUBLIC,
 | 
			
		||||
        'scheduleUpdate[updateAt]': 'toto'
 | 
			
		||||
      })
 | 
			
		||||
      const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
      await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail with a bad originally published at attribute', async function () {
 | 
			
		||||
      const fields = immutableAssign(baseCorrectParams, { originallyPublishedAt: 'toto' })
 | 
			
		||||
      const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
      await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail without an input file', async function () {
 | 
			
		||||
      const fields = baseCorrectParams
 | 
			
		||||
      const attaches = {}
 | 
			
		||||
      await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail with an incorrect input file', async function () {
 | 
			
		||||
      const fields = baseCorrectParams
 | 
			
		||||
      let attaches = {
 | 
			
		||||
        videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short_fake.webm')
 | 
			
		||||
      }
 | 
			
		||||
      await makeUploadRequest({
 | 
			
		||||
        url: server.url,
 | 
			
		||||
        path: path + '/upload',
 | 
			
		||||
        token: server.accessToken,
 | 
			
		||||
        fields,
 | 
			
		||||
        attaches,
 | 
			
		||||
        statusCodeExpected: HttpStatusCode.UNPROCESSABLE_ENTITY_422
 | 
			
		||||
      it('Should fail with nothing', async function () {
 | 
			
		||||
        const fields = {}
 | 
			
		||||
        const attaches = {}
 | 
			
		||||
        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      attaches = {
 | 
			
		||||
        videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mkv')
 | 
			
		||||
      }
 | 
			
		||||
      await makeUploadRequest({
 | 
			
		||||
        url: server.url,
 | 
			
		||||
        path: path + '/upload',
 | 
			
		||||
        token: server.accessToken,
 | 
			
		||||
        fields,
 | 
			
		||||
        attaches,
 | 
			
		||||
        statusCodeExpected: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail with an incorrect thumbnail file', async function () {
 | 
			
		||||
      const fields = baseCorrectParams
 | 
			
		||||
      const attaches = {
 | 
			
		||||
        thumbnailfile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4'),
 | 
			
		||||
        videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4')
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail with a big thumbnail file', async function () {
 | 
			
		||||
      const fields = baseCorrectParams
 | 
			
		||||
      const attaches = {
 | 
			
		||||
        thumbnailfile: join(root(), 'server', 'tests', 'fixtures', 'preview-big.png'),
 | 
			
		||||
        videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4')
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail with an incorrect preview file', async function () {
 | 
			
		||||
      const fields = baseCorrectParams
 | 
			
		||||
      const attaches = {
 | 
			
		||||
        previewfile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4'),
 | 
			
		||||
        videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4')
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail with a big preview file', async function () {
 | 
			
		||||
      const fields = baseCorrectParams
 | 
			
		||||
      const attaches = {
 | 
			
		||||
        previewfile: join(root(), 'server', 'tests', 'fixtures', 'preview-big.png'),
 | 
			
		||||
        videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4')
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should succeed with the correct parameters', async function () {
 | 
			
		||||
      this.timeout(10000)
 | 
			
		||||
 | 
			
		||||
      const fields = baseCorrectParams
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
      it('Should fail without name', async function () {
 | 
			
		||||
        const fields = omit(baseCorrectParams, 'name')
 | 
			
		||||
        const attaches = baseCorrectAttaches
 | 
			
		||||
        await makeUploadRequest({
 | 
			
		||||
          url: server.url,
 | 
			
		||||
          path: path + '/upload',
 | 
			
		||||
          token: server.accessToken,
 | 
			
		||||
          fields,
 | 
			
		||||
          attaches,
 | 
			
		||||
          statusCodeExpected: HttpStatusCode.OK_200
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
        const attaches = immutableAssign(baseCorrectAttaches, {
 | 
			
		||||
          videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4')
 | 
			
		||||
        })
 | 
			
		||||
        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
        await makeUploadRequest({
 | 
			
		||||
          url: server.url,
 | 
			
		||||
          path: path + '/upload',
 | 
			
		||||
          token: server.accessToken,
 | 
			
		||||
          fields,
 | 
			
		||||
          attaches,
 | 
			
		||||
          statusCodeExpected: HttpStatusCode.OK_200
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
      it('Should fail with a long name', async function () {
 | 
			
		||||
        const fields = immutableAssign(baseCorrectParams, { name: 'super'.repeat(65) })
 | 
			
		||||
        const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
        const attaches = immutableAssign(baseCorrectAttaches, {
 | 
			
		||||
          videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.ogv')
 | 
			
		||||
        })
 | 
			
		||||
        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
        await makeUploadRequest({
 | 
			
		||||
          url: server.url,
 | 
			
		||||
          path: path + '/upload',
 | 
			
		||||
          token: server.accessToken,
 | 
			
		||||
          fields,
 | 
			
		||||
          attaches,
 | 
			
		||||
          statusCodeExpected: HttpStatusCode.OK_200
 | 
			
		||||
      it('Should fail with a bad category', async function () {
 | 
			
		||||
        const fields = immutableAssign(baseCorrectParams, { category: 125 })
 | 
			
		||||
        const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      it('Should fail with a bad licence', async function () {
 | 
			
		||||
        const fields = immutableAssign(baseCorrectParams, { licence: 125 })
 | 
			
		||||
        const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      it('Should fail with a bad language', async function () {
 | 
			
		||||
        const fields = immutableAssign(baseCorrectParams, { language: 'a'.repeat(15) })
 | 
			
		||||
        const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      it('Should fail with a long description', async function () {
 | 
			
		||||
        const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(2500) })
 | 
			
		||||
        const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      it('Should fail with a long support text', async function () {
 | 
			
		||||
        const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) })
 | 
			
		||||
        const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      it('Should fail without a channel', async function () {
 | 
			
		||||
        const fields = omit(baseCorrectParams, 'channelId')
 | 
			
		||||
        const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      it('Should fail with a bad channel', async function () {
 | 
			
		||||
        const fields = immutableAssign(baseCorrectParams, { channelId: 545454 })
 | 
			
		||||
        const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      it('Should fail with another user channel', async function () {
 | 
			
		||||
        const user = {
 | 
			
		||||
          username: 'fake' + randomInt(0, 1500),
 | 
			
		||||
          password: 'fake_password'
 | 
			
		||||
        }
 | 
			
		||||
        await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
 | 
			
		||||
 | 
			
		||||
        const accessTokenUser = await userLogin(server, user)
 | 
			
		||||
        const res = await getMyUserInformation(server.url, accessTokenUser)
 | 
			
		||||
        const customChannelId = res.body.videoChannels[0].id
 | 
			
		||||
 | 
			
		||||
        const fields = immutableAssign(baseCorrectParams, { channelId: customChannelId })
 | 
			
		||||
        const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
        await checkUploadVideoParam(server.url, userAccessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      it('Should fail with too many tags', async function () {
 | 
			
		||||
        const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] })
 | 
			
		||||
        const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      it('Should fail with a tag length too low', async function () {
 | 
			
		||||
        const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 't' ] })
 | 
			
		||||
        const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      it('Should fail with a tag length too big', async function () {
 | 
			
		||||
        const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] })
 | 
			
		||||
        const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      it('Should fail with a bad schedule update (miss updateAt)', async function () {
 | 
			
		||||
        const fields = immutableAssign(baseCorrectParams, { scheduleUpdate: { privacy: VideoPrivacy.PUBLIC } })
 | 
			
		||||
        const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      it('Should fail with a bad schedule update (wrong updateAt)', async function () {
 | 
			
		||||
        const fields = immutableAssign(baseCorrectParams, {
 | 
			
		||||
          scheduleUpdate: {
 | 
			
		||||
            privacy: VideoPrivacy.PUBLIC,
 | 
			
		||||
            updateAt: 'toto'
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
        const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      it('Should fail with a bad originally published at attribute', async function () {
 | 
			
		||||
        const fields = immutableAssign(baseCorrectParams, { originallyPublishedAt: 'toto' })
 | 
			
		||||
        const attaches = baseCorrectAttaches
 | 
			
		||||
 | 
			
		||||
        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      it('Should fail without an input file', async function () {
 | 
			
		||||
        const fields = baseCorrectParams
 | 
			
		||||
        const attaches = {}
 | 
			
		||||
        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      it('Should fail with an incorrect input file', async function () {
 | 
			
		||||
        const fields = baseCorrectParams
 | 
			
		||||
        let attaches = { fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short_fake.webm') }
 | 
			
		||||
 | 
			
		||||
        await checkUploadVideoParam(
 | 
			
		||||
          server.url,
 | 
			
		||||
          server.accessToken,
 | 
			
		||||
          { ...fields, ...attaches },
 | 
			
		||||
          HttpStatusCode.UNPROCESSABLE_ENTITY_422,
 | 
			
		||||
          mode
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        attaches = { fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mkv') }
 | 
			
		||||
        await checkUploadVideoParam(
 | 
			
		||||
          server.url,
 | 
			
		||||
          server.accessToken,
 | 
			
		||||
          { ...fields, ...attaches },
 | 
			
		||||
          HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415,
 | 
			
		||||
          mode
 | 
			
		||||
        )
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      it('Should fail with an incorrect thumbnail file', async function () {
 | 
			
		||||
        const fields = baseCorrectParams
 | 
			
		||||
        const attaches = {
 | 
			
		||||
          thumbnailfile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4'),
 | 
			
		||||
          fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4')
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      it('Should fail with a big thumbnail file', async function () {
 | 
			
		||||
        const fields = baseCorrectParams
 | 
			
		||||
        const attaches = {
 | 
			
		||||
          thumbnailfile: join(root(), 'server', 'tests', 'fixtures', 'preview-big.png'),
 | 
			
		||||
          fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4')
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      it('Should fail with an incorrect preview file', async function () {
 | 
			
		||||
        const fields = baseCorrectParams
 | 
			
		||||
        const attaches = {
 | 
			
		||||
          previewfile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4'),
 | 
			
		||||
          fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4')
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      it('Should fail with a big preview file', async function () {
 | 
			
		||||
        const fields = baseCorrectParams
 | 
			
		||||
        const attaches = {
 | 
			
		||||
          previewfile: join(root(), 'server', 'tests', 'fixtures', 'preview-big.png'),
 | 
			
		||||
          fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4')
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      it('Should succeed with the correct parameters', async function () {
 | 
			
		||||
        this.timeout(10000)
 | 
			
		||||
 | 
			
		||||
        const fields = baseCorrectParams
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
          const attaches = baseCorrectAttaches
 | 
			
		||||
          await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.OK_200, mode)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
          const attaches = immutableAssign(baseCorrectAttaches, {
 | 
			
		||||
            videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4')
 | 
			
		||||
          })
 | 
			
		||||
 | 
			
		||||
          await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.OK_200, mode)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
          const attaches = immutableAssign(baseCorrectAttaches, {
 | 
			
		||||
            videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.ogv')
 | 
			
		||||
          })
 | 
			
		||||
 | 
			
		||||
          await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.OK_200, mode)
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    describe('Resumable upload', function () {
 | 
			
		||||
      runSuite('resumable')
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    describe('Legacy upload', function () {
 | 
			
		||||
      runSuite('legacy')
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -678,7 +667,7 @@ describe('Test videos API validator', function () {
 | 
			
		|||
      })
 | 
			
		||||
 | 
			
		||||
      expect(res.body.data).to.be.an('array')
 | 
			
		||||
      expect(res.body.data.length).to.equal(3)
 | 
			
		||||
      expect(res.body.data.length).to.equal(6)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail without a correct uuid', async function () {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
import './audio-only'
 | 
			
		||||
import './multiple-servers'
 | 
			
		||||
import './resumable-upload'
 | 
			
		||||
import './single-server'
 | 
			
		||||
import './video-captions'
 | 
			
		||||
import './video-change-ownership'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -181,7 +181,7 @@ describe('Test multiple servers', function () {
 | 
			
		|||
        thumbnailfile: 'thumbnail.jpg',
 | 
			
		||||
        previewfile: 'preview.jpg'
 | 
			
		||||
      }
 | 
			
		||||
      await uploadVideo(servers[1].url, userAccessToken, videoAttributes)
 | 
			
		||||
      await uploadVideo(servers[1].url, userAccessToken, videoAttributes, HttpStatusCode.OK_200, 'resumable')
 | 
			
		||||
 | 
			
		||||
      // Transcoding
 | 
			
		||||
      await waitJobs(servers)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										187
									
								
								server/tests/api/videos/resumable-upload.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								server/tests/api/videos/resumable-upload.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,187 @@
 | 
			
		|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 | 
			
		||||
 | 
			
		||||
import 'mocha'
 | 
			
		||||
import * as chai from 'chai'
 | 
			
		||||
import { pathExists, readdir, stat } from 'fs-extra'
 | 
			
		||||
import { join } from 'path'
 | 
			
		||||
import { HttpStatusCode } from '@shared/core-utils'
 | 
			
		||||
import {
 | 
			
		||||
  buildAbsoluteFixturePath,
 | 
			
		||||
  buildServerDirectory,
 | 
			
		||||
  flushAndRunServer,
 | 
			
		||||
  getMyUserInformation,
 | 
			
		||||
  prepareResumableUpload,
 | 
			
		||||
  sendDebugCommand,
 | 
			
		||||
  sendResumableChunks,
 | 
			
		||||
  ServerInfo,
 | 
			
		||||
  setAccessTokensToServers,
 | 
			
		||||
  setDefaultVideoChannel,
 | 
			
		||||
  updateUser
 | 
			
		||||
} from '@shared/extra-utils'
 | 
			
		||||
import { MyUser, VideoPrivacy } from '@shared/models'
 | 
			
		||||
 | 
			
		||||
const expect = chai.expect
 | 
			
		||||
 | 
			
		||||
// Most classic resumable upload tests are done in other test suites
 | 
			
		||||
 | 
			
		||||
describe('Test resumable upload', function () {
 | 
			
		||||
  const defaultFixture = 'video_short.mp4'
 | 
			
		||||
  let server: ServerInfo
 | 
			
		||||
  let rootId: number
 | 
			
		||||
 | 
			
		||||
  async function buildSize (fixture: string, size?: number) {
 | 
			
		||||
    if (size !== undefined) return size
 | 
			
		||||
 | 
			
		||||
    const baseFixture = buildAbsoluteFixturePath(fixture)
 | 
			
		||||
    return (await stat(baseFixture)).size
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function prepareUpload (sizeArg?: number) {
 | 
			
		||||
    const size = await buildSize(defaultFixture, sizeArg)
 | 
			
		||||
 | 
			
		||||
    const attributes = {
 | 
			
		||||
      name: 'video',
 | 
			
		||||
      channelId: server.videoChannel.id,
 | 
			
		||||
      privacy: VideoPrivacy.PUBLIC,
 | 
			
		||||
      fixture: defaultFixture
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const mimetype = 'video/mp4'
 | 
			
		||||
 | 
			
		||||
    const res = await prepareResumableUpload({ url: server.url, token: server.accessToken, attributes, size, mimetype })
 | 
			
		||||
 | 
			
		||||
    return res.header['location'].split('?')[1]
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function sendChunks (options: {
 | 
			
		||||
    pathUploadId: string
 | 
			
		||||
    size?: number
 | 
			
		||||
    expectedStatus?: HttpStatusCode
 | 
			
		||||
    contentLength?: number
 | 
			
		||||
    contentRange?: string
 | 
			
		||||
    contentRangeBuilder?: (start: number, chunk: any) => string
 | 
			
		||||
  }) {
 | 
			
		||||
    const { pathUploadId, expectedStatus, contentLength, contentRangeBuilder } = options
 | 
			
		||||
 | 
			
		||||
    const size = await buildSize(defaultFixture, options.size)
 | 
			
		||||
    const absoluteFilePath = buildAbsoluteFixturePath(defaultFixture)
 | 
			
		||||
 | 
			
		||||
    return sendResumableChunks({
 | 
			
		||||
      url: server.url,
 | 
			
		||||
      token: server.accessToken,
 | 
			
		||||
      pathUploadId,
 | 
			
		||||
      videoFilePath: absoluteFilePath,
 | 
			
		||||
      size,
 | 
			
		||||
      contentLength,
 | 
			
		||||
      contentRangeBuilder,
 | 
			
		||||
      specialStatus: expectedStatus
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function checkFileSize (uploadIdArg: string, expectedSize: number | null) {
 | 
			
		||||
    const uploadId = uploadIdArg.replace(/^upload_id=/, '')
 | 
			
		||||
 | 
			
		||||
    const subPath = join('tmp', 'resumable-uploads', uploadId)
 | 
			
		||||
    const filePath = buildServerDirectory(server, subPath)
 | 
			
		||||
    const exists = await pathExists(filePath)
 | 
			
		||||
 | 
			
		||||
    if (expectedSize === null) {
 | 
			
		||||
      expect(exists).to.be.false
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    expect(exists).to.be.true
 | 
			
		||||
 | 
			
		||||
    expect((await stat(filePath)).size).to.equal(expectedSize)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function countResumableUploads () {
 | 
			
		||||
    const subPath = join('tmp', 'resumable-uploads')
 | 
			
		||||
    const filePath = buildServerDirectory(server, subPath)
 | 
			
		||||
 | 
			
		||||
    const files = await readdir(filePath)
 | 
			
		||||
    return files.length
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  before(async function () {
 | 
			
		||||
    this.timeout(30000)
 | 
			
		||||
 | 
			
		||||
    server = await flushAndRunServer(1)
 | 
			
		||||
    await setAccessTokensToServers([ server ])
 | 
			
		||||
    await setDefaultVideoChannel([ server ])
 | 
			
		||||
 | 
			
		||||
    const res = await getMyUserInformation(server.url, server.accessToken)
 | 
			
		||||
    rootId = (res.body as MyUser).id
 | 
			
		||||
 | 
			
		||||
    await updateUser({
 | 
			
		||||
      url: server.url,
 | 
			
		||||
      userId: rootId,
 | 
			
		||||
      accessToken: server.accessToken,
 | 
			
		||||
      videoQuota: 10_000_000
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe('Directory cleaning', function () {
 | 
			
		||||
 | 
			
		||||
    it('Should correctly delete files after an upload', async function () {
 | 
			
		||||
      const uploadId = await prepareUpload()
 | 
			
		||||
      await sendChunks({ pathUploadId: uploadId })
 | 
			
		||||
 | 
			
		||||
      expect(await countResumableUploads()).to.equal(0)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should not delete files after an unfinished upload', async function () {
 | 
			
		||||
      await prepareUpload()
 | 
			
		||||
 | 
			
		||||
      expect(await countResumableUploads()).to.equal(2)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should not delete recent uploads', async function () {
 | 
			
		||||
      await sendDebugCommand(server.url, server.accessToken, { command: 'remove-dandling-resumable-uploads' })
 | 
			
		||||
 | 
			
		||||
      expect(await countResumableUploads()).to.equal(2)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should delete old uploads', async function () {
 | 
			
		||||
      await sendDebugCommand(server.url, server.accessToken, { command: 'remove-dandling-resumable-uploads' })
 | 
			
		||||
 | 
			
		||||
      expect(await countResumableUploads()).to.equal(0)
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe('Resumable upload and chunks', function () {
 | 
			
		||||
 | 
			
		||||
    it('Should accept the same amount of chunks', async function () {
 | 
			
		||||
      const uploadId = await prepareUpload()
 | 
			
		||||
      await sendChunks({ pathUploadId: uploadId })
 | 
			
		||||
 | 
			
		||||
      await checkFileSize(uploadId, null)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should not accept more chunks than expected', async function () {
 | 
			
		||||
      const size = 100
 | 
			
		||||
      const uploadId = await prepareUpload(size)
 | 
			
		||||
 | 
			
		||||
      await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.CONFLICT_409 })
 | 
			
		||||
      await checkFileSize(uploadId, 0)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should not accept more chunks than expected with an invalid content length/content range', async function () {
 | 
			
		||||
      const uploadId = await prepareUpload(1500)
 | 
			
		||||
 | 
			
		||||
      await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.BAD_REQUEST_400, contentLength: 1000 })
 | 
			
		||||
      await checkFileSize(uploadId, 0)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should not accept more chunks than expected with an invalid content length', async function () {
 | 
			
		||||
      const uploadId = await prepareUpload(500)
 | 
			
		||||
 | 
			
		||||
      const size = 1000
 | 
			
		||||
 | 
			
		||||
      const contentRangeBuilder = start => `bytes ${start}-${start + size - 1}/${size}`
 | 
			
		||||
      await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.BAD_REQUEST_400, contentRangeBuilder, contentLength: size })
 | 
			
		||||
      await checkFileSize(uploadId, 0)
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +1,9 @@
 | 
			
		|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 | 
			
		||||
 | 
			
		||||
import 'mocha'
 | 
			
		||||
import * as chai from 'chai'
 | 
			
		||||
import { keyBy } from 'lodash'
 | 
			
		||||
import 'mocha'
 | 
			
		||||
import { VideoPrivacy } from '../../../../shared/models/videos'
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  checkVideoFilesWereRemoved,
 | 
			
		||||
  cleanupTests,
 | 
			
		||||
| 
						 | 
				
			
			@ -28,430 +28,432 @@ import {
 | 
			
		|||
  viewVideo,
 | 
			
		||||
  wait
 | 
			
		||||
} from '../../../../shared/extra-utils'
 | 
			
		||||
import { VideoPrivacy } from '../../../../shared/models/videos'
 | 
			
		||||
import { HttpStatusCode } from '@shared/core-utils'
 | 
			
		||||
 | 
			
		||||
const expect = chai.expect
 | 
			
		||||
 | 
			
		||||
describe('Test a single server', function () {
 | 
			
		||||
  let server: ServerInfo = null
 | 
			
		||||
  let videoId = -1
 | 
			
		||||
  let videoId2 = -1
 | 
			
		||||
  let videoUUID = ''
 | 
			
		||||
  let videosListBase: any[] = null
 | 
			
		||||
 | 
			
		||||
  const getCheckAttributes = () => ({
 | 
			
		||||
    name: 'my super name',
 | 
			
		||||
    category: 2,
 | 
			
		||||
    licence: 6,
 | 
			
		||||
    language: 'zh',
 | 
			
		||||
    nsfw: true,
 | 
			
		||||
    description: 'my super description',
 | 
			
		||||
    support: 'my super support text',
 | 
			
		||||
    account: {
 | 
			
		||||
      name: 'root',
 | 
			
		||||
      host: 'localhost:' + server.port
 | 
			
		||||
    },
 | 
			
		||||
    isLocal: true,
 | 
			
		||||
    duration: 5,
 | 
			
		||||
    tags: [ 'tag1', 'tag2', 'tag3' ],
 | 
			
		||||
    privacy: VideoPrivacy.PUBLIC,
 | 
			
		||||
    commentsEnabled: true,
 | 
			
		||||
    downloadEnabled: true,
 | 
			
		||||
    channel: {
 | 
			
		||||
      displayName: 'Main root channel',
 | 
			
		||||
      name: 'root_channel',
 | 
			
		||||
      description: '',
 | 
			
		||||
      isLocal: true
 | 
			
		||||
    },
 | 
			
		||||
    fixture: 'video_short.webm',
 | 
			
		||||
    files: [
 | 
			
		||||
      {
 | 
			
		||||
        resolution: 720,
 | 
			
		||||
        size: 218910
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  })
 | 
			
		||||
  function runSuite (mode: 'legacy' | 'resumable') {
 | 
			
		||||
    let server: ServerInfo = null
 | 
			
		||||
    let videoId = -1
 | 
			
		||||
    let videoId2 = -1
 | 
			
		||||
    let videoUUID = ''
 | 
			
		||||
    let videosListBase: any[] = null
 | 
			
		||||
 | 
			
		||||
  const updateCheckAttributes = () => ({
 | 
			
		||||
    name: 'my super video updated',
 | 
			
		||||
    category: 4,
 | 
			
		||||
    licence: 2,
 | 
			
		||||
    language: 'ar',
 | 
			
		||||
    nsfw: false,
 | 
			
		||||
    description: 'my super description updated',
 | 
			
		||||
    support: 'my super support text updated',
 | 
			
		||||
    account: {
 | 
			
		||||
      name: 'root',
 | 
			
		||||
      host: 'localhost:' + server.port
 | 
			
		||||
    },
 | 
			
		||||
    isLocal: true,
 | 
			
		||||
    tags: [ 'tagup1', 'tagup2' ],
 | 
			
		||||
    privacy: VideoPrivacy.PUBLIC,
 | 
			
		||||
    duration: 5,
 | 
			
		||||
    commentsEnabled: false,
 | 
			
		||||
    downloadEnabled: false,
 | 
			
		||||
    channel: {
 | 
			
		||||
      name: 'root_channel',
 | 
			
		||||
      displayName: 'Main root channel',
 | 
			
		||||
      description: '',
 | 
			
		||||
      isLocal: true
 | 
			
		||||
    },
 | 
			
		||||
    fixture: 'video_short3.webm',
 | 
			
		||||
    files: [
 | 
			
		||||
      {
 | 
			
		||||
        resolution: 720,
 | 
			
		||||
        size: 292677
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  before(async function () {
 | 
			
		||||
    this.timeout(30000)
 | 
			
		||||
 | 
			
		||||
    server = await flushAndRunServer(1)
 | 
			
		||||
 | 
			
		||||
    await setAccessTokensToServers([ server ])
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should list video categories', async function () {
 | 
			
		||||
    const res = await getVideoCategories(server.url)
 | 
			
		||||
 | 
			
		||||
    const categories = res.body
 | 
			
		||||
    expect(Object.keys(categories)).to.have.length.above(10)
 | 
			
		||||
 | 
			
		||||
    expect(categories[11]).to.equal('News & Politics')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should list video licences', async function () {
 | 
			
		||||
    const res = await getVideoLicences(server.url)
 | 
			
		||||
 | 
			
		||||
    const licences = res.body
 | 
			
		||||
    expect(Object.keys(licences)).to.have.length.above(5)
 | 
			
		||||
 | 
			
		||||
    expect(licences[3]).to.equal('Attribution - No Derivatives')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should list video languages', async function () {
 | 
			
		||||
    const res = await getVideoLanguages(server.url)
 | 
			
		||||
 | 
			
		||||
    const languages = res.body
 | 
			
		||||
    expect(Object.keys(languages)).to.have.length.above(5)
 | 
			
		||||
 | 
			
		||||
    expect(languages['ru']).to.equal('Russian')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should list video privacies', async function () {
 | 
			
		||||
    const res = await getVideoPrivacies(server.url)
 | 
			
		||||
 | 
			
		||||
    const privacies = res.body
 | 
			
		||||
    expect(Object.keys(privacies)).to.have.length.at.least(3)
 | 
			
		||||
 | 
			
		||||
    expect(privacies[3]).to.equal('Private')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should not have videos', async function () {
 | 
			
		||||
    const res = await getVideosList(server.url)
 | 
			
		||||
 | 
			
		||||
    expect(res.body.total).to.equal(0)
 | 
			
		||||
    expect(res.body.data).to.be.an('array')
 | 
			
		||||
    expect(res.body.data.length).to.equal(0)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should upload the video', async function () {
 | 
			
		||||
    this.timeout(10000)
 | 
			
		||||
 | 
			
		||||
    const videoAttributes = {
 | 
			
		||||
    const getCheckAttributes = () => ({
 | 
			
		||||
      name: 'my super name',
 | 
			
		||||
      category: 2,
 | 
			
		||||
      nsfw: true,
 | 
			
		||||
      licence: 6,
 | 
			
		||||
      tags: [ 'tag1', 'tag2', 'tag3' ]
 | 
			
		||||
    }
 | 
			
		||||
    const res = await uploadVideo(server.url, server.accessToken, videoAttributes)
 | 
			
		||||
    expect(res.body.video).to.not.be.undefined
 | 
			
		||||
    expect(res.body.video.id).to.equal(1)
 | 
			
		||||
    expect(res.body.video.uuid).to.have.length.above(5)
 | 
			
		||||
      language: 'zh',
 | 
			
		||||
      nsfw: true,
 | 
			
		||||
      description: 'my super description',
 | 
			
		||||
      support: 'my super support text',
 | 
			
		||||
      account: {
 | 
			
		||||
        name: 'root',
 | 
			
		||||
        host: 'localhost:' + server.port
 | 
			
		||||
      },
 | 
			
		||||
      isLocal: true,
 | 
			
		||||
      duration: 5,
 | 
			
		||||
      tags: [ 'tag1', 'tag2', 'tag3' ],
 | 
			
		||||
      privacy: VideoPrivacy.PUBLIC,
 | 
			
		||||
      commentsEnabled: true,
 | 
			
		||||
      downloadEnabled: true,
 | 
			
		||||
      channel: {
 | 
			
		||||
        displayName: 'Main root channel',
 | 
			
		||||
        name: 'root_channel',
 | 
			
		||||
        description: '',
 | 
			
		||||
        isLocal: true
 | 
			
		||||
      },
 | 
			
		||||
      fixture: 'video_short.webm',
 | 
			
		||||
      files: [
 | 
			
		||||
        {
 | 
			
		||||
          resolution: 720,
 | 
			
		||||
          size: 218910
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    videoId = res.body.video.id
 | 
			
		||||
    videoUUID = res.body.video.uuid
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should get and seed the uploaded video', async function () {
 | 
			
		||||
    this.timeout(5000)
 | 
			
		||||
 | 
			
		||||
    const res = await getVideosList(server.url)
 | 
			
		||||
 | 
			
		||||
    expect(res.body.total).to.equal(1)
 | 
			
		||||
    expect(res.body.data).to.be.an('array')
 | 
			
		||||
    expect(res.body.data.length).to.equal(1)
 | 
			
		||||
 | 
			
		||||
    const video = res.body.data[0]
 | 
			
		||||
    await completeVideoCheck(server.url, video, getCheckAttributes())
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should get the video by UUID', async function () {
 | 
			
		||||
    this.timeout(5000)
 | 
			
		||||
 | 
			
		||||
    const res = await getVideo(server.url, videoUUID)
 | 
			
		||||
 | 
			
		||||
    const video = res.body
 | 
			
		||||
    await completeVideoCheck(server.url, video, getCheckAttributes())
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should have the views updated', async function () {
 | 
			
		||||
    this.timeout(20000)
 | 
			
		||||
 | 
			
		||||
    await viewVideo(server.url, videoId)
 | 
			
		||||
    await viewVideo(server.url, videoId)
 | 
			
		||||
    await viewVideo(server.url, videoId)
 | 
			
		||||
 | 
			
		||||
    await wait(1500)
 | 
			
		||||
 | 
			
		||||
    await viewVideo(server.url, videoId)
 | 
			
		||||
    await viewVideo(server.url, videoId)
 | 
			
		||||
 | 
			
		||||
    await wait(1500)
 | 
			
		||||
 | 
			
		||||
    await viewVideo(server.url, videoId)
 | 
			
		||||
    await viewVideo(server.url, videoId)
 | 
			
		||||
 | 
			
		||||
    // Wait the repeatable job
 | 
			
		||||
    await wait(8000)
 | 
			
		||||
 | 
			
		||||
    const res = await getVideo(server.url, videoId)
 | 
			
		||||
 | 
			
		||||
    const video = res.body
 | 
			
		||||
    expect(video.views).to.equal(3)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should remove the video', async function () {
 | 
			
		||||
    await removeVideo(server.url, server.accessToken, videoId)
 | 
			
		||||
 | 
			
		||||
    await checkVideoFilesWereRemoved(videoUUID, 1)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should not have videos', async function () {
 | 
			
		||||
    const res = await getVideosList(server.url)
 | 
			
		||||
 | 
			
		||||
    expect(res.body.total).to.equal(0)
 | 
			
		||||
    expect(res.body.data).to.be.an('array')
 | 
			
		||||
    expect(res.body.data).to.have.lengthOf(0)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should upload 6 videos', async function () {
 | 
			
		||||
    this.timeout(25000)
 | 
			
		||||
 | 
			
		||||
    const videos = new Set([
 | 
			
		||||
      'video_short.mp4', 'video_short.ogv', 'video_short.webm',
 | 
			
		||||
      'video_short1.webm', 'video_short2.webm', 'video_short3.webm'
 | 
			
		||||
    ])
 | 
			
		||||
 | 
			
		||||
    for (const video of videos) {
 | 
			
		||||
      const videoAttributes = {
 | 
			
		||||
        name: video + ' name',
 | 
			
		||||
        description: video + ' description',
 | 
			
		||||
        category: 2,
 | 
			
		||||
        licence: 1,
 | 
			
		||||
        language: 'en',
 | 
			
		||||
        nsfw: true,
 | 
			
		||||
        tags: [ 'tag1', 'tag2', 'tag3' ],
 | 
			
		||||
        fixture: video
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await uploadVideo(server.url, server.accessToken, videoAttributes)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should have the correct durations', async function () {
 | 
			
		||||
    const res = await getVideosList(server.url)
 | 
			
		||||
 | 
			
		||||
    expect(res.body.total).to.equal(6)
 | 
			
		||||
    const videos = res.body.data
 | 
			
		||||
    expect(videos).to.be.an('array')
 | 
			
		||||
    expect(videos).to.have.lengthOf(6)
 | 
			
		||||
 | 
			
		||||
    const videosByName = keyBy<{ duration: number }>(videos, 'name')
 | 
			
		||||
    expect(videosByName['video_short.mp4 name'].duration).to.equal(5)
 | 
			
		||||
    expect(videosByName['video_short.ogv name'].duration).to.equal(5)
 | 
			
		||||
    expect(videosByName['video_short.webm name'].duration).to.equal(5)
 | 
			
		||||
    expect(videosByName['video_short1.webm name'].duration).to.equal(10)
 | 
			
		||||
    expect(videosByName['video_short2.webm name'].duration).to.equal(5)
 | 
			
		||||
    expect(videosByName['video_short3.webm name'].duration).to.equal(5)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should have the correct thumbnails', async function () {
 | 
			
		||||
    const res = await getVideosList(server.url)
 | 
			
		||||
 | 
			
		||||
    const videos = res.body.data
 | 
			
		||||
    // For the next test
 | 
			
		||||
    videosListBase = videos
 | 
			
		||||
 | 
			
		||||
    for (const video of videos) {
 | 
			
		||||
      const videoName = video.name.replace(' name', '')
 | 
			
		||||
      await testImage(server.url, videoName, video.thumbnailPath)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should list only the two first videos', async function () {
 | 
			
		||||
    const res = await getVideosListPagination(server.url, 0, 2, 'name')
 | 
			
		||||
 | 
			
		||||
    const videos = res.body.data
 | 
			
		||||
    expect(res.body.total).to.equal(6)
 | 
			
		||||
    expect(videos.length).to.equal(2)
 | 
			
		||||
    expect(videos[0].name).to.equal(videosListBase[0].name)
 | 
			
		||||
    expect(videos[1].name).to.equal(videosListBase[1].name)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should list only the next three videos', async function () {
 | 
			
		||||
    const res = await getVideosListPagination(server.url, 2, 3, 'name')
 | 
			
		||||
 | 
			
		||||
    const videos = res.body.data
 | 
			
		||||
    expect(res.body.total).to.equal(6)
 | 
			
		||||
    expect(videos.length).to.equal(3)
 | 
			
		||||
    expect(videos[0].name).to.equal(videosListBase[2].name)
 | 
			
		||||
    expect(videos[1].name).to.equal(videosListBase[3].name)
 | 
			
		||||
    expect(videos[2].name).to.equal(videosListBase[4].name)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should list the last video', async function () {
 | 
			
		||||
    const res = await getVideosListPagination(server.url, 5, 6, 'name')
 | 
			
		||||
 | 
			
		||||
    const videos = res.body.data
 | 
			
		||||
    expect(res.body.total).to.equal(6)
 | 
			
		||||
    expect(videos.length).to.equal(1)
 | 
			
		||||
    expect(videos[0].name).to.equal(videosListBase[5].name)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should not have the total field', async function () {
 | 
			
		||||
    const res = await getVideosListPagination(server.url, 5, 6, 'name', true)
 | 
			
		||||
 | 
			
		||||
    const videos = res.body.data
 | 
			
		||||
    expect(res.body.total).to.not.exist
 | 
			
		||||
    expect(videos.length).to.equal(1)
 | 
			
		||||
    expect(videos[0].name).to.equal(videosListBase[5].name)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should list and sort by name in descending order', async function () {
 | 
			
		||||
    const res = await getVideosListSort(server.url, '-name')
 | 
			
		||||
 | 
			
		||||
    const videos = res.body.data
 | 
			
		||||
    expect(res.body.total).to.equal(6)
 | 
			
		||||
    expect(videos.length).to.equal(6)
 | 
			
		||||
    expect(videos[0].name).to.equal('video_short.webm name')
 | 
			
		||||
    expect(videos[1].name).to.equal('video_short.ogv name')
 | 
			
		||||
    expect(videos[2].name).to.equal('video_short.mp4 name')
 | 
			
		||||
    expect(videos[3].name).to.equal('video_short3.webm name')
 | 
			
		||||
    expect(videos[4].name).to.equal('video_short2.webm name')
 | 
			
		||||
    expect(videos[5].name).to.equal('video_short1.webm name')
 | 
			
		||||
 | 
			
		||||
    videoId = videos[3].uuid
 | 
			
		||||
    videoId2 = videos[5].uuid
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should list and sort by trending in descending order', async function () {
 | 
			
		||||
    const res = await getVideosListPagination(server.url, 0, 2, '-trending')
 | 
			
		||||
 | 
			
		||||
    const videos = res.body.data
 | 
			
		||||
    expect(res.body.total).to.equal(6)
 | 
			
		||||
    expect(videos.length).to.equal(2)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should list and sort by hotness in descending order', async function () {
 | 
			
		||||
    const res = await getVideosListPagination(server.url, 0, 2, '-hot')
 | 
			
		||||
 | 
			
		||||
    const videos = res.body.data
 | 
			
		||||
    expect(res.body.total).to.equal(6)
 | 
			
		||||
    expect(videos.length).to.equal(2)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should list and sort by best in descending order', async function () {
 | 
			
		||||
    const res = await getVideosListPagination(server.url, 0, 2, '-best')
 | 
			
		||||
 | 
			
		||||
    const videos = res.body.data
 | 
			
		||||
    expect(res.body.total).to.equal(6)
 | 
			
		||||
    expect(videos.length).to.equal(2)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should update a video', async function () {
 | 
			
		||||
    const attributes = {
 | 
			
		||||
    const updateCheckAttributes = () => ({
 | 
			
		||||
      name: 'my super video updated',
 | 
			
		||||
      category: 4,
 | 
			
		||||
      licence: 2,
 | 
			
		||||
      language: 'ar',
 | 
			
		||||
      nsfw: false,
 | 
			
		||||
      description: 'my super description updated',
 | 
			
		||||
      support: 'my super support text updated',
 | 
			
		||||
      account: {
 | 
			
		||||
        name: 'root',
 | 
			
		||||
        host: 'localhost:' + server.port
 | 
			
		||||
      },
 | 
			
		||||
      isLocal: true,
 | 
			
		||||
      tags: [ 'tagup1', 'tagup2' ],
 | 
			
		||||
      privacy: VideoPrivacy.PUBLIC,
 | 
			
		||||
      duration: 5,
 | 
			
		||||
      commentsEnabled: false,
 | 
			
		||||
      downloadEnabled: false,
 | 
			
		||||
      tags: [ 'tagup1', 'tagup2' ]
 | 
			
		||||
    }
 | 
			
		||||
    await updateVideo(server.url, server.accessToken, videoId, attributes)
 | 
			
		||||
  })
 | 
			
		||||
      channel: {
 | 
			
		||||
        name: 'root_channel',
 | 
			
		||||
        displayName: 'Main root channel',
 | 
			
		||||
        description: '',
 | 
			
		||||
        isLocal: true
 | 
			
		||||
      },
 | 
			
		||||
      fixture: 'video_short3.webm',
 | 
			
		||||
      files: [
 | 
			
		||||
        {
 | 
			
		||||
          resolution: 720,
 | 
			
		||||
          size: 292677
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
  it('Should filter by tags and category', async function () {
 | 
			
		||||
    const res1 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 4 ] })
 | 
			
		||||
    expect(res1.body.total).to.equal(1)
 | 
			
		||||
    expect(res1.body.data[0].name).to.equal('my super video updated')
 | 
			
		||||
    before(async function () {
 | 
			
		||||
      this.timeout(30000)
 | 
			
		||||
 | 
			
		||||
    const res2 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 3 ] })
 | 
			
		||||
    expect(res2.body.total).to.equal(0)
 | 
			
		||||
  })
 | 
			
		||||
      server = await flushAndRunServer(1)
 | 
			
		||||
 | 
			
		||||
  it('Should have the video updated', async function () {
 | 
			
		||||
    this.timeout(60000)
 | 
			
		||||
      await setAccessTokensToServers([ server ])
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const res = await getVideo(server.url, videoId)
 | 
			
		||||
    const video = res.body
 | 
			
		||||
    it('Should list video categories', async function () {
 | 
			
		||||
      const res = await getVideoCategories(server.url)
 | 
			
		||||
 | 
			
		||||
    await completeVideoCheck(server.url, video, updateCheckAttributes())
 | 
			
		||||
  })
 | 
			
		||||
      const categories = res.body
 | 
			
		||||
      expect(Object.keys(categories)).to.have.length.above(10)
 | 
			
		||||
 | 
			
		||||
  it('Should update only the tags of a video', async function () {
 | 
			
		||||
    const attributes = {
 | 
			
		||||
      tags: [ 'supertag', 'tag1', 'tag2' ]
 | 
			
		||||
    }
 | 
			
		||||
    await updateVideo(server.url, server.accessToken, videoId, attributes)
 | 
			
		||||
      expect(categories[11]).to.equal('News & Politics')
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const res = await getVideo(server.url, videoId)
 | 
			
		||||
    const video = res.body
 | 
			
		||||
    it('Should list video licences', async function () {
 | 
			
		||||
      const res = await getVideoLicences(server.url)
 | 
			
		||||
 | 
			
		||||
    await completeVideoCheck(server.url, video, Object.assign(updateCheckAttributes(), attributes))
 | 
			
		||||
  })
 | 
			
		||||
      const licences = res.body
 | 
			
		||||
      expect(Object.keys(licences)).to.have.length.above(5)
 | 
			
		||||
 | 
			
		||||
  it('Should update only the description of a video', async function () {
 | 
			
		||||
    const attributes = {
 | 
			
		||||
      description: 'hello everybody'
 | 
			
		||||
    }
 | 
			
		||||
    await updateVideo(server.url, server.accessToken, videoId, attributes)
 | 
			
		||||
      expect(licences[3]).to.equal('Attribution - No Derivatives')
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const res = await getVideo(server.url, videoId)
 | 
			
		||||
    const video = res.body
 | 
			
		||||
    it('Should list video languages', async function () {
 | 
			
		||||
      const res = await getVideoLanguages(server.url)
 | 
			
		||||
 | 
			
		||||
    const expectedAttributes = Object.assign(updateCheckAttributes(), { tags: [ 'supertag', 'tag1', 'tag2' ] }, attributes)
 | 
			
		||||
    await completeVideoCheck(server.url, video, expectedAttributes)
 | 
			
		||||
  })
 | 
			
		||||
      const languages = res.body
 | 
			
		||||
      expect(Object.keys(languages)).to.have.length.above(5)
 | 
			
		||||
 | 
			
		||||
  it('Should like a video', async function () {
 | 
			
		||||
    await rateVideo(server.url, server.accessToken, videoId, 'like')
 | 
			
		||||
      expect(languages['ru']).to.equal('Russian')
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const res = await getVideo(server.url, videoId)
 | 
			
		||||
    const video = res.body
 | 
			
		||||
    it('Should list video privacies', async function () {
 | 
			
		||||
      const res = await getVideoPrivacies(server.url)
 | 
			
		||||
 | 
			
		||||
    expect(video.likes).to.equal(1)
 | 
			
		||||
    expect(video.dislikes).to.equal(0)
 | 
			
		||||
  })
 | 
			
		||||
      const privacies = res.body
 | 
			
		||||
      expect(Object.keys(privacies)).to.have.length.at.least(3)
 | 
			
		||||
 | 
			
		||||
  it('Should dislike the same video', async function () {
 | 
			
		||||
    await rateVideo(server.url, server.accessToken, videoId, 'dislike')
 | 
			
		||||
      expect(privacies[3]).to.equal('Private')
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const res = await getVideo(server.url, videoId)
 | 
			
		||||
    const video = res.body
 | 
			
		||||
    it('Should not have videos', async function () {
 | 
			
		||||
      const res = await getVideosList(server.url)
 | 
			
		||||
 | 
			
		||||
    expect(video.likes).to.equal(0)
 | 
			
		||||
    expect(video.dislikes).to.equal(1)
 | 
			
		||||
  })
 | 
			
		||||
      expect(res.body.total).to.equal(0)
 | 
			
		||||
      expect(res.body.data).to.be.an('array')
 | 
			
		||||
      expect(res.body.data.length).to.equal(0)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
  it('Should sort by originallyPublishedAt', async function () {
 | 
			
		||||
    {
 | 
			
		||||
    it('Should upload the video', async function () {
 | 
			
		||||
      this.timeout(10000)
 | 
			
		||||
 | 
			
		||||
      const videoAttributes = {
 | 
			
		||||
        name: 'my super name',
 | 
			
		||||
        category: 2,
 | 
			
		||||
        nsfw: true,
 | 
			
		||||
        licence: 6,
 | 
			
		||||
        tags: [ 'tag1', 'tag2', 'tag3' ]
 | 
			
		||||
      }
 | 
			
		||||
      const res = await uploadVideo(server.url, server.accessToken, videoAttributes, HttpStatusCode.OK_200, mode)
 | 
			
		||||
      expect(res.body.video).to.not.be.undefined
 | 
			
		||||
      expect(res.body.video.id).to.equal(1)
 | 
			
		||||
      expect(res.body.video.uuid).to.have.length.above(5)
 | 
			
		||||
 | 
			
		||||
      videoId = res.body.video.id
 | 
			
		||||
      videoUUID = res.body.video.uuid
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should get and seed the uploaded video', async function () {
 | 
			
		||||
      this.timeout(5000)
 | 
			
		||||
 | 
			
		||||
      const res = await getVideosList(server.url)
 | 
			
		||||
 | 
			
		||||
      expect(res.body.total).to.equal(1)
 | 
			
		||||
      expect(res.body.data).to.be.an('array')
 | 
			
		||||
      expect(res.body.data.length).to.equal(1)
 | 
			
		||||
 | 
			
		||||
      const video = res.body.data[0]
 | 
			
		||||
      await completeVideoCheck(server.url, video, getCheckAttributes())
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should get the video by UUID', async function () {
 | 
			
		||||
      this.timeout(5000)
 | 
			
		||||
 | 
			
		||||
      const res = await getVideo(server.url, videoUUID)
 | 
			
		||||
 | 
			
		||||
      const video = res.body
 | 
			
		||||
      await completeVideoCheck(server.url, video, getCheckAttributes())
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should have the views updated', async function () {
 | 
			
		||||
      this.timeout(20000)
 | 
			
		||||
 | 
			
		||||
      await viewVideo(server.url, videoId)
 | 
			
		||||
      await viewVideo(server.url, videoId)
 | 
			
		||||
      await viewVideo(server.url, videoId)
 | 
			
		||||
 | 
			
		||||
      await wait(1500)
 | 
			
		||||
 | 
			
		||||
      await viewVideo(server.url, videoId)
 | 
			
		||||
      await viewVideo(server.url, videoId)
 | 
			
		||||
 | 
			
		||||
      await wait(1500)
 | 
			
		||||
 | 
			
		||||
      await viewVideo(server.url, videoId)
 | 
			
		||||
      await viewVideo(server.url, videoId)
 | 
			
		||||
 | 
			
		||||
      // Wait the repeatable job
 | 
			
		||||
      await wait(8000)
 | 
			
		||||
 | 
			
		||||
      const res = await getVideo(server.url, videoId)
 | 
			
		||||
 | 
			
		||||
      const video = res.body
 | 
			
		||||
      expect(video.views).to.equal(3)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should remove the video', async function () {
 | 
			
		||||
      await removeVideo(server.url, server.accessToken, videoId)
 | 
			
		||||
 | 
			
		||||
      await checkVideoFilesWereRemoved(videoUUID, 1)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should not have videos', async function () {
 | 
			
		||||
      const res = await getVideosList(server.url)
 | 
			
		||||
 | 
			
		||||
      expect(res.body.total).to.equal(0)
 | 
			
		||||
      expect(res.body.data).to.be.an('array')
 | 
			
		||||
      expect(res.body.data).to.have.lengthOf(0)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should upload 6 videos', async function () {
 | 
			
		||||
      this.timeout(25000)
 | 
			
		||||
 | 
			
		||||
      const videos = new Set([
 | 
			
		||||
        'video_short.mp4', 'video_short.ogv', 'video_short.webm',
 | 
			
		||||
        'video_short1.webm', 'video_short2.webm', 'video_short3.webm'
 | 
			
		||||
      ])
 | 
			
		||||
 | 
			
		||||
      for (const video of videos) {
 | 
			
		||||
        const videoAttributes = {
 | 
			
		||||
          name: video + ' name',
 | 
			
		||||
          description: video + ' description',
 | 
			
		||||
          category: 2,
 | 
			
		||||
          licence: 1,
 | 
			
		||||
          language: 'en',
 | 
			
		||||
          nsfw: true,
 | 
			
		||||
          tags: [ 'tag1', 'tag2', 'tag3' ],
 | 
			
		||||
          fixture: video
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await uploadVideo(server.url, server.accessToken, videoAttributes, HttpStatusCode.OK_200, mode)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should have the correct durations', async function () {
 | 
			
		||||
      const res = await getVideosList(server.url)
 | 
			
		||||
 | 
			
		||||
      expect(res.body.total).to.equal(6)
 | 
			
		||||
      const videos = res.body.data
 | 
			
		||||
      expect(videos).to.be.an('array')
 | 
			
		||||
      expect(videos).to.have.lengthOf(6)
 | 
			
		||||
 | 
			
		||||
      const videosByName = keyBy<{ duration: number }>(videos, 'name')
 | 
			
		||||
      expect(videosByName['video_short.mp4 name'].duration).to.equal(5)
 | 
			
		||||
      expect(videosByName['video_short.ogv name'].duration).to.equal(5)
 | 
			
		||||
      expect(videosByName['video_short.webm name'].duration).to.equal(5)
 | 
			
		||||
      expect(videosByName['video_short1.webm name'].duration).to.equal(10)
 | 
			
		||||
      expect(videosByName['video_short2.webm name'].duration).to.equal(5)
 | 
			
		||||
      expect(videosByName['video_short3.webm name'].duration).to.equal(5)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should have the correct thumbnails', async function () {
 | 
			
		||||
      const res = await getVideosList(server.url)
 | 
			
		||||
 | 
			
		||||
      const videos = res.body.data
 | 
			
		||||
      // For the next test
 | 
			
		||||
      videosListBase = videos
 | 
			
		||||
 | 
			
		||||
      for (const video of videos) {
 | 
			
		||||
        const videoName = video.name.replace(' name', '')
 | 
			
		||||
        await testImage(server.url, videoName, video.thumbnailPath)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should list only the two first videos', async function () {
 | 
			
		||||
      const res = await getVideosListPagination(server.url, 0, 2, 'name')
 | 
			
		||||
 | 
			
		||||
      const videos = res.body.data
 | 
			
		||||
      expect(res.body.total).to.equal(6)
 | 
			
		||||
      expect(videos.length).to.equal(2)
 | 
			
		||||
      expect(videos[0].name).to.equal(videosListBase[0].name)
 | 
			
		||||
      expect(videos[1].name).to.equal(videosListBase[1].name)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should list only the next three videos', async function () {
 | 
			
		||||
      const res = await getVideosListPagination(server.url, 2, 3, 'name')
 | 
			
		||||
 | 
			
		||||
      const videos = res.body.data
 | 
			
		||||
      expect(res.body.total).to.equal(6)
 | 
			
		||||
      expect(videos.length).to.equal(3)
 | 
			
		||||
      expect(videos[0].name).to.equal(videosListBase[2].name)
 | 
			
		||||
      expect(videos[1].name).to.equal(videosListBase[3].name)
 | 
			
		||||
      expect(videos[2].name).to.equal(videosListBase[4].name)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should list the last video', async function () {
 | 
			
		||||
      const res = await getVideosListPagination(server.url, 5, 6, 'name')
 | 
			
		||||
 | 
			
		||||
      const videos = res.body.data
 | 
			
		||||
      expect(res.body.total).to.equal(6)
 | 
			
		||||
      expect(videos.length).to.equal(1)
 | 
			
		||||
      expect(videos[0].name).to.equal(videosListBase[5].name)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should not have the total field', async function () {
 | 
			
		||||
      const res = await getVideosListPagination(server.url, 5, 6, 'name', true)
 | 
			
		||||
 | 
			
		||||
      const videos = res.body.data
 | 
			
		||||
      expect(res.body.total).to.not.exist
 | 
			
		||||
      expect(videos.length).to.equal(1)
 | 
			
		||||
      expect(videos[0].name).to.equal(videosListBase[5].name)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should list and sort by name in descending order', async function () {
 | 
			
		||||
      const res = await getVideosListSort(server.url, '-name')
 | 
			
		||||
 | 
			
		||||
      const videos = res.body.data
 | 
			
		||||
      expect(res.body.total).to.equal(6)
 | 
			
		||||
      expect(videos.length).to.equal(6)
 | 
			
		||||
      expect(videos[0].name).to.equal('video_short.webm name')
 | 
			
		||||
      expect(videos[1].name).to.equal('video_short.ogv name')
 | 
			
		||||
      expect(videos[2].name).to.equal('video_short.mp4 name')
 | 
			
		||||
      expect(videos[3].name).to.equal('video_short3.webm name')
 | 
			
		||||
      expect(videos[4].name).to.equal('video_short2.webm name')
 | 
			
		||||
      expect(videos[5].name).to.equal('video_short1.webm name')
 | 
			
		||||
 | 
			
		||||
      videoId = videos[3].uuid
 | 
			
		||||
      videoId2 = videos[5].uuid
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should list and sort by trending in descending order', async function () {
 | 
			
		||||
      const res = await getVideosListPagination(server.url, 0, 2, '-trending')
 | 
			
		||||
 | 
			
		||||
      const videos = res.body.data
 | 
			
		||||
      expect(res.body.total).to.equal(6)
 | 
			
		||||
      expect(videos.length).to.equal(2)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should list and sort by hotness in descending order', async function () {
 | 
			
		||||
      const res = await getVideosListPagination(server.url, 0, 2, '-hot')
 | 
			
		||||
 | 
			
		||||
      const videos = res.body.data
 | 
			
		||||
      expect(res.body.total).to.equal(6)
 | 
			
		||||
      expect(videos.length).to.equal(2)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should list and sort by best in descending order', async function () {
 | 
			
		||||
      const res = await getVideosListPagination(server.url, 0, 2, '-best')
 | 
			
		||||
 | 
			
		||||
      const videos = res.body.data
 | 
			
		||||
      expect(res.body.total).to.equal(6)
 | 
			
		||||
      expect(videos.length).to.equal(2)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should update a video', async function () {
 | 
			
		||||
      const attributes = {
 | 
			
		||||
        name: 'my super video updated',
 | 
			
		||||
        category: 4,
 | 
			
		||||
        licence: 2,
 | 
			
		||||
        language: 'ar',
 | 
			
		||||
        nsfw: false,
 | 
			
		||||
        description: 'my super description updated',
 | 
			
		||||
        commentsEnabled: false,
 | 
			
		||||
        downloadEnabled: false,
 | 
			
		||||
        tags: [ 'tagup1', 'tagup2' ]
 | 
			
		||||
      }
 | 
			
		||||
      await updateVideo(server.url, server.accessToken, videoId, attributes)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should filter by tags and category', async function () {
 | 
			
		||||
      const res1 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 4 ] })
 | 
			
		||||
      expect(res1.body.total).to.equal(1)
 | 
			
		||||
      expect(res1.body.data[0].name).to.equal('my super video updated')
 | 
			
		||||
 | 
			
		||||
      const res2 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 3 ] })
 | 
			
		||||
      expect(res2.body.total).to.equal(0)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should have the video updated', async function () {
 | 
			
		||||
      this.timeout(60000)
 | 
			
		||||
 | 
			
		||||
      const res = await getVideo(server.url, videoId)
 | 
			
		||||
      const video = res.body
 | 
			
		||||
 | 
			
		||||
      await completeVideoCheck(server.url, video, updateCheckAttributes())
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should update only the tags of a video', async function () {
 | 
			
		||||
      const attributes = {
 | 
			
		||||
        tags: [ 'supertag', 'tag1', 'tag2' ]
 | 
			
		||||
      }
 | 
			
		||||
      await updateVideo(server.url, server.accessToken, videoId, attributes)
 | 
			
		||||
 | 
			
		||||
      const res = await getVideo(server.url, videoId)
 | 
			
		||||
      const video = res.body
 | 
			
		||||
 | 
			
		||||
      await completeVideoCheck(server.url, video, Object.assign(updateCheckAttributes(), attributes))
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should update only the description of a video', async function () {
 | 
			
		||||
      const attributes = {
 | 
			
		||||
        description: 'hello everybody'
 | 
			
		||||
      }
 | 
			
		||||
      await updateVideo(server.url, server.accessToken, videoId, attributes)
 | 
			
		||||
 | 
			
		||||
      const res = await getVideo(server.url, videoId)
 | 
			
		||||
      const video = res.body
 | 
			
		||||
 | 
			
		||||
      const expectedAttributes = Object.assign(updateCheckAttributes(), { tags: [ 'supertag', 'tag1', 'tag2' ] }, attributes)
 | 
			
		||||
      await completeVideoCheck(server.url, video, expectedAttributes)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should like a video', async function () {
 | 
			
		||||
      await rateVideo(server.url, server.accessToken, videoId, 'like')
 | 
			
		||||
 | 
			
		||||
      const res = await getVideo(server.url, videoId)
 | 
			
		||||
      const video = res.body
 | 
			
		||||
 | 
			
		||||
      expect(video.likes).to.equal(1)
 | 
			
		||||
      expect(video.dislikes).to.equal(0)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should dislike the same video', async function () {
 | 
			
		||||
      await rateVideo(server.url, server.accessToken, videoId, 'dislike')
 | 
			
		||||
 | 
			
		||||
      const res = await getVideo(server.url, videoId)
 | 
			
		||||
      const video = res.body
 | 
			
		||||
 | 
			
		||||
      expect(video.likes).to.equal(0)
 | 
			
		||||
      expect(video.dislikes).to.equal(1)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should sort by originallyPublishedAt', async function () {
 | 
			
		||||
      {
 | 
			
		||||
        const now = new Date()
 | 
			
		||||
        const attributes = { originallyPublishedAt: now.toISOString() }
 | 
			
		||||
| 
						 | 
				
			
			@ -483,10 +485,18 @@ describe('Test a single server', function () {
 | 
			
		|||
        expect(names[4]).to.equal('video_short.ogv name')
 | 
			
		||||
        expect(names[5]).to.equal('video_short.mp4 name')
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    after(async function () {
 | 
			
		||||
      await cleanupTests([ server ])
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  describe('Legacy upload', function () {
 | 
			
		||||
    runSuite('legacy')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  after(async function () {
 | 
			
		||||
    await cleanupTests([ server ])
 | 
			
		||||
  describe('Resumable upload', function () {
 | 
			
		||||
    runSuite('resumable')
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -361,106 +361,117 @@ describe('Test video transcoding', function () {
 | 
			
		|||
 | 
			
		||||
  describe('Audio upload', function () {
 | 
			
		||||
 | 
			
		||||
    before(async function () {
 | 
			
		||||
      await updateCustomSubConfig(servers[1].url, servers[1].accessToken, {
 | 
			
		||||
        transcoding: {
 | 
			
		||||
          hls: { enabled: true },
 | 
			
		||||
          webtorrent: { enabled: true },
 | 
			
		||||
          resolutions: {
 | 
			
		||||
            '0p': false,
 | 
			
		||||
            '240p': false,
 | 
			
		||||
            '360p': false,
 | 
			
		||||
            '480p': false,
 | 
			
		||||
            '720p': false,
 | 
			
		||||
            '1080p': false,
 | 
			
		||||
            '1440p': false,
 | 
			
		||||
            '2160p': false
 | 
			
		||||
    function runSuite (mode: 'legacy' | 'resumable') {
 | 
			
		||||
 | 
			
		||||
      before(async function () {
 | 
			
		||||
        await updateCustomSubConfig(servers[1].url, servers[1].accessToken, {
 | 
			
		||||
          transcoding: {
 | 
			
		||||
            hls: { enabled: true },
 | 
			
		||||
            webtorrent: { enabled: true },
 | 
			
		||||
            resolutions: {
 | 
			
		||||
              '0p': false,
 | 
			
		||||
              '240p': false,
 | 
			
		||||
              '360p': false,
 | 
			
		||||
              '480p': false,
 | 
			
		||||
              '720p': false,
 | 
			
		||||
              '1080p': false,
 | 
			
		||||
              '1440p': false,
 | 
			
		||||
              '2160p': false
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        })
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should merge an audio file with the preview file', async function () {
 | 
			
		||||
      this.timeout(60_000)
 | 
			
		||||
      it('Should merge an audio file with the preview file', async function () {
 | 
			
		||||
        this.timeout(60_000)
 | 
			
		||||
 | 
			
		||||
      const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' }
 | 
			
		||||
      await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg)
 | 
			
		||||
        const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' }
 | 
			
		||||
        await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg, HttpStatusCode.OK_200, mode)
 | 
			
		||||
 | 
			
		||||
      await waitJobs(servers)
 | 
			
		||||
        await waitJobs(servers)
 | 
			
		||||
 | 
			
		||||
      for (const server of servers) {
 | 
			
		||||
        const res = await getVideosList(server.url)
 | 
			
		||||
        for (const server of servers) {
 | 
			
		||||
          const res = await getVideosList(server.url)
 | 
			
		||||
 | 
			
		||||
        const video = res.body.data.find(v => v.name === 'audio_with_preview')
 | 
			
		||||
        const res2 = await getVideo(server.url, video.id)
 | 
			
		||||
        const videoDetails: VideoDetails = res2.body
 | 
			
		||||
          const video = res.body.data.find(v => v.name === 'audio_with_preview')
 | 
			
		||||
          const res2 = await getVideo(server.url, video.id)
 | 
			
		||||
          const videoDetails: VideoDetails = res2.body
 | 
			
		||||
 | 
			
		||||
        expect(videoDetails.files).to.have.lengthOf(1)
 | 
			
		||||
          expect(videoDetails.files).to.have.lengthOf(1)
 | 
			
		||||
 | 
			
		||||
        await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: HttpStatusCode.OK_200 })
 | 
			
		||||
        await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: HttpStatusCode.OK_200 })
 | 
			
		||||
          await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: HttpStatusCode.OK_200 })
 | 
			
		||||
          await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: HttpStatusCode.OK_200 })
 | 
			
		||||
 | 
			
		||||
        const magnetUri = videoDetails.files[0].magnetUri
 | 
			
		||||
        expect(magnetUri).to.contain('.mp4')
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should upload an audio file and choose a default background image', async function () {
 | 
			
		||||
      this.timeout(60_000)
 | 
			
		||||
 | 
			
		||||
      const videoAttributesArg = { name: 'audio_without_preview', fixture: 'sample.ogg' }
 | 
			
		||||
      await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg)
 | 
			
		||||
 | 
			
		||||
      await waitJobs(servers)
 | 
			
		||||
 | 
			
		||||
      for (const server of servers) {
 | 
			
		||||
        const res = await getVideosList(server.url)
 | 
			
		||||
 | 
			
		||||
        const video = res.body.data.find(v => v.name === 'audio_without_preview')
 | 
			
		||||
        const res2 = await getVideo(server.url, video.id)
 | 
			
		||||
        const videoDetails = res2.body
 | 
			
		||||
 | 
			
		||||
        expect(videoDetails.files).to.have.lengthOf(1)
 | 
			
		||||
 | 
			
		||||
        await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: HttpStatusCode.OK_200 })
 | 
			
		||||
        await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: HttpStatusCode.OK_200 })
 | 
			
		||||
 | 
			
		||||
        const magnetUri = videoDetails.files[0].magnetUri
 | 
			
		||||
        expect(magnetUri).to.contain('.mp4')
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should upload an audio file and create an audio version only', async function () {
 | 
			
		||||
      this.timeout(60_000)
 | 
			
		||||
 | 
			
		||||
      await updateCustomSubConfig(servers[1].url, servers[1].accessToken, {
 | 
			
		||||
        transcoding: {
 | 
			
		||||
          hls: { enabled: true },
 | 
			
		||||
          webtorrent: { enabled: true },
 | 
			
		||||
          resolutions: {
 | 
			
		||||
            '0p': true,
 | 
			
		||||
            '240p': false,
 | 
			
		||||
            '360p': false
 | 
			
		||||
          }
 | 
			
		||||
          const magnetUri = videoDetails.files[0].magnetUri
 | 
			
		||||
          expect(magnetUri).to.contain('.mp4')
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' }
 | 
			
		||||
      const resVideo = await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg)
 | 
			
		||||
      it('Should upload an audio file and choose a default background image', async function () {
 | 
			
		||||
        this.timeout(60_000)
 | 
			
		||||
 | 
			
		||||
      await waitJobs(servers)
 | 
			
		||||
        const videoAttributesArg = { name: 'audio_without_preview', fixture: 'sample.ogg' }
 | 
			
		||||
        await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg, HttpStatusCode.OK_200, mode)
 | 
			
		||||
 | 
			
		||||
      for (const server of servers) {
 | 
			
		||||
        const res2 = await getVideo(server.url, resVideo.body.video.id)
 | 
			
		||||
        const videoDetails: VideoDetails = res2.body
 | 
			
		||||
        await waitJobs(servers)
 | 
			
		||||
 | 
			
		||||
        for (const files of [ videoDetails.files, videoDetails.streamingPlaylists[0].files ]) {
 | 
			
		||||
          expect(files).to.have.lengthOf(2)
 | 
			
		||||
          expect(files.find(f => f.resolution.id === 0)).to.not.be.undefined
 | 
			
		||||
        for (const server of servers) {
 | 
			
		||||
          const res = await getVideosList(server.url)
 | 
			
		||||
 | 
			
		||||
          const video = res.body.data.find(v => v.name === 'audio_without_preview')
 | 
			
		||||
          const res2 = await getVideo(server.url, video.id)
 | 
			
		||||
          const videoDetails = res2.body
 | 
			
		||||
 | 
			
		||||
          expect(videoDetails.files).to.have.lengthOf(1)
 | 
			
		||||
 | 
			
		||||
          await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: HttpStatusCode.OK_200 })
 | 
			
		||||
          await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: HttpStatusCode.OK_200 })
 | 
			
		||||
 | 
			
		||||
          const magnetUri = videoDetails.files[0].magnetUri
 | 
			
		||||
          expect(magnetUri).to.contain('.mp4')
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await updateConfigForTranscoding(servers[1])
 | 
			
		||||
      it('Should upload an audio file and create an audio version only', async function () {
 | 
			
		||||
        this.timeout(60_000)
 | 
			
		||||
 | 
			
		||||
        await updateCustomSubConfig(servers[1].url, servers[1].accessToken, {
 | 
			
		||||
          transcoding: {
 | 
			
		||||
            hls: { enabled: true },
 | 
			
		||||
            webtorrent: { enabled: true },
 | 
			
		||||
            resolutions: {
 | 
			
		||||
              '0p': true,
 | 
			
		||||
              '240p': false,
 | 
			
		||||
              '360p': false
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' }
 | 
			
		||||
        const resVideo = await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg, HttpStatusCode.OK_200, mode)
 | 
			
		||||
 | 
			
		||||
        await waitJobs(servers)
 | 
			
		||||
 | 
			
		||||
        for (const server of servers) {
 | 
			
		||||
          const res2 = await getVideo(server.url, resVideo.body.video.id)
 | 
			
		||||
          const videoDetails: VideoDetails = res2.body
 | 
			
		||||
 | 
			
		||||
          for (const files of [ videoDetails.files, videoDetails.streamingPlaylists[0].files ]) {
 | 
			
		||||
            expect(files).to.have.lengthOf(2)
 | 
			
		||||
            expect(files.find(f => f.resolution.id === 0)).to.not.be.undefined
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await updateConfigForTranscoding(servers[1])
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    describe('Legacy upload', function () {
 | 
			
		||||
      runSuite('legacy')
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    describe('Resumable upload', function () {
 | 
			
		||||
      runSuite('resumable')
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										194
									
								
								server/typings/express/index.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										194
									
								
								server/typings/express/index.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -19,6 +19,9 @@ import { MPlugin, MServer, MServerBlocklist } from '@server/types/models/server'
 | 
			
		|||
import { MVideoImportDefault } from '@server/types/models/video/video-import'
 | 
			
		||||
import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element'
 | 
			
		||||
import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate'
 | 
			
		||||
import { HttpMethod } from '@shared/core-utils/miscs/http-methods'
 | 
			
		||||
import { VideoCreate } from '@shared/models'
 | 
			
		||||
import { File as UploadXFile, Metadata } from '@uploadx/core'
 | 
			
		||||
import { RegisteredPlugin } from '../../lib/plugins/plugin-manager'
 | 
			
		||||
import {
 | 
			
		||||
  MAccountDefault,
 | 
			
		||||
| 
						 | 
				
			
			@ -37,86 +40,125 @@ import {
 | 
			
		|||
  MVideoThumbnail,
 | 
			
		||||
  MVideoWithRights
 | 
			
		||||
} from '../../types/models'
 | 
			
		||||
 | 
			
		||||
declare module 'express' {
 | 
			
		||||
  export interface Request {
 | 
			
		||||
    query: any
 | 
			
		||||
    method: HttpMethod
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Upload using multer or uploadx middleware
 | 
			
		||||
  export type MulterOrUploadXFile = UploadXFile | Express.Multer.File
 | 
			
		||||
 | 
			
		||||
  export type UploadFiles = {
 | 
			
		||||
    [fieldname: string]: MulterOrUploadXFile[]
 | 
			
		||||
  } | MulterOrUploadXFile[]
 | 
			
		||||
 | 
			
		||||
  // Partial object used by some functions to check the file mimetype/extension
 | 
			
		||||
  export type UploadFileForCheck = {
 | 
			
		||||
    originalname: string
 | 
			
		||||
    mimetype: string
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export type UploadFilesForCheck = {
 | 
			
		||||
    [fieldname: string]: UploadFileForCheck[]
 | 
			
		||||
  } | UploadFileForCheck[]
 | 
			
		||||
 | 
			
		||||
  // Upload file with a duration added by our middleware
 | 
			
		||||
  export type VideoUploadFile = Pick<Express.Multer.File, 'path' | 'filename' | 'size'> & {
 | 
			
		||||
    duration: number
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Extends Metadata property of UploadX object
 | 
			
		||||
  export type UploadXFileMetadata = Metadata & VideoCreate & {
 | 
			
		||||
    previewfile: Express.Multer.File[]
 | 
			
		||||
    thumbnailfile: Express.Multer.File[]
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Our custom UploadXFile object using our custom metadata
 | 
			
		||||
  export type CustomUploadXFile <T extends Metadata> = UploadXFile & { metadata: T }
 | 
			
		||||
 | 
			
		||||
  export type EnhancedUploadXFile = CustomUploadXFile<UploadXFileMetadata> & {
 | 
			
		||||
    duration: number
 | 
			
		||||
    path: string
 | 
			
		||||
    filename: string
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Extends locals property from Response
 | 
			
		||||
  interface Response {
 | 
			
		||||
    locals: PeerTubeLocals
 | 
			
		||||
    locals: {
 | 
			
		||||
      videoAll?: MVideoFullLight
 | 
			
		||||
      onlyImmutableVideo?: MVideoImmutable
 | 
			
		||||
      onlyVideo?: MVideoThumbnail
 | 
			
		||||
      onlyVideoWithRights?: MVideoWithRights
 | 
			
		||||
      videoId?: MVideoIdThumbnail
 | 
			
		||||
 | 
			
		||||
      videoLive?: MVideoLive
 | 
			
		||||
 | 
			
		||||
      videoShare?: MVideoShareActor
 | 
			
		||||
 | 
			
		||||
      videoFile?: MVideoFile
 | 
			
		||||
 | 
			
		||||
      videoFileResumable?: EnhancedUploadXFile
 | 
			
		||||
 | 
			
		||||
      videoImport?: MVideoImportDefault
 | 
			
		||||
 | 
			
		||||
      videoBlacklist?: MVideoBlacklist
 | 
			
		||||
 | 
			
		||||
      videoCaption?: MVideoCaptionVideo
 | 
			
		||||
 | 
			
		||||
      abuse?: MAbuseReporter
 | 
			
		||||
      abuseMessage?: MAbuseMessage
 | 
			
		||||
 | 
			
		||||
      videoStreamingPlaylist?: MStreamingPlaylist
 | 
			
		||||
 | 
			
		||||
      videoChannel?: MChannelBannerAccountDefault
 | 
			
		||||
 | 
			
		||||
      videoPlaylistFull?: MVideoPlaylistFull
 | 
			
		||||
      videoPlaylistSummary?: MVideoPlaylistFullSummary
 | 
			
		||||
 | 
			
		||||
      videoPlaylistElement?: MVideoPlaylistElement
 | 
			
		||||
      videoPlaylistElementAP?: MVideoPlaylistElementVideoUrlPlaylistPrivacy
 | 
			
		||||
 | 
			
		||||
      accountVideoRate?: MAccountVideoRateAccountVideo
 | 
			
		||||
 | 
			
		||||
      videoCommentFull?: MCommentOwnerVideoReply
 | 
			
		||||
      videoCommentThread?: MComment
 | 
			
		||||
 | 
			
		||||
      follow?: MActorFollowActorsDefault
 | 
			
		||||
      subscription?: MActorFollowActorsDefaultSubscription
 | 
			
		||||
 | 
			
		||||
      nextOwner?: MAccountDefault
 | 
			
		||||
      videoChangeOwnership?: MVideoChangeOwnershipFull
 | 
			
		||||
 | 
			
		||||
      account?: MAccountDefault
 | 
			
		||||
 | 
			
		||||
      actorUrl?: MActorUrl
 | 
			
		||||
      actorFull?: MActorFull
 | 
			
		||||
 | 
			
		||||
      user?: MUserDefault
 | 
			
		||||
 | 
			
		||||
      server?: MServer
 | 
			
		||||
 | 
			
		||||
      videoRedundancy?: MVideoRedundancyVideo
 | 
			
		||||
 | 
			
		||||
      accountBlock?: MAccountBlocklist
 | 
			
		||||
      serverBlock?: MServerBlocklist
 | 
			
		||||
 | 
			
		||||
      oauth?: {
 | 
			
		||||
        token: MOAuthTokenUser
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      signature?: {
 | 
			
		||||
        actor: MActorAccountChannelId
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      authenticated?: boolean
 | 
			
		||||
 | 
			
		||||
      registeredPlugin?: RegisteredPlugin
 | 
			
		||||
 | 
			
		||||
      externalAuth?: RegisterServerAuthExternalOptions
 | 
			
		||||
 | 
			
		||||
      plugin?: MPlugin
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface PeerTubeLocals {
 | 
			
		||||
  videoAll?: MVideoFullLight
 | 
			
		||||
  onlyImmutableVideo?: MVideoImmutable
 | 
			
		||||
  onlyVideo?: MVideoThumbnail
 | 
			
		||||
  onlyVideoWithRights?: MVideoWithRights
 | 
			
		||||
  videoId?: MVideoIdThumbnail
 | 
			
		||||
 | 
			
		||||
  videoLive?: MVideoLive
 | 
			
		||||
 | 
			
		||||
  videoShare?: MVideoShareActor
 | 
			
		||||
 | 
			
		||||
  videoFile?: MVideoFile
 | 
			
		||||
 | 
			
		||||
  videoImport?: MVideoImportDefault
 | 
			
		||||
 | 
			
		||||
  videoBlacklist?: MVideoBlacklist
 | 
			
		||||
 | 
			
		||||
  videoCaption?: MVideoCaptionVideo
 | 
			
		||||
 | 
			
		||||
  abuse?: MAbuseReporter
 | 
			
		||||
  abuseMessage?: MAbuseMessage
 | 
			
		||||
 | 
			
		||||
  videoStreamingPlaylist?: MStreamingPlaylist
 | 
			
		||||
 | 
			
		||||
  videoChannel?: MChannelBannerAccountDefault
 | 
			
		||||
 | 
			
		||||
  videoPlaylistFull?: MVideoPlaylistFull
 | 
			
		||||
  videoPlaylistSummary?: MVideoPlaylistFullSummary
 | 
			
		||||
 | 
			
		||||
  videoPlaylistElement?: MVideoPlaylistElement
 | 
			
		||||
  videoPlaylistElementAP?: MVideoPlaylistElementVideoUrlPlaylistPrivacy
 | 
			
		||||
 | 
			
		||||
  accountVideoRate?: MAccountVideoRateAccountVideo
 | 
			
		||||
 | 
			
		||||
  videoCommentFull?: MCommentOwnerVideoReply
 | 
			
		||||
  videoCommentThread?: MComment
 | 
			
		||||
 | 
			
		||||
  follow?: MActorFollowActorsDefault
 | 
			
		||||
  subscription?: MActorFollowActorsDefaultSubscription
 | 
			
		||||
 | 
			
		||||
  nextOwner?: MAccountDefault
 | 
			
		||||
  videoChangeOwnership?: MVideoChangeOwnershipFull
 | 
			
		||||
 | 
			
		||||
  account?: MAccountDefault
 | 
			
		||||
 | 
			
		||||
  actorUrl?: MActorUrl
 | 
			
		||||
  actorFull?: MActorFull
 | 
			
		||||
 | 
			
		||||
  user?: MUserDefault
 | 
			
		||||
 | 
			
		||||
  server?: MServer
 | 
			
		||||
 | 
			
		||||
  videoRedundancy?: MVideoRedundancyVideo
 | 
			
		||||
 | 
			
		||||
  accountBlock?: MAccountBlocklist
 | 
			
		||||
  serverBlock?: MServerBlocklist
 | 
			
		||||
 | 
			
		||||
  oauth?: {
 | 
			
		||||
    token: MOAuthTokenUser
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  signature?: {
 | 
			
		||||
    actor: MActorAccountChannelId
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  authenticated?: boolean
 | 
			
		||||
 | 
			
		||||
  registeredPlugin?: RegisteredPlugin
 | 
			
		||||
 | 
			
		||||
  externalAuth?: RegisterServerAuthExternalOptions
 | 
			
		||||
 | 
			
		||||
  plugin?: MPlugin
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										21
									
								
								shared/core-utils/miscs/http-methods.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								shared/core-utils/miscs/http-methods.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
/** HTTP request method to indicate the desired action to be performed for a given resource. */
 | 
			
		||||
export enum HttpMethod {
 | 
			
		||||
  /** The CONNECT method establishes a tunnel to the server identified by the target resource. */
 | 
			
		||||
  CONNECT = 'CONNECT',
 | 
			
		||||
  /** The DELETE method deletes the specified resource. */
 | 
			
		||||
  DELETE = 'DELETE',
 | 
			
		||||
  /** The GET method requests a representation of the specified resource. Requests using GET should only retrieve data. */
 | 
			
		||||
  GET = 'GET',
 | 
			
		||||
  /** The HEAD method asks for a response identical to that of a GET request, but without the response body. */
 | 
			
		||||
  HEAD = 'HEAD',
 | 
			
		||||
  /** The OPTIONS method is used to describe the communication options for the target resource. */
 | 
			
		||||
  OPTIONS = 'OPTIONS',
 | 
			
		||||
  /** The PATCH method is used to apply partial modifications to a resource. */
 | 
			
		||||
  PATCH = 'PATCH',
 | 
			
		||||
  /** The POST method is used to submit an entity to the specified resource */
 | 
			
		||||
  POST = 'POST',
 | 
			
		||||
  /** The PUT method replaces all current representations of the target resource with the request payload. */
 | 
			
		||||
  PUT = 'PUT',
 | 
			
		||||
  /** The TRACE method performs a message loop-back test along the path to the target resource. */
 | 
			
		||||
  TRACE = 'TRACE'
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2,3 +2,4 @@ export * from './date'
 | 
			
		|||
export * from './miscs'
 | 
			
		||||
export * from './types'
 | 
			
		||||
export * from './http-error-codes'
 | 
			
		||||
export * from './http-methods'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
import { makeGetRequest } from '../requests/requests'
 | 
			
		||||
import { makeGetRequest, makePostBodyRequest } from '../requests/requests'
 | 
			
		||||
import { HttpStatusCode } from '../../core-utils/miscs/http-error-codes'
 | 
			
		||||
import { SendDebugCommand } from '@shared/models'
 | 
			
		||||
 | 
			
		||||
function getDebug (url: string, token: string) {
 | 
			
		||||
  const path = '/api/v1/server/debug'
 | 
			
		||||
| 
						 | 
				
			
			@ -12,8 +13,21 @@ function getDebug (url: string, token: string) {
 | 
			
		|||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function sendDebugCommand (url: string, token: string, body: SendDebugCommand) {
 | 
			
		||||
  const path = '/api/v1/server/debug/run-command'
 | 
			
		||||
 | 
			
		||||
  return makePostBodyRequest({
 | 
			
		||||
    url,
 | 
			
		||||
    path,
 | 
			
		||||
    token,
 | 
			
		||||
    fields: body,
 | 
			
		||||
    statusCodeExpected: HttpStatusCode.NO_CONTENT_204
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  getDebug
 | 
			
		||||
  getDebug,
 | 
			
		||||
  sendDebugCommand
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -274,7 +274,7 @@ async function reRunServer (server: ServerInfo, configOverride?: any) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
async function checkTmpIsEmpty (server: ServerInfo) {
 | 
			
		||||
  await checkDirectoryIsEmpty(server, 'tmp', [ 'plugins-global.css', 'hls' ])
 | 
			
		||||
  await checkDirectoryIsEmpty(server, 'tmp', [ 'plugins-global.css', 'hls', 'resumable-uploads' ])
 | 
			
		||||
 | 
			
		||||
  if (await pathExists(join('test' + server.internalServerNumber, 'tmp', 'hls'))) {
 | 
			
		||||
    await checkDirectoryIsEmpty(server, 'tmp/hls')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,7 @@ import { VideoChannelUpdate } from '../../models/videos/channel/video-channel-up
 | 
			
		|||
import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model'
 | 
			
		||||
import { makeDeleteRequest, makeGetRequest, updateImageRequest } from '../requests/requests'
 | 
			
		||||
import { ServerInfo } from '../server/servers'
 | 
			
		||||
import { User } from '../../models/users/user.model'
 | 
			
		||||
import { MyUser, User } from '../../models/users/user.model'
 | 
			
		||||
import { getMyUserInformation } from '../users/users'
 | 
			
		||||
import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -170,6 +170,12 @@ function setDefaultVideoChannel (servers: ServerInfo[]) {
 | 
			
		|||
  return Promise.all(tasks)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getDefaultVideoChannel (url: string, token: string) {
 | 
			
		||||
  const res = await getMyUserInformation(url, token)
 | 
			
		||||
 | 
			
		||||
  return (res.body as MyUser).videoChannels[0].id
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
| 
						 | 
				
			
			@ -181,5 +187,6 @@ export {
 | 
			
		|||
  deleteVideoChannel,
 | 
			
		||||
  getVideoChannel,
 | 
			
		||||
  setDefaultVideoChannel,
 | 
			
		||||
  deleteVideoChannelImage
 | 
			
		||||
  deleteVideoChannelImage,
 | 
			
		||||
  getDefaultVideoChannel
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,8 @@
 | 
			
		|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
 | 
			
		||||
 | 
			
		||||
import { expect } from 'chai'
 | 
			
		||||
import { pathExists, readdir, readFile } from 'fs-extra'
 | 
			
		||||
import { createReadStream, pathExists, readdir, readFile, stat } from 'fs-extra'
 | 
			
		||||
import got, { Response as GotResponse } from 'got/dist/source'
 | 
			
		||||
import * as parseTorrent from 'parse-torrent'
 | 
			
		||||
import { extname, join } from 'path'
 | 
			
		||||
import * as request from 'supertest'
 | 
			
		||||
| 
						 | 
				
			
			@ -42,6 +43,7 @@ type VideoAttributes = {
 | 
			
		|||
  channelId?: number
 | 
			
		||||
  privacy?: VideoPrivacy
 | 
			
		||||
  fixture?: string
 | 
			
		||||
  support?: string
 | 
			
		||||
  thumbnailfile?: string
 | 
			
		||||
  previewfile?: string
 | 
			
		||||
  scheduleUpdate?: {
 | 
			
		||||
| 
						 | 
				
			
			@ -364,8 +366,13 @@ async function checkVideoFilesWereRemoved (
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function uploadVideo (url: string, accessToken: string, videoAttributesArg: VideoAttributes, specialStatus = HttpStatusCode.OK_200) {
 | 
			
		||||
  const path = '/api/v1/videos/upload'
 | 
			
		||||
async function uploadVideo (
 | 
			
		||||
  url: string,
 | 
			
		||||
  accessToken: string,
 | 
			
		||||
  videoAttributesArg: VideoAttributes,
 | 
			
		||||
  specialStatus = HttpStatusCode.OK_200,
 | 
			
		||||
  mode: 'legacy' | 'resumable' = 'legacy'
 | 
			
		||||
) {
 | 
			
		||||
  let defaultChannelId = '1'
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
| 
						 | 
				
			
			@ -391,61 +398,9 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg
 | 
			
		|||
    fixture: 'video_short.webm'
 | 
			
		||||
  }, videoAttributesArg)
 | 
			
		||||
 | 
			
		||||
  const req = request(url)
 | 
			
		||||
              .post(path)
 | 
			
		||||
              .set('Accept', 'application/json')
 | 
			
		||||
              .set('Authorization', 'Bearer ' + accessToken)
 | 
			
		||||
              .field('name', attributes.name)
 | 
			
		||||
              .field('nsfw', JSON.stringify(attributes.nsfw))
 | 
			
		||||
              .field('commentsEnabled', JSON.stringify(attributes.commentsEnabled))
 | 
			
		||||
              .field('downloadEnabled', JSON.stringify(attributes.downloadEnabled))
 | 
			
		||||
              .field('waitTranscoding', JSON.stringify(attributes.waitTranscoding))
 | 
			
		||||
              .field('privacy', attributes.privacy.toString())
 | 
			
		||||
              .field('channelId', attributes.channelId)
 | 
			
		||||
 | 
			
		||||
  if (attributes.support !== undefined) {
 | 
			
		||||
    req.field('support', attributes.support)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (attributes.description !== undefined) {
 | 
			
		||||
    req.field('description', attributes.description)
 | 
			
		||||
  }
 | 
			
		||||
  if (attributes.language !== undefined) {
 | 
			
		||||
    req.field('language', attributes.language.toString())
 | 
			
		||||
  }
 | 
			
		||||
  if (attributes.category !== undefined) {
 | 
			
		||||
    req.field('category', attributes.category.toString())
 | 
			
		||||
  }
 | 
			
		||||
  if (attributes.licence !== undefined) {
 | 
			
		||||
    req.field('licence', attributes.licence.toString())
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const tags = attributes.tags || []
 | 
			
		||||
  for (let i = 0; i < tags.length; i++) {
 | 
			
		||||
    req.field('tags[' + i + ']', attributes.tags[i])
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (attributes.thumbnailfile !== undefined) {
 | 
			
		||||
    req.attach('thumbnailfile', buildAbsoluteFixturePath(attributes.thumbnailfile))
 | 
			
		||||
  }
 | 
			
		||||
  if (attributes.previewfile !== undefined) {
 | 
			
		||||
    req.attach('previewfile', buildAbsoluteFixturePath(attributes.previewfile))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (attributes.scheduleUpdate) {
 | 
			
		||||
    req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt)
 | 
			
		||||
 | 
			
		||||
    if (attributes.scheduleUpdate.privacy) {
 | 
			
		||||
      req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (attributes.originallyPublishedAt !== undefined) {
 | 
			
		||||
    req.field('originallyPublishedAt', attributes.originallyPublishedAt)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const res = await req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
 | 
			
		||||
            .expect(specialStatus)
 | 
			
		||||
  const res = mode === 'legacy'
 | 
			
		||||
    ? await buildLegacyUpload(url, accessToken, attributes, specialStatus)
 | 
			
		||||
    : await buildResumeUpload(url, accessToken, attributes, specialStatus)
 | 
			
		||||
 | 
			
		||||
  // Wait torrent generation
 | 
			
		||||
  if (specialStatus === HttpStatusCode.OK_200) {
 | 
			
		||||
| 
						 | 
				
			
			@ -461,6 +416,154 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg
 | 
			
		|||
  return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function checkUploadVideoParam (
 | 
			
		||||
  url: string,
 | 
			
		||||
  token: string,
 | 
			
		||||
  attributes: Partial<VideoAttributes>,
 | 
			
		||||
  specialStatus = HttpStatusCode.OK_200,
 | 
			
		||||
  mode: 'legacy' | 'resumable' = 'legacy'
 | 
			
		||||
) {
 | 
			
		||||
  return mode === 'legacy'
 | 
			
		||||
    ? buildLegacyUpload(url, token, attributes, specialStatus)
 | 
			
		||||
    : buildResumeUpload(url, token, attributes, specialStatus)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function buildLegacyUpload (url: string, token: string, attributes: VideoAttributes, specialStatus = HttpStatusCode.OK_200) {
 | 
			
		||||
  const path = '/api/v1/videos/upload'
 | 
			
		||||
  const req = request(url)
 | 
			
		||||
              .post(path)
 | 
			
		||||
              .set('Accept', 'application/json')
 | 
			
		||||
              .set('Authorization', 'Bearer ' + token)
 | 
			
		||||
 | 
			
		||||
  buildUploadReq(req, attributes)
 | 
			
		||||
 | 
			
		||||
  if (attributes.fixture !== undefined) {
 | 
			
		||||
    req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return req.expect(specialStatus)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function buildResumeUpload (url: string, token: string, attributes: VideoAttributes, specialStatus = HttpStatusCode.OK_200) {
 | 
			
		||||
  let size = 0
 | 
			
		||||
  let videoFilePath: string
 | 
			
		||||
  let mimetype = 'video/mp4'
 | 
			
		||||
 | 
			
		||||
  if (attributes.fixture) {
 | 
			
		||||
    videoFilePath = buildAbsoluteFixturePath(attributes.fixture)
 | 
			
		||||
    size = (await stat(videoFilePath)).size
 | 
			
		||||
 | 
			
		||||
    if (videoFilePath.endsWith('.mkv')) {
 | 
			
		||||
      mimetype = 'video/x-matroska'
 | 
			
		||||
    } else if (videoFilePath.endsWith('.webm')) {
 | 
			
		||||
      mimetype = 'video/webm'
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const initializeSessionRes = await prepareResumableUpload({ url, token, attributes, size, mimetype })
 | 
			
		||||
  const initStatus = initializeSessionRes.status
 | 
			
		||||
 | 
			
		||||
  if (videoFilePath && initStatus === HttpStatusCode.CREATED_201) {
 | 
			
		||||
    const locationHeader = initializeSessionRes.header['location']
 | 
			
		||||
    expect(locationHeader).to.not.be.undefined
 | 
			
		||||
 | 
			
		||||
    const pathUploadId = locationHeader.split('?')[1]
 | 
			
		||||
 | 
			
		||||
    return sendResumableChunks({ url, token, pathUploadId, videoFilePath, size, specialStatus })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const expectedInitStatus = specialStatus === HttpStatusCode.OK_200
 | 
			
		||||
    ? HttpStatusCode.CREATED_201
 | 
			
		||||
    : specialStatus
 | 
			
		||||
 | 
			
		||||
  expect(initStatus).to.equal(expectedInitStatus)
 | 
			
		||||
 | 
			
		||||
  return initializeSessionRes
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function prepareResumableUpload (options: {
 | 
			
		||||
  url: string
 | 
			
		||||
  token: string
 | 
			
		||||
  attributes: VideoAttributes
 | 
			
		||||
  size: number
 | 
			
		||||
  mimetype: string
 | 
			
		||||
}) {
 | 
			
		||||
  const { url, token, attributes, size, mimetype } = options
 | 
			
		||||
 | 
			
		||||
  const path = '/api/v1/videos/upload-resumable'
 | 
			
		||||
 | 
			
		||||
  const req = request(url)
 | 
			
		||||
              .post(path)
 | 
			
		||||
              .set('Authorization', 'Bearer ' + token)
 | 
			
		||||
              .set('X-Upload-Content-Type', mimetype)
 | 
			
		||||
              .set('X-Upload-Content-Length', size.toString())
 | 
			
		||||
 | 
			
		||||
  buildUploadReq(req, attributes)
 | 
			
		||||
 | 
			
		||||
  if (attributes.fixture) {
 | 
			
		||||
    req.field('filename', attributes.fixture)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return req
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function sendResumableChunks (options: {
 | 
			
		||||
  url: string
 | 
			
		||||
  token: string
 | 
			
		||||
  pathUploadId: string
 | 
			
		||||
  videoFilePath: string
 | 
			
		||||
  size: number
 | 
			
		||||
  specialStatus?: HttpStatusCode
 | 
			
		||||
  contentLength?: number
 | 
			
		||||
  contentRangeBuilder?: (start: number, chunk: any) => string
 | 
			
		||||
}) {
 | 
			
		||||
  const { url, token, pathUploadId, videoFilePath, size, specialStatus, contentLength, contentRangeBuilder } = options
 | 
			
		||||
 | 
			
		||||
  const expectedStatus = specialStatus || HttpStatusCode.OK_200
 | 
			
		||||
 | 
			
		||||
  const path = '/api/v1/videos/upload-resumable'
 | 
			
		||||
  let start = 0
 | 
			
		||||
 | 
			
		||||
  const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 })
 | 
			
		||||
  return new Promise<GotResponse>((resolve, reject) => {
 | 
			
		||||
    readable.on('data', async function onData (chunk) {
 | 
			
		||||
      readable.pause()
 | 
			
		||||
 | 
			
		||||
      const headers = {
 | 
			
		||||
        'Authorization': 'Bearer ' + token,
 | 
			
		||||
        'Content-Type': 'application/octet-stream',
 | 
			
		||||
        'Content-Range': contentRangeBuilder
 | 
			
		||||
          ? contentRangeBuilder(start, chunk)
 | 
			
		||||
          : `bytes ${start}-${start + chunk.length - 1}/${size}`,
 | 
			
		||||
        'Content-Length': contentLength ? contentLength + '' : chunk.length + ''
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const res = await got({
 | 
			
		||||
        url,
 | 
			
		||||
        method: 'put',
 | 
			
		||||
        headers,
 | 
			
		||||
        path: path + '?' + pathUploadId,
 | 
			
		||||
        body: chunk,
 | 
			
		||||
        responseType: 'json',
 | 
			
		||||
        throwHttpErrors: false
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      start += chunk.length
 | 
			
		||||
 | 
			
		||||
      if (res.statusCode === expectedStatus) {
 | 
			
		||||
        return resolve(res)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) {
 | 
			
		||||
        readable.off('data', onData)
 | 
			
		||||
        return reject(new Error('Incorrect transient behaviour sending intermediary chunks'))
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      readable.resume()
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function updateVideo (
 | 
			
		||||
  url: string,
 | 
			
		||||
  accessToken: string,
 | 
			
		||||
| 
						 | 
				
			
			@ -749,11 +852,13 @@ export {
 | 
			
		|||
  getVideoWithToken,
 | 
			
		||||
  getVideosList,
 | 
			
		||||
  removeAllVideos,
 | 
			
		||||
  checkUploadVideoParam,
 | 
			
		||||
  getVideosListPagination,
 | 
			
		||||
  getVideosListSort,
 | 
			
		||||
  removeVideo,
 | 
			
		||||
  getVideosListWithToken,
 | 
			
		||||
  uploadVideo,
 | 
			
		||||
  sendResumableChunks,
 | 
			
		||||
  getVideosWithFilters,
 | 
			
		||||
  uploadRandomVideoOnServers,
 | 
			
		||||
  updateVideo,
 | 
			
		||||
| 
						 | 
				
			
			@ -767,5 +872,50 @@ export {
 | 
			
		|||
  getMyVideosWithFilter,
 | 
			
		||||
  uploadVideoAndGetId,
 | 
			
		||||
  getLocalIdByUUID,
 | 
			
		||||
  getVideoIdFromUUID
 | 
			
		||||
  getVideoIdFromUUID,
 | 
			
		||||
  prepareResumableUpload
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
function buildUploadReq (req: request.Test, attributes: VideoAttributes) {
 | 
			
		||||
 | 
			
		||||
  for (const key of [ 'name', 'support', 'channelId', 'description', 'originallyPublishedAt' ]) {
 | 
			
		||||
    if (attributes[key] !== undefined) {
 | 
			
		||||
      req.field(key, attributes[key])
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (const key of [ 'nsfw', 'commentsEnabled', 'downloadEnabled', 'waitTranscoding' ]) {
 | 
			
		||||
    if (attributes[key] !== undefined) {
 | 
			
		||||
      req.field(key, JSON.stringify(attributes[key]))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (const key of [ 'language', 'privacy', 'category', 'licence' ]) {
 | 
			
		||||
    if (attributes[key] !== undefined) {
 | 
			
		||||
      req.field(key, attributes[key].toString())
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const tags = attributes.tags || []
 | 
			
		||||
  for (let i = 0; i < tags.length; i++) {
 | 
			
		||||
    req.field('tags[' + i + ']', attributes.tags[i])
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (const key of [ 'thumbnailfile', 'previewfile' ]) {
 | 
			
		||||
    if (attributes[key] !== undefined) {
 | 
			
		||||
      req.attach(key, buildAbsoluteFixturePath(attributes[key]))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (attributes.scheduleUpdate) {
 | 
			
		||||
    if (attributes.scheduleUpdate.updateAt) {
 | 
			
		||||
      req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (attributes.scheduleUpdate.privacy) {
 | 
			
		||||
      req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,7 @@
 | 
			
		|||
export interface Debug {
 | 
			
		||||
  ip: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface SendDebugCommand {
 | 
			
		||||
  command: 'remove-dandling-resumable-uploads'
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -125,7 +125,7 @@ tags:
 | 
			
		|||
      Redundancy is part of the inter-server solidarity that PeerTube fosters.
 | 
			
		||||
      Manage the list of instances you wish to help by seeding their videos according
 | 
			
		||||
      to the policy of video selection of your choice. Note that you have a similar functionality
 | 
			
		||||
      to mirror individual videos, see `Video Mirroring`.
 | 
			
		||||
      to mirror individual videos, see [video mirroring](#tag/Video-Mirroring).
 | 
			
		||||
    externalDocs:
 | 
			
		||||
      url: https://docs.joinpeertube.org/admin-following-instances?id=instances-redundancy
 | 
			
		||||
  - name: Plugins
 | 
			
		||||
| 
						 | 
				
			
			@ -139,6 +139,50 @@ tags:
 | 
			
		|||
  - name: Video
 | 
			
		||||
    description: |
 | 
			
		||||
      Operations dealing with listing, uploading, fetching or modifying videos.
 | 
			
		||||
  - name: Video Upload
 | 
			
		||||
    description: |
 | 
			
		||||
      Operations dealing with adding video or audio. PeerTube supports two upload modes, and three import modes.
 | 
			
		||||
 | 
			
		||||
      ### Upload
 | 
			
		||||
 | 
			
		||||
      - [_legacy_](#tag/Video-Upload/paths/~1videos~1upload/post), where the video file is sent in a single request
 | 
			
		||||
      - [_resumable_](#tag/Video-Upload/paths/~1videos~1upload-resumable/post), where the video file is sent in chunks
 | 
			
		||||
 | 
			
		||||
      You can upload videos more reliably by using the resumable variant. Its protocol lets
 | 
			
		||||
      you resume an upload operation after a network interruption or other transmission failure,
 | 
			
		||||
      saving time and bandwidth in the event of network failures.
 | 
			
		||||
 | 
			
		||||
      Favor using resumable uploads in any of the following cases:
 | 
			
		||||
      - You are transferring large files
 | 
			
		||||
      - The likelihood of a network interruption is high
 | 
			
		||||
      - Uploads are originating from a device with a low-bandwidth or unstable Internet connection,
 | 
			
		||||
        such as a mobile device
 | 
			
		||||
 | 
			
		||||
      ### Import
 | 
			
		||||
 | 
			
		||||
      - _URL_-based: where the URL points to any service supported by [youtube-dl](https://ytdl-org.github.io/youtube-dl/)
 | 
			
		||||
      - _magnet_-based: where the URI resolves to a BitTorrent ressource containing a single supported video file
 | 
			
		||||
      - _torrent_-based: where the metainfo file resolves to a BitTorrent ressource containing a single supported video file
 | 
			
		||||
 | 
			
		||||
      The import function is practical when the desired video/audio is available online. It makes PeerTube
 | 
			
		||||
      download it for you, saving you as much bandwidth and avoiding any instability or limitation your network might have.
 | 
			
		||||
  - name: Video Captions
 | 
			
		||||
    description: Operations dealing with listing, adding and removing closed captions of a video.
 | 
			
		||||
  - name: Video Channels
 | 
			
		||||
    description: Operations dealing with the creation, modification and listing of videos within a channel.
 | 
			
		||||
  - name: Video Comments
 | 
			
		||||
    description: >
 | 
			
		||||
      Operations dealing with comments to a video. Comments are organized in threads: adding a
 | 
			
		||||
      comment in response to the video starts a thread, adding a reply to a comment adds it to
 | 
			
		||||
      its root comment thread.
 | 
			
		||||
  - name: Video Blocks
 | 
			
		||||
    description: Operations dealing with blocking videos (removing them from view and preventing interactions).
 | 
			
		||||
  - name: Video Rates
 | 
			
		||||
    description: Like/dislike a video.
 | 
			
		||||
  - name: Video Playlists
 | 
			
		||||
    description: Operations dealing with playlists of videos. Playlists are bound to users and/or channels.
 | 
			
		||||
  - name: Feeds
 | 
			
		||||
    description: Server syndication feeds
 | 
			
		||||
  - name: Search
 | 
			
		||||
    description: |
 | 
			
		||||
      The search helps to find _videos_ or _channels_ from within the instance and beyond.
 | 
			
		||||
| 
						 | 
				
			
			@ -148,27 +192,11 @@ tags:
 | 
			
		|||
 | 
			
		||||
      Administrators can also enable the use of a remote search system, indexing
 | 
			
		||||
      videos and channels not could be not federated by the instance.
 | 
			
		||||
  - name: Video Comments
 | 
			
		||||
    description: >
 | 
			
		||||
      Operations dealing with comments to a video. Comments are organized in
 | 
			
		||||
      threads.
 | 
			
		||||
  - name: Video Playlists
 | 
			
		||||
    description: >
 | 
			
		||||
      Operations dealing with playlists of videos. Playlists are bound to users
 | 
			
		||||
      and/or channels.
 | 
			
		||||
  - name: Video Channels
 | 
			
		||||
    description: >
 | 
			
		||||
      Operations dealing with the creation, modification and listing of videos within a channel.
 | 
			
		||||
  - name: Video Blocks
 | 
			
		||||
    description: >
 | 
			
		||||
      Operations dealing with blocking videos (removing them from view and
 | 
			
		||||
      preventing interactions).
 | 
			
		||||
  - name: Video Rates
 | 
			
		||||
    description: >
 | 
			
		||||
      Like/dislike a video.
 | 
			
		||||
  - name: Feeds
 | 
			
		||||
    description: >
 | 
			
		||||
      Server syndication feeds
 | 
			
		||||
  - name: Video Mirroring
 | 
			
		||||
    description: |
 | 
			
		||||
      PeerTube instances can mirror videos from one another, and help distribute some videos.
 | 
			
		||||
 | 
			
		||||
      For importing videos as your own, refer to [video imports](#tag/Video-Upload/paths/~1videos~1imports/post).
 | 
			
		||||
x-tagGroups:
 | 
			
		||||
  - name: Accounts
 | 
			
		||||
    tags:
 | 
			
		||||
| 
						 | 
				
			
			@ -181,6 +209,7 @@ x-tagGroups:
 | 
			
		|||
  - name: Videos
 | 
			
		||||
    tags:
 | 
			
		||||
      - Video
 | 
			
		||||
      - Video Upload
 | 
			
		||||
      - Video Captions
 | 
			
		||||
      - Video Channels
 | 
			
		||||
      - Video Comments
 | 
			
		||||
| 
						 | 
				
			
			@ -1347,10 +1376,12 @@ paths:
 | 
			
		|||
  /videos/upload:
 | 
			
		||||
    post:
 | 
			
		||||
      summary: Upload a video
 | 
			
		||||
      description: Uses a single request to upload a video.
 | 
			
		||||
      security:
 | 
			
		||||
        - OAuth2: []
 | 
			
		||||
      tags:
 | 
			
		||||
        - Video
 | 
			
		||||
        - Video Upload
 | 
			
		||||
      responses:
 | 
			
		||||
        '200':
 | 
			
		||||
          description: successful operation
 | 
			
		||||
| 
						 | 
				
			
			@ -1380,80 +1411,7 @@ paths:
 | 
			
		|||
        content:
 | 
			
		||||
          multipart/form-data:
 | 
			
		||||
            schema:
 | 
			
		||||
              type: object
 | 
			
		||||
              properties:
 | 
			
		||||
                videofile:
 | 
			
		||||
                  description: Video file
 | 
			
		||||
                  type: string
 | 
			
		||||
                  format: binary
 | 
			
		||||
                channelId:
 | 
			
		||||
                  description: Channel id that will contain this video
 | 
			
		||||
                  type: integer
 | 
			
		||||
                thumbnailfile:
 | 
			
		||||
                  description: Video thumbnail file
 | 
			
		||||
                  type: string
 | 
			
		||||
                  format: binary
 | 
			
		||||
                previewfile:
 | 
			
		||||
                  description: Video preview file
 | 
			
		||||
                  type: string
 | 
			
		||||
                  format: binary
 | 
			
		||||
                privacy:
 | 
			
		||||
                  $ref: '#/components/schemas/VideoPrivacySet'
 | 
			
		||||
                category:
 | 
			
		||||
                  description: Video category
 | 
			
		||||
                  type: integer
 | 
			
		||||
                  example: 4
 | 
			
		||||
                licence:
 | 
			
		||||
                  description: Video licence
 | 
			
		||||
                  type: integer
 | 
			
		||||
                  example: 2
 | 
			
		||||
                language:
 | 
			
		||||
                  description: Video language
 | 
			
		||||
                  type: string
 | 
			
		||||
                description:
 | 
			
		||||
                  description: Video description
 | 
			
		||||
                  type: string
 | 
			
		||||
                waitTranscoding:
 | 
			
		||||
                  description: Whether or not we wait transcoding before publish the video
 | 
			
		||||
                  type: boolean
 | 
			
		||||
                support:
 | 
			
		||||
                  description: A text tell the audience how to support the video creator
 | 
			
		||||
                  example: Please support my work on <insert crowdfunding plateform>! <3
 | 
			
		||||
                  type: string
 | 
			
		||||
                nsfw:
 | 
			
		||||
                  description: Whether or not this video contains sensitive content
 | 
			
		||||
                  type: boolean
 | 
			
		||||
                name:
 | 
			
		||||
                  description: Video name
 | 
			
		||||
                  type: string
 | 
			
		||||
                  minLength: 3
 | 
			
		||||
                  maxLength: 120
 | 
			
		||||
                tags:
 | 
			
		||||
                  description: Video tags (maximum 5 tags each between 2 and 30 characters)
 | 
			
		||||
                  type: array
 | 
			
		||||
                  minItems: 1
 | 
			
		||||
                  maxItems: 5
 | 
			
		||||
                  uniqueItems: true
 | 
			
		||||
                  items:
 | 
			
		||||
                    type: string
 | 
			
		||||
                    minLength: 2
 | 
			
		||||
                    maxLength: 30
 | 
			
		||||
                commentsEnabled:
 | 
			
		||||
                  description: Enable or disable comments for this video
 | 
			
		||||
                  type: boolean
 | 
			
		||||
                downloadEnabled:
 | 
			
		||||
                  description: Enable or disable downloading for this video
 | 
			
		||||
                  type: boolean
 | 
			
		||||
                originallyPublishedAt:
 | 
			
		||||
                  description: Date when the content was originally published
 | 
			
		||||
                  type: string
 | 
			
		||||
                  format: date-time
 | 
			
		||||
                scheduleUpdate:
 | 
			
		||||
                  $ref: '#/components/schemas/VideoScheduledUpdate'
 | 
			
		||||
              required:
 | 
			
		||||
                - videofile
 | 
			
		||||
                - channelId
 | 
			
		||||
                - name
 | 
			
		||||
              $ref: '#/components/schemas/VideoUploadRequestLegacy'
 | 
			
		||||
            encoding:
 | 
			
		||||
              videofile:
 | 
			
		||||
                contentType: video/mp4, video/webm, video/ogg, video/avi, video/quicktime, video/x-msvideo, video/x-flv, video/x-matroska, application/octet-stream
 | 
			
		||||
| 
						 | 
				
			
			@ -1490,6 +1448,164 @@ paths:
 | 
			
		|||
              --form videofile=@"$FILE_PATH" \
 | 
			
		||||
              --form channelId=$CHANNEL_ID \
 | 
			
		||||
              --form name="$NAME"
 | 
			
		||||
  /videos/upload-resumable:
 | 
			
		||||
    post:
 | 
			
		||||
      summary: Initialize the resumable upload of a video
 | 
			
		||||
      description: Uses [a resumable protocol](https://github.com/kukhariev/node-uploadx/blob/master/proto.md) to initialize the upload of a video
 | 
			
		||||
      security:
 | 
			
		||||
        - OAuth2: []
 | 
			
		||||
      tags:
 | 
			
		||||
        - Video
 | 
			
		||||
        - Video Upload
 | 
			
		||||
      parameters:
 | 
			
		||||
        - name: X-Upload-Content-Length
 | 
			
		||||
          in: header
 | 
			
		||||
          schema:
 | 
			
		||||
            type: number
 | 
			
		||||
            example: 2469036
 | 
			
		||||
          required: true
 | 
			
		||||
          description: Number of bytes that will be uploaded in subsequent requests. Set this value to the size of the file you are uploading.
 | 
			
		||||
        - name: X-Upload-Content-Type
 | 
			
		||||
          in: header
 | 
			
		||||
          schema:
 | 
			
		||||
            type: string
 | 
			
		||||
            format: mimetype
 | 
			
		||||
            example: video/mp4
 | 
			
		||||
          required: true
 | 
			
		||||
          description: MIME type of the file that you are uploading. Depending on your instance settings, acceptable values might vary.
 | 
			
		||||
      requestBody:
 | 
			
		||||
        content:
 | 
			
		||||
          application/json:
 | 
			
		||||
            schema:
 | 
			
		||||
              $ref: '#/components/schemas/VideoUploadRequestResumable'
 | 
			
		||||
      responses:
 | 
			
		||||
        '200':
 | 
			
		||||
          description: file already exists, send a [`resume`](https://github.com/kukhariev/node-uploadx/blob/master/proto.md) request instead
 | 
			
		||||
        '201':
 | 
			
		||||
          description: created
 | 
			
		||||
          headers:
 | 
			
		||||
            Location:
 | 
			
		||||
              schema:
 | 
			
		||||
                type: string
 | 
			
		||||
                format: url
 | 
			
		||||
                example: /api/v1/videos/upload-resumable?upload_id=471e97554f21dec3b8bb5d4602939c51
 | 
			
		||||
            Content-Length:
 | 
			
		||||
              schema:
 | 
			
		||||
                type: number
 | 
			
		||||
                example: 0
 | 
			
		||||
        '400':
 | 
			
		||||
          description: invalid file field, schedule date or parameter
 | 
			
		||||
        '413':
 | 
			
		||||
          description: video file too large, due to quota, absolute max file size or concurrent partial upload limit
 | 
			
		||||
        '415':
 | 
			
		||||
          description: video type unsupported
 | 
			
		||||
    put:
 | 
			
		||||
      summary: Send chunk for the resumable upload of a video
 | 
			
		||||
      description: Uses [a resumable protocol](https://github.com/kukhariev/node-uploadx/blob/master/proto.md) to continue, pause or resume the upload of a video
 | 
			
		||||
      security:
 | 
			
		||||
        - OAuth2: []
 | 
			
		||||
      tags:
 | 
			
		||||
        - Video
 | 
			
		||||
        - Video Upload
 | 
			
		||||
      parameters:
 | 
			
		||||
        - name: upload_id
 | 
			
		||||
          in: path
 | 
			
		||||
          required: true
 | 
			
		||||
          description: |
 | 
			
		||||
            Created session id to proceed with. If you didn't send chunks in the last 12 hours, it is
 | 
			
		||||
            not valid anymore and you need to initialize a new upload.
 | 
			
		||||
          schema:
 | 
			
		||||
            type: string
 | 
			
		||||
        - name: Content-Range
 | 
			
		||||
          in: header
 | 
			
		||||
          schema:
 | 
			
		||||
            type: string
 | 
			
		||||
            example: bytes 0-262143/2469036
 | 
			
		||||
          required: true
 | 
			
		||||
          description: |
 | 
			
		||||
            Specifies the bytes in the file that the request is uploading.
 | 
			
		||||
 | 
			
		||||
            For example, a value of `bytes 0-262143/1000000` shows that the request is sending the first
 | 
			
		||||
            262144 bytes (256 x 1024) in a 2,469,036 byte file.
 | 
			
		||||
        - name: Content-Length
 | 
			
		||||
          in: header
 | 
			
		||||
          schema:
 | 
			
		||||
            type: number
 | 
			
		||||
            example: 262144
 | 
			
		||||
          required: true
 | 
			
		||||
          description: |
 | 
			
		||||
            Size of the chunk that the request is sending.
 | 
			
		||||
 | 
			
		||||
            The chunk size __must be a multiple of 256 KB__, and unlike [Google Resumable](https://developers.google.com/youtube/v3/guides/using_resumable_upload_protocol)
 | 
			
		||||
            doesn't mandate for chunks to have the same size throughout the upload sequence.
 | 
			
		||||
 | 
			
		||||
            Remember that larger chunks are more efficient. PeerTube's web client uses chunks varying from
 | 
			
		||||
            1048576 bytes (~1MB) and increases or reduces size depending on connection health.
 | 
			
		||||
      requestBody:
 | 
			
		||||
        content:
 | 
			
		||||
          application/octet-stream:
 | 
			
		||||
            schema:
 | 
			
		||||
              type: string
 | 
			
		||||
              format: binary
 | 
			
		||||
      responses:
 | 
			
		||||
        '200':
 | 
			
		||||
          description: last chunk received
 | 
			
		||||
          headers:
 | 
			
		||||
            Content-Length:
 | 
			
		||||
              schema:
 | 
			
		||||
                type: number
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                $ref: '#/components/schemas/VideoUploadResponse'
 | 
			
		||||
        '308':
 | 
			
		||||
          description: resume incomplete
 | 
			
		||||
          headers:
 | 
			
		||||
            Range:
 | 
			
		||||
              schema:
 | 
			
		||||
                type: string
 | 
			
		||||
                example: bytes=0-262143
 | 
			
		||||
            Content-Length:
 | 
			
		||||
              schema:
 | 
			
		||||
                type: number
 | 
			
		||||
                example: 0
 | 
			
		||||
        '403':
 | 
			
		||||
          description: video didn't pass upload filter
 | 
			
		||||
        '413':
 | 
			
		||||
          description: video file too large, due to quota or max body size limit set by the reverse-proxy
 | 
			
		||||
        '422':
 | 
			
		||||
          description: video unreadable
 | 
			
		||||
    delete:
 | 
			
		||||
      summary: Cancel the resumable upload of a video, deleting any data uploaded so far
 | 
			
		||||
      description: Uses [a resumable protocol](https://github.com/kukhariev/node-uploadx/blob/master/proto.md) to cancel the upload of a video
 | 
			
		||||
      security:
 | 
			
		||||
        - OAuth2: []
 | 
			
		||||
      tags:
 | 
			
		||||
        - Video
 | 
			
		||||
        - Video Upload
 | 
			
		||||
      parameters:
 | 
			
		||||
        - name: upload_id
 | 
			
		||||
          in: path
 | 
			
		||||
          required: true
 | 
			
		||||
          description: |
 | 
			
		||||
            Created session id to proceed with. If you didn't send chunks in the last 12 hours, it is
 | 
			
		||||
            not valid anymore and the upload session has already been deleted with its data ;-)
 | 
			
		||||
          schema:
 | 
			
		||||
            type: string
 | 
			
		||||
        - name: Content-Length
 | 
			
		||||
          in: header
 | 
			
		||||
          required: true
 | 
			
		||||
          schema:
 | 
			
		||||
            type: number
 | 
			
		||||
            example: 0
 | 
			
		||||
      responses:
 | 
			
		||||
        '204':
 | 
			
		||||
          description: upload cancelled
 | 
			
		||||
          headers:
 | 
			
		||||
            Content-Length:
 | 
			
		||||
              schema:
 | 
			
		||||
                type: number
 | 
			
		||||
                example: 0
 | 
			
		||||
  /videos/imports:
 | 
			
		||||
    post:
 | 
			
		||||
      summary: Import a video
 | 
			
		||||
| 
						 | 
				
			
			@ -1498,6 +1614,7 @@ paths:
 | 
			
		|||
        - OAuth2: []
 | 
			
		||||
      tags:
 | 
			
		||||
        - Video
 | 
			
		||||
        - Video Upload
 | 
			
		||||
      requestBody:
 | 
			
		||||
        content:
 | 
			
		||||
          multipart/form-data:
 | 
			
		||||
| 
						 | 
				
			
			@ -1688,7 +1805,7 @@ paths:
 | 
			
		|||
 | 
			
		||||
  /videos/live/{id}:
 | 
			
		||||
    get:
 | 
			
		||||
      summary: Get a live information
 | 
			
		||||
      summary: Get information about a live
 | 
			
		||||
      security:
 | 
			
		||||
        - OAuth2: []
 | 
			
		||||
      tags:
 | 
			
		||||
| 
						 | 
				
			
			@ -1704,7 +1821,7 @@ paths:
 | 
			
		|||
              schema:
 | 
			
		||||
                $ref: '#/components/schemas/LiveVideoResponse'
 | 
			
		||||
    put:
 | 
			
		||||
      summary: Update a live information
 | 
			
		||||
      summary: Update information about a live
 | 
			
		||||
      security:
 | 
			
		||||
        - OAuth2: []
 | 
			
		||||
      tags:
 | 
			
		||||
| 
						 | 
				
			
			@ -3940,6 +4057,7 @@ components:
 | 
			
		|||
        oneOf:
 | 
			
		||||
        - type: string
 | 
			
		||||
        - type: array
 | 
			
		||||
          maxItems: 5
 | 
			
		||||
          items:
 | 
			
		||||
            type: string
 | 
			
		||||
      style: form
 | 
			
		||||
| 
						 | 
				
			
			@ -4636,7 +4754,7 @@ components:
 | 
			
		|||
        message:
 | 
			
		||||
          type: string
 | 
			
		||||
          minLength: 2
 | 
			
		||||
          maxLength: 3000 
 | 
			
		||||
          maxLength: 3000
 | 
			
		||||
        byModerator:
 | 
			
		||||
          type: boolean
 | 
			
		||||
        createdAt:
 | 
			
		||||
| 
						 | 
				
			
			@ -5229,6 +5347,7 @@ components:
 | 
			
		|||
    PredefinedAbuseReasons:
 | 
			
		||||
      description: Reason categories that help triage reports
 | 
			
		||||
      type: array
 | 
			
		||||
      maxItems: 8
 | 
			
		||||
      items:
 | 
			
		||||
        type: string
 | 
			
		||||
        enum:
 | 
			
		||||
| 
						 | 
				
			
			@ -5298,6 +5417,103 @@ components:
 | 
			
		|||
                id:
 | 
			
		||||
                  type: integer
 | 
			
		||||
                  example: 37
 | 
			
		||||
    VideoUploadRequestCommon:
 | 
			
		||||
      properties:
 | 
			
		||||
        name:
 | 
			
		||||
          description: Video name
 | 
			
		||||
          type: string
 | 
			
		||||
        channelId:
 | 
			
		||||
          description: Channel id that will contain this video
 | 
			
		||||
          type: integer
 | 
			
		||||
        privacy:
 | 
			
		||||
          $ref: '#/components/schemas/VideoPrivacySet'
 | 
			
		||||
        category:
 | 
			
		||||
          description: Video category
 | 
			
		||||
          type: integer
 | 
			
		||||
          example: 4
 | 
			
		||||
        licence:
 | 
			
		||||
          description: Video licence
 | 
			
		||||
          type: integer
 | 
			
		||||
          example: 2
 | 
			
		||||
        language:
 | 
			
		||||
          description: Video language
 | 
			
		||||
          type: string
 | 
			
		||||
        description:
 | 
			
		||||
          description: Video description
 | 
			
		||||
          type: string
 | 
			
		||||
        waitTranscoding:
 | 
			
		||||
          description: Whether or not we wait transcoding before publish the video
 | 
			
		||||
          type: boolean
 | 
			
		||||
        support:
 | 
			
		||||
          description: A text tell the audience how to support the video creator
 | 
			
		||||
          example: Please support my work on <insert crowdfunding plateform>! <3
 | 
			
		||||
          type: string
 | 
			
		||||
        nsfw:
 | 
			
		||||
          description: Whether or not this video contains sensitive content
 | 
			
		||||
          type: boolean
 | 
			
		||||
        tags:
 | 
			
		||||
          description: Video tags (maximum 5 tags each between 2 and 30 characters)
 | 
			
		||||
          type: array
 | 
			
		||||
          minItems: 1
 | 
			
		||||
          maxItems: 5
 | 
			
		||||
          uniqueItems: true
 | 
			
		||||
          items:
 | 
			
		||||
            type: string
 | 
			
		||||
            minLength: 2
 | 
			
		||||
            maxLength: 30
 | 
			
		||||
        commentsEnabled:
 | 
			
		||||
          description: Enable or disable comments for this video
 | 
			
		||||
          type: boolean
 | 
			
		||||
        downloadEnabled:
 | 
			
		||||
          description: Enable or disable downloading for this video
 | 
			
		||||
          type: boolean
 | 
			
		||||
        originallyPublishedAt:
 | 
			
		||||
          description: Date when the content was originally published
 | 
			
		||||
          type: string
 | 
			
		||||
          format: date-time
 | 
			
		||||
        scheduleUpdate:
 | 
			
		||||
          $ref: '#/components/schemas/VideoScheduledUpdate'
 | 
			
		||||
        thumbnailfile:
 | 
			
		||||
          description: Video thumbnail file
 | 
			
		||||
          type: string
 | 
			
		||||
          format: binary
 | 
			
		||||
        previewfile:
 | 
			
		||||
          description: Video preview file
 | 
			
		||||
          type: string
 | 
			
		||||
          format: binary
 | 
			
		||||
      required:
 | 
			
		||||
        - channelId
 | 
			
		||||
        - name
 | 
			
		||||
    VideoUploadRequestLegacy:
 | 
			
		||||
      allOf:
 | 
			
		||||
        - $ref: '#/components/schemas/VideoUploadRequestCommon'
 | 
			
		||||
        - type: object
 | 
			
		||||
          required:
 | 
			
		||||
            - videofile
 | 
			
		||||
          properties:
 | 
			
		||||
            videofile:
 | 
			
		||||
              description: Video file
 | 
			
		||||
              type: string
 | 
			
		||||
              format: binary
 | 
			
		||||
    VideoUploadRequestResumable:
 | 
			
		||||
      allOf:
 | 
			
		||||
        - $ref: '#/components/schemas/VideoUploadRequestCommon'
 | 
			
		||||
        - type: object
 | 
			
		||||
          required:
 | 
			
		||||
            - filename
 | 
			
		||||
          properties:
 | 
			
		||||
            filename:
 | 
			
		||||
              description: Video filename including extension
 | 
			
		||||
              type: string
 | 
			
		||||
              format: filename
 | 
			
		||||
            thumbnailfile:
 | 
			
		||||
              description: Video thumbnail file
 | 
			
		||||
              type: string
 | 
			
		||||
              format: binary
 | 
			
		||||
            previewfile:
 | 
			
		||||
              description: Video preview file
 | 
			
		||||
              type: string
 | 
			
		||||
              format: binary
 | 
			
		||||
    VideoUploadResponse:
 | 
			
		||||
      properties:
 | 
			
		||||
        video:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -78,6 +78,13 @@ server {
 | 
			
		|||
    try_files /dev/null @api;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  location = /api/v1/videos/upload-resumable {
 | 
			
		||||
    client_max_body_size 0;
 | 
			
		||||
    proxy_request_buffering off;
 | 
			
		||||
 | 
			
		||||
    try_files /dev/null @api;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  location = /api/v1/videos/upload {
 | 
			
		||||
    limit_except POST HEAD { deny all; }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										50
									
								
								yarn.lock
									
										
									
									
									
								
							
							
						
						
									
										50
									
								
								yarn.lock
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1061,6 +1061,15 @@
 | 
			
		|||
  resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44"
 | 
			
		||||
  integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==
 | 
			
		||||
 | 
			
		||||
"@uploadx/core@^4.4.0":
 | 
			
		||||
  version "4.4.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@uploadx/core/-/core-4.4.0.tgz#27ea2b0d28125e81a6bdd65637dc5c7829306cc7"
 | 
			
		||||
  integrity sha512-dU0oDURYR5RvuAzf63EL9e/fCY4OOQKOs237UTbZDulbRbiyxwEZR+IpRYYr3hKRjjij03EF/Y5j54VGkebAKg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    bytes "^3.1.0"
 | 
			
		||||
    debug "^4.3.1"
 | 
			
		||||
    multiparty "^4.2.2"
 | 
			
		||||
 | 
			
		||||
abbrev@1:
 | 
			
		||||
  version "1.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
 | 
			
		||||
| 
						 | 
				
			
			@ -1794,7 +1803,7 @@ busboy@^0.2.11:
 | 
			
		|||
    dicer "0.2.5"
 | 
			
		||||
    readable-stream "1.1.x"
 | 
			
		||||
 | 
			
		||||
bytes@3.1.0, bytes@^3.0.0:
 | 
			
		||||
bytes@3.1.0, bytes@^3.0.0, bytes@^3.1.0:
 | 
			
		||||
  version "3.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
 | 
			
		||||
  integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
 | 
			
		||||
| 
						 | 
				
			
			@ -4098,6 +4107,17 @@ http-errors@~1.7.2:
 | 
			
		|||
    statuses ">= 1.5.0 < 2"
 | 
			
		||||
    toidentifier "1.0.0"
 | 
			
		||||
 | 
			
		||||
http-errors@~1.8.0:
 | 
			
		||||
  version "1.8.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.0.tgz#75d1bbe497e1044f51e4ee9e704a62f28d336507"
 | 
			
		||||
  integrity sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    depd "~1.1.2"
 | 
			
		||||
    inherits "2.0.4"
 | 
			
		||||
    setprototypeof "1.2.0"
 | 
			
		||||
    statuses ">= 1.5.0 < 2"
 | 
			
		||||
    toidentifier "1.0.0"
 | 
			
		||||
 | 
			
		||||
"http-node@github:feross/http-node#webtorrent":
 | 
			
		||||
  version "1.2.0"
 | 
			
		||||
  resolved "https://codeload.github.com/feross/http-node/tar.gz/342ef8624495343ffd050bd0808b3750cf0e3974"
 | 
			
		||||
| 
						 | 
				
			
			@ -5567,6 +5587,15 @@ multimatch@^5.0.0:
 | 
			
		|||
    arrify "^2.0.1"
 | 
			
		||||
    minimatch "^3.0.4"
 | 
			
		||||
 | 
			
		||||
multiparty@^4.2.2:
 | 
			
		||||
  version "4.2.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/multiparty/-/multiparty-4.2.2.tgz#bee5fb5737247628d39dab4979ffd6d57bf60ef6"
 | 
			
		||||
  integrity sha512-NtZLjlvsjcoGrzojtwQwn/Tm90aWJ6XXtPppYF4WmOk/6ncdwMMKggFY2NlRRN9yiCEIVxpOfPWahVEG2HAG8Q==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    http-errors "~1.8.0"
 | 
			
		||||
    safe-buffer "5.2.1"
 | 
			
		||||
    uid-safe "2.1.5"
 | 
			
		||||
 | 
			
		||||
multistream@^4.0.1, multistream@^4.1.0:
 | 
			
		||||
  version "4.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/multistream/-/multistream-4.1.0.tgz#7bf00dfd119556fbc153cff3de4c6d477909f5a8"
 | 
			
		||||
| 
						 | 
				
			
			@ -6656,6 +6685,11 @@ random-access-storage@^1.1.1:
 | 
			
		|||
  dependencies:
 | 
			
		||||
    inherits "^2.0.3"
 | 
			
		||||
 | 
			
		||||
random-bytes@~1.0.0:
 | 
			
		||||
  version "1.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b"
 | 
			
		||||
  integrity sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=
 | 
			
		||||
 | 
			
		||||
random-iterate@^1.0.1:
 | 
			
		||||
  version "1.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/random-iterate/-/random-iterate-1.0.1.tgz#f7d97d92dee6665ec5f6da08c7f963cad4b2ac99"
 | 
			
		||||
| 
						 | 
				
			
			@ -7040,7 +7074,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
 | 
			
		|||
  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
 | 
			
		||||
  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
 | 
			
		||||
 | 
			
		||||
safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.2.0:
 | 
			
		||||
safe-buffer@5.2.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.2.0:
 | 
			
		||||
  version "5.2.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
 | 
			
		||||
  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
 | 
			
		||||
| 
						 | 
				
			
			@ -7186,6 +7220,11 @@ setprototypeof@1.1.1:
 | 
			
		|||
  resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
 | 
			
		||||
  integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
 | 
			
		||||
 | 
			
		||||
setprototypeof@1.2.0:
 | 
			
		||||
  version "1.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
 | 
			
		||||
  integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
 | 
			
		||||
 | 
			
		||||
shebang-command@^1.2.0:
 | 
			
		||||
  version "1.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
 | 
			
		||||
| 
						 | 
				
			
			@ -8139,6 +8178,13 @@ uc.micro@^1.0.1, uc.micro@^1.0.5:
 | 
			
		|||
  resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
 | 
			
		||||
  integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
 | 
			
		||||
 | 
			
		||||
uid-safe@2.1.5:
 | 
			
		||||
  version "2.1.5"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a"
 | 
			
		||||
  integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    random-bytes "~1.0.0"
 | 
			
		||||
 | 
			
		||||
uint64be@^2.0.2:
 | 
			
		||||
  version "2.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/uint64be/-/uint64be-2.0.2.tgz#ef4a179752fe8f9ddaa29544ecfc13490031e8e5"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue