]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/tests/api/videos/resumable-upload.ts
642c115d030c5be820c17af76f771c8bf27d5337
[github/Chocobozzz/PeerTube.git] / server / tests / api / videos / resumable-upload.ts
1 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3 import 'mocha'
4 import * as chai from 'chai'
5 import { pathExists, readdir, stat } from 'fs-extra'
6 import { join } from 'path'
7 import { HttpStatusCode } from '@shared/core-utils'
8 import {
9 buildAbsoluteFixturePath,
10 cleanupTests,
11 flushAndRunServer,
12 prepareResumableUpload,
13 sendResumableChunks,
14 ServerInfo,
15 setAccessTokensToServers,
16 setDefaultVideoChannel
17 } from '@shared/extra-utils'
18 import { VideoPrivacy } from '@shared/models'
19
20 const expect = chai.expect
21
22 // Most classic resumable upload tests are done in other test suites
23
24 describe('Test resumable upload', function () {
25 const defaultFixture = 'video_short.mp4'
26 let server: ServerInfo
27 let rootId: number
28
29 async function buildSize (fixture: string, size?: number) {
30 if (size !== undefined) return size
31
32 const baseFixture = buildAbsoluteFixturePath(fixture)
33 return (await stat(baseFixture)).size
34 }
35
36 async function prepareUpload (sizeArg?: number) {
37 const size = await buildSize(defaultFixture, sizeArg)
38
39 const attributes = {
40 name: 'video',
41 channelId: server.videoChannel.id,
42 privacy: VideoPrivacy.PUBLIC,
43 fixture: defaultFixture
44 }
45
46 const mimetype = 'video/mp4'
47
48 const res = await prepareResumableUpload({ url: server.url, token: server.accessToken, attributes, size, mimetype })
49
50 return res.header['location'].split('?')[1]
51 }
52
53 async function sendChunks (options: {
54 pathUploadId: string
55 size?: number
56 expectedStatus?: HttpStatusCode
57 contentLength?: number
58 contentRange?: string
59 contentRangeBuilder?: (start: number, chunk: any) => string
60 }) {
61 const { pathUploadId, expectedStatus, contentLength, contentRangeBuilder } = options
62
63 const size = await buildSize(defaultFixture, options.size)
64 const absoluteFilePath = buildAbsoluteFixturePath(defaultFixture)
65
66 return sendResumableChunks({
67 url: server.url,
68 token: server.accessToken,
69 pathUploadId,
70 videoFilePath: absoluteFilePath,
71 size,
72 contentLength,
73 contentRangeBuilder,
74 specialStatus: expectedStatus
75 })
76 }
77
78 async function checkFileSize (uploadIdArg: string, expectedSize: number | null) {
79 const uploadId = uploadIdArg.replace(/^upload_id=/, '')
80
81 const subPath = join('tmp', 'resumable-uploads', uploadId)
82 const filePath = server.serversCommand.buildDirectory(subPath)
83 const exists = await pathExists(filePath)
84
85 if (expectedSize === null) {
86 expect(exists).to.be.false
87 return
88 }
89
90 expect(exists).to.be.true
91
92 expect((await stat(filePath)).size).to.equal(expectedSize)
93 }
94
95 async function countResumableUploads () {
96 const subPath = join('tmp', 'resumable-uploads')
97 const filePath = server.serversCommand.buildDirectory(subPath)
98
99 const files = await readdir(filePath)
100 return files.length
101 }
102
103 before(async function () {
104 this.timeout(30000)
105
106 server = await flushAndRunServer(1)
107 await setAccessTokensToServers([ server ])
108 await setDefaultVideoChannel([ server ])
109
110 const body = await server.usersCommand.getMyInfo()
111 rootId = body.id
112
113 await server.usersCommand.update({ userId: rootId, videoQuota: 10_000_000 })
114 })
115
116 describe('Directory cleaning', function () {
117
118 it('Should correctly delete files after an upload', async function () {
119 const uploadId = await prepareUpload()
120 await sendChunks({ pathUploadId: uploadId })
121
122 expect(await countResumableUploads()).to.equal(0)
123 })
124
125 it('Should not delete files after an unfinished upload', async function () {
126 await prepareUpload()
127
128 expect(await countResumableUploads()).to.equal(2)
129 })
130
131 it('Should not delete recent uploads', async function () {
132 await server.debugCommand.sendCommand({ body: { command: 'remove-dandling-resumable-uploads' } })
133
134 expect(await countResumableUploads()).to.equal(2)
135 })
136
137 it('Should delete old uploads', async function () {
138 await server.debugCommand.sendCommand({ body: { command: 'remove-dandling-resumable-uploads' } })
139
140 expect(await countResumableUploads()).to.equal(0)
141 })
142 })
143
144 describe('Resumable upload and chunks', function () {
145
146 it('Should accept the same amount of chunks', async function () {
147 const uploadId = await prepareUpload()
148 await sendChunks({ pathUploadId: uploadId })
149
150 await checkFileSize(uploadId, null)
151 })
152
153 it('Should not accept more chunks than expected', async function () {
154 const size = 100
155 const uploadId = await prepareUpload(size)
156
157 await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.CONFLICT_409 })
158 await checkFileSize(uploadId, 0)
159 })
160
161 it('Should not accept more chunks than expected with an invalid content length/content range', async function () {
162 const uploadId = await prepareUpload(1500)
163
164 await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.BAD_REQUEST_400, contentLength: 1000 })
165 await checkFileSize(uploadId, 0)
166 })
167
168 it('Should not accept more chunks than expected with an invalid content length', async function () {
169 const uploadId = await prepareUpload(500)
170
171 const size = 1000
172
173 const contentRangeBuilder = start => `bytes ${start}-${start + size - 1}/${size}`
174 await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.BAD_REQUEST_400, contentRangeBuilder, contentLength: size })
175 await checkFileSize(uploadId, 0)
176 })
177 })
178
179 after(async function () {
180 await cleanupTests([ server ])
181 })
182 })