1 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
4 import * as chai from 'chai'
5 import { readdir } from 'fs-extra'
6 import * as magnetUtil from 'magnet-uri'
7 import { join } from 'path'
8 import { HttpStatusCode } from '@shared/models'
11 checkVideoFilesWereRemoved,
14 createMultipleServers,
19 setAccessTokensToServers,
22 } from '@shared/extra-utils'
23 import { VideoPrivacy, VideoRedundancyStrategy, VideoRedundancyStrategyWithManual } from '@shared/models'
25 const expect = chai.expect
27 let servers: PeerTubeServer[] = []
28 let video1Server2UUID: string
29 let video1Server2Id: number
31 function checkMagnetWebseeds (file: { magnetUri: string, resolution: { id: number } }, baseWebseeds: string[], server: PeerTubeServer) {
32 const parsed = magnetUtil.decode(file.magnetUri)
34 for (const ws of baseWebseeds) {
35 const found = parsed.urlList.find(url => url === `${ws}-${file.resolution.id}.mp4`)
36 expect(found, `Webseed ${ws} not found in ${file.magnetUri} on server ${server.url}`).to.not.be.undefined
39 expect(parsed.urlList).to.have.lengthOf(baseWebseeds.length)
42 async function createSingleServers (strategy: VideoRedundancyStrategy | null, additionalParams: any = {}, withWebtorrent = true) {
43 const strategies: any[] = []
45 if (strategy !== null) {
48 min_lifetime: '1 hour',
60 enabled: withWebtorrent
68 check_interval: '5 seconds',
74 servers = await createMultipleServers(3, config)
76 // Get the access tokens
77 await setAccessTokensToServers(servers)
80 const { uuid, id } = await servers[1].videos.upload({ attributes: { name: 'video 1 server 2' } })
81 video1Server2UUID = uuid
84 await servers[1].videos.view({ id: video1Server2UUID })
87 await waitJobs(servers)
89 // Server 1 and server 2 follow each other
90 await doubleFollow(servers[0], servers[1])
91 // Server 1 and server 3 follow each other
92 await doubleFollow(servers[0], servers[2])
93 // Server 2 and server 3 follow each other
94 await doubleFollow(servers[1], servers[2])
96 await waitJobs(servers)
99 async function check1WebSeed (videoUUID?: string) {
100 if (!videoUUID) videoUUID = video1Server2UUID
103 `http://localhost:${servers[1].port}/static/webseed/${videoUUID}`
106 for (const server of servers) {
107 // With token to avoid issues with video follow constraints
108 const video = await server.videos.getWithToken({ id: videoUUID })
110 for (const f of video.files) {
111 checkMagnetWebseeds(f, webseeds, server)
116 async function check2Webseeds (videoUUID?: string) {
117 if (!videoUUID) videoUUID = video1Server2UUID
120 `http://localhost:${servers[0].port}/static/redundancy/${videoUUID}`,
121 `http://localhost:${servers[1].port}/static/webseed/${videoUUID}`
124 for (const server of servers) {
125 const video = await server.videos.get({ id: videoUUID })
127 for (const file of video.files) {
128 checkMagnetWebseeds(file, webseeds, server)
130 await makeGetRequest({
132 expectedStatus: HttpStatusCode.OK_200,
133 path: '/static/redundancy/' + `${videoUUID}-${file.resolution.id}.mp4`,
136 await makeGetRequest({
138 expectedStatus: HttpStatusCode.OK_200,
139 path: `/static/webseed/${videoUUID}-${file.resolution.id}.mp4`,
145 const directories = [
146 'test' + servers[0].internalServerNumber + '/redundancy',
147 'test' + servers[1].internalServerNumber + '/videos'
150 for (const directory of directories) {
151 const files = await readdir(join(root(), directory))
152 expect(files).to.have.length.at.least(4)
154 for (const resolution of [ 240, 360, 480, 720 ]) {
155 expect(files.find(f => f === `${videoUUID}-${resolution}.mp4`)).to.not.be.undefined
160 async function check0PlaylistRedundancies (videoUUID?: string) {
161 if (!videoUUID) videoUUID = video1Server2UUID
163 for (const server of servers) {
164 // With token to avoid issues with video follow constraints
165 const video = await server.videos.getWithToken({ id: videoUUID })
167 expect(video.streamingPlaylists).to.be.an('array')
168 expect(video.streamingPlaylists).to.have.lengthOf(1)
169 expect(video.streamingPlaylists[0].redundancies).to.have.lengthOf(0)
173 async function check1PlaylistRedundancies (videoUUID?: string) {
174 if (!videoUUID) videoUUID = video1Server2UUID
176 for (const server of servers) {
177 const video = await server.videos.get({ id: videoUUID })
179 expect(video.streamingPlaylists).to.have.lengthOf(1)
180 expect(video.streamingPlaylists[0].redundancies).to.have.lengthOf(1)
182 const redundancy = video.streamingPlaylists[0].redundancies[0]
184 expect(redundancy.baseUrl).to.equal(servers[0].url + '/static/redundancy/hls/' + videoUUID)
187 const baseUrlPlaylist = servers[1].url + '/static/streaming-playlists/hls'
188 const baseUrlSegment = servers[0].url + '/static/redundancy/hls'
190 const video = await servers[0].videos.get({ id: videoUUID })
191 const hlsPlaylist = video.streamingPlaylists[0]
193 for (const resolution of [ 240, 360, 480, 720 ]) {
194 await checkSegmentHash({ server: servers[1], baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist })
197 const directories = [
198 'test' + servers[0].internalServerNumber + '/redundancy/hls',
199 'test' + servers[1].internalServerNumber + '/streaming-playlists/hls'
202 for (const directory of directories) {
203 const files = await readdir(join(root(), directory, videoUUID))
204 expect(files).to.have.length.at.least(4)
206 for (const resolution of [ 240, 360, 480, 720 ]) {
207 const filename = `${videoUUID}-${resolution}-fragmented.mp4`
209 expect(files.find(f => f === filename)).to.not.be.undefined
214 async function checkStatsGlobal (strategy: VideoRedundancyStrategyWithManual) {
215 let totalSize: number = null
218 if (strategy !== 'manual') {
223 const data = await servers[0].stats.get()
224 expect(data.videosRedundancy).to.have.lengthOf(statsLength)
226 const stat = data.videosRedundancy[0]
227 expect(stat.strategy).to.equal(strategy)
228 expect(stat.totalSize).to.equal(totalSize)
233 async function checkStatsWith1Redundancy (strategy: VideoRedundancyStrategyWithManual, onlyHls = false) {
234 const stat = await checkStatsGlobal(strategy)
236 expect(stat.totalUsed).to.be.at.least(1).and.below(409601)
237 expect(stat.totalVideoFiles).to.equal(onlyHls ? 4 : 8)
238 expect(stat.totalVideos).to.equal(1)
241 async function checkStatsWithoutRedundancy (strategy: VideoRedundancyStrategyWithManual) {
242 const stat = await checkStatsGlobal(strategy)
244 expect(stat.totalUsed).to.equal(0)
245 expect(stat.totalVideoFiles).to.equal(0)
246 expect(stat.totalVideos).to.equal(0)
249 async function findServerFollows () {
250 const body = await servers[0].follows.getFollowings({ start: 0, count: 5, sort: '-createdAt' })
251 const follows = body.data
252 const server2 = follows.find(f => f.following.host === `localhost:${servers[1].port}`)
253 const server3 = follows.find(f => f.following.host === `localhost:${servers[2].port}`)
255 return { server2, server3 }
258 async function enableRedundancyOnServer1 () {
259 await servers[0].redundancy.updateRedundancy({ host: servers[1].host, redundancyAllowed: true })
261 const { server2, server3 } = await findServerFollows()
263 expect(server3).to.not.be.undefined
264 expect(server3.following.hostRedundancyAllowed).to.be.false
266 expect(server2).to.not.be.undefined
267 expect(server2.following.hostRedundancyAllowed).to.be.true
270 async function disableRedundancyOnServer1 () {
271 await servers[0].redundancy.updateRedundancy({ host: servers[1].host, redundancyAllowed: false })
273 const { server2, server3 } = await findServerFollows()
275 expect(server3).to.not.be.undefined
276 expect(server3.following.hostRedundancyAllowed).to.be.false
278 expect(server2).to.not.be.undefined
279 expect(server2.following.hostRedundancyAllowed).to.be.false
282 describe('Test videos redundancy', function () {
284 describe('With most-views strategy', function () {
285 const strategy = 'most-views'
290 return createSingleServers(strategy)
293 it('Should have 1 webseed on the first video', async function () {
294 await check1WebSeed()
295 await check0PlaylistRedundancies()
296 await checkStatsWithoutRedundancy(strategy)
299 it('Should enable redundancy on server 1', function () {
300 return enableRedundancyOnServer1()
303 it('Should have 2 webseeds on the first video', async function () {
306 await waitJobs(servers)
307 await servers[0].servers.waitUntilLog('Duplicated ', 5)
308 await waitJobs(servers)
310 await check2Webseeds()
311 await check1PlaylistRedundancies()
312 await checkStatsWith1Redundancy(strategy)
315 it('Should undo redundancy on server 1 and remove duplicated videos', async function () {
318 await disableRedundancyOnServer1()
320 await waitJobs(servers)
323 await check1WebSeed()
324 await check0PlaylistRedundancies()
326 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0], [ 'videos', join('playlists', 'hls') ])
329 after(async function () {
330 return cleanupTests(servers)
334 describe('With trending strategy', function () {
335 const strategy = 'trending'
340 return createSingleServers(strategy)
343 it('Should have 1 webseed on the first video', async function () {
344 await check1WebSeed()
345 await check0PlaylistRedundancies()
346 await checkStatsWithoutRedundancy(strategy)
349 it('Should enable redundancy on server 1', function () {
350 return enableRedundancyOnServer1()
353 it('Should have 2 webseeds on the first video', async function () {
356 await waitJobs(servers)
357 await servers[0].servers.waitUntilLog('Duplicated ', 5)
358 await waitJobs(servers)
360 await check2Webseeds()
361 await check1PlaylistRedundancies()
362 await checkStatsWith1Redundancy(strategy)
365 it('Should unfollow on server 1 and remove duplicated videos', async function () {
368 await servers[0].follows.unfollow({ target: servers[1] })
370 await waitJobs(servers)
373 await check1WebSeed()
374 await check0PlaylistRedundancies()
376 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0], [ 'videos' ])
379 after(async function () {
380 await cleanupTests(servers)
384 describe('With recently added strategy', function () {
385 const strategy = 'recently-added'
390 return createSingleServers(strategy, { min_views: 3 })
393 it('Should have 1 webseed on the first video', async function () {
394 await check1WebSeed()
395 await check0PlaylistRedundancies()
396 await checkStatsWithoutRedundancy(strategy)
399 it('Should enable redundancy on server 1', function () {
400 return enableRedundancyOnServer1()
403 it('Should still have 1 webseed on the first video', async function () {
406 await waitJobs(servers)
408 await waitJobs(servers)
410 await check1WebSeed()
411 await check0PlaylistRedundancies()
412 await checkStatsWithoutRedundancy(strategy)
415 it('Should view 2 times the first video to have > min_views config', async function () {
418 await servers[0].videos.view({ id: video1Server2UUID })
419 await servers[2].videos.view({ id: video1Server2UUID })
422 await waitJobs(servers)
425 it('Should have 2 webseeds on the first video', async function () {
428 await waitJobs(servers)
429 await servers[0].servers.waitUntilLog('Duplicated ', 5)
430 await waitJobs(servers)
432 await check2Webseeds()
433 await check1PlaylistRedundancies()
434 await checkStatsWith1Redundancy(strategy)
437 it('Should remove the video and the redundancy files', async function () {
440 await servers[1].videos.remove({ id: video1Server2UUID })
442 await waitJobs(servers)
444 for (const server of servers) {
445 await checkVideoFilesWereRemoved(video1Server2UUID, server)
449 after(async function () {
450 await cleanupTests(servers)
454 describe('With only HLS files', function () {
455 const strategy = 'recently-added'
457 before(async function () {
460 await createSingleServers(strategy, { min_views: 3 }, false)
463 it('Should have 0 playlist redundancy on the first video', async function () {
464 await check1WebSeed()
465 await check0PlaylistRedundancies()
468 it('Should enable redundancy on server 1', function () {
469 return enableRedundancyOnServer1()
472 it('Should still have 0 redundancy on the first video', async function () {
475 await waitJobs(servers)
477 await waitJobs(servers)
479 await check0PlaylistRedundancies()
480 await checkStatsWithoutRedundancy(strategy)
483 it('Should have 1 redundancy on the first video', async function () {
486 await servers[0].videos.view({ id: video1Server2UUID })
487 await servers[2].videos.view({ id: video1Server2UUID })
490 await waitJobs(servers)
492 await waitJobs(servers)
493 await servers[0].servers.waitUntilLog('Duplicated ', 1)
494 await waitJobs(servers)
496 await check1PlaylistRedundancies()
497 await checkStatsWith1Redundancy(strategy, true)
500 it('Should remove the video and the redundancy files', async function () {
503 await servers[1].videos.remove({ id: video1Server2UUID })
505 await waitJobs(servers)
507 for (const server of servers) {
508 await checkVideoFilesWereRemoved(video1Server2UUID, server)
512 after(async function () {
513 await cleanupTests(servers)
517 describe('With manual strategy', function () {
521 return createSingleServers(null)
524 it('Should have 1 webseed on the first video', async function () {
525 await check1WebSeed()
526 await check0PlaylistRedundancies()
527 await checkStatsWithoutRedundancy('manual')
530 it('Should create a redundancy on first video', async function () {
531 await servers[0].redundancy.addVideo({ videoId: video1Server2Id })
534 it('Should have 2 webseeds on the first video', async function () {
537 await waitJobs(servers)
538 await servers[0].servers.waitUntilLog('Duplicated ', 5)
539 await waitJobs(servers)
541 await check2Webseeds()
542 await check1PlaylistRedundancies()
543 await checkStatsWith1Redundancy('manual')
546 it('Should manually remove redundancies on server 1 and remove duplicated videos', async function () {
549 const body = await servers[0].redundancy.listVideos({ target: 'remote-videos' })
551 const videos = body.data
552 expect(videos).to.have.lengthOf(1)
554 const video = videos[0]
556 for (const r of video.redundancies.files.concat(video.redundancies.streamingPlaylists)) {
557 await servers[0].redundancy.removeVideo({ redundancyId: r.id })
560 await waitJobs(servers)
563 await check1WebSeed()
564 await check0PlaylistRedundancies()
566 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0], [ 'videos' ])
569 after(async function () {
570 await cleanupTests(servers)
574 describe('Test expiration', function () {
575 const strategy = 'recently-added'
577 async function checkContains (servers: PeerTubeServer[], str: string) {
578 for (const server of servers) {
579 const video = await server.videos.get({ id: video1Server2UUID })
581 for (const f of video.files) {
582 expect(f.magnetUri).to.contain(str)
587 async function checkNotContains (servers: PeerTubeServer[], str: string) {
588 for (const server of servers) {
589 const video = await server.videos.get({ id: video1Server2UUID })
591 for (const f of video.files) {
592 expect(f.magnetUri).to.not.contain(str)
597 before(async function () {
600 await createSingleServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
602 await enableRedundancyOnServer1()
605 it('Should still have 2 webseeds after 10 seconds', async function () {
611 await checkContains(servers, 'http%3A%2F%2Flocalhost%3A' + servers[0].port)
613 // Maybe a server deleted a redundancy in the scheduler
616 await checkContains(servers, 'http%3A%2F%2Flocalhost%3A' + servers[0].port)
620 it('Should stop server 1 and expire video redundancy', async function () {
623 await killallServers([ servers[0] ])
627 await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A' + servers[0].port)
630 after(async function () {
631 await cleanupTests(servers)
635 describe('Test file replacement', function () {
636 let video2Server2UUID: string
637 const strategy = 'recently-added'
639 before(async function () {
642 await createSingleServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
644 await enableRedundancyOnServer1()
646 await waitJobs(servers)
647 await servers[0].servers.waitUntilLog('Duplicated ', 5)
648 await waitJobs(servers)
650 await check2Webseeds(video1Server2UUID)
651 await check1PlaylistRedundancies(video1Server2UUID)
652 await checkStatsWith1Redundancy(strategy)
654 const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video 2 server 2', privacy: VideoPrivacy.PRIVATE } })
655 video2Server2UUID = uuid
657 // Wait transcoding before federation
658 await waitJobs(servers)
660 await servers[1].videos.update({ id: video2Server2UUID, attributes: { privacy: VideoPrivacy.PUBLIC } })
663 it('Should cache video 2 webseeds on the first video', async function () {
666 await waitJobs(servers)
670 while (checked === false) {
674 await check1WebSeed(video1Server2UUID)
675 await check0PlaylistRedundancies(video1Server2UUID)
677 await check2Webseeds(video2Server2UUID)
678 await check1PlaylistRedundancies(video2Server2UUID)
687 it('Should disable strategy and remove redundancies', async function () {
690 await waitJobs(servers)
692 await killallServers([ servers[0] ])
693 await servers[0].run({
696 check_interval: '1 second',
702 await waitJobs(servers)
704 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0], [ join('redundancy', 'hls') ])
707 after(async function () {
708 await cleanupTests(servers)