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,
- cleanupTests,
- flushAndRunServer,
- getMyUserInformation,
- prepareResumableUpload,
- sendDebugCommand,
- sendResumableChunks,
- ServerInfo,
- setAccessTokensToServers,
- setDefaultVideoChannel,
- updateUser
-} from '@shared/extra-utils'
-import { MyUser, VideoPrivacy } from '@shared/models'
+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: ServerInfo
+ let server: PeerTubeServer
let rootId: number
+ let userAccessToken: string
+ let userChannelId: number
async function buildSize (fixture: string, size?: number) {
if (size !== undefined) return size
return (await stat(baseFixture)).size
}
- async function prepareUpload (sizeArg?: number) {
- const size = await buildSize(defaultFixture, sizeArg)
+ async function prepareUpload (options: {
+ channelId?: number
+ token?: string
+ size?: number
+ originalName?: string
+ lastModified?: number
+ } = {}) {
+ const { token, originalName, lastModified } = options
+
+ const size = await buildSize(defaultFixture, options.size)
const attributes = {
name: 'video',
- channelId: server.videoChannel.id,
+ channelId: options.channelId ?? server.store.channel.id,
privacy: VideoPrivacy.PUBLIC,
fixture: defaultFixture
}
const mimetype = 'video/mp4'
- const res = await prepareResumableUpload({ url: server.url, token: server.accessToken, attributes, size, mimetype })
+ const res = await server.videos.prepareResumableUpload({ token, attributes, size, mimetype, originalName, lastModified })
return res.header['location'].split('?')[1]
}
async function sendChunks (options: {
+ token?: string
pathUploadId: string
size?: number
expectedStatus?: HttpStatusCode
contentRange?: string
contentRangeBuilder?: (start: number, chunk: any) => string
}) {
- const { pathUploadId, expectedStatus, contentLength, contentRangeBuilder } = options
+ const { token, pathUploadId, expectedStatus, contentLength, contentRangeBuilder } = options
const size = await buildSize(defaultFixture, options.size)
const absoluteFilePath = buildAbsoluteFixturePath(defaultFixture)
- return sendResumableChunks({
- url: server.url,
- token: server.accessToken,
+ return server.videos.sendResumableChunks({
+ token,
pathUploadId,
videoFilePath: absoluteFilePath,
size,
contentLength,
contentRangeBuilder,
- specialStatus: expectedStatus
+ expectedStatus
})
}
const uploadId = uploadIdArg.replace(/^upload_id=/, '')
const subPath = join('tmp', 'resumable-uploads', uploadId)
- const filePath = buildServerDirectory(server, subPath)
+ const filePath = server.servers.buildDirectory(subPath)
const exists = await pathExists(filePath)
if (expectedSize === null) {
async function countResumableUploads () {
const subPath = join('tmp', 'resumable-uploads')
- const filePath = buildServerDirectory(server, subPath)
+ const filePath = server.servers.buildDirectory(subPath)
const files = await readdir(filePath)
return files.length
before(async function () {
this.timeout(30000)
- server = await flushAndRunServer(1)
+ server = await createSingleServer(1)
await setAccessTokensToServers([ server ])
await setDefaultVideoChannel([ server ])
- const res = await getMyUserInformation(server.url, server.accessToken)
- rootId = (res.body as MyUser).id
+ const body = await server.users.getMyInfo()
+ rootId = body.id
- await updateUser({
- url: server.url,
- userId: rootId,
- accessToken: server.accessToken,
- videoQuota: 10_000_000
- })
+ {
+ 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: 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 })
+ await server.videos.endResumableUpload({ pathUploadId: uploadId })
expect(await countResumableUploads()).to.equal(0)
})
})
it('Should not delete recent uploads', async function () {
- await sendDebugCommand(server.url, server.accessToken, { command: 'remove-dandling-resumable-uploads' })
+ await server.debug.sendCommand({ body: { 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' })
+ await server.debug.sendCommand({ body: { command: 'remove-dandling-resumable-uploads' } })
expect(await countResumableUploads()).to.equal(0)
})
})
it('Should not accept more chunks than expected', async function () {
- const size = 100
- const uploadId = await prepareUpload(size)
+ 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(1500)
-
- await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.BAD_REQUEST_400, contentLength: 1000 })
- await checkFileSize(uploadId, 0)
+ const uploadId = await prepareUpload({ size: 1500 })
+
+ // Content length check seems to have changed in v16
+ 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(500)
+ const uploadId = await prepareUpload({ size: 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 })
+ // Content length check seems to have changed in v16
+ const expectedStatus = process.version.startsWith('v16')
+ ? HttpStatusCode.CONFLICT_409
+ : HttpStatusCode.BAD_REQUEST_400
+
+ const contentRangeBuilder = (start: number) => `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 () {