aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-06-27 11:53:12 +0200
committerChocobozzz <me@florianbigard.com>2022-06-27 11:53:12 +0200
commit3a54605d4e7ec5b4f47131e8d23255be35b7beac (patch)
treefce9d34812a7638d4a0253b076f05aabd15a2ce9
parent88edc66edadcab1b0372679e23bf2a7a6ff50131 (diff)
downloadPeerTube-3a54605d4e7ec5b4f47131e8d23255be35b7beac.tar.gz
PeerTube-3a54605d4e7ec5b4f47131e8d23255be35b7beac.tar.zst
PeerTube-3a54605d4e7ec5b4f47131e8d23255be35b7beac.zip
Process images in a dedicated worker
-rw-r--r--scripts/migrations/peertube-4.2.ts2
-rw-r--r--scripts/regenerate-thumbnails.ts2
-rw-r--r--server/helpers/image-utils.ts25
-rw-r--r--server/initializers/constants.ts4
-rw-r--r--server/lib/local-actor.ts5
-rw-r--r--server/lib/thumbnail.ts27
-rw-r--r--server/lib/worker/parent-process.ts16
-rw-r--r--server/lib/worker/workers/image-downloader.ts2
-rw-r--r--server/lib/worker/workers/image-processor.ts7
-rw-r--r--server/tests/helpers/image.ts14
10 files changed, 77 insertions, 27 deletions
diff --git a/scripts/migrations/peertube-4.2.ts b/scripts/migrations/peertube-4.2.ts
index b5e5dfebd..6a9007265 100644
--- a/scripts/migrations/peertube-4.2.ts
+++ b/scripts/migrations/peertube-4.2.ts
@@ -110,7 +110,7 @@ async function generateSmallerAvatar (actor: MActorDefault) {
110 const source = join(CONFIG.STORAGE.ACTOR_IMAGES, sourceFilename) 110 const source = join(CONFIG.STORAGE.ACTOR_IMAGES, sourceFilename)
111 const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, newImageName) 111 const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, newImageName)
112 112
113 await processImage(source, destination, imageSize, true) 113 await processImage({ path: source, destination, newSize: imageSize, keepOriginal: true })
114 114
115 const actorImageInfo = { 115 const actorImageInfo = {
116 name: newImageName, 116 name: newImageName,
diff --git a/scripts/regenerate-thumbnails.ts b/scripts/regenerate-thumbnails.ts
index a377baa61..061819387 100644
--- a/scripts/regenerate-thumbnails.ts
+++ b/scripts/regenerate-thumbnails.ts
@@ -52,7 +52,7 @@ async function processVideo (id: number) {
52 thumbnail.height = size.height 52 thumbnail.height = size.height
53 53
54 const thumbnailPath = thumbnail.getPath() 54 const thumbnailPath = thumbnail.getPath()
55 await processImage(previewPath, thumbnailPath, size, true) 55 await processImage({ path: previewPath, destination: thumbnailPath, newSize: size, keepOriginal: true })
56 56
57 // Save new attributes 57 // Save new attributes
58 await thumbnail.save() 58 await thumbnail.save()
diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts
index ebb102a0d..bbd4692ef 100644
--- a/server/helpers/image-utils.ts
+++ b/server/helpers/image-utils.ts
@@ -12,12 +12,14 @@ function generateImageFilename (extension = '.jpg') {
12 return buildUUID() + extension 12 return buildUUID() + extension
13} 13}
14 14
15async function processImage ( 15async function processImage (options: {
16 path: string, 16 path: string
17 destination: string, 17 destination: string
18 newSize: { width: number, height: number }, 18 newSize: { width: number, height: number }
19 keepOriginal = false 19 keepOriginal?: boolean // default false
20) { 20}) {
21 const { path, destination, newSize, keepOriginal = false } = options
22
21 const extension = getLowercaseExtension(path) 23 const extension = getLowercaseExtension(path)
22 24
23 if (path === destination) { 25 if (path === destination) {
@@ -36,7 +38,14 @@ async function processImage (
36 if (keepOriginal !== true) await remove(path) 38 if (keepOriginal !== true) await remove(path)
37} 39}
38 40
39async function generateImageFromVideoFile (fromPath: string, folder: string, imageName: string, size: { width: number, height: number }) { 41async function generateImageFromVideoFile (options: {
42 fromPath: string
43 folder: string
44 imageName: string
45 size: { width: number, height: number }
46}) {
47 const { fromPath, folder, imageName, size } = options
48
40 const pendingImageName = 'pending-' + imageName 49 const pendingImageName = 'pending-' + imageName
41 const pendingImagePath = join(folder, pendingImageName) 50 const pendingImagePath = join(folder, pendingImageName)
42 51
@@ -44,7 +53,7 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima
44 await generateThumbnailFromVideo(fromPath, folder, imageName) 53 await generateThumbnailFromVideo(fromPath, folder, imageName)
45 54
46 const destination = join(folder, imageName) 55 const destination = join(folder, imageName)
47 await processImage(pendingImagePath, destination, size) 56 await processImage({ path: pendingImagePath, destination, newSize: size })
48 } catch (err) { 57 } catch (err) {
49 logger.error('Cannot generate image from video %s.', fromPath, { err, ...lTags() }) 58 logger.error('Cannot generate image from video %s.', fromPath, { err, ...lTags() })
50 59
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 175935835..c6989c38b 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -748,6 +748,10 @@ const WORKER_THREADS = {
748 DOWNLOAD_IMAGE: { 748 DOWNLOAD_IMAGE: {
749 CONCURRENCY: 3, 749 CONCURRENCY: 3,
750 MAX_THREADS: 1 750 MAX_THREADS: 1
751 },
752 PROCESS_IMAGE: {
753 CONCURRENCY: 1,
754 MAX_THREADS: 5
751 } 755 }
752} 756}
753 757
diff --git a/server/lib/local-actor.ts b/server/lib/local-actor.ts
index e3b04c094..1d9be76e2 100644
--- a/server/lib/local-actor.ts
+++ b/server/lib/local-actor.ts
@@ -6,14 +6,13 @@ import { getLowercaseExtension } from '@shared/core-utils'
6import { buildUUID } from '@shared/extra-utils' 6import { buildUUID } from '@shared/extra-utils'
7import { ActivityPubActorType, ActorImageType } from '@shared/models' 7import { ActivityPubActorType, ActorImageType } from '@shared/models'
8import { retryTransactionWrapper } from '../helpers/database-utils' 8import { retryTransactionWrapper } from '../helpers/database-utils'
9import { processImage } from '../helpers/image-utils'
10import { CONFIG } from '../initializers/config' 9import { CONFIG } from '../initializers/config'
11import { ACTOR_IMAGES_SIZE, LRU_CACHE, WEBSERVER } from '../initializers/constants' 10import { ACTOR_IMAGES_SIZE, LRU_CACHE, WEBSERVER } from '../initializers/constants'
12import { sequelizeTypescript } from '../initializers/database' 11import { sequelizeTypescript } from '../initializers/database'
13import { MAccountDefault, MActor, MChannelDefault } from '../types/models' 12import { MAccountDefault, MActor, MChannelDefault } from '../types/models'
14import { deleteActorImages, updateActorImages } from './activitypub/actors' 13import { deleteActorImages, updateActorImages } from './activitypub/actors'
15import { sendUpdateActor } from './activitypub/send' 14import { sendUpdateActor } from './activitypub/send'
16import { downloadImageFromWorker } from './worker/parent-process' 15import { downloadImageFromWorker, processImageFromWorker } from './worker/parent-process'
17 16
18function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) { 17function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) {
19 return new ActorModel({ 18 return new ActorModel({
@@ -42,7 +41,7 @@ async function updateLocalActorImageFiles (
42 41
43 const imageName = buildUUID() + extension 42 const imageName = buildUUID() + extension
44 const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName) 43 const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName)
45 await processImage(imagePhysicalFile.path, destination, imageSize, true) 44 await processImageFromWorker({ path: imagePhysicalFile.path, destination, newSize: imageSize, keepOriginal: true })
46 45
47 return { 46 return {
48 imageName, 47 imageName,
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts
index f00c87623..02b867a91 100644
--- a/server/lib/thumbnail.ts
+++ b/server/lib/thumbnail.ts
@@ -1,6 +1,6 @@
1import { join } from 'path' 1import { join } from 'path'
2import { ThumbnailType } from '@shared/models' 2import { ThumbnailType } from '@shared/models'
3import { generateImageFilename, generateImageFromVideoFile, processImage } from '../helpers/image-utils' 3import { generateImageFilename, generateImageFromVideoFile } from '../helpers/image-utils'
4import { CONFIG } from '../initializers/config' 4import { CONFIG } from '../initializers/config'
5import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' 5import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants'
6import { ThumbnailModel } from '../models/video/thumbnail' 6import { ThumbnailModel } from '../models/video/thumbnail'
@@ -9,6 +9,7 @@ import { MThumbnail } from '../types/models/video/thumbnail'
9import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist' 9import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist'
10import { downloadImageFromWorker } from './local-actor' 10import { downloadImageFromWorker } from './local-actor'
11import { VideoPathManager } from './video-path-manager' 11import { VideoPathManager } from './video-path-manager'
12import { processImageFromWorker } from './worker/parent-process'
12 13
13type ImageSize = { height?: number, width?: number } 14type ImageSize = { height?: number, width?: number }
14 15
@@ -23,7 +24,10 @@ function updatePlaylistMiniatureFromExisting (options: {
23 const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size) 24 const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size)
24 const type = ThumbnailType.MINIATURE 25 const type = ThumbnailType.MINIATURE
25 26
26 const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal) 27 const thumbnailCreator = () => {
28 return processImageFromWorker({ path: inputPath, destination: outputPath, newSize: { width, height }, keepOriginal })
29 }
30
27 return updateThumbnailFromFunction({ 31 return updateThumbnailFromFunction({
28 thumbnailCreator, 32 thumbnailCreator,
29 filename, 33 filename,
@@ -99,7 +103,10 @@ function updateVideoMiniatureFromExisting (options: {
99 const { inputPath, video, type, automaticallyGenerated, size, keepOriginal = false } = options 103 const { inputPath, video, type, automaticallyGenerated, size, keepOriginal = false } = options
100 104
101 const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) 105 const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
102 const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal) 106
107 const thumbnailCreator = () => {
108 return processImageFromWorker({ path: inputPath, destination: outputPath, newSize: { width, height }, keepOriginal })
109 }
103 110
104 return updateThumbnailFromFunction({ 111 return updateThumbnailFromFunction({
105 thumbnailCreator, 112 thumbnailCreator,
@@ -123,8 +130,18 @@ function generateVideoMiniature (options: {
123 const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type) 130 const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type)
124 131
125 const thumbnailCreator = videoFile.isAudio() 132 const thumbnailCreator = videoFile.isAudio()
126 ? () => processImage(ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND, outputPath, { width, height }, true) 133 ? () => processImageFromWorker({
127 : () => generateImageFromVideoFile(input, basePath, filename, { height, width }) 134 path: ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND,
135 destination: outputPath,
136 newSize: { width, height },
137 keepOriginal: true
138 })
139 : () => generateImageFromVideoFile({
140 fromPath: input,
141 folder: basePath,
142 imageName: filename,
143 size: { height, width }
144 })
128 145
129 return updateThumbnailFromFunction({ 146 return updateThumbnailFromFunction({
130 thumbnailCreator, 147 thumbnailCreator,
diff --git a/server/lib/worker/parent-process.ts b/server/lib/worker/parent-process.ts
index 18dabd97f..188001677 100644
--- a/server/lib/worker/parent-process.ts
+++ b/server/lib/worker/parent-process.ts
@@ -2,6 +2,7 @@ import { join } from 'path'
2import Piscina from 'piscina' 2import Piscina from 'piscina'
3import { WORKER_THREADS } from '@server/initializers/constants' 3import { WORKER_THREADS } from '@server/initializers/constants'
4import { downloadImage } from './workers/image-downloader' 4import { downloadImage } from './workers/image-downloader'
5import { processImage } from '@server/helpers/image-utils'
5 6
6const downloadImagerWorker = new Piscina({ 7const downloadImagerWorker = new Piscina({
7 filename: join(__dirname, 'workers', 'image-downloader.js'), 8 filename: join(__dirname, 'workers', 'image-downloader.js'),
@@ -13,6 +14,19 @@ function downloadImageFromWorker (options: Parameters<typeof downloadImage>[0]):
13 return downloadImagerWorker.run(options) 14 return downloadImagerWorker.run(options)
14} 15}
15 16
17// ---------------------------------------------------------------------------
18
19const processImageWorker = new Piscina({
20 filename: join(__dirname, 'workers', 'image-processor.js'),
21 concurrentTasksPerWorker: WORKER_THREADS.DOWNLOAD_IMAGE.CONCURRENCY,
22 maxThreads: WORKER_THREADS.DOWNLOAD_IMAGE.MAX_THREADS
23})
24
25function processImageFromWorker (options: Parameters<typeof processImage>[0]): Promise<ReturnType<typeof processImage>> {
26 return processImageWorker.run(options)
27}
28
16export { 29export {
17 downloadImageFromWorker 30 downloadImageFromWorker,
31 processImageFromWorker
18} 32}
diff --git a/server/lib/worker/workers/image-downloader.ts b/server/lib/worker/workers/image-downloader.ts
index 8d4a6b37e..4b32f723e 100644
--- a/server/lib/worker/workers/image-downloader.ts
+++ b/server/lib/worker/workers/image-downloader.ts
@@ -18,7 +18,7 @@ async function downloadImage (options: {
18 const destPath = join(destDir, destName) 18 const destPath = join(destDir, destName)
19 19
20 try { 20 try {
21 await processImage(tmpPath, destPath, size) 21 await processImage({ path: tmpPath, destination: destPath, newSize: size })
22 } catch (err) { 22 } catch (err) {
23 await remove(tmpPath) 23 await remove(tmpPath)
24 24
diff --git a/server/lib/worker/workers/image-processor.ts b/server/lib/worker/workers/image-processor.ts
new file mode 100644
index 000000000..0ab41a5a0
--- /dev/null
+++ b/server/lib/worker/workers/image-processor.ts
@@ -0,0 +1,7 @@
1import { processImage } from '@server/helpers/image-utils'
2
3module.exports = processImage
4
5export {
6 processImage
7}
diff --git a/server/tests/helpers/image.ts b/server/tests/helpers/image.ts
index 475ca8fb2..7c5da69b5 100644
--- a/server/tests/helpers/image.ts
+++ b/server/tests/helpers/image.ts
@@ -37,28 +37,28 @@ describe('Image helpers', function () {
37 37
38 it('Should skip processing if the source image is okay', async function () { 38 it('Should skip processing if the source image is okay', async function () {
39 const input = buildAbsoluteFixturePath('thumbnail.jpg') 39 const input = buildAbsoluteFixturePath('thumbnail.jpg')
40 await processImage(input, imageDestJPG, thumbnailSize, true) 40 await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true })
41 41
42 await checkBuffers(input, imageDestJPG, true) 42 await checkBuffers(input, imageDestJPG, true)
43 }) 43 })
44 44
45 it('Should not skip processing if the source image does not have the appropriate extension', async function () { 45 it('Should not skip processing if the source image does not have the appropriate extension', async function () {
46 const input = buildAbsoluteFixturePath('thumbnail.png') 46 const input = buildAbsoluteFixturePath('thumbnail.png')
47 await processImage(input, imageDestJPG, thumbnailSize, true) 47 await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true })
48 48
49 await checkBuffers(input, imageDestJPG, false) 49 await checkBuffers(input, imageDestJPG, false)
50 }) 50 })
51 51
52 it('Should not skip processing if the source image does not have the appropriate size', async function () { 52 it('Should not skip processing if the source image does not have the appropriate size', async function () {
53 const input = buildAbsoluteFixturePath('preview.jpg') 53 const input = buildAbsoluteFixturePath('preview.jpg')
54 await processImage(input, imageDestJPG, thumbnailSize, true) 54 await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true })
55 55
56 await checkBuffers(input, imageDestJPG, false) 56 await checkBuffers(input, imageDestJPG, false)
57 }) 57 })
58 58
59 it('Should not skip processing if the source image does not have the appropriate size', async function () { 59 it('Should not skip processing if the source image does not have the appropriate size', async function () {
60 const input = buildAbsoluteFixturePath('thumbnail-big.jpg') 60 const input = buildAbsoluteFixturePath('thumbnail-big.jpg')
61 await processImage(input, imageDestJPG, thumbnailSize, true) 61 await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true })
62 62
63 await checkBuffers(input, imageDestJPG, false) 63 await checkBuffers(input, imageDestJPG, false)
64 }) 64 })
@@ -67,7 +67,7 @@ describe('Image helpers', function () {
67 const input = buildAbsoluteFixturePath('exif.jpg') 67 const input = buildAbsoluteFixturePath('exif.jpg')
68 expect(await hasTitleExif(input)).to.be.true 68 expect(await hasTitleExif(input)).to.be.true
69 69
70 await processImage(input, imageDestJPG, { width: 100, height: 100 }, true) 70 await processImage({ path: input, destination: imageDestJPG, newSize: { width: 100, height: 100 }, keepOriginal: true })
71 await checkBuffers(input, imageDestJPG, false) 71 await checkBuffers(input, imageDestJPG, false)
72 72
73 expect(await hasTitleExif(imageDestJPG)).to.be.false 73 expect(await hasTitleExif(imageDestJPG)).to.be.false
@@ -77,7 +77,7 @@ describe('Image helpers', function () {
77 const input = buildAbsoluteFixturePath('exif.jpg') 77 const input = buildAbsoluteFixturePath('exif.jpg')
78 expect(await hasTitleExif(input)).to.be.true 78 expect(await hasTitleExif(input)).to.be.true
79 79
80 await processImage(input, imageDestJPG, thumbnailSize, true) 80 await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true })
81 await checkBuffers(input, imageDestJPG, false) 81 await checkBuffers(input, imageDestJPG, false)
82 82
83 expect(await hasTitleExif(imageDestJPG)).to.be.false 83 expect(await hasTitleExif(imageDestJPG)).to.be.false
@@ -87,7 +87,7 @@ describe('Image helpers', function () {
87 const input = buildAbsoluteFixturePath('exif.png') 87 const input = buildAbsoluteFixturePath('exif.png')
88 expect(await hasTitleExif(input)).to.be.true 88 expect(await hasTitleExif(input)).to.be.true
89 89
90 await processImage(input, imageDestPNG, thumbnailSize, true) 90 await processImage({ path: input, destination: imageDestPNG, newSize: thumbnailSize, keepOriginal: true })
91 expect(await hasTitleExif(imageDestPNG)).to.be.false 91 expect(await hasTitleExif(imageDestPNG)).to.be.false
92 }) 92 })
93 93