Process images in a dedicated worker
This commit is contained in:
parent
88edc66eda
commit
3a54605d4e
10 changed files with 78 additions and 28 deletions
|
@ -110,7 +110,7 @@ async function generateSmallerAvatar (actor: MActorDefault) {
|
|||
const source = join(CONFIG.STORAGE.ACTOR_IMAGES, sourceFilename)
|
||||
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 = {
|
||||
name: newImageName,
|
||||
|
|
|
@ -52,7 +52,7 @@ async function processVideo (id: number) {
|
|||
thumbnail.height = size.height
|
||||
|
||||
const thumbnailPath = thumbnail.getPath()
|
||||
await processImage(previewPath, thumbnailPath, size, true)
|
||||
await processImage({ path: previewPath, destination: thumbnailPath, newSize: size, keepOriginal: true })
|
||||
|
||||
// Save new attributes
|
||||
await thumbnail.save()
|
||||
|
|
|
@ -12,12 +12,14 @@ function generateImageFilename (extension = '.jpg') {
|
|||
return buildUUID() + extension
|
||||
}
|
||||
|
||||
async function processImage (
|
||||
path: string,
|
||||
destination: string,
|
||||
newSize: { width: number, height: number },
|
||||
keepOriginal = false
|
||||
) {
|
||||
async function processImage (options: {
|
||||
path: string
|
||||
destination: string
|
||||
newSize: { width: number, height: number }
|
||||
keepOriginal?: boolean // default false
|
||||
}) {
|
||||
const { path, destination, newSize, keepOriginal = false } = options
|
||||
|
||||
const extension = getLowercaseExtension(path)
|
||||
|
||||
if (path === destination) {
|
||||
|
@ -36,7 +38,14 @@ async function processImage (
|
|||
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 pendingImagePath = join(folder, pendingImageName)
|
||||
|
||||
|
@ -44,7 +53,7 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima
|
|||
await generateThumbnailFromVideo(fromPath, folder, imageName)
|
||||
|
||||
const destination = join(folder, imageName)
|
||||
await processImage(pendingImagePath, destination, size)
|
||||
await processImage({ path: pendingImagePath, destination, newSize: size })
|
||||
} catch (err) {
|
||||
logger.error('Cannot generate image from video %s.', fromPath, { err, ...lTags() })
|
||||
|
||||
|
|
|
@ -748,6 +748,10 @@ const WORKER_THREADS = {
|
|||
DOWNLOAD_IMAGE: {
|
||||
CONCURRENCY: 3,
|
||||
MAX_THREADS: 1
|
||||
},
|
||||
PROCESS_IMAGE: {
|
||||
CONCURRENCY: 1,
|
||||
MAX_THREADS: 5
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,14 +6,13 @@ import { getLowercaseExtension } from '@shared/core-utils'
|
|||
import { buildUUID } from '@shared/extra-utils'
|
||||
import { ActivityPubActorType, ActorImageType } from '@shared/models'
|
||||
import { retryTransactionWrapper } from '../helpers/database-utils'
|
||||
import { processImage } from '../helpers/image-utils'
|
||||
import { CONFIG } from '../initializers/config'
|
||||
import { ACTOR_IMAGES_SIZE, LRU_CACHE, WEBSERVER } from '../initializers/constants'
|
||||
import { sequelizeTypescript } from '../initializers/database'
|
||||
import { MAccountDefault, MActor, MChannelDefault } from '../types/models'
|
||||
import { deleteActorImages, updateActorImages } from './activitypub/actors'
|
||||
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) {
|
||||
return new ActorModel({
|
||||
|
@ -42,7 +41,7 @@ async function updateLocalActorImageFiles (
|
|||
|
||||
const imageName = buildUUID() + extension
|
||||
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 {
|
||||
imageName,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { join } from 'path'
|
||||
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 { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants'
|
||||
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 { downloadImageFromWorker } from './local-actor'
|
||||
import { VideoPathManager } from './video-path-manager'
|
||||
import { processImageFromWorker } from './worker/parent-process'
|
||||
|
||||
type ImageSize = { height?: number, width?: number }
|
||||
|
||||
|
@ -23,7 +24,10 @@ function updatePlaylistMiniatureFromExisting (options: {
|
|||
const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size)
|
||||
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({
|
||||
thumbnailCreator,
|
||||
filename,
|
||||
|
@ -99,7 +103,10 @@ function updateVideoMiniatureFromExisting (options: {
|
|||
const { inputPath, video, type, automaticallyGenerated, size, keepOriginal = false } = options
|
||||
|
||||
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({
|
||||
thumbnailCreator,
|
||||
|
@ -123,8 +130,18 @@ function generateVideoMiniature (options: {
|
|||
const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type)
|
||||
|
||||
const thumbnailCreator = videoFile.isAudio()
|
||||
? () => processImage(ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND, outputPath, { width, height }, true)
|
||||
: () => generateImageFromVideoFile(input, basePath, filename, { height, width })
|
||||
? () => processImageFromWorker({
|
||||
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({
|
||||
thumbnailCreator,
|
||||
|
|
|
@ -2,6 +2,7 @@ import { join } from 'path'
|
|||
import Piscina from 'piscina'
|
||||
import { WORKER_THREADS } from '@server/initializers/constants'
|
||||
import { downloadImage } from './workers/image-downloader'
|
||||
import { processImage } from '@server/helpers/image-utils'
|
||||
|
||||
const downloadImagerWorker = new Piscina({
|
||||
filename: join(__dirname, 'workers', 'image-downloader.js'),
|
||||
|
@ -13,6 +14,19 @@ function downloadImageFromWorker (options: Parameters<typeof downloadImage>[0]):
|
|||
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
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ async function downloadImage (options: {
|
|||
const destPath = join(destDir, destName)
|
||||
|
||||
try {
|
||||
await processImage(tmpPath, destPath, size)
|
||||
await processImage({ path: tmpPath, destination: destPath, newSize: size })
|
||||
} catch (err) {
|
||||
await remove(tmpPath)
|
||||
|
||||
|
|
7
server/lib/worker/workers/image-processor.ts
Normal file
7
server/lib/worker/workers/image-processor.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { processImage } from '@server/helpers/image-utils'
|
||||
|
||||
module.exports = processImage
|
||||
|
||||
export {
|
||||
processImage
|
||||
}
|
|
@ -37,28 +37,28 @@ describe('Image helpers', function () {
|
|||
|
||||
it('Should skip processing if the source image is okay', async function () {
|
||||
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)
|
||||
})
|
||||
|
||||
it('Should not skip processing if the source image does not have the appropriate extension', async function () {
|
||||
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)
|
||||
})
|
||||
|
||||
it('Should not skip processing if the source image does not have the appropriate size', async function () {
|
||||
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)
|
||||
})
|
||||
|
||||
it('Should not skip processing if the source image does not have the appropriate size', async function () {
|
||||
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)
|
||||
})
|
||||
|
@ -67,7 +67,7 @@ describe('Image helpers', function () {
|
|||
const input = buildAbsoluteFixturePath('exif.jpg')
|
||||
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)
|
||||
|
||||
expect(await hasTitleExif(imageDestJPG)).to.be.false
|
||||
|
@ -77,7 +77,7 @@ describe('Image helpers', function () {
|
|||
const input = buildAbsoluteFixturePath('exif.jpg')
|
||||
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)
|
||||
|
||||
expect(await hasTitleExif(imageDestJPG)).to.be.false
|
||||
|
@ -87,7 +87,7 @@ describe('Image helpers', function () {
|
|||
const input = buildAbsoluteFixturePath('exif.png')
|
||||
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
|
||||
})
|
||||
|
||||
|
|
Loading…
Reference in a new issue