Chocobozzz/PeerTube

View on GitHub
server/core/controllers/api/videos/upload.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import { ffprobePromise, getChaptersFromContainer } from '@peertube/peertube-ffmpeg'
import { ThumbnailType, VideoCreate } from '@peertube/peertube-models'
import { uuidToShort } from '@peertube/peertube-node-utils'
import { getResumableUploadPath } from '@server/helpers/upload.js'
import { LocalVideoCreator } from '@server/lib/local-video-creator.js'
import { Redis } from '@server/lib/redis.js'
import { setupUploadResumableRoutes, uploadx } from '@server/lib/uploadx.js'
import { buildNextVideoState } from '@server/lib/video-state.js'
import { openapiOperationDoc } from '@server/middlewares/doc.js'
import express from 'express'
import { VideoAuditView, auditLoggerFactory, getAuditIdFromRes } from '../../../helpers/audit-logger.js'
import { createReqFiles } from '../../../helpers/express-utils.js'
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants.js'
import { Hooks } from '../../../lib/plugins/hooks.js'
import {
  asyncMiddleware,
  asyncRetryTransactionMiddleware,
  authenticate,
  setReqTimeout,
  videosAddLegacyValidator,
  videosAddResumableInitValidator,
  videosAddResumableValidator
} from '../../../middlewares/index.js'

const lTags = loggerTagsFactory('api', 'video')
const auditLogger = auditLoggerFactory('videos')
const uploadRouter = express.Router()

const reqVideoFileAdd = createReqFiles(
  [ 'videofile', 'thumbnailfile', 'previewfile' ],
  { ...MIMETYPES.VIDEO.MIMETYPE_EXT, ...MIMETYPES.IMAGE.MIMETYPE_EXT }
)

const reqVideoFileAddResumable = createReqFiles(
  [ 'thumbnailfile', 'previewfile' ],
  MIMETYPES.IMAGE.MIMETYPE_EXT,
  getResumableUploadPath()
)

uploadRouter.post('/upload',
  openapiOperationDoc({ operationId: 'uploadLegacy' }),
  authenticate,
  setReqTimeout(1000 * 60 * 10), // Uploading the video could be long
  reqVideoFileAdd,
  asyncMiddleware(videosAddLegacyValidator),
  asyncRetryTransactionMiddleware(addVideoLegacy)
)

setupUploadResumableRoutes({
  routePath: '/upload-resumable',
  router: uploadRouter,

  uploadInitBeforeMiddlewares: [
    openapiOperationDoc({ operationId: 'uploadResumableInit' }),
    reqVideoFileAddResumable
  ],

  uploadInitAfterMiddlewares: [ asyncMiddleware(videosAddResumableInitValidator) ],

  uploadDeleteMiddlewares: [ asyncMiddleware(deleteUploadResumableCache) ],

  uploadedMiddlewares: [
    openapiOperationDoc({ operationId: 'uploadResumable' }),
    asyncMiddleware(videosAddResumableValidator)
  ],
  uploadedController: asyncMiddleware(addVideoResumable)
})

// ---------------------------------------------------------------------------

export {
  uploadRouter
}

// ---------------------------------------------------------------------------

async function addVideoLegacy (req: express.Request, res: express.Response) {
  const videoPhysicalFile = req.files['videofile'][0]
  const videoInfo: VideoCreate = req.body
  const files = req.files

  const response = await addVideo({ req, res, videoPhysicalFile, videoInfo, files })

  return res.json(response)
}

async function addVideoResumable (req: express.Request, res: express.Response) {
  const videoPhysicalFile = res.locals.uploadVideoFileResumable
  const videoInfo = videoPhysicalFile.metadata
  const files = { previewfile: videoInfo.previewfile, thumbnailfile: videoInfo.thumbnailfile }

  const response = await addVideo({ req, res, videoPhysicalFile, videoInfo, files })
  await Redis.Instance.deleteUploadSession(req.query.upload_id)
  await uploadx.storage.delete(res.locals.uploadVideoFileResumable)

  return res.json(response)
}

async function addVideo (options: {
  req: express.Request
  res: express.Response
  videoPhysicalFile: express.VideoLegacyUploadFile
  videoInfo: VideoCreate
  files: express.UploadFiles
}) {
  const { req, res, videoPhysicalFile, videoInfo, files } = options

  const ffprobe = await ffprobePromise(videoPhysicalFile.path)

  const containerChapters = await getChaptersFromContainer({
    path: videoPhysicalFile.path,
    maxTitleLength: CONSTRAINTS_FIELDS.VIDEO_CHAPTERS.TITLE.max,
    ffprobe
  })
  logger.debug(`Got ${containerChapters.length} chapters from video "${videoInfo.name}" container`, { containerChapters, ...lTags() })

  const thumbnails = [ { type: ThumbnailType.MINIATURE, field: 'thumbnailfile' }, { type: ThumbnailType.PREVIEW, field: 'previewfile' } ]
    .filter(({ field }) => !!files?.[field]?.[0])
    .map(({ type, field }) => ({
      path: files[field][0].path,
      type,
      automaticallyGenerated: false,
      keepOriginal: false
    }))

  const localVideoCreator = new LocalVideoCreator({
    lTags,

    videoFile: {
      path: videoPhysicalFile.path,
      probe: res.locals.ffprobe
    },

    user: res.locals.oauth.token.User,
    channel: res.locals.videoChannel,

    chapters: undefined,
    fallbackChapters: {
      fromDescription: true,
      finalFallback: containerChapters
    },

    videoAttributes: {
      ...videoInfo,

      duration: videoPhysicalFile.duration,
      inputFilename: videoPhysicalFile.originalname,
      state: buildNextVideoState(),
      isLive: false
    },

    liveAttributes: undefined,

    videoAttributeResultHook: 'filter:api.video.upload.video-attribute.result',

    thumbnails
  })

  const { video } = await localVideoCreator.create()

  auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(video.toFormattedDetailsJSON()))
  logger.info('Video with name %s and uuid %s created.', videoInfo.name, video.uuid, lTags(video.uuid))

  Hooks.runAction('action:api.video.uploaded', { video, req, res })

  return {
    video: {
      id: video.id,
      shortUUID: uuidToShort(video.uuid),
      uuid: video.uuid
    }
  }
}

async function deleteUploadResumableCache (req: express.Request, res: express.Response, next: express.NextFunction) {
  await Redis.Instance.deleteUploadSession(req.query.upload_id)

  return next()
}