1 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
3 import bytes from 'bytes'
4 import { expect } from 'chai'
5 import { stat } from 'fs-extra'
6 import { merge } from 'lodash'
10 expectLogDoesNotContain,
12 generateHighBitrateVideo,
13 MockObjectStorageProxy,
15 } from '@server/tests/shared'
16 import { areMockObjectStorageTestsDisabled } from '@shared/core-utils'
17 import { sha1 } from '@shared/extra-utils'
18 import { HttpStatusCode, VideoDetails } from '@shared/models'
21 createMultipleServers,
28 setAccessTokensToServers,
30 } from '@shared/server-commands'
32 async function checkFiles (options: {
33 server: PeerTubeServer
34 originServer: PeerTubeServer
35 originSQLCommand: SQLCommand
41 playlistBucket: string
42 playlistPrefix?: string
44 webtorrentBucket: string
45 webtorrentPrefix?: string
59 let allFiles = video.files
61 for (const file of video.files) {
62 const baseUrl = baseMockUrl
63 ? `${baseMockUrl}/${webtorrentBucket}/`
64 : `http://${webtorrentBucket}.${ObjectStorageCommand.getMockEndpointHost()}/`
66 const prefix = webtorrentPrefix || ''
67 const start = baseUrl + prefix
69 expectStartWith(file.fileUrl, start)
71 const res = await makeRawRequest({ url: file.fileDownloadUrl, expectedStatus: HttpStatusCode.FOUND_302 })
72 const location = res.headers['location']
73 expectStartWith(location, start)
75 await makeRawRequest({ url: location, expectedStatus: HttpStatusCode.OK_200 })
78 const hls = video.streamingPlaylists[0]
81 allFiles = allFiles.concat(hls.files)
83 const baseUrl = baseMockUrl
84 ? `${baseMockUrl}/${playlistBucket}/`
85 : `http://${playlistBucket}.${ObjectStorageCommand.getMockEndpointHost()}/`
87 const prefix = playlistPrefix || ''
88 const start = baseUrl + prefix
90 expectStartWith(hls.playlistUrl, start)
91 expectStartWith(hls.segmentsSha256Url, start)
93 await makeRawRequest({ url: hls.playlistUrl, expectedStatus: HttpStatusCode.OK_200 })
95 const resSha = await makeRawRequest({ url: hls.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 })
96 expect(JSON.stringify(resSha.body)).to.not.throw
99 for (const file of hls.files) {
100 expectStartWith(file.fileUrl, start)
102 const res = await makeRawRequest({ url: file.fileDownloadUrl, expectedStatus: HttpStatusCode.FOUND_302 })
103 const location = res.headers['location']
104 expectStartWith(location, start)
106 await makeRawRequest({ url: location, expectedStatus: HttpStatusCode.OK_200 })
108 if (originServer.internalServerNumber === server.internalServerNumber) {
109 const infohash = sha1(`${2 + hls.playlistUrl}+V${i}`)
110 const dbInfohashes = await originSQLCommand.getPlaylistInfohash(hls.id)
112 expect(dbInfohashes).to.include(infohash)
119 for (const file of allFiles) {
120 await checkWebTorrentWorks(file.magnetUri)
122 const res = await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
123 expect(res.body).to.have.length.above(100)
126 return allFiles.map(f => f.fileUrl)
129 function runTestSuite (options: {
132 maxUploadPart?: string
134 playlistBucket: string
135 playlistPrefix?: string
137 webtorrentBucket: string
138 webtorrentPrefix?: string
140 useMockBaseUrl?: boolean
142 const mockObjectStorageProxy = new MockObjectStorageProxy()
143 const { fixture } = options
144 let baseMockUrl: string
146 let servers: PeerTubeServer[]
147 let sqlCommands: SQLCommand[] = []
148 const objectStorage = new ObjectStorageCommand()
150 let keptUrls: string[] = []
152 const uuidsToDelete: string[] = []
153 let deletedUrls: string[] = []
155 before(async function () {
158 const port = await mockObjectStorageProxy.initialize()
159 baseMockUrl = options.useMockBaseUrl
160 ? `http://127.0.0.1:${port}`
163 await objectStorage.createMockBucket(options.playlistBucket)
164 await objectStorage.createMockBucket(options.webtorrentBucket)
169 endpoint: 'http://' + ObjectStorageCommand.getMockEndpointHost(),
170 region: ObjectStorageCommand.getMockRegion(),
172 credentials: ObjectStorageCommand.getMockCredentialsConfig(),
174 max_upload_part: options.maxUploadPart || '5MB',
176 streaming_playlists: {
177 bucket_name: options.playlistBucket,
178 prefix: options.playlistPrefix,
179 base_url: baseMockUrl
180 ? `${baseMockUrl}/${options.playlistBucket}`
185 bucket_name: options.webtorrentBucket,
186 prefix: options.webtorrentPrefix,
187 base_url: baseMockUrl
188 ? `${baseMockUrl}/${options.webtorrentBucket}`
194 servers = await createMultipleServers(2, config)
196 await setAccessTokensToServers(servers)
197 await doubleFollow(servers[0], servers[1])
199 for (const server of servers) {
200 const { uuid } = await server.videos.quickUpload({ name: 'video to keep' })
201 await waitJobs(servers)
203 const files = await server.videos.listFiles({ id: uuid })
204 keptUrls = keptUrls.concat(files.map(f => f.fileUrl))
207 sqlCommands = servers.map(s => new SQLCommand(s))
210 it('Should upload a video and move it to the object storage without transcoding', async function () {
213 const { uuid } = await servers[0].videos.quickUpload({ name: 'video 1', fixture })
214 uuidsToDelete.push(uuid)
216 await waitJobs(servers)
218 for (const server of servers) {
219 const video = await server.videos.get({ id: uuid })
220 const files = await checkFiles({ ...options, server, originServer: servers[0], originSQLCommand: sqlCommands[0], video, baseMockUrl })
222 deletedUrls = deletedUrls.concat(files)
226 it('Should upload a video and move it to the object storage with transcoding', async function () {
229 const { uuid } = await servers[1].videos.quickUpload({ name: 'video 2', fixture })
230 uuidsToDelete.push(uuid)
232 await waitJobs(servers)
234 for (const server of servers) {
235 const video = await server.videos.get({ id: uuid })
236 const files = await checkFiles({ ...options, server, originServer: servers[0], originSQLCommand: sqlCommands[0], video, baseMockUrl })
238 deletedUrls = deletedUrls.concat(files)
242 it('Should fetch correctly all the files', async function () {
243 for (const url of deletedUrls.concat(keptUrls)) {
244 await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 })
248 it('Should correctly delete the files', async function () {
249 await servers[0].videos.remove({ id: uuidsToDelete[0] })
250 await servers[1].videos.remove({ id: uuidsToDelete[1] })
252 await waitJobs(servers)
254 for (const url of deletedUrls) {
255 await makeRawRequest({ url, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
259 it('Should have kept other files', async function () {
260 for (const url of keptUrls) {
261 await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 })
265 it('Should have an empty tmp directory', async function () {
266 for (const server of servers) {
267 await checkTmpIsEmpty(server)
271 it('Should not have downloaded files from object storage', async function () {
272 for (const server of servers) {
273 await expectLogDoesNotContain(server, 'from object storage')
277 after(async function () {
278 await mockObjectStorageProxy.terminate()
279 await objectStorage.cleanupMock()
281 for (const sqlCommand of sqlCommands) {
282 await sqlCommand.cleanup()
285 await cleanupTests(servers)
289 describe('Object storage for videos', function () {
290 if (areMockObjectStorageTestsDisabled()) return
292 const objectStorage = new ObjectStorageCommand()
294 describe('Test config', function () {
295 let server: PeerTubeServer
297 const baseConfig = objectStorage.getDefaultMockConfig()
299 const badCredentials = {
300 access_key_id: 'AKIAIOSFODNN7EXAMPLE',
301 secret_access_key: 'aJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
304 it('Should fail with same bucket names without prefix', function (done) {
305 const config = merge({}, baseConfig, {
307 streaming_playlists: {
317 createSingleServer(1, config)
318 .then(() => done(new Error('Did not throw')))
322 it('Should fail with bad credentials', async function () {
325 await objectStorage.prepareDefaultMockBuckets()
327 const config = merge({}, baseConfig, {
329 credentials: badCredentials
333 server = await createSingleServer(1, config)
334 await setAccessTokensToServers([ server ])
336 const { uuid } = await server.videos.quickUpload({ name: 'video' })
338 await waitJobs([ server ], { skipDelayed: true })
339 const video = await server.videos.get({ id: uuid })
341 expectStartWith(video.files[0].fileUrl, server.url)
343 await killallServers([ server ])
346 it('Should succeed with credentials from env', async function () {
349 await objectStorage.prepareDefaultMockBuckets()
351 const config = merge({}, baseConfig, {
355 secret_access_key: ''
360 const goodCredentials = ObjectStorageCommand.getMockCredentialsConfig()
362 server = await createSingleServer(1, config, {
364 AWS_ACCESS_KEY_ID: goodCredentials.access_key_id,
365 AWS_SECRET_ACCESS_KEY: goodCredentials.secret_access_key
369 await setAccessTokensToServers([ server ])
371 const { uuid } = await server.videos.quickUpload({ name: 'video' })
373 await waitJobs([ server ], { skipDelayed: true })
374 const video = await server.videos.get({ id: uuid })
376 expectStartWith(video.files[0].fileUrl, objectStorage.getMockWebVideosBaseUrl())
379 after(async function () {
380 await objectStorage.cleanupMock()
382 await cleanupTests([ server ])
386 describe('Test simple object storage', function () {
388 playlistBucket: objectStorage.getMockBucketName('streaming-playlists'),
389 webtorrentBucket: objectStorage.getMockBucketName('videos')
393 describe('Test object storage with prefix', function () {
395 playlistBucket: objectStorage.getMockBucketName('mybucket'),
396 webtorrentBucket: objectStorage.getMockBucketName('mybucket'),
398 playlistPrefix: 'streaming-playlists_',
399 webtorrentPrefix: 'webtorrent_'
403 describe('Test object storage with prefix and base URL', function () {
405 playlistBucket: objectStorage.getMockBucketName('mybucket'),
406 webtorrentBucket: objectStorage.getMockBucketName('mybucket'),
408 playlistPrefix: 'streaming-playlists/',
409 webtorrentPrefix: 'webtorrent/',
415 describe('Test object storage with file bigger than upload part', function () {
417 const maxUploadPart = '5MB'
419 before(async function () {
422 fixture = await generateHighBitrateVideo()
424 const { size } = await stat(fixture)
426 if (bytes.parse(maxUploadPart) > size) {
427 throw Error(`Fixture file is too small (${size}) to make sense for this test.`)
433 playlistBucket: objectStorage.getMockBucketName('streaming-playlists'),
434 webtorrentBucket: objectStorage.getMockBucketName('videos'),