Process images in a dedicated worker

pull/5098/head
Chocobozzz 2022-06-27 11:53:12 +02:00
parent 88edc66eda
commit 3a54605d4e
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
10 changed files with 78 additions and 28 deletions

View File

@ -110,7 +110,7 @@ async function generateSmallerAvatar (actor: MActorDefault) {
const source = join(CONFIG.STORAGE.ACTOR_IMAGES, sourceFilename) const source = join(CONFIG.STORAGE.ACTOR_IMAGES, sourceFilename)
const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, newImageName) const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, newImageName)
await processImage(source, destination, imageSize, true) await processImage({ path: source, destination, newSize: imageSize, keepOriginal: true })
const actorImageInfo = { const actorImageInfo = {
name: newImageName, name: newImageName,

View File

@ -52,7 +52,7 @@ async function processVideo (id: number) {
thumbnail.height = size.height thumbnail.height = size.height
const thumbnailPath = thumbnail.getPath() const thumbnailPath = thumbnail.getPath()
await processImage(previewPath, thumbnailPath, size, true) await processImage({ path: previewPath, destination: thumbnailPath, newSize: size, keepOriginal: true })
// Save new attributes // Save new attributes
await thumbnail.save() await thumbnail.save()

View File

@ -12,12 +12,14 @@ function generateImageFilename (extension = '.jpg') {
return buildUUID() + extension return buildUUID() + extension
} }
async function processImage ( async function processImage (options: {
path: string, path: string
destination: string, destination: string
newSize: { width: number, height: number }, newSize: { width: number, height: number }
keepOriginal = false keepOriginal?: boolean // default false
) { }) {
const { path, destination, newSize, keepOriginal = false } = options
const extension = getLowercaseExtension(path) const extension = getLowercaseExtension(path)
if (path === destination) { if (path === destination) {
@ -36,7 +38,14 @@ async function processImage (
if (keepOriginal !== true) await remove(path) if (keepOriginal !== true) await remove(path)
} }
async function generateImageFromVideoFile (fromPath: string, folder: string, imageName: string, size: { width: number, height: number }) { async function generateImageFromVideoFile (options: {
fromPath: string
folder: string
imageName: string
size: { width: number, height: number }
}) {
const { fromPath, folder, imageName, size } = options
const pendingImageName = 'pending-' + imageName const pendingImageName = 'pending-' + imageName
const pendingImagePath = join(folder, pendingImageName) const pendingImagePath = join(folder, pendingImageName)
@ -44,7 +53,7 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima
await generateThumbnailFromVideo(fromPath, folder, imageName) await generateThumbnailFromVideo(fromPath, folder, imageName)
const destination = join(folder, imageName) const destination = join(folder, imageName)
await processImage(pendingImagePath, destination, size) await processImage({ path: pendingImagePath, destination, newSize: size })
} catch (err) { } catch (err) {
logger.error('Cannot generate image from video %s.', fromPath, { err, ...lTags() }) logger.error('Cannot generate image from video %s.', fromPath, { err, ...lTags() })

View File

@ -748,6 +748,10 @@ const WORKER_THREADS = {
DOWNLOAD_IMAGE: { DOWNLOAD_IMAGE: {
CONCURRENCY: 3, CONCURRENCY: 3,
MAX_THREADS: 1 MAX_THREADS: 1
},
PROCESS_IMAGE: {
CONCURRENCY: 1,
MAX_THREADS: 5
} }
} }

View File

@ -6,14 +6,13 @@ import { getLowercaseExtension } from '@shared/core-utils'
import { buildUUID } from '@shared/extra-utils' import { buildUUID } from '@shared/extra-utils'
import { ActivityPubActorType, ActorImageType } from '@shared/models' import { ActivityPubActorType, ActorImageType } from '@shared/models'
import { retryTransactionWrapper } from '../helpers/database-utils' import { retryTransactionWrapper } from '../helpers/database-utils'
import { processImage } from '../helpers/image-utils'
import { CONFIG } from '../initializers/config' import { CONFIG } from '../initializers/config'
import { ACTOR_IMAGES_SIZE, LRU_CACHE, WEBSERVER } from '../initializers/constants' import { ACTOR_IMAGES_SIZE, LRU_CACHE, WEBSERVER } from '../initializers/constants'
import { sequelizeTypescript } from '../initializers/database' import { sequelizeTypescript } from '../initializers/database'
import { MAccountDefault, MActor, MChannelDefault } from '../types/models' import { MAccountDefault, MActor, MChannelDefault } from '../types/models'
import { deleteActorImages, updateActorImages } from './activitypub/actors' import { deleteActorImages, updateActorImages } from './activitypub/actors'
import { sendUpdateActor } from './activitypub/send' import { sendUpdateActor } from './activitypub/send'
import { downloadImageFromWorker } from './worker/parent-process' import { downloadImageFromWorker, processImageFromWorker } from './worker/parent-process'
function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) { function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) {
return new ActorModel({ return new ActorModel({
@ -42,7 +41,7 @@ async function updateLocalActorImageFiles (
const imageName = buildUUID() + extension const imageName = buildUUID() + extension
const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName) const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName)
await processImage(imagePhysicalFile.path, destination, imageSize, true) await processImageFromWorker({ path: imagePhysicalFile.path, destination, newSize: imageSize, keepOriginal: true })
return { return {
imageName, imageName,

View File

@ -1,6 +1,6 @@
import { join } from 'path' import { join } from 'path'
import { ThumbnailType } from '@shared/models' import { ThumbnailType } from '@shared/models'
import { generateImageFilename, generateImageFromVideoFile, processImage } from '../helpers/image-utils' import { generateImageFilename, generateImageFromVideoFile } from '../helpers/image-utils'
import { CONFIG } from '../initializers/config' import { CONFIG } from '../initializers/config'
import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants'
import { ThumbnailModel } from '../models/video/thumbnail' import { ThumbnailModel } from '../models/video/thumbnail'
@ -9,6 +9,7 @@ import { MThumbnail } from '../types/models/video/thumbnail'
import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist' import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist'
import { downloadImageFromWorker } from './local-actor' import { downloadImageFromWorker } from './local-actor'
import { VideoPathManager } from './video-path-manager' import { VideoPathManager } from './video-path-manager'
import { processImageFromWorker } from './worker/parent-process'
type ImageSize = { height?: number, width?: number } type ImageSize = { height?: number, width?: number }
@ -23,7 +24,10 @@ function updatePlaylistMiniatureFromExisting (options: {
const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size) const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size)
const type = ThumbnailType.MINIATURE const type = ThumbnailType.MINIATURE
const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal) const thumbnailCreator = () => {
return processImageFromWorker({ path: inputPath, destination: outputPath, newSize: { width, height }, keepOriginal })
}
return updateThumbnailFromFunction({ return updateThumbnailFromFunction({
thumbnailCreator, thumbnailCreator,
filename, filename,
@ -99,7 +103,10 @@ function updateVideoMiniatureFromExisting (options: {
const { inputPath, video, type, automaticallyGenerated, size, keepOriginal = false } = options const { inputPath, video, type, automaticallyGenerated, size, keepOriginal = false } = options
const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal)
const thumbnailCreator = () => {
return processImageFromWorker({ path: inputPath, destination: outputPath, newSize: { width, height }, keepOriginal })
}
return updateThumbnailFromFunction({ return updateThumbnailFromFunction({
thumbnailCreator, thumbnailCreator,
@ -123,8 +130,18 @@ function generateVideoMiniature (options: {
const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type) const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type)
const thumbnailCreator = videoFile.isAudio() const thumbnailCreator = videoFile.isAudio()
? () => processImage(ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND, outputPath, { width, height }, true) ? () => processImageFromWorker({
: () => generateImageFromVideoFile(input, basePath, filename, { height, width }) path: ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND,
destination: outputPath,
newSize: { width, height },
keepOriginal: true
})
: () => generateImageFromVideoFile({
fromPath: input,
folder: basePath,
imageName: filename,
size: { height, width }
})
return updateThumbnailFromFunction({ return updateThumbnailFromFunction({
thumbnailCreator, thumbnailCreator,

View File

@ -2,6 +2,7 @@ import { join } from 'path'
import Piscina from 'piscina' import Piscina from 'piscina'
import { WORKER_THREADS } from '@server/initializers/constants' import { WORKER_THREADS } from '@server/initializers/constants'
import { downloadImage } from './workers/image-downloader' import { downloadImage } from './workers/image-downloader'
import { processImage } from '@server/helpers/image-utils'
const downloadImagerWorker = new Piscina({ const downloadImagerWorker = new Piscina({
filename: join(__dirname, 'workers', 'image-downloader.js'), filename: join(__dirname, 'workers', 'image-downloader.js'),
@ -13,6 +14,19 @@ function downloadImageFromWorker (options: Parameters<typeof downloadImage>[0]):
return downloadImagerWorker.run(options) return downloadImagerWorker.run(options)
} }
export { // ---------------------------------------------------------------------------
downloadImageFromWorker
const processImageWorker = new Piscina({
filename: join(__dirname, 'workers', 'image-processor.js'),
concurrentTasksPerWorker: WORKER_THREADS.DOWNLOAD_IMAGE.CONCURRENCY,
maxThreads: WORKER_THREADS.DOWNLOAD_IMAGE.MAX_THREADS
})
function processImageFromWorker (options: Parameters<typeof processImage>[0]): Promise<ReturnType<typeof processImage>> {
return processImageWorker.run(options)
}
export {
downloadImageFromWorker,
processImageFromWorker
} }

View File

@ -18,7 +18,7 @@ async function downloadImage (options: {
const destPath = join(destDir, destName) const destPath = join(destDir, destName)
try { try {
await processImage(tmpPath, destPath, size) await processImage({ path: tmpPath, destination: destPath, newSize: size })
} catch (err) { } catch (err) {
await remove(tmpPath) await remove(tmpPath)

View File

@ -0,0 +1,7 @@
import { processImage } from '@server/helpers/image-utils'
module.exports = processImage
export {
processImage
}

View File

@ -37,28 +37,28 @@ describe('Image helpers', function () {
it('Should skip processing if the source image is okay', async function () { it('Should skip processing if the source image is okay', async function () {
const input = buildAbsoluteFixturePath('thumbnail.jpg') const input = buildAbsoluteFixturePath('thumbnail.jpg')
await processImage(input, imageDestJPG, thumbnailSize, true) await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true })
await checkBuffers(input, imageDestJPG, true) await checkBuffers(input, imageDestJPG, true)
}) })
it('Should not skip processing if the source image does not have the appropriate extension', async function () { it('Should not skip processing if the source image does not have the appropriate extension', async function () {
const input = buildAbsoluteFixturePath('thumbnail.png') const input = buildAbsoluteFixturePath('thumbnail.png')
await processImage(input, imageDestJPG, thumbnailSize, true) await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true })
await checkBuffers(input, imageDestJPG, false) await checkBuffers(input, imageDestJPG, false)
}) })
it('Should not skip processing if the source image does not have the appropriate size', async function () { it('Should not skip processing if the source image does not have the appropriate size', async function () {
const input = buildAbsoluteFixturePath('preview.jpg') const input = buildAbsoluteFixturePath('preview.jpg')
await processImage(input, imageDestJPG, thumbnailSize, true) await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true })
await checkBuffers(input, imageDestJPG, false) await checkBuffers(input, imageDestJPG, false)
}) })
it('Should not skip processing if the source image does not have the appropriate size', async function () { it('Should not skip processing if the source image does not have the appropriate size', async function () {
const input = buildAbsoluteFixturePath('thumbnail-big.jpg') const input = buildAbsoluteFixturePath('thumbnail-big.jpg')
await processImage(input, imageDestJPG, thumbnailSize, true) await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true })
await checkBuffers(input, imageDestJPG, false) await checkBuffers(input, imageDestJPG, false)
}) })
@ -67,7 +67,7 @@ describe('Image helpers', function () {
const input = buildAbsoluteFixturePath('exif.jpg') const input = buildAbsoluteFixturePath('exif.jpg')
expect(await hasTitleExif(input)).to.be.true expect(await hasTitleExif(input)).to.be.true
await processImage(input, imageDestJPG, { width: 100, height: 100 }, true) await processImage({ path: input, destination: imageDestJPG, newSize: { width: 100, height: 100 }, keepOriginal: true })
await checkBuffers(input, imageDestJPG, false) await checkBuffers(input, imageDestJPG, false)
expect(await hasTitleExif(imageDestJPG)).to.be.false expect(await hasTitleExif(imageDestJPG)).to.be.false
@ -77,7 +77,7 @@ describe('Image helpers', function () {
const input = buildAbsoluteFixturePath('exif.jpg') const input = buildAbsoluteFixturePath('exif.jpg')
expect(await hasTitleExif(input)).to.be.true expect(await hasTitleExif(input)).to.be.true
await processImage(input, imageDestJPG, thumbnailSize, true) await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true })
await checkBuffers(input, imageDestJPG, false) await checkBuffers(input, imageDestJPG, false)
expect(await hasTitleExif(imageDestJPG)).to.be.false expect(await hasTitleExif(imageDestJPG)).to.be.false
@ -87,7 +87,7 @@ describe('Image helpers', function () {
const input = buildAbsoluteFixturePath('exif.png') const input = buildAbsoluteFixturePath('exif.png')
expect(await hasTitleExif(input)).to.be.true expect(await hasTitleExif(input)).to.be.true
await processImage(input, imageDestPNG, thumbnailSize, true) await processImage({ path: input, destination: imageDestPNG, newSize: thumbnailSize, keepOriginal: true })
expect(await hasTitleExif(imageDestPNG)).to.be.false expect(await hasTitleExif(imageDestPNG)).to.be.false
}) })