import 'mocha' ; import * as chai from 'chai' ; import { pathExists , readdir , stat } from 'fs-extra' ; import { join } from 'path' ; import { buildAbsoluteFixturePath } from '@shared/core-utils' ; import { HttpStatusCode , VideoPrivacy } from '@shared/models' ; import { cleanupTests , createSingleServer , PeerTubeServer , setAccessTokensToServers , setDefaultVideoChannel } from '@shared/server-commands' ; const expect = chai . expect ; describe ( 'Test resumable upload' , function ( ) { const defaultFixture = 'video_short.mp4' ; let server ; let rootId ; let userAccessToken ; let userChannelId ; async function buildSize ( fixture , size ? ) { if ( size !== undefined ) return size ; const baseFixture = buildAbsoluteFixturePath ( fixture ) ; return ( await stat ( baseFixture ) ) . size ; } async function prepareUpload ( options = { } ) { const { token , originalName , lastModified } = options ; const size = await buildSize ( defaultFixture , options . size ) ; const attributes = { name : 'video' , channelId : options . channelId ?? server . store . channel . id , privacy : VideoPrivacy . PUBLIC , fixture : defaultFixture } ; const mimetype = 'video/mp4' ; const res = await server . videos . prepareResumableUpload ( { token , attributes , size , mimetype , originalName , lastModified } ) ; return res . header [ 'location' ] . split ( '?' ) [ 1 ] ; } async function sendChunks ( options ) { const { token , pathUploadId , expectedStatus , contentLength , contentRangeBuilder } = options ; const size = await buildSize ( defaultFixture , options . size ) ; const absoluteFilePath = buildAbsoluteFixturePath ( defaultFixture ) ; return server . videos . sendResumableChunks ( { token , pathUploadId , videoFilePath : absoluteFilePath , size , contentLength , contentRangeBuilder , expectedStatus } ) ; } async function checkFileSize ( uploadIdArg , expectedSize ) { const uploadId = uploadIdArg . replace ( /^upload_id=/ , '' ) ; const subPath = join ( 'tmp' , 'resumable-uploads' , uploadId ) ; const filePath = server . servers . buildDirectory ( 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 = server . servers . buildDirectory ( subPath ) ; const files = await readdir ( filePath ) ; return files . length ; } before ( async function ( ) { this . timeout ( 30000 ) ; server = await createSingleServer ( 1 ) ; await setAccessTokensToServers ( [ server ] ) ; await setDefaultVideoChannel ( [ server ] ) ; const body = await server . users . getMyInfo ( ) ; rootId = body . id ; { userAccessToken = await server . users . generateUserAndToken ( 'user1' ) ; const { videoChannels } = await server . users . getMyInfo ( { token : userAccessToken } ) ; userChannelId = videoChannels [ 0 ] . id ; } await server . users . update ( { userId : rootId , videoQuota : 10000000 } ) ; } ) ; describe ( 'Directory cleaning' , function ( ) { it ( 'Should correctly delete files after an upload' , async function ( ) { const uploadId = await prepareUpload ( ) ; await sendChunks ( { pathUploadId : uploadId } ) ; await server . videos . endResumableUpload ( { 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 server . debug . sendCommand ( { body : { command : 'remove-dandling-resumable-uploads' } } ) ; expect ( await countResumableUploads ( ) ) . to . equal ( 2 ) ; } ) ; it ( 'Should delete old uploads' , async function ( ) { await server . debug . sendCommand ( { body : { 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 uploadId = await prepareUpload ( { size : 100 } ) ; 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 ( { size : 1500 } ) ; if ( process . version . startsWith ( 'v16' ) ) { await sendChunks ( { pathUploadId : uploadId , expectedStatus : HttpStatusCode . CONFLICT_409 , contentLength : 1000 } ) ; await checkFileSize ( uploadId , 1000 ) ; } else { 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 ( { size : 500 } ) ; const size = 1000 ; const expectedStatus = process . version . startsWith ( 'v16' ) ? HttpStatusCode . CONFLICT_409 : HttpStatusCode . BAD_REQUEST_400 ; const contentRangeBuilder = ( start ) => `bytes ${ start }-${ start + size - 1 }/${ size }` ; await sendChunks ( { pathUploadId : uploadId , expectedStatus , contentRangeBuilder , contentLength : size } ) ; await checkFileSize ( uploadId , 0 ) ; } ) ; it ( 'Should be able to accept 2 PUT requests' , async function ( ) { const uploadId = await prepareUpload ( ) ; const result1 = await sendChunks ( { pathUploadId : uploadId } ) ; const result2 = await sendChunks ( { pathUploadId : uploadId } ) ; expect ( result1 . body . video . uuid ) . to . exist ; expect ( result1 . body . video . uuid ) . to . equal ( result2 . body . video . uuid ) ; expect ( result1 . headers [ 'x-resumable-upload-cached' ] ) . to . not . exist ; expect ( result2 . headers [ 'x-resumable-upload-cached' ] ) . to . equal ( 'true' ) ; await checkFileSize ( uploadId , null ) ; } ) ; it ( 'Should not have the same upload id with 2 different users' , async function ( ) { const originalName = 'toto.mp4' ; const lastModified = new Date ( ) . getTime ( ) ; const uploadId1 = await prepareUpload ( { originalName , lastModified , token : server . accessToken } ) ; const uploadId2 = await prepareUpload ( { originalName , lastModified , channelId : userChannelId , token : userAccessToken } ) ; expect ( uploadId1 ) . to . not . equal ( uploadId2 ) ; } ) ; it ( 'Should have the same upload id with the same user' , async function ( ) { const originalName = 'toto.mp4' ; const lastModified = new Date ( ) . getTime ( ) ; const uploadId1 = await prepareUpload ( { originalName , lastModified } ) ; const uploadId2 = await prepareUpload ( { originalName , lastModified } ) ; expect ( uploadId1 ) . to . equal ( uploadId2 ) ; } ) ; it ( 'Should not cache a request with 2 different users' , async function ( ) { const originalName = 'toto.mp4' ; const lastModified = new Date ( ) . getTime ( ) ; const uploadId = await prepareUpload ( { originalName , lastModified , token : server . accessToken } ) ; await sendChunks ( { pathUploadId : uploadId , token : server . accessToken } ) ; await sendChunks ( { pathUploadId : uploadId , token : userAccessToken , expectedStatus : HttpStatusCode . FORBIDDEN_403 } ) ; } ) ; it ( 'Should not cache a request after a delete' , async function ( ) { const originalName = 'toto.mp4' ; const lastModified = new Date ( ) . getTime ( ) ; const uploadId1 = await prepareUpload ( { originalName , lastModified , token : server . accessToken } ) ; await sendChunks ( { pathUploadId : uploadId1 } ) ; await server . videos . endResumableUpload ( { pathUploadId : uploadId1 } ) ; const uploadId2 = await prepareUpload ( { originalName , lastModified , token : server . accessToken } ) ; expect ( uploadId1 ) . to . equal ( uploadId2 ) ; const result2 = await sendChunks ( { pathUploadId : uploadId1 } ) ; expect ( result2 . headers [ 'x-resumable-upload-cached' ] ) . to . not . exist ; } ) ; } ) ; after ( async function ( ) { await cleanupTests ( [ server ] ) ; } ) ; } ) ;