Skip to content

Commit

Permalink
filter upload-resumable middlewares down to POST, PUT, DELETE
Browse files Browse the repository at this point in the history
also begin to type metadata
  • Loading branch information
rigelk committed Apr 22, 2021
1 parent acf9e74 commit 2a5d59d
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 132 deletions.
19 changes: 8 additions & 11 deletions server/controllers/api/videos/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,15 @@ import {
authenticate,
checkVideoFollowConstraints,
commonVideosFiltersValidator,
executeIfPOST,
onlyAllowMethods,
optionalAuthenticate,
paginationValidator,
setDefaultPagination,
setDefaultVideosSort,
videoFileMetadataGetValidator,
videosAddLegacyValidator,
videosAddResumableInitValidator,
videosAddResumableValidator,
videosCustomGetValidator,
videosGetValidator,
Expand All @@ -67,7 +70,7 @@ import { liveRouter } from './live'
import { ownershipVideoRouter } from './ownership'
import { rateVideoRouter } from './rate'
import { watchingRouter } from './watching'
import { DiskStorageOptions, uploadx } from '@uploadx/core'
import { DiskStorageOptions, Metadata, uploadx } from '@uploadx/core'
import { VideoCreate } from '../../../../shared/models/videos/video-create.model'

const lTags = loggerTagsFactory('api', 'video')
Expand Down Expand Up @@ -126,9 +129,11 @@ videosRouter.post('/upload',
)

videosRouter.use('/upload-resumable',
onlyAllowMethods([ 'POST', 'PUT', 'DELETE' ]), // uploadx also allows GET and PATCH
authenticate,
uploadx.upload(uploadxOptions),
asyncMiddleware(videosAddResumableValidator),
executeIfPOST(asyncMiddleware(videosAddResumableInitValidator)),
asyncMiddleware(addVideoResumable)
)

Expand Down Expand Up @@ -204,15 +209,7 @@ async function addVideoLegacy (req: express.Request, res: express.Response) {
}

async function addVideoResumable (req: express.Request, res: express.Response) {
interface VideoPhysicalFile {
duration: number
filename: string
size: number
path: string
metadata: VideoCreate & express.FileUploadMetadata
}

const videoPhysicalFile = res.locals.videoFileResumable as VideoPhysicalFile
const videoPhysicalFile = res.locals.videoFileResumable
const videoInfo = videoPhysicalFile.metadata
const files = { bg: { path: await getResumableUploadPath(videoPhysicalFile.filename) } }

Expand All @@ -221,7 +218,7 @@ async function addVideoResumable (req: express.Request, res: express.Response) {

async function addVideo (req: express.Request, res: express.Response, parameters: {
videoPhysicalFile: { duration: number, filename: string, size: number, path: string }
videoInfo: VideoCreate | VideoCreate & express.FileUploadMetadata
videoInfo: VideoCreate | Metadata
files
}) {
const { videoPhysicalFile, videoInfo, files } = parameters
Expand Down
25 changes: 24 additions & 1 deletion server/middlewares/async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { NextFunction, Request, RequestHandler, Response } from 'express'
import { ValidationChain } from 'express-validator'
import { ExpressPromiseHandler } from '@server/types/express'
import { retryTransactionWrapper } from '../helpers/database-utils'
import { HttpStatusCode } from '@shared/core-utils'

// Syntactic sugar to avoid try/catch in express controllers
// Thanks: https://medium.com/@Abazhenov/using-async-await-in-express-with-node-8-b8af872c0016
Expand Down Expand Up @@ -31,9 +32,31 @@ function asyncRetryTransactionMiddleware (fun: (req: Request, res: Response, nex
}
}

function executeIfMethod (fun: (req: Request, res: Response, next: NextFunction) => void | Promise<void>, method: string) {
return (req: Request, res: Response, next: NextFunction) => {
return req.method === method
? Promise.resolve((fun as RequestHandler)(req, res, next))
: next()
}
}

function executeIfPOST (fun) {
return executeIfMethod(fun, 'POST')
}

function onlyAllowMethods (methods: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!methods.includes(req.method)) return res.status(HttpStatusCode.METHOD_NOT_ALLOWED_405)

next()
}
}

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

export {
asyncMiddleware,
asyncRetryTransactionMiddleware
asyncRetryTransactionMiddleware,
executeIfPOST,
onlyAllowMethods
}
85 changes: 53 additions & 32 deletions server/middlewares/validators/videos/videos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import { VideoModel } from '../../../models/video/video'
import { authenticatePromiseIfNeeded } from '../../auth'
import { areValidationErrors } from '../utils'
import { getResumableUploadPath, deleteFileAsync as clearUploadFile } from '../../../helpers/utils'
import { Metadata } from '@uploadx/core'

const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
body('videofile')
Expand Down Expand Up @@ -96,9 +97,7 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([

if (!isVideoFileSizeValid(videoFile.size.toString())) {
res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413)
.json({
error: 'This file is too large.'
})
.json({ error: 'This file is too large.' })

return cleanUpReqFiles(req)
}
Expand Down Expand Up @@ -130,31 +129,18 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
}
])

const videosAddResumableValidator = getCommonVideoEditAttributes().concat([
body('name')
.trim()
.custom(isVideoNameValid)
.withMessage('Should have a valid name'),
body('channelId')
.customSanitizer(toIntOrNull)
.custom(isIdValid).withMessage('Should have correct video channel id'),

const videosAddResumableValidator = [
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
const videoFileMetadata = {
mimetype: req.body.mimeType,
size: req.body.size,
originalname: req.body.name
} as express.FileUploadMetadata
const user = res.locals.oauth.token.User
const file: Express.Multer.File & { id: string, metadata: any } = req.body
const file: { id: string, metadata: Metadata, duration: number, size: number, path: string, filename: string } = req.body
file.path = join(CONFIG.STORAGE.VIDEOS_DIR, file.id)

if (
!file.metadata.isPreviewForAudio &&
!await doesVideoChannelOfAccountExist(file.metadata.channelId, user, res)
) return clearUploadFile(file.path)

if (!await isVideoAccepted(req, res, file)) return clearUploadFile(file.path)
if (!await isVideoAccepted(req, res, file as any)) return clearUploadFile(file.path)

if (file.metadata.isPreviewForAudio) {
const filename = `${file.id}-${uuidv4()}`
Expand All @@ -164,18 +150,47 @@ const videosAddResumableValidator = getCommonVideoEditAttributes().concat([
}

try {
await addDurationToVideo(file)
if (!file.duration) await addDurationToVideo(file)
} catch (err) {
logger.error('Invalid input file in videosAddValidator.', { err })
await clearUploadFile(file.path)
return res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422).json({ error: 'Video file unreadable.' })
res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422)
.json({ error: 'Video file unreadable.' })

return clearUploadFile(file.path)
}

res.locals.videoFileResumable = file

if (req.method !== 'POST' || req.body.isPreviewForAudio) {
return next()
} /** The middleware applies what follows only to non-audio POST */
return next()
}
]

/**
* File is created in POST initialisation, and metadata is saved by uploadx for
* later use.
*
* see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/uploadx.ts#L41
*/
const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([
body('name')
.trim()
.custom(isVideoNameValid)
.withMessage('Should have a valid name'),
body('channelId')
.customSanitizer(toIntOrNull)
.custom(isIdValid).withMessage('Should have correct video channel id'),

async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (req.method !== 'POST') return next()
if (req.body.isPreviewForAudio) return next()

const videoFileMetadata = {
mimetype: req.body.mimeType,
size: req.body.size,
originalname: req.body.name
} as express.FileUploadMetadata
const user = res.locals.oauth.token.User
const file = res.locals.videoFileResumable

logger.debug('Checking videosAddResumable parameters', { parameters: req.body, files: req.files })

Expand All @@ -189,22 +204,27 @@ const videosAddResumableValidator = getCommonVideoEditAttributes().concat([
videoFileMetadata
]
})) {
await clearUploadFile(file.path)
return res.status(HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415)
res.status(HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415)
.json({
error: 'This file is not supported. Please, make sure it is of the following type: ' +
CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
})

return clearUploadFile(file.path)
}

if (!isVideoFileSizeValid(videoFileMetadata.size.toString())) {
await clearUploadFile(file.path)
return res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413).json({ error: 'This file is too large.' })
res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413)
.json({ error: 'This file is too large.' })

return clearUploadFile(file.path)
}

if (await isAbleToUploadVideo(user.id, videoFileMetadata.size) === false) {
await clearUploadFile(file.path)
return res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413).json({ error: 'The user video quota is exceeded with this video.' })
res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413)
.json({ error: 'The user video quota is exceeded with this video.' })

return clearUploadFile(file.path)
}

return next()
Expand Down Expand Up @@ -561,6 +581,7 @@ const commonVideosFiltersValidator = [
export {
videosAddLegacyValidator,
videosAddResumableValidator,
videosAddResumableInitValidator,
videosUpdateValidator,
videosGetValidator,
videoFileMetadataGetValidator,
Expand Down Expand Up @@ -625,7 +646,7 @@ export async function isVideoAccepted (
return true
}

async function addDurationToVideo (videoFile: Express.Multer.File & { duration?: number, metadata?: any }) {
async function addDurationToVideo (videoFile: any) {
const duration: number = await getDurationFromVideoFile(videoFile.path)

if (isNaN(duration)) throw new Error("Couldn't get video duration")
Expand Down
12 changes: 6 additions & 6 deletions server/typings/express/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ import { MPlugin, MServer, MServerBlocklist } from '@server/types/models/server'
import { MVideoImportDefault } from '@server/types/models/video/video-import'
import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element'
import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate'
import { VideoCreate } from '@shared/models'
import { FileUploadMetadata } from 'express'
import { Metadata } from '@uploadx/core'
import { RegisteredPlugin } from '../../lib/plugins/plugin-manager'
import {
MAccountDefault,
Expand Down Expand Up @@ -67,12 +66,13 @@ interface PeerTubeLocals {

videoFile?: MVideoFile

videoFileResumable?: Express.Multer.File & {
videoFileResumable?: {
id: string
path: string
metadata: VideoCreate & FileUploadMetadata
duration?: number
filename?: string
metadata: Metadata
duration: number
filename: string
size: number
}

videoImport?: MVideoImportDefault
Expand Down
Loading

0 comments on commit 2a5d59d

Please sign in to comment.