]>
Commit | Line | Data |
---|---|---|
f6d6e7f8 | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | ||
f6d6e7f8 | 3 | import * as chai from 'chai' |
4 | import { pathExists, readdir, stat } from 'fs-extra' | |
5 | import { join } from 'path' | |
c55e3d72 | 6 | import { buildAbsoluteFixturePath } from '@shared/core-utils' |
33ac85bf | 7 | import { sha1 } from '@shared/extra-utils' |
4c7e60bc | 8 | import { HttpStatusCode, VideoPrivacy } from '@shared/models' |
c55e3d72 | 9 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers, setDefaultVideoChannel } from '@shared/server-commands' |
f6d6e7f8 | 10 | |
11 | const expect = chai.expect | |
12 | ||
13 | // Most classic resumable upload tests are done in other test suites | |
14 | ||
15 | describe('Test resumable upload', function () { | |
16 | const defaultFixture = 'video_short.mp4' | |
254d3579 | 17 | let server: PeerTubeServer |
f6d6e7f8 | 18 | let rootId: number |
020d3d3d C |
19 | let userAccessToken: string |
20 | let userChannelId: number | |
f6d6e7f8 | 21 | |
22 | async function buildSize (fixture: string, size?: number) { | |
23 | if (size !== undefined) return size | |
24 | ||
25 | const baseFixture = buildAbsoluteFixturePath(fixture) | |
26 | return (await stat(baseFixture)).size | |
27 | } | |
28 | ||
020d3d3d C |
29 | async function prepareUpload (options: { |
30 | channelId?: number | |
31 | token?: string | |
32 | size?: number | |
33 | originalName?: string | |
34 | lastModified?: number | |
35 | } = {}) { | |
36 | const { token, originalName, lastModified } = options | |
37 | ||
38 | const size = await buildSize(defaultFixture, options.size) | |
f6d6e7f8 | 39 | |
40 | const attributes = { | |
41 | name: 'video', | |
020d3d3d | 42 | channelId: options.channelId ?? server.store.channel.id, |
f6d6e7f8 | 43 | privacy: VideoPrivacy.PUBLIC, |
44 | fixture: defaultFixture | |
45 | } | |
46 | ||
47 | const mimetype = 'video/mp4' | |
48 | ||
020d3d3d | 49 | const res = await server.videos.prepareResumableUpload({ token, attributes, size, mimetype, originalName, lastModified }) |
f6d6e7f8 | 50 | |
51 | return res.header['location'].split('?')[1] | |
52 | } | |
53 | ||
54 | async function sendChunks (options: { | |
020d3d3d | 55 | token?: string |
f6d6e7f8 | 56 | pathUploadId: string |
57 | size?: number | |
58 | expectedStatus?: HttpStatusCode | |
59 | contentLength?: number | |
60 | contentRange?: string | |
61 | contentRangeBuilder?: (start: number, chunk: any) => string | |
33ac85bf | 62 | digestBuilder?: (chunk: any) => string |
f6d6e7f8 | 63 | }) { |
33ac85bf | 64 | const { token, pathUploadId, expectedStatus, contentLength, contentRangeBuilder, digestBuilder } = options |
f6d6e7f8 | 65 | |
66 | const size = await buildSize(defaultFixture, options.size) | |
67 | const absoluteFilePath = buildAbsoluteFixturePath(defaultFixture) | |
68 | ||
89d241a7 | 69 | return server.videos.sendResumableChunks({ |
020d3d3d | 70 | token, |
f6d6e7f8 | 71 | pathUploadId, |
72 | videoFilePath: absoluteFilePath, | |
73 | size, | |
74 | contentLength, | |
75 | contentRangeBuilder, | |
33ac85bf | 76 | digestBuilder, |
d23dd9fb | 77 | expectedStatus |
f6d6e7f8 | 78 | }) |
79 | } | |
80 | ||
81 | async function checkFileSize (uploadIdArg: string, expectedSize: number | null) { | |
82 | const uploadId = uploadIdArg.replace(/^upload_id=/, '') | |
83 | ||
84 | const subPath = join('tmp', 'resumable-uploads', uploadId) | |
89d241a7 | 85 | const filePath = server.servers.buildDirectory(subPath) |
f6d6e7f8 | 86 | const exists = await pathExists(filePath) |
87 | ||
88 | if (expectedSize === null) { | |
89 | expect(exists).to.be.false | |
90 | return | |
91 | } | |
92 | ||
93 | expect(exists).to.be.true | |
94 | ||
95 | expect((await stat(filePath)).size).to.equal(expectedSize) | |
96 | } | |
97 | ||
98 | async function countResumableUploads () { | |
99 | const subPath = join('tmp', 'resumable-uploads') | |
89d241a7 | 100 | const filePath = server.servers.buildDirectory(subPath) |
f6d6e7f8 | 101 | |
102 | const files = await readdir(filePath) | |
103 | return files.length | |
104 | } | |
105 | ||
106 | before(async function () { | |
107 | this.timeout(30000) | |
108 | ||
254d3579 | 109 | server = await createSingleServer(1) |
83903cb6 C |
110 | await setAccessTokensToServers([ server ]) |
111 | await setDefaultVideoChannel([ server ]) | |
f6d6e7f8 | 112 | |
89d241a7 | 113 | const body = await server.users.getMyInfo() |
7926c5f9 | 114 | rootId = body.id |
f6d6e7f8 | 115 | |
020d3d3d C |
116 | { |
117 | userAccessToken = await server.users.generateUserAndToken('user1') | |
118 | const { videoChannels } = await server.users.getMyInfo({ token: userAccessToken }) | |
119 | userChannelId = videoChannels[0].id | |
120 | } | |
121 | ||
89d241a7 | 122 | await server.users.update({ userId: rootId, videoQuota: 10_000_000 }) |
f6d6e7f8 | 123 | }) |
124 | ||
125 | describe('Directory cleaning', function () { | |
126 | ||
33ac85bf C |
127 | // FIXME: https://github.com/kukhariev/node-uploadx/pull/524/files#r852989382 |
128 | // it('Should correctly delete files after an upload', async function () { | |
129 | // const uploadId = await prepareUpload() | |
130 | // await sendChunks({ pathUploadId: uploadId }) | |
131 | // await server.videos.endResumableUpload({ pathUploadId: uploadId }) | |
f6d6e7f8 | 132 | |
33ac85bf C |
133 | // expect(await countResumableUploads()).to.equal(0) |
134 | // }) | |
f6d6e7f8 | 135 | |
136 | it('Should not delete files after an unfinished upload', async function () { | |
137 | await prepareUpload() | |
138 | ||
139 | expect(await countResumableUploads()).to.equal(2) | |
140 | }) | |
141 | ||
142 | it('Should not delete recent uploads', async function () { | |
89d241a7 | 143 | await server.debug.sendCommand({ body: { command: 'remove-dandling-resumable-uploads' } }) |
f6d6e7f8 | 144 | |
145 | expect(await countResumableUploads()).to.equal(2) | |
146 | }) | |
147 | ||
148 | it('Should delete old uploads', async function () { | |
89d241a7 | 149 | await server.debug.sendCommand({ body: { command: 'remove-dandling-resumable-uploads' } }) |
f6d6e7f8 | 150 | |
151 | expect(await countResumableUploads()).to.equal(0) | |
152 | }) | |
153 | }) | |
154 | ||
155 | describe('Resumable upload and chunks', function () { | |
156 | ||
157 | it('Should accept the same amount of chunks', async function () { | |
158 | const uploadId = await prepareUpload() | |
159 | await sendChunks({ pathUploadId: uploadId }) | |
160 | ||
161 | await checkFileSize(uploadId, null) | |
162 | }) | |
163 | ||
164 | it('Should not accept more chunks than expected', async function () { | |
020d3d3d | 165 | const uploadId = await prepareUpload({ size: 100 }) |
f6d6e7f8 | 166 | |
167 | await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.CONFLICT_409 }) | |
168 | await checkFileSize(uploadId, 0) | |
169 | }) | |
170 | ||
171 | it('Should not accept more chunks than expected with an invalid content length/content range', async function () { | |
020d3d3d | 172 | const uploadId = await prepareUpload({ size: 1500 }) |
f6d6e7f8 | 173 | |
fea11cf2 C |
174 | // Content length check seems to have changed in v16 |
175 | if (process.version.startsWith('v16')) { | |
176 | await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.CONFLICT_409, contentLength: 1000 }) | |
177 | await checkFileSize(uploadId, 1000) | |
178 | } else { | |
179 | await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.BAD_REQUEST_400, contentLength: 1000 }) | |
180 | await checkFileSize(uploadId, 0) | |
181 | } | |
f6d6e7f8 | 182 | }) |
183 | ||
184 | it('Should not accept more chunks than expected with an invalid content length', async function () { | |
020d3d3d | 185 | const uploadId = await prepareUpload({ size: 500 }) |
f6d6e7f8 | 186 | |
187 | const size = 1000 | |
188 | ||
764b1a14 C |
189 | // Content length check seems to have changed in v16 |
190 | const expectedStatus = process.version.startsWith('v16') | |
191 | ? HttpStatusCode.CONFLICT_409 | |
192 | : HttpStatusCode.BAD_REQUEST_400 | |
193 | ||
83903cb6 | 194 | const contentRangeBuilder = (start: number) => `bytes ${start}-${start + size - 1}/${size}` |
764b1a14 | 195 | await sendChunks({ pathUploadId: uploadId, expectedStatus, contentRangeBuilder, contentLength: size }) |
f6d6e7f8 | 196 | await checkFileSize(uploadId, 0) |
197 | }) | |
276250f0 RK |
198 | |
199 | it('Should be able to accept 2 PUT requests', async function () { | |
200 | const uploadId = await prepareUpload() | |
201 | ||
202 | const result1 = await sendChunks({ pathUploadId: uploadId }) | |
203 | const result2 = await sendChunks({ pathUploadId: uploadId }) | |
204 | ||
205 | expect(result1.body.video.uuid).to.exist | |
206 | expect(result1.body.video.uuid).to.equal(result2.body.video.uuid) | |
207 | ||
208 | expect(result1.headers['x-resumable-upload-cached']).to.not.exist | |
209 | expect(result2.headers['x-resumable-upload-cached']).to.equal('true') | |
210 | ||
211 | await checkFileSize(uploadId, null) | |
212 | }) | |
020d3d3d C |
213 | |
214 | it('Should not have the same upload id with 2 different users', async function () { | |
215 | const originalName = 'toto.mp4' | |
216 | const lastModified = new Date().getTime() | |
217 | ||
218 | const uploadId1 = await prepareUpload({ originalName, lastModified, token: server.accessToken }) | |
219 | const uploadId2 = await prepareUpload({ originalName, lastModified, channelId: userChannelId, token: userAccessToken }) | |
220 | ||
221 | expect(uploadId1).to.not.equal(uploadId2) | |
222 | }) | |
223 | ||
224 | it('Should have the same upload id with the same user', async function () { | |
225 | const originalName = 'toto.mp4' | |
226 | const lastModified = new Date().getTime() | |
227 | ||
228 | const uploadId1 = await prepareUpload({ originalName, lastModified }) | |
229 | const uploadId2 = await prepareUpload({ originalName, lastModified }) | |
230 | ||
231 | expect(uploadId1).to.equal(uploadId2) | |
232 | }) | |
233 | ||
234 | it('Should not cache a request with 2 different users', async function () { | |
235 | const originalName = 'toto.mp4' | |
236 | const lastModified = new Date().getTime() | |
237 | ||
238 | const uploadId = await prepareUpload({ originalName, lastModified, token: server.accessToken }) | |
239 | ||
240 | await sendChunks({ pathUploadId: uploadId, token: server.accessToken }) | |
241 | await sendChunks({ pathUploadId: uploadId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | |
242 | }) | |
243 | ||
244 | it('Should not cache a request after a delete', async function () { | |
245 | const originalName = 'toto.mp4' | |
246 | const lastModified = new Date().getTime() | |
247 | const uploadId1 = await prepareUpload({ originalName, lastModified, token: server.accessToken }) | |
248 | ||
249 | await sendChunks({ pathUploadId: uploadId1 }) | |
250 | await server.videos.endResumableUpload({ pathUploadId: uploadId1 }) | |
251 | ||
252 | const uploadId2 = await prepareUpload({ originalName, lastModified, token: server.accessToken }) | |
253 | expect(uploadId1).to.equal(uploadId2) | |
254 | ||
255 | const result2 = await sendChunks({ pathUploadId: uploadId1 }) | |
256 | expect(result2.headers['x-resumable-upload-cached']).to.not.exist | |
257 | }) | |
33ac85bf C |
258 | |
259 | it('Should refuse an invalid digest', async function () { | |
260 | const uploadId = await prepareUpload({ token: server.accessToken }) | |
261 | ||
262 | await sendChunks({ | |
263 | pathUploadId: uploadId, | |
264 | token: server.accessToken, | |
265 | digestBuilder: () => 'sha=' + 'a'.repeat(40), | |
266 | expectedStatus: 460 | |
267 | }) | |
268 | }) | |
269 | ||
270 | it('Should accept an appropriate digest', async function () { | |
271 | const uploadId = await prepareUpload({ token: server.accessToken }) | |
272 | ||
273 | await sendChunks({ | |
274 | pathUploadId: uploadId, | |
275 | token: server.accessToken, | |
276 | digestBuilder: (chunk: Buffer) => { | |
277 | return 'sha1=' + sha1(chunk, 'base64') | |
278 | } | |
279 | }) | |
280 | }) | |
f6d6e7f8 | 281 | }) |
282 | ||
06c27593 | 283 | after(async function () { |
83903cb6 | 284 | await cleanupTests([ server ]) |
06c27593 | 285 | }) |
f6d6e7f8 | 286 | }) |