From c158a5faabb8ef0bc5d121fda4522d63603e8bc5 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 12 May 2021 14:51:17 +0200 Subject: Refactor a little bit controllers --- server/controllers/api/videos/upload.ts | 269 ++++++++++++++++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 server/controllers/api/videos/upload.ts (limited to 'server/controllers/api/videos/upload.ts') diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts new file mode 100644 index 000000000..ebc17c760 --- /dev/null +++ b/server/controllers/api/videos/upload.ts @@ -0,0 +1,269 @@ +import * as express from 'express' +import { move } from 'fs-extra' +import { extname } from 'path' +import { deleteResumableUploadMetaFile, getResumableUploadPath } from '@server/helpers/upload' +import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' +import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' +import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' +import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' +import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' +import { uploadx } from '@uploadx/core' +import { VideoCreate, VideoState } from '../../../../shared' +import { HttpStatusCode } from '../../../../shared/core-utils/miscs' +import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' +import { retryTransactionWrapper } from '../../../helpers/database-utils' +import { createReqFiles } from '../../../helpers/express-utils' +import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' +import { logger, loggerTagsFactory } from '../../../helpers/logger' +import { CONFIG } from '../../../initializers/config' +import { DEFAULT_AUDIO_RESOLUTION, MIMETYPES } from '../../../initializers/constants' +import { sequelizeTypescript } from '../../../initializers/database' +import { federateVideoIfNeeded } from '../../../lib/activitypub/videos' +import { Notifier } from '../../../lib/notifier' +import { Hooks } from '../../../lib/plugins/hooks' +import { generateVideoMiniature } from '../../../lib/thumbnail' +import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' +import { + asyncMiddleware, + asyncRetryTransactionMiddleware, + authenticate, + videosAddLegacyValidator, + videosAddResumableInitValidator, + videosAddResumableValidator +} from '../../../middlewares' +import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' +import { VideoModel } from '../../../models/video/video' +import { VideoFileModel } from '../../../models/video/video-file' + +const lTags = loggerTagsFactory('api', 'video') +const auditLogger = auditLoggerFactory('videos') +const uploadRouter = express.Router() +const uploadxMiddleware = uploadx.upload({ directory: getResumableUploadPath() }) + +const reqVideoFileAdd = createReqFiles( + [ 'videofile', 'thumbnailfile', 'previewfile' ], + Object.assign({}, MIMETYPES.VIDEO.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT), + { + videofile: CONFIG.STORAGE.TMP_DIR, + thumbnailfile: CONFIG.STORAGE.TMP_DIR, + previewfile: CONFIG.STORAGE.TMP_DIR + } +) + +const reqVideoFileAddResumable = createReqFiles( + [ 'thumbnailfile', 'previewfile' ], + MIMETYPES.IMAGE.MIMETYPE_EXT, + { + thumbnailfile: getResumableUploadPath(), + previewfile: getResumableUploadPath() + } +) + +uploadRouter.post('/upload', + authenticate, + reqVideoFileAdd, + asyncMiddleware(videosAddLegacyValidator), + asyncRetryTransactionMiddleware(addVideoLegacy) +) + +uploadRouter.post('/upload-resumable', + authenticate, + reqVideoFileAddResumable, + asyncMiddleware(videosAddResumableInitValidator), + uploadxMiddleware +) + +uploadRouter.delete('/upload-resumable', + authenticate, + uploadxMiddleware +) + +uploadRouter.put('/upload-resumable', + authenticate, + uploadxMiddleware, // uploadx doesn't use call next() before the file upload completes + asyncMiddleware(videosAddResumableValidator), + asyncMiddleware(addVideoResumable) +) + +// --------------------------------------------------------------------------- + +export { + uploadRouter +} + +// --------------------------------------------------------------------------- + +export async function addVideoLegacy (req: express.Request, res: express.Response) { + // Uploading the video could be long + // Set timeout to 10 minutes, as Express's default is 2 minutes + req.setTimeout(1000 * 60 * 10, () => { + logger.error('Upload video has timed out.') + return res.sendStatus(HttpStatusCode.REQUEST_TIMEOUT_408) + }) + + const videoPhysicalFile = req.files['videofile'][0] + const videoInfo: VideoCreate = req.body + const files = req.files + + return addVideo({ res, videoPhysicalFile, videoInfo, files }) +} + +export async function addVideoResumable (_req: express.Request, res: express.Response) { + const videoPhysicalFile = res.locals.videoFileResumable + const videoInfo = videoPhysicalFile.metadata + const files = { previewfile: videoInfo.previewfile } + + // Don't need the meta file anymore + await deleteResumableUploadMetaFile(videoPhysicalFile.path) + + return addVideo({ res, videoPhysicalFile, videoInfo, files }) +} + +async function addVideo (options: { + res: express.Response + videoPhysicalFile: express.VideoUploadFile + videoInfo: VideoCreate + files: express.UploadFiles +}) { + const { res, videoPhysicalFile, videoInfo, files } = options + const videoChannel = res.locals.videoChannel + const user = res.locals.oauth.token.User + + const videoData = buildLocalVideoFromReq(videoInfo, videoChannel.id) + + videoData.state = CONFIG.TRANSCODING.ENABLED + ? VideoState.TO_TRANSCODE + : VideoState.PUBLISHED + + videoData.duration = videoPhysicalFile.duration // duration was added by a previous middleware + + const video = new VideoModel(videoData) as MVideoFullLight + video.VideoChannel = videoChannel + video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object + + const videoFile = await buildNewFile(video, videoPhysicalFile) + + // Move physical file + const destination = getVideoFilePath(video, videoFile) + await move(videoPhysicalFile.path, destination) + // This is important in case if there is another attempt in the retry process + videoPhysicalFile.filename = getVideoFilePath(video, videoFile) + videoPhysicalFile.path = destination + + const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ + video, + files, + fallback: type => generateVideoMiniature({ video, videoFile, type }) + }) + + const { videoCreated } = await sequelizeTypescript.transaction(async t => { + const sequelizeOptions = { transaction: t } + + const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight + + await videoCreated.addAndSaveThumbnail(thumbnailModel, t) + await videoCreated.addAndSaveThumbnail(previewModel, t) + + // Do not forget to add video channel information to the created video + videoCreated.VideoChannel = res.locals.videoChannel + + videoFile.videoId = video.id + await videoFile.save(sequelizeOptions) + + video.VideoFiles = [ videoFile ] + + await setVideoTags({ video, tags: videoInfo.tags, transaction: t }) + + // Schedule an update in the future? + if (videoInfo.scheduleUpdate) { + await ScheduleVideoUpdateModel.create({ + videoId: video.id, + updateAt: new Date(videoInfo.scheduleUpdate.updateAt), + privacy: videoInfo.scheduleUpdate.privacy || null + }, sequelizeOptions) + } + + // Channel has a new content, set as updated + await videoCreated.VideoChannel.setAsUpdated(t) + + await autoBlacklistVideoIfNeeded({ + video, + user, + isRemote: false, + isNew: true, + transaction: t + }) + + auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON())) + logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid)) + + return { videoCreated } + }) + + createTorrentFederate(video, videoFile) + + if (video.state === VideoState.TO_TRANSCODE) { + await addOptimizeOrMergeAudioJob(videoCreated, videoFile, user) + } + + Hooks.runAction('action:api.video.uploaded', { video: videoCreated }) + + return res.json({ + video: { + id: videoCreated.id, + uuid: videoCreated.uuid + } + }) +} + +async function buildNewFile (video: MVideo, videoPhysicalFile: express.VideoUploadFile) { + const videoFile = new VideoFileModel({ + extname: extname(videoPhysicalFile.filename), + size: videoPhysicalFile.size, + videoStreamingPlaylistId: null, + metadata: await getMetadataFromFile(videoPhysicalFile.path) + }) + + if (videoFile.isAudio()) { + videoFile.resolution = DEFAULT_AUDIO_RESOLUTION + } else { + videoFile.fps = await getVideoFileFPS(videoPhysicalFile.path) + videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).videoFileResolution + } + + videoFile.filename = generateVideoFilename(video, false, videoFile.resolution, videoFile.extname) + + return videoFile +} + +async function createTorrentAndSetInfoHashAsync (video: MVideo, fileArg: MVideoFile) { + await createTorrentAndSetInfoHash(video, fileArg) + + // Refresh videoFile because the createTorrentAndSetInfoHash could be long + const refreshedFile = await VideoFileModel.loadWithVideo(fileArg.id) + // File does not exist anymore, remove the generated torrent + if (!refreshedFile) return fileArg.removeTorrent() + + refreshedFile.infoHash = fileArg.infoHash + refreshedFile.torrentFilename = fileArg.torrentFilename + + return refreshedFile.save() +} + +function createTorrentFederate (video: MVideoFullLight, videoFile: MVideoFile): void { + // Create the torrent file in async way because it could be long + createTorrentAndSetInfoHashAsync(video, videoFile) + .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err, ...lTags(video.uuid) })) + .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id)) + .then(refreshedVideo => { + if (!refreshedVideo) return + + // Only federate and notify after the torrent creation + Notifier.Instance.notifyOnNewVideoIfNeeded(refreshedVideo) + + return retryTransactionWrapper(() => { + return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t)) + }) + }) + .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) })) +} -- cgit v1.2.3