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