Add TMP persistent directory
To store files that must be preserved between peertube restarts
This commit is contained in:
parent
3a0c2a77b1
commit
6a49056026
|
@ -120,6 +120,7 @@ defaults:
|
|||
# From the project root directory
|
||||
storage:
|
||||
tmp: 'storage/tmp/' # Use to download data (imports etc), store uploaded files before and during processing...
|
||||
tmp_persistent: 'storage/tmp-persistent/' # As tmp but the directory is not cleaned up between PeerTube restarts
|
||||
bin: 'storage/bin/'
|
||||
avatars: 'storage/avatars/'
|
||||
videos: 'storage/videos/'
|
||||
|
|
|
@ -118,6 +118,7 @@ defaults:
|
|||
# From the project root directory
|
||||
storage:
|
||||
tmp: '/var/www/peertube/storage/tmp/' # Use to download data (imports etc), store uploaded files before and during processing...
|
||||
tmp_persistent: '/var/www/peertube/storage/tmp-persistent/' # As tmp but the directory is not cleaned up between PeerTube restarts
|
||||
bin: '/var/www/peertube/storage/bin/'
|
||||
avatars: '/var/www/peertube/storage/avatars/'
|
||||
videos: '/var/www/peertube/storage/videos/'
|
||||
|
|
|
@ -10,6 +10,7 @@ database:
|
|||
# From the project root directory
|
||||
storage:
|
||||
tmp: 'test1/tmp/'
|
||||
tmp_persistent: 'test1/tmp-persistent/'
|
||||
bin: 'test1/bin/'
|
||||
avatars: 'test1/avatars/'
|
||||
videos: 'test1/videos/'
|
||||
|
|
|
@ -10,6 +10,7 @@ database:
|
|||
# From the project root directory
|
||||
storage:
|
||||
tmp: 'test2/tmp/'
|
||||
tmp_persistent: 'test2/tmp-persistent/'
|
||||
bin: 'test2/bin/'
|
||||
avatars: 'test2/avatars/'
|
||||
videos: 'test2/videos/'
|
||||
|
|
|
@ -10,6 +10,7 @@ database:
|
|||
# From the project root directory
|
||||
storage:
|
||||
tmp: 'test3/tmp/'
|
||||
tmp_persistent: 'test3/tmp-persistent/'
|
||||
bin: 'test3/bin/'
|
||||
avatars: 'test3/avatars/'
|
||||
videos: 'test3/videos/'
|
||||
|
|
|
@ -10,6 +10,7 @@ database:
|
|||
# From the project root directory
|
||||
storage:
|
||||
tmp: 'test4/tmp/'
|
||||
tmp_persistent: 'test4/tmp-persistent/'
|
||||
bin: 'test4/bin/'
|
||||
avatars: 'test4/avatars/'
|
||||
videos: 'test4/videos/'
|
||||
|
|
|
@ -10,6 +10,7 @@ database:
|
|||
# From the project root directory
|
||||
storage:
|
||||
tmp: 'test5/tmp/'
|
||||
tmp_persistent: 'test5/tmp-persistent/'
|
||||
bin: 'test5/bin/'
|
||||
avatars: 'test5/avatars/'
|
||||
videos: 'test5/videos/'
|
||||
|
|
|
@ -10,6 +10,7 @@ database:
|
|||
# From the project root directory
|
||||
storage:
|
||||
tmp: 'test6/tmp/'
|
||||
tmp_persistent: 'test6/tmp-persistent/'
|
||||
bin: 'test6/bin/'
|
||||
avatars: 'test6/avatars/'
|
||||
videos: 'test6/videos/'
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import Bluebird from 'bluebird'
|
||||
import express from 'express'
|
||||
import { move } from 'fs-extra'
|
||||
import { basename, join } from 'path'
|
||||
import { createAnyReqFiles } from '@server/helpers/express-utils'
|
||||
import { CONFIG } from '@server/initializers/config'
|
||||
import { MIMETYPES } from '@server/initializers/constants'
|
||||
import { JobQueue } from '@server/lib/job-queue'
|
||||
import { buildTaskFileFieldname, getTaskFile } from '@server/lib/video-studio'
|
||||
import { buildTaskFileFieldname, getTaskFileFromReq } from '@server/lib/video-studio'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
VideoState,
|
||||
|
@ -68,7 +72,7 @@ async function createEditionTasks (req: express.Request, res: express.Response)
|
|||
|
||||
const payload = {
|
||||
videoUUID: video.uuid,
|
||||
tasks: body.tasks.map((t, i) => buildTaskPayload(t, i, files))
|
||||
tasks: await Bluebird.mapSeries(body.tasks, (t, i) => buildTaskPayload(t, i, files))
|
||||
}
|
||||
|
||||
JobQueue.Instance.createJobAsync({ type: 'video-studio-edition', payload })
|
||||
|
@ -77,7 +81,11 @@ async function createEditionTasks (req: express.Request, res: express.Response)
|
|||
}
|
||||
|
||||
const taskPayloadBuilders: {
|
||||
[id in VideoStudioTask['name']]: (task: VideoStudioTask, indice?: number, files?: Express.Multer.File[]) => VideoStudioTaskPayload
|
||||
[id in VideoStudioTask['name']]: (
|
||||
task: VideoStudioTask,
|
||||
indice?: number,
|
||||
files?: Express.Multer.File[]
|
||||
) => Promise<VideoStudioTaskPayload>
|
||||
} = {
|
||||
'add-intro': buildIntroOutroTask,
|
||||
'add-outro': buildIntroOutroTask,
|
||||
|
@ -85,34 +93,46 @@ const taskPayloadBuilders: {
|
|||
'add-watermark': buildWatermarkTask
|
||||
}
|
||||
|
||||
function buildTaskPayload (task: VideoStudioTask, indice: number, files: Express.Multer.File[]): VideoStudioTaskPayload {
|
||||
function buildTaskPayload (task: VideoStudioTask, indice: number, files: Express.Multer.File[]): Promise<VideoStudioTaskPayload> {
|
||||
return taskPayloadBuilders[task.name](task, indice, files)
|
||||
}
|
||||
|
||||
function buildIntroOutroTask (task: VideoStudioTaskIntro | VideoStudioTaskOutro, indice: number, files: Express.Multer.File[]) {
|
||||
async function buildIntroOutroTask (task: VideoStudioTaskIntro | VideoStudioTaskOutro, indice: number, files: Express.Multer.File[]) {
|
||||
const destination = await moveStudioFileToPersistentTMP(getTaskFileFromReq(files, indice).path)
|
||||
|
||||
return {
|
||||
name: task.name,
|
||||
options: {
|
||||
file: getTaskFile(files, indice).path
|
||||
file: destination
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildCutTask (task: VideoStudioTaskCut) {
|
||||
return {
|
||||
return Promise.resolve({
|
||||
name: task.name,
|
||||
options: {
|
||||
start: task.options.start,
|
||||
end: task.options.end
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function buildWatermarkTask (task: VideoStudioTaskWatermark, indice: number, files: Express.Multer.File[]) {
|
||||
async function buildWatermarkTask (task: VideoStudioTaskWatermark, indice: number, files: Express.Multer.File[]) {
|
||||
const destination = await moveStudioFileToPersistentTMP(getTaskFileFromReq(files, indice).path)
|
||||
|
||||
return {
|
||||
name: task.name,
|
||||
options: {
|
||||
file: getTaskFile(files, indice).path
|
||||
file: destination
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function moveStudioFileToPersistentTMP (file: string) {
|
||||
const destination = join(CONFIG.STORAGE.TMP_PERSISTENT_DIR, basename(file))
|
||||
|
||||
await move(file, destination)
|
||||
|
||||
return destination
|
||||
}
|
||||
|
|
|
@ -98,6 +98,7 @@ const CONFIG = {
|
|||
|
||||
STORAGE: {
|
||||
TMP_DIR: buildPath(config.get<string>('storage.tmp')),
|
||||
TMP_PERSISTENT_DIR: buildPath(config.get<string>('storage.tmp_persistent')),
|
||||
BIN_DIR: buildPath(config.get<string>('storage.bin')),
|
||||
ACTOR_IMAGES: buildPath(config.get<string>('storage.avatars')),
|
||||
LOG_DIR: buildPath(config.get<string>('storage.logs')),
|
||||
|
|
|
@ -12,7 +12,7 @@ import { VideoTranscodingProfilesManager } from '@server/lib/transcoding/default
|
|||
import { isAbleToUploadVideo } from '@server/lib/user'
|
||||
import { buildFileMetadata, removeHLSPlaylist, removeWebTorrentFile } from '@server/lib/video-file'
|
||||
import { VideoPathManager } from '@server/lib/video-path-manager'
|
||||
import { approximateIntroOutroAdditionalSize } from '@server/lib/video-studio'
|
||||
import { approximateIntroOutroAdditionalSize, safeCleanupStudioTMPFiles } from '@server/lib/video-studio'
|
||||
import { UserModel } from '@server/models/user/user'
|
||||
import { VideoModel } from '@server/models/video/video'
|
||||
import { VideoFileModel } from '@server/models/video/video-file'
|
||||
|
@ -39,63 +39,73 @@ async function processVideoStudioEdition (job: Job) {
|
|||
|
||||
logger.info('Process video studio edition of %s in job %s.', payload.videoUUID, job.id, lTags)
|
||||
|
||||
const video = await VideoModel.loadFull(payload.videoUUID)
|
||||
try {
|
||||
const video = await VideoModel.loadFull(payload.videoUUID)
|
||||
|
||||
// No video, maybe deleted?
|
||||
if (!video) {
|
||||
logger.info('Can\'t process job %d, video does not exist.', job.id, lTags)
|
||||
return undefined
|
||||
}
|
||||
// No video, maybe deleted?
|
||||
if (!video) {
|
||||
logger.info('Can\'t process job %d, video does not exist.', job.id, lTags)
|
||||
|
||||
await checkUserQuotaOrThrow(video, payload)
|
||||
|
||||
const inputFile = video.getMaxQualityFile()
|
||||
|
||||
const editionResultPath = await VideoPathManager.Instance.makeAvailableVideoFile(inputFile, async originalFilePath => {
|
||||
let tmpInputFilePath: string
|
||||
let outputPath: string
|
||||
|
||||
for (const task of payload.tasks) {
|
||||
const outputFilename = buildUUID() + inputFile.extname
|
||||
outputPath = join(CONFIG.STORAGE.TMP_DIR, outputFilename)
|
||||
|
||||
await processTask({
|
||||
inputPath: tmpInputFilePath ?? originalFilePath,
|
||||
video,
|
||||
outputPath,
|
||||
task,
|
||||
lTags
|
||||
})
|
||||
|
||||
if (tmpInputFilePath) await remove(tmpInputFilePath)
|
||||
|
||||
// For the next iteration
|
||||
tmpInputFilePath = outputPath
|
||||
await safeCleanupStudioTMPFiles(payload)
|
||||
return undefined
|
||||
}
|
||||
|
||||
return outputPath
|
||||
})
|
||||
await checkUserQuotaOrThrow(video, payload)
|
||||
|
||||
logger.info('Video edition ended for video %s.', video.uuid, lTags)
|
||||
const inputFile = video.getMaxQualityFile()
|
||||
|
||||
const newFile = await buildNewFile(video, editionResultPath)
|
||||
const editionResultPath = await VideoPathManager.Instance.makeAvailableVideoFile(inputFile, async originalFilePath => {
|
||||
let tmpInputFilePath: string
|
||||
let outputPath: string
|
||||
|
||||
const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newFile)
|
||||
await move(editionResultPath, outputPath)
|
||||
for (const task of payload.tasks) {
|
||||
const outputFilename = buildUUID() + inputFile.extname
|
||||
outputPath = join(CONFIG.STORAGE.TMP_DIR, outputFilename)
|
||||
|
||||
await createTorrentAndSetInfoHashFromPath(video, newFile, outputPath)
|
||||
await removeAllFiles(video, newFile)
|
||||
await processTask({
|
||||
inputPath: tmpInputFilePath ?? originalFilePath,
|
||||
video,
|
||||
outputPath,
|
||||
task,
|
||||
lTags
|
||||
})
|
||||
|
||||
await newFile.save()
|
||||
if (tmpInputFilePath) await remove(tmpInputFilePath)
|
||||
|
||||
video.duration = await getVideoStreamDuration(outputPath)
|
||||
await video.save()
|
||||
// For the next iteration
|
||||
tmpInputFilePath = outputPath
|
||||
}
|
||||
|
||||
await federateVideoIfNeeded(video, false, undefined)
|
||||
return outputPath
|
||||
})
|
||||
|
||||
const user = await UserModel.loadByVideoId(video.id)
|
||||
logger.info('Video edition ended for video %s.', video.uuid, lTags)
|
||||
|
||||
await createOptimizeOrMergeAudioJobs({ video, videoFile: newFile, isNewVideo: false, user, videoFileAlreadyLocked: false })
|
||||
const newFile = await buildNewFile(video, editionResultPath)
|
||||
|
||||
const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newFile)
|
||||
await move(editionResultPath, outputPath)
|
||||
|
||||
await safeCleanupStudioTMPFiles(payload)
|
||||
|
||||
await createTorrentAndSetInfoHashFromPath(video, newFile, outputPath)
|
||||
await removeAllFiles(video, newFile)
|
||||
|
||||
await newFile.save()
|
||||
|
||||
video.duration = await getVideoStreamDuration(outputPath)
|
||||
await video.save()
|
||||
|
||||
await federateVideoIfNeeded(video, false, undefined)
|
||||
|
||||
const user = await UserModel.loadByVideoId(video.id)
|
||||
|
||||
await createOptimizeOrMergeAudioJobs({ video, videoFile: newFile, isNewVideo: false, user, videoFileAlreadyLocked: false })
|
||||
} catch (err) {
|
||||
await safeCleanupStudioTMPFiles(payload)
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -1,15 +1,31 @@
|
|||
import { logger } from '@server/helpers/logger'
|
||||
import { MVideoFullLight } from '@server/types/models'
|
||||
import { getVideoStreamDuration } from '@shared/ffmpeg'
|
||||
import { VideoStudioTask } from '@shared/models'
|
||||
import { VideoStudioEditionPayload, VideoStudioTask } from '@shared/models'
|
||||
import { remove } from 'fs-extra'
|
||||
|
||||
function buildTaskFileFieldname (indice: number, fieldName = 'file') {
|
||||
return `tasks[${indice}][options][${fieldName}]`
|
||||
}
|
||||
|
||||
function getTaskFile (files: Express.Multer.File[], indice: number, fieldName = 'file') {
|
||||
function getTaskFileFromReq (files: Express.Multer.File[], indice: number, fieldName = 'file') {
|
||||
return files.find(f => f.fieldname === buildTaskFileFieldname(indice, fieldName))
|
||||
}
|
||||
|
||||
async function safeCleanupStudioTMPFiles (payload: VideoStudioEditionPayload) {
|
||||
for (const task of payload.tasks) {
|
||||
try {
|
||||
if (task.name === 'add-intro' || task.name === 'add-outro') {
|
||||
await remove(task.options.file)
|
||||
} else if (task.name === 'add-watermark') {
|
||||
await remove(task.options.file)
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Cannot remove studio file', { err })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function approximateIntroOutroAdditionalSize (video: MVideoFullLight, tasks: VideoStudioTask[], fileFinder: (i: number) => string) {
|
||||
let additionalDuration = 0
|
||||
|
||||
|
@ -28,5 +44,6 @@ async function approximateIntroOutroAdditionalSize (video: MVideoFullLight, task
|
|||
export {
|
||||
approximateIntroOutroAdditionalSize,
|
||||
buildTaskFileFieldname,
|
||||
getTaskFile
|
||||
getTaskFileFromReq,
|
||||
safeCleanupStudioTMPFiles
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
} from '@server/helpers/custom-validators/video-studio'
|
||||
import { cleanUpReqFiles } from '@server/helpers/express-utils'
|
||||
import { CONFIG } from '@server/initializers/config'
|
||||
import { approximateIntroOutroAdditionalSize, getTaskFile } from '@server/lib/video-studio'
|
||||
import { approximateIntroOutroAdditionalSize, getTaskFileFromReq } from '@server/lib/video-studio'
|
||||
import { isAudioFile } from '@shared/ffmpeg'
|
||||
import { HttpStatusCode, UserRight, VideoState, VideoStudioCreateEdition, VideoStudioTask } from '@shared/models'
|
||||
import { areValidationErrors, checkUserCanManageVideo, checkUserQuota, doesVideoExist } from '../shared'
|
||||
|
@ -49,7 +49,7 @@ const videoStudioAddEditionValidator = [
|
|||
}
|
||||
|
||||
if (task.name === 'add-intro' || task.name === 'add-outro') {
|
||||
const filePath = getTaskFile(files, i).path
|
||||
const filePath = getTaskFileFromReq(files, i).path
|
||||
|
||||
// Our concat filter needs a video stream
|
||||
if (await isAudioFile(filePath)) {
|
||||
|
@ -79,7 +79,7 @@ const videoStudioAddEditionValidator = [
|
|||
if (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
|
||||
|
||||
// Try to make an approximation of bytes added by the intro/outro
|
||||
const additionalBytes = await approximateIntroOutroAdditionalSize(video, body.tasks, i => getTaskFile(files, i).path)
|
||||
const additionalBytes = await approximateIntroOutroAdditionalSize(video, body.tasks, i => getTaskFileFromReq(files, i).path)
|
||||
if (await checkUserQuota(user, additionalBytes, res) === false) return cleanUpReqFiles(req)
|
||||
|
||||
return next()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { expect } from 'chai'
|
||||
import { expectStartWith } from '@server/tests/shared'
|
||||
import { checkPersistentTmpIsEmpty, expectStartWith } from '@server/tests/shared'
|
||||
import { areMockObjectStorageTestsDisabled, getAllFiles } from '@shared/core-utils'
|
||||
import { VideoStudioTask } from '@shared/models'
|
||||
import {
|
||||
|
@ -356,6 +356,29 @@ describe('Test video studio', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Server restart', function () {
|
||||
|
||||
it('Should still be able to run video edition after a server restart', async function () {
|
||||
this.timeout(240_000)
|
||||
|
||||
await renewVideo()
|
||||
await servers[0].videoStudio.createEditionTasks({ videoId: videoUUID, tasks: VideoStudioCommand.getComplexTask() })
|
||||
|
||||
await servers[0].kill()
|
||||
await servers[0].run()
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
await checkDuration(server, 9)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should have an empty persistent tmp directory', async function () {
|
||||
await checkPersistentTmpIsEmpty(servers[0])
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
|
|
|
@ -12,6 +12,10 @@ async function checkTmpIsEmpty (server: PeerTubeServer) {
|
|||
}
|
||||
}
|
||||
|
||||
async function checkPersistentTmpIsEmpty (server: PeerTubeServer) {
|
||||
await checkDirectoryIsEmpty(server, 'tmp-persistent')
|
||||
}
|
||||
|
||||
async function checkDirectoryIsEmpty (server: PeerTubeServer, directory: string, exceptions: string[] = []) {
|
||||
const directoryPath = server.getDirectoryPath(directory)
|
||||
|
||||
|
@ -26,5 +30,6 @@ async function checkDirectoryIsEmpty (server: PeerTubeServer, directory: string,
|
|||
|
||||
export {
|
||||
checkTmpIsEmpty,
|
||||
checkPersistentTmpIsEmpty,
|
||||
checkDirectoryIsEmpty
|
||||
}
|
||||
|
|
|
@ -364,6 +364,7 @@ export class PeerTubeServer {
|
|||
},
|
||||
storage: {
|
||||
tmp: this.getDirectoryPath('tmp') + '/',
|
||||
tmp_persistent: this.getDirectoryPath('tmp-persistent') + '/',
|
||||
bin: this.getDirectoryPath('bin') + '/',
|
||||
avatars: this.getDirectoryPath('avatars') + '/',
|
||||
videos: this.getDirectoryPath('videos') + '/',
|
||||
|
|
|
@ -44,6 +44,7 @@ redis:
|
|||
# From the project root directory
|
||||
storage:
|
||||
tmp: '../data/tmp/' # Use to download data (imports etc), store uploaded files before and during processing...
|
||||
tmp_persistent: '../data/tmp-persistent/' # As tmp but the directory is not cleaned up between PeerTube restarts
|
||||
bin: '../data/bin/'
|
||||
avatars: '../data/avatars/'
|
||||
videos: '../data/videos/'
|
||||
|
|
Loading…
Reference in New Issue