1 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
3 import bytes from 'bytes'
4 import * as chai from 'chai'
5 import { stat } from 'fs-extra'
6 import { merge } from 'lodash'
9 expectLogDoesNotContain,
11 generateHighBitrateVideo,
13 } from '@server/tests/shared'
14 import { areObjectStorageTestsDisabled } from '@shared/core-utils'
15 import { HttpStatusCode, VideoDetails } from '@shared/models'
18 createMultipleServers,
25 setAccessTokensToServers,
28 } from '@shared/server-commands'
30 const expect = chai.expect
32 async function checkFiles (options: {
37 playlistBucket: string
38 playlistPrefix?: string
40 webtorrentBucket: string
41 webtorrentPrefix?: string
52 let allFiles = video.files
54 for (const file of video.files) {
55 const baseUrl = baseMockUrl
56 ? `${baseMockUrl}/${webtorrentBucket}/`
57 : `http://${webtorrentBucket}.${ObjectStorageCommand.getEndpointHost()}/`
59 const prefix = webtorrentPrefix || ''
60 const start = baseUrl + prefix
62 expectStartWith(file.fileUrl, start)
64 const res = await makeRawRequest(file.fileDownloadUrl, HttpStatusCode.FOUND_302)
65 const location = res.headers['location']
66 expectStartWith(location, start)
68 await makeRawRequest(location, HttpStatusCode.OK_200)
71 const hls = video.streamingPlaylists[0]
74 allFiles = allFiles.concat(hls.files)
76 const baseUrl = baseMockUrl
77 ? `${baseMockUrl}/${playlistBucket}/`
78 : `http://${playlistBucket}.${ObjectStorageCommand.getEndpointHost()}/`
80 const prefix = playlistPrefix || ''
81 const start = baseUrl + prefix
83 expectStartWith(hls.playlistUrl, start)
84 expectStartWith(hls.segmentsSha256Url, start)
86 await makeRawRequest(hls.playlistUrl, HttpStatusCode.OK_200)
88 const resSha = await makeRawRequest(hls.segmentsSha256Url, HttpStatusCode.OK_200)
89 expect(JSON.stringify(resSha.body)).to.not.throw
91 for (const file of hls.files) {
92 expectStartWith(file.fileUrl, start)
94 const res = await makeRawRequest(file.fileDownloadUrl, HttpStatusCode.FOUND_302)
95 const location = res.headers['location']
96 expectStartWith(location, start)
98 await makeRawRequest(location, HttpStatusCode.OK_200)
102 for (const file of allFiles) {
103 const torrent = await webtorrentAdd(file.magnetUri, true)
105 expect(torrent.files).to.be.an('array')
106 expect(torrent.files.length).to.equal(1)
107 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
109 const res = await makeRawRequest(file.fileUrl, HttpStatusCode.OK_200)
110 expect(res.body).to.have.length.above(100)
113 return allFiles.map(f => f.fileUrl)
116 function runTestSuite (options: {
119 maxUploadPart?: string
121 playlistBucket: string
122 playlistPrefix?: string
124 webtorrentBucket: string
125 webtorrentPrefix?: string
127 useMockBaseUrl?: boolean
129 const mockObjectStorage = new MockObjectStorage()
130 const { fixture } = options
131 let baseMockUrl: string
133 let servers: PeerTubeServer[]
135 let keptUrls: string[] = []
137 const uuidsToDelete: string[] = []
138 let deletedUrls: string[] = []
140 before(async function () {
143 const port = await mockObjectStorage.initialize()
144 baseMockUrl = options.useMockBaseUrl ? `http://localhost:${port}` : undefined
146 await ObjectStorageCommand.createBucket(options.playlistBucket)
147 await ObjectStorageCommand.createBucket(options.webtorrentBucket)
152 endpoint: 'http://' + ObjectStorageCommand.getEndpointHost(),
153 region: ObjectStorageCommand.getRegion(),
155 credentials: ObjectStorageCommand.getCredentialsConfig(),
157 max_upload_part: options.maxUploadPart || '5MB',
159 streaming_playlists: {
160 bucket_name: options.playlistBucket,
161 prefix: options.playlistPrefix,
162 base_url: baseMockUrl
163 ? `${baseMockUrl}/${options.playlistBucket}`
168 bucket_name: options.webtorrentBucket,
169 prefix: options.webtorrentPrefix,
170 base_url: baseMockUrl
171 ? `${baseMockUrl}/${options.webtorrentBucket}`
177 servers = await createMultipleServers(2, config)
179 await setAccessTokensToServers(servers)
180 await doubleFollow(servers[0], servers[1])
182 for (const server of servers) {
183 const { uuid } = await server.videos.quickUpload({ name: 'video to keep' })
184 await waitJobs(servers)
186 const files = await server.videos.listFiles({ id: uuid })
187 keptUrls = keptUrls.concat(files.map(f => f.fileUrl))
191 it('Should upload a video and move it to the object storage without transcoding', async function () {
194 const { uuid } = await servers[0].videos.quickUpload({ name: 'video 1', fixture })
195 uuidsToDelete.push(uuid)
197 await waitJobs(servers)
199 for (const server of servers) {
200 const video = await server.videos.get({ id: uuid })
201 const files = await checkFiles({ ...options, video, baseMockUrl })
203 deletedUrls = deletedUrls.concat(files)
207 it('Should upload a video and move it to the object storage with transcoding', async function () {
210 const { uuid } = await servers[1].videos.quickUpload({ name: 'video 2', fixture })
211 uuidsToDelete.push(uuid)
213 await waitJobs(servers)
215 for (const server of servers) {
216 const video = await server.videos.get({ id: uuid })
217 const files = await checkFiles({ ...options, video, baseMockUrl })
219 deletedUrls = deletedUrls.concat(files)
223 it('Should fetch correctly all the files', async function () {
224 for (const url of deletedUrls.concat(keptUrls)) {
225 await makeRawRequest(url, HttpStatusCode.OK_200)
229 it('Should correctly delete the files', async function () {
230 await servers[0].videos.remove({ id: uuidsToDelete[0] })
231 await servers[1].videos.remove({ id: uuidsToDelete[1] })
233 await waitJobs(servers)
235 for (const url of deletedUrls) {
236 await makeRawRequest(url, HttpStatusCode.NOT_FOUND_404)
240 it('Should have kept other files', async function () {
241 for (const url of keptUrls) {
242 await makeRawRequest(url, HttpStatusCode.OK_200)
246 it('Should have an empty tmp directory', async function () {
247 for (const server of servers) {
248 await checkTmpIsEmpty(server)
252 it('Should not have downloaded files from object storage', async function () {
253 for (const server of servers) {
254 await expectLogDoesNotContain(server, 'from object storage')
258 after(async function () {
259 await mockObjectStorage.terminate()
261 await cleanupTests(servers)
265 describe('Object storage for videos', function () {
266 if (areObjectStorageTestsDisabled()) return
268 describe('Test config', function () {
269 let server: PeerTubeServer
274 endpoint: 'http://' + ObjectStorageCommand.getEndpointHost(),
275 region: ObjectStorageCommand.getRegion(),
277 credentials: ObjectStorageCommand.getCredentialsConfig(),
279 streaming_playlists: {
280 bucket_name: ObjectStorageCommand.DEFAULT_PLAYLIST_BUCKET
284 bucket_name: ObjectStorageCommand.DEFAULT_WEBTORRENT_BUCKET
289 const badCredentials = {
290 access_key_id: 'AKIAIOSFODNN7EXAMPLE',
291 secret_access_key: 'aJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
294 it('Should fail with same bucket names without prefix', function (done) {
295 const config = merge({}, baseConfig, {
297 streaming_playlists: {
307 createSingleServer(1, config)
308 .then(() => done(new Error('Did not throw')))
312 it('Should fail with bad credentials', async function () {
315 await ObjectStorageCommand.prepareDefaultBuckets()
317 const config = merge({}, baseConfig, {
319 credentials: badCredentials
323 server = await createSingleServer(1, config)
324 await setAccessTokensToServers([ server ])
326 const { uuid } = await server.videos.quickUpload({ name: 'video' })
328 await waitJobs([ server ], true)
329 const video = await server.videos.get({ id: uuid })
331 expectStartWith(video.files[0].fileUrl, server.url)
333 await killallServers([ server ])
336 it('Should succeed with credentials from env', async function () {
339 await ObjectStorageCommand.prepareDefaultBuckets()
341 const config = merge({}, baseConfig, {
345 secret_access_key: ''
350 const goodCredentials = ObjectStorageCommand.getCredentialsConfig()
352 server = await createSingleServer(1, config, {
354 AWS_ACCESS_KEY_ID: goodCredentials.access_key_id,
355 AWS_SECRET_ACCESS_KEY: goodCredentials.secret_access_key
359 await setAccessTokensToServers([ server ])
361 const { uuid } = await server.videos.quickUpload({ name: 'video' })
363 await waitJobs([ server ], true)
364 const video = await server.videos.get({ id: uuid })
366 expectStartWith(video.files[0].fileUrl, ObjectStorageCommand.getWebTorrentBaseUrl())
369 after(async function () {
370 await killallServers([ server ])
374 describe('Test simple object storage', function () {
376 playlistBucket: 'streaming-playlists',
377 webtorrentBucket: 'videos'
381 describe('Test object storage with prefix', function () {
383 playlistBucket: 'mybucket',
384 webtorrentBucket: 'mybucket',
386 playlistPrefix: 'streaming-playlists_',
387 webtorrentPrefix: 'webtorrent_'
391 describe('Test object storage with prefix and base URL', function () {
393 playlistBucket: 'mybucket',
394 webtorrentBucket: 'mybucket',
396 playlistPrefix: 'streaming-playlists/',
397 webtorrentPrefix: 'webtorrent/',
403 describe('Test object storage with file bigger than upload part', function () {
405 const maxUploadPart = '5MB'
407 before(async function () {
410 fixture = await generateHighBitrateVideo()
412 const { size } = await stat(fixture)
414 if (bytes.parse(maxUploadPart) > size) {
415 throw Error(`Fixture file is too small (${size}) to make sense for this test.`)
421 playlistBucket: 'streaming-playlists',
422 webtorrentBucket: 'videos',