Add ability to set video thumbnail/preview

pull/297/head
Chocobozzz 2018-02-13 18:17:05 +01:00
parent e883399fa6
commit ac81d1a06d
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
22 changed files with 454 additions and 143 deletions

View File

@ -1,4 +1,4 @@
import { ElementRef, OnInit, ViewChild, ViewChildren } from '@angular/core'
import { ElementRef, OnInit, ViewChild } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { isInMobileView } from '@app/shared/misc/utils'
import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'

View File

@ -158,7 +158,7 @@ app.use(function (req, res, next) {
})
app.use(function (err, req, res, next) {
logger.error(err, err)
logger.error('Error in controller.', { error: err.stack || err.message || err })
res.sendStatus(err.status || 500)
})

View File

@ -1,13 +1,13 @@
import * as express from 'express'
import 'multer'
import { extname, join } from 'path'
import * as sharp from 'sharp'
import * as uuidv4 from 'uuid/v4'
import { UserCreate, UserRight, UserRole, UserUpdate, UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../shared'
import { unlinkPromise } from '../../helpers/core-utils'
import { retryTransactionWrapper } from '../../helpers/database-utils'
import { processImage } from '../../helpers/image-utils'
import { logger } from '../../helpers/logger'
import { createReqFiles, getFormattedObjects } from '../../helpers/utils'
import { AVATAR_MIMETYPE_EXT, AVATARS_SIZE, CONFIG, sequelizeTypescript } from '../../initializers'
import { AVATARS_SIZE, CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers'
import { updateActorAvatarInstance } from '../../lib/activitypub'
import { sendUpdateUser } from '../../lib/activitypub/send'
import { Emailer } from '../../lib/emailer'
@ -42,7 +42,7 @@ import { UserModel } from '../../models/account/user'
import { OAuthTokenModel } from '../../models/oauth/oauth-token'
import { VideoModel } from '../../models/video/video'
const reqAvatarFile = createReqFiles('avatarfile', CONFIG.STORAGE.AVATARS_DIR, AVATAR_MIMETYPE_EXT)
const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR })
const usersRouter = express.Router()
@ -288,17 +288,10 @@ async function updateMyAvatar (req: express.Request, res: express.Response, next
const user = res.locals.oauth.token.user
const actor = user.Account.Actor
const avatarDir = CONFIG.STORAGE.AVATARS_DIR
const source = join(avatarDir, avatarPhysicalFile.filename)
const extension = extname(avatarPhysicalFile.filename)
const avatarName = uuidv4() + extension
const destination = join(avatarDir, avatarName)
await sharp(source)
.resize(AVATARS_SIZE.width, AVATARS_SIZE.height)
.toFile(destination)
await unlinkPromise(source)
const destination = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
await processImage(avatarPhysicalFile, destination, AVATARS_SIZE)
const avatar = await sequelizeTypescript.transaction(async t => {
const updatedActor = await updateActorAvatarInstance(actor, avatarName, t)

View File

@ -4,18 +4,36 @@ import { VideoCreate, VideoPrivacy, VideoUpdate } from '../../../../shared'
import { renamePromise } from '../../../helpers/core-utils'
import { retryTransactionWrapper } from '../../../helpers/database-utils'
import { getVideoFileHeight } from '../../../helpers/ffmpeg-utils'
import { processImage } from '../../../helpers/image-utils'
import { logger } from '../../../helpers/logger'
import { createReqFiles, getFormattedObjects, getServerActor, resetSequelizeInstance } from '../../../helpers/utils'
import {
CONFIG, sequelizeTypescript, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_MIMETYPE_EXT,
CONFIG,
IMAGE_MIMETYPE_EXT,
PREVIEWS_SIZE,
sequelizeTypescript,
THUMBNAILS_SIZE,
VIDEO_CATEGORIES,
VIDEO_LANGUAGES,
VIDEO_LICENCES,
VIDEO_MIMETYPE_EXT,
VIDEO_PRIVACIES
} from '../../../initializers'
import { fetchRemoteVideoDescription, getVideoActivityPubUrl, shareVideoByServerAndChannel } from '../../../lib/activitypub'
import { sendCreateVideo, sendCreateViewToOrigin, sendCreateViewToVideoFollowers, sendUpdateVideo } from '../../../lib/activitypub/send'
import { JobQueue } from '../../../lib/job-queue'
import {
asyncMiddleware, authenticate, paginationValidator, setDefaultSort, setDefaultPagination, videosAddValidator, videosGetValidator,
videosRemoveValidator, videosSearchValidator, videosSortValidator, videosUpdateValidator
asyncMiddleware,
authenticate,
paginationValidator,
setDefaultPagination,
setDefaultSort,
videosAddValidator,
videosGetValidator,
videosRemoveValidator,
videosSearchValidator,
videosSortValidator,
videosUpdateValidator
} from '../../../middlewares'
import { TagModel } from '../../../models/video/tag'
import { VideoModel } from '../../../models/video/video'
@ -28,7 +46,23 @@ import { rateVideoRouter } from './rate'
const videosRouter = express.Router()
const reqVideoFile = createReqFiles('videofile', CONFIG.STORAGE.VIDEOS_DIR, VIDEO_MIMETYPE_EXT)
const reqVideoFileAdd = createReqFiles(
[ 'videofile', 'thumbnailfile', 'previewfile' ],
Object.assign({}, VIDEO_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT),
{
videofile: CONFIG.STORAGE.VIDEOS_DIR,
thumbnailfile: CONFIG.STORAGE.THUMBNAILS_DIR,
previewfile: CONFIG.STORAGE.PREVIEWS_DIR
}
)
const reqVideoFileUpdate = createReqFiles(
[ 'thumbnailfile', 'previewfile' ],
IMAGE_MIMETYPE_EXT,
{
thumbnailfile: CONFIG.STORAGE.THUMBNAILS_DIR,
previewfile: CONFIG.STORAGE.PREVIEWS_DIR
}
)
videosRouter.use('/', abuseVideoRouter)
videosRouter.use('/', blacklistRouter)
@ -58,12 +92,13 @@ videosRouter.get('/search',
)
videosRouter.put('/:id',
authenticate,
reqVideoFileUpdate,
asyncMiddleware(videosUpdateValidator),
asyncMiddleware(updateVideoRetryWrapper)
)
videosRouter.post('/upload',
authenticate,
reqVideoFile,
reqVideoFileAdd,
asyncMiddleware(videosAddValidator),
asyncMiddleware(addVideoRetryWrapper)
)
@ -150,8 +185,7 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
const video = new VideoModel(videoData)
video.url = getVideoActivityPubUrl(video)
const videoFilePath = join(CONFIG.STORAGE.VIDEOS_DIR, videoPhysicalFile.filename)
const videoFileHeight = await getVideoFileHeight(videoFilePath)
const videoFileHeight = await getVideoFileHeight(videoPhysicalFile.path)
const videoFileData = {
extname: extname(videoPhysicalFile.filename),
@ -160,21 +194,28 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
}
const videoFile = new VideoFileModel(videoFileData)
const videoDir = CONFIG.STORAGE.VIDEOS_DIR
const source = join(videoDir, videoPhysicalFile.filename)
const destination = join(videoDir, video.getVideoFilename(videoFile))
await renamePromise(videoPhysicalFile.path, destination)
await renamePromise(source, destination)
// This is important in case if there is another attempt in the retry process
videoPhysicalFile.filename = video.getVideoFilename(videoFile)
// Process thumbnail or create it from the video
const thumbnailField = req.files['thumbnailfile']
if (thumbnailField) {
const thumbnailPhysicalFile = thumbnailField[0]
await processImage(thumbnailPhysicalFile, join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName()), THUMBNAILS_SIZE)
} else {
await video.createThumbnail(videoFile)
}
const tasks = []
// Process preview or create it from the video
const previewField = req.files['previewfile']
if (previewField) {
const previewPhysicalFile = previewField[0]
await processImage(previewPhysicalFile, join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName()), PREVIEWS_SIZE)
} else {
await video.createPreview(videoFile)
}
tasks.push(
video.createTorrentAndSetInfoHash(videoFile),
video.createThumbnail(videoFile),
video.createPreview(videoFile)
)
await Promise.all(tasks)
await video.createTorrentAndSetInfoHash(videoFile)
const videoCreated = await sequelizeTypescript.transaction(async t => {
const sequelizeOptions = { transaction: t }
@ -237,6 +278,18 @@ async function updateVideo (req: express.Request, res: express.Response) {
const videoInfoToUpdate: VideoUpdate = req.body
const wasPrivateVideo = videoInstance.privacy === VideoPrivacy.PRIVATE
// Process thumbnail or create it from the video
if (req.files && req.files['thumbnailfile']) {
const thumbnailPhysicalFile = req.files['thumbnailfile'][0]
await processImage(thumbnailPhysicalFile, join(CONFIG.STORAGE.THUMBNAILS_DIR, videoInstance.getThumbnailName()), THUMBNAILS_SIZE)
}
// Process preview or create it from the video
if (req.files && req.files['previewfile']) {
const previewPhysicalFile = req.files['previewfile'][0]
await processImage(previewPhysicalFile, join(CONFIG.STORAGE.PREVIEWS_DIR, videoInstance.getPreviewName()), PREVIEWS_SIZE)
}
try {
await sequelizeTypescript.transaction(async t => {
const sequelizeOptions = {

View File

@ -1,3 +1,4 @@
import 'multer'
import * as validator from 'validator'
function exists (value: any) {
@ -28,6 +29,29 @@ function isBooleanValid (value: string) {
return typeof value === 'boolean' || (typeof value === 'string' && validator.isBoolean(value))
}
function isFileValid (
files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[],
mimeTypeRegex: string,
field: string,
optional = false
) {
// Should have files
if (!files) return optional
if (isArray(files)) return optional
// Should have a file
const fileArray = files[ field ]
if (!fileArray || fileArray.length === 0) {
return optional
}
// The file should exist
const file = fileArray[ 0 ]
if (!file || !file.originalname) return false
return new RegExp(`^${mimeTypeRegex}$`, 'i').test(file.mimetype)
}
// ---------------------------------------------------------------------------
export {
@ -37,5 +61,6 @@ export {
isUUIDValid,
isIdOrUUIDValid,
isDateValid,
isBooleanValid
isBooleanValid,
isFileValid
}

View File

@ -1,9 +1,9 @@
import * as validator from 'validator'
import 'express-validator'
import { exists, isArray } from './misc'
import { CONSTRAINTS_FIELDS } from '../../initializers'
import * as validator from 'validator'
import { UserRole } from '../../../shared'
import { CONSTRAINTS_FIELDS } from '../../initializers'
import { exists, isFileValid } from './misc'
const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS
@ -37,20 +37,12 @@ function isUserRoleValid (value: any) {
return exists(value) && validator.isInt('' + value) && UserRole[value] !== undefined
}
const avatarMimeTypes = CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME
.map(v => v.replace('.', ''))
.join('|')
const avatarMimeTypesRegex = `image/(${avatarMimeTypes})`
function isAvatarFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
// Should have files
if (!files) return false
if (isArray(files)) return false
// Should have videofile file
const avatarfile = files['avatarfile']
if (!avatarfile || avatarfile.length === 0) return false
// The file should exist
const file = avatarfile[0]
if (!file || !file.originalname) return false
return new RegExp('^image/(png|jpeg)$', 'i').test(file.mimetype)
return isFileValid(files, avatarMimeTypesRegex, 'avatarfile')
}
// ---------------------------------------------------------------------------

View File

@ -8,12 +8,12 @@ import {
CONSTRAINTS_FIELDS,
VIDEO_CATEGORIES,
VIDEO_LANGUAGES,
VIDEO_LICENCES,
VIDEO_LICENCES, VIDEO_MIMETYPE_EXT,
VIDEO_PRIVACIES,
VIDEO_RATE_TYPES
} from '../../initializers'
import { VideoModel } from '../../models/video/video'
import { exists, isArray } from './misc'
import { exists, isArray, isFileValid } from './misc'
const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES
@ -68,20 +68,18 @@ function isVideoRatingTypeValid (value: string) {
return value === 'none' || values(VIDEO_RATE_TYPES).indexOf(value as VideoRateType) !== -1
}
const videoFileTypes = Object.keys(VIDEO_MIMETYPE_EXT).map(m => `(${m})`)
const videoFileTypesRegex = videoFileTypes.join('|')
function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
// Should have files
if (!files) return false
if (isArray(files)) return false
return isFileValid(files, videoFileTypesRegex, 'videofile')
}
// Should have videofile file
const videofile = files['videofile']
if (!videofile || videofile.length === 0) return false
// The file should exist
const file = videofile[0]
if (!file || !file.originalname) return false
return new RegExp('^video/(webm|mp4|ogg)$', 'i').test(file.mimetype)
const videoImageTypes = CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME
.map(v => v.replace('.', ''))
.join('|')
const videoImageTypesRegex = `image/(${videoImageTypes})`
function isVideoImage (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], field: string) {
return isFileValid(files, videoImageTypesRegex, field, true)
}
function isVideoPrivacyValid (value: string) {
@ -141,5 +139,6 @@ export {
isVideoPrivacyValid,
isVideoFileResolutionValid,
isVideoFileSizeValid,
isVideoExist
isVideoExist,
isVideoImage
}

View File

@ -0,0 +1,21 @@
import 'multer'
import * as sharp from 'sharp'
import { unlinkPromise } from './core-utils'
async function processImage (
physicalFile: Express.Multer.File,
destination: string,
newSize: { width: number, height: number }
) {
await sharp(physicalFile.path)
.resize(newSize.width, newSize.height)
.toFile(destination)
await unlinkPromise(physicalFile.path)
}
// ---------------------------------------------------------------------------
export {
processImage
}

View File

@ -27,10 +27,14 @@ function badRequest (req: express.Request, res: express.Response, next: express.
return res.type('json').status(400).end()
}
function createReqFiles (fieldName: string, storageDir: string, mimeTypes: { [ id: string ]: string }) {
function createReqFiles (
fieldNames: string[],
mimeTypes: { [ id: string ]: string },
destinations: { [ fieldName: string ]: string }
) {
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, storageDir)
cb(null, destinations[file.fieldname])
},
filename: async (req, file, cb) => {
@ -48,7 +52,15 @@ function createReqFiles (fieldName: string, storageDir: string, mimeTypes: { [ i
}
})
return multer({ storage }).fields([{ name: fieldName, maxCount: 1 }])
const fields = []
for (const fieldName of fieldNames) {
fields.push({
name: fieldName,
maxCount: 1
})
}
return multer({ storage }).fields(fields)
}
async function generateRandomString (size: number) {

View File

@ -182,6 +182,12 @@ const CONSTRAINTS_FIELDS = {
NAME: { min: 3, max: 120 }, // Length
TRUNCATED_DESCRIPTION: { min: 3, max: 250 }, // Length
DESCRIPTION: { min: 3, max: 3000 }, // Length
IMAGE: {
EXTNAME: [ '.jpg', '.jpeg' ],
FILE_SIZE: {
max: 2 * 1024 * 1024 // 2MB
}
},
EXTNAME: [ '.mp4', '.ogv', '.webm' ],
INFO_HASH: { min: 40, max: 40 }, // Length, info hash is 20 bytes length but we represent it in hexadecimal so 20 * 2
DURATION: { min: 1 }, // Number
@ -285,7 +291,7 @@ const VIDEO_MIMETYPE_EXT = {
'video/mp4': '.mp4'
}
const AVATAR_MIMETYPE_EXT = {
const IMAGE_MIMETYPE_EXT = {
'image/png': '.png',
'image/jpg': '.jpg',
'image/jpeg': '.jpg'
@ -427,7 +433,7 @@ export {
VIDEO_RATE_TYPES,
VIDEO_MIMETYPE_EXT,
USER_PASSWORD_RESET_LIFETIME,
AVATAR_MIMETYPE_EXT,
IMAGE_MIMETYPE_EXT,
SCHEDULER_INTERVAL,
JOB_COMPLETED_LIFETIME
}

View File

@ -12,7 +12,7 @@ import { logger } from '../../helpers/logger'
import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
import { getUrlFromWebfinger } from '../../helpers/webfinger'
import { AVATAR_MIMETYPE_EXT, CONFIG, sequelizeTypescript } from '../../initializers'
import { IMAGE_MIMETYPE_EXT, CONFIG, sequelizeTypescript } from '../../initializers'
import { AccountModel } from '../../models/account/account'
import { ActorModel } from '../../models/activitypub/actor'
import { AvatarModel } from '../../models/avatar/avatar'
@ -147,10 +147,10 @@ async function fetchActorTotalItems (url: string) {
async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
if (
actorJSON.icon && actorJSON.icon.type === 'Image' && AVATAR_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
actorJSON.icon && actorJSON.icon.type === 'Image' && IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
isActivityPubUrlValid(actorJSON.icon.url)
) {
const extension = AVATAR_MIMETYPE_EXT[actorJSON.icon.mediaType]
const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType]
const avatarName = uuidv4() + extension
const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)

View File

@ -4,8 +4,18 @@ import { body, param, query } from 'express-validator/check'
import { UserRight, VideoPrivacy } from '../../../shared'
import { isBooleanValid, isIdOrUUIDValid, isIdValid, isUUIDValid } from '../../helpers/custom-validators/misc'
import {
isVideoAbuseReasonValid, isVideoCategoryValid, isVideoDescriptionValid, isVideoExist, isVideoFile, isVideoLanguageValid,
isVideoLicenceValid, isVideoNameValid, isVideoPrivacyValid, isVideoRatingTypeValid, isVideoTagsValid
isVideoAbuseReasonValid,
isVideoCategoryValid,
isVideoDescriptionValid,
isVideoExist,
isVideoFile,
isVideoImage,
isVideoLanguageValid,
isVideoLicenceValid,
isVideoNameValid,
isVideoPrivacyValid,
isVideoRatingTypeValid,
isVideoTagsValid
} from '../../helpers/custom-validators/videos'
import { getDurationFromVideoFile } from '../../helpers/ffmpeg-utils'
import { logger } from '../../helpers/logger'
@ -22,6 +32,14 @@ const videosAddValidator = [
'This file is not supported. Please, make sure it is of the following type : '
+ CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
),
body('thumbnailfile').custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
'This thumbnail file is not supported. Please, make sure it is of the following type : '
+ CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
),
body('previewfile').custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
'This preview file is not supported. Please, make sure it is of the following type : '
+ CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
),
body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
body('category').optional().custom(isVideoCategoryValid).withMessage('Should have a valid category'),
body('licence').optional().custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
@ -37,6 +55,7 @@ const videosAddValidator = [
logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
if (areValidationErrors(req, res)) return
if (areErrorsInVideoImageFiles(req, res)) return
const videoFile: Express.Multer.File = req.files['videofile'][0]
const user = res.locals.oauth.token.User
@ -82,6 +101,14 @@ const videosAddValidator = [
const videosUpdateValidator = [
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
body('thumbnailfile').custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
'This thumbnail file is not supported. Please, make sure it is of the following type : '
+ CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
),
body('previewfile').custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
'This preview file is not supported. Please, make sure it is of the following type : '
+ CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
),
body('name').optional().custom(isVideoNameValid).withMessage('Should have a valid name'),
body('category').optional().custom(isVideoCategoryValid).withMessage('Should have a valid category'),
body('licence').optional().custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
@ -96,6 +123,7 @@ const videosUpdateValidator = [
logger.debug('Checking videosUpdate parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return
if (areErrorsInVideoImageFiles(req, res)) return
if (!await isVideoExist(req.params.id, res)) return
const video = res.locals.video
@ -274,3 +302,22 @@ function checkUserCanDeleteVideo (user: UserModel, video: VideoModel, res: expre
return true
}
function areErrorsInVideoImageFiles (req: express.Request, res: express.Response) {
// Files are optional
if (!req.files) return false
for (const imageField of [ 'thumbnail', 'preview' ]) {
if (!req.files[ imageField ]) continue
const imageFile = req.files[ imageField ][ 0 ] as Express.Multer.File
if (imageFile.size > CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max) {
res.status(400)
.send({ error: `The size of the ${imageField} is too big (>${CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max}).` })
.end()
return true
}
}
return false
}

View File

@ -7,7 +7,7 @@ import { UserRole } from '../../../../shared'
import {
createUser, flushTests, getMyUserInformation, getMyUserVideoRating, getUsersList, immutableAssign, killallServers, makeGetRequest,
makePostBodyRequest, makePostUploadRequest, makePutBodyRequest, registerUser, removeUser, runServer, ServerInfo, setAccessTokensToServers,
makePostBodyRequest, makeUploadRequest, makePutBodyRequest, registerUser, removeUser, runServer, ServerInfo, setAccessTokensToServers,
updateUser, uploadVideo, userLogin
} from '../../utils'
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params'
@ -273,7 +273,7 @@ describe('Test users API validators', function () {
const attaches = {
'avatarfile': join(__dirname, '..', 'fixtures', 'video_short.mp4')
}
await makePostUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches })
await makeUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches })
})
it('Should fail with a big file', async function () {
@ -281,7 +281,7 @@ describe('Test users API validators', function () {
const attaches = {
'avatarfile': join(__dirname, '..', 'fixtures', 'avatar-big.png')
}
await makePostUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches })
await makeUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches })
})
it('Should succeed with the correct params', async function () {
@ -289,7 +289,7 @@ describe('Test users API validators', function () {
const attaches = {
'avatarfile': join(__dirname, '..', 'fixtures', 'avatar.png')
}
await makePostUploadRequest({
await makeUploadRequest({
url: server.url,
path: path + '/me/avatar/pick',
token: server.accessToken,

View File

@ -7,7 +7,7 @@ import { join } from 'path'
import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum'
import {
createUser, flushTests, getMyUserInformation, getVideo, getVideosList, immutableAssign, killallServers, makeDeleteRequest,
makeGetRequest, makePostUploadRequest, makePutBodyRequest, removeVideo, runServer, ServerInfo, setAccessTokensToServers, userLogin
makeGetRequest, makeUploadRequest, makePutBodyRequest, removeVideo, runServer, ServerInfo, setAccessTokensToServers, userLogin
} from '../../utils'
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params'
@ -111,91 +111,91 @@ describe('Test videos API validator', function () {
it('Should fail with nothing', async function () {
const fields = {}
const attaches = {}
await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail without name', async function () {
const fields = omit(baseCorrectParams, 'name')
const attaches = baseCorrectAttaches
await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail with a long name', async function () {
const fields = immutableAssign(baseCorrectParams, { name: 'super'.repeat(65) })
const attaches = baseCorrectAttaches
await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail with a bad category', async function () {
const fields = immutableAssign(baseCorrectParams, { category: 125 })
const attaches = baseCorrectAttaches
await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail with a bad licence', async function () {
const fields = immutableAssign(baseCorrectParams, { licence: 125 })
const attaches = baseCorrectAttaches
await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail with a bad language', async function () {
const fields = immutableAssign(baseCorrectParams, { language: 125 })
const attaches = baseCorrectAttaches
await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail without nsfw attribute', async function () {
const fields = omit(baseCorrectParams, 'nsfw')
const attaches = baseCorrectAttaches
await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail with a bad nsfw attribute', async function () {
const fields = immutableAssign(baseCorrectParams, { nsfw: 2 })
const attaches = baseCorrectAttaches
await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail without commentsEnabled attribute', async function () {
const fields = omit(baseCorrectParams, 'commentsEnabled')
const attaches = baseCorrectAttaches
await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail with a bad commentsEnabled attribute', async function () {
const fields = immutableAssign(baseCorrectParams, { commentsEnabled: 2 })
const attaches = baseCorrectAttaches
await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail with a long description', async function () {
const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(1500) })
const attaches = baseCorrectAttaches
await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail without a channel', async function () {
const fields = omit(baseCorrectParams, 'channelId')
const attaches = baseCorrectAttaches
await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail with a bad channel', async function () {
const fields = immutableAssign(baseCorrectParams, { channelId: 545454 })
const attaches = baseCorrectAttaches
await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail with another user channel', async function () {
@ -212,34 +212,34 @@ describe('Test videos API validator', function () {
const fields = immutableAssign(baseCorrectParams, { channelId: customChannelId })
const attaches = baseCorrectAttaches
await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail with too many tags', async function () {
const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] })
const attaches = baseCorrectAttaches
await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail with a tag length too low', async function () {
const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 't' ] })
const attaches = baseCorrectAttaches
await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail with a tag length too big', async function () {
const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] })
const attaches = baseCorrectAttaches
await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail without an input file', async function () {
const fields = baseCorrectParams
const attaches = {}
await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail without an incorrect input file', async function () {
@ -247,7 +247,47 @@ describe('Test videos API validator', function () {
const attaches = {
'videofile': join(__dirname, '..', 'fixtures', 'video_short_fake.webm')
}
await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail with an incorrect thumbnail file', async function () {
const fields = baseCorrectParams
const attaches = {
'thumbnailfile': join(__dirname, '..', 'fixtures', 'avatar.png'),
'videofile': join(__dirname, '..', 'fixtures', 'video_short_fake.webm')
}
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail with a big thumbnail file', async function () {
const fields = baseCorrectParams
const attaches = {
'thumbnailfile': join(__dirname, '..', 'fixtures', 'avatar-big.png'),
'videofile': join(__dirname, '..', 'fixtures', 'video_short_fake.webm')
}
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail with an incorrect preview file', async function () {
const fields = baseCorrectParams
const attaches = {
'previewfile': join(__dirname, '..', 'fixtures', 'avatar.png'),
'videofile': join(__dirname, '..', 'fixtures', 'video_short_fake.webm')
}
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail with a big preview file', async function () {
const fields = baseCorrectParams
const attaches = {
'previewfile': join(__dirname, '..', 'fixtures', 'avatar-big.png'),
'videofile': join(__dirname, '..', 'fixtures', 'video_short_fake.webm')
}
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should succeed with the correct parameters', async function () {
@ -257,7 +297,7 @@ describe('Test videos API validator', function () {
{
const attaches = baseCorrectAttaches
await makePostUploadRequest({
await makeUploadRequest({
url: server.url,
path: path + '/upload',
token: server.accessToken,
@ -272,7 +312,7 @@ describe('Test videos API validator', function () {
videofile: join(__dirname, '..', 'fixtures', 'video_short.mp4')
})
await makePostUploadRequest({
await makeUploadRequest({
url: server.url,
path: path + '/upload',
token: server.accessToken,
@ -287,7 +327,7 @@ describe('Test videos API validator', function () {
videofile: join(__dirname, '..', 'fixtures', 'video_short.ogv')
})
await makePostUploadRequest({
await makeUploadRequest({
url: server.url,
path: path + '/upload',
token: server.accessToken,
@ -400,6 +440,70 @@ describe('Test videos API validator', function () {
await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields })
})
it('Should fail with an incorrect thumbnail file', async function () {
const fields = baseCorrectParams
const attaches = {
'thumbnailfile': join(__dirname, '..', 'fixtures', 'avatar.png')
}
await makeUploadRequest({
url: server.url,
method: 'PUT',
path: path + videoId,
token: server.accessToken,
fields,
attaches
})
})
it('Should fail with a big thumbnail file', async function () {
const fields = baseCorrectParams
const attaches = {
'thumbnailfile': join(__dirname, '..', 'fixtures', 'avatar-big.png')
}
await makeUploadRequest({
url: server.url,
method: 'PUT',
path: path + videoId,
token: server.accessToken,
fields,
attaches
})
})
it('Should fail with an incorrect preview file', async function () {
const fields = baseCorrectParams
const attaches = {
'previewfile': join(__dirname, '..', 'fixtures', 'avatar.png')
}
await makeUploadRequest({
url: server.url,
method: 'PUT',
path: path + videoId,
token: server.accessToken,
fields,
attaches
})
})
it('Should fail with a big preview file', async function () {
const fields = baseCorrectParams
const attaches = {
'previewfile': join(__dirname, '..', 'fixtures', 'avatar-big.png')
}
await makeUploadRequest({
url: server.url,
method: 'PUT',
path: path + videoId,
token: server.accessToken,
fields,
attaches
})
})
it('Should fail with a video of another user')
it('Should fail with a video of another server')

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -137,7 +137,9 @@ describe('Test multiple servers', function () {
nsfw: true,
description: 'my super description for server 2',
tags: [ 'tag1p2', 'tag2p2', 'tag3p2' ],
fixture: 'video_short2.webm'
fixture: 'video_short2.webm',
thumbnailfile: 'thumbnail.jpg',
previewfile: 'preview.jpg'
}
await uploadVideo(servers[1].url, userAccessToken, videoAttributes)
@ -184,7 +186,9 @@ describe('Test multiple servers', function () {
resolution: 720,
size: 710000
}
]
],
thumbnailfile: 'thumbnail',
previewfile: 'preview'
}
const res = await getVideosList(server.url)
@ -521,7 +525,9 @@ describe('Test multiple servers', function () {
language: 13,
nsfw: true,
description: 'my super description updated',
tags: [ 'tag_up_1', 'tag_up_2' ]
tags: [ 'tag_up_1', 'tag_up_2' ],
thumbnailfile: 'thumbnail.jpg',
previewfile: 'preview.jpg'
}
await updateVideo(servers[2].url, servers[2].accessToken, toRemove[0].id, attributes)
@ -565,7 +571,9 @@ describe('Test multiple servers', function () {
resolution: 720,
size: 292677
}
]
],
thumbnailfile: 'thumbnail',
previewfile: 'preview'
}
await completeVideoCheck(server.url, videoUpdated, checkAttributes)
}

View File

@ -1,6 +1,6 @@
/* tslint:disable:no-unused-expression */
import { join } from 'path'
import { isAbsolute, join } from 'path'
import * as request from 'supertest'
import * as WebTorrent from 'webtorrent'
import { readFileBufferPromise } from '../../../helpers/core-utils'
@ -45,8 +45,8 @@ async function testImage (url: string, imageName: string, imagePath: string, ext
const body = res.body
const data = await readFileBufferPromise(join(__dirname, '..', '..', 'api', 'fixtures', imageName + extension))
const minLength = body.length - ((50 * body.length) / 100)
const maxLength = body.length + ((50 * body.length) / 100)
const minLength = body.length - ((20 * body.length) / 100)
const maxLength = body.length + ((20 * body.length) / 100)
return data.length > minLength && data.length < maxLength
} else {
@ -55,6 +55,14 @@ async function testImage (url: string, imageName: string, imagePath: string, ext
}
}
function buildAbsoluteFixturePath (path: string) {
if (isAbsolute(path)) {
return path
}
return join(__dirname, '..', '..', 'api', 'fixtures', path)
}
// ---------------------------------------------------------------------------
export {
@ -63,5 +71,6 @@ export {
webtorrentAdd,
immutableAssign,
testImage,
buildAbsoluteFixturePath,
root
}

View File

@ -1,4 +1,5 @@
import * as request from 'supertest'
import { buildAbsoluteFixturePath } from '../'
function makeGetRequest (options: {
url: string,
@ -40,8 +41,9 @@ function makeDeleteRequest (options: {
.expect(options.statusCodeExpected)
}
function makePostUploadRequest (options: {
function makeUploadRequest (options: {
url: string,
method?: 'POST' | 'PUT',
path: string,
token: string,
fields: { [ fieldName: string ]: any },
@ -50,9 +52,14 @@ function makePostUploadRequest (options: {
}) {
if (!options.statusCodeExpected) options.statusCodeExpected = 400
const req = request(options.url)
.post(options.path)
.set('Accept', 'application/json')
let req: request.Test
if (options.method === 'PUT') {
req = request(options.url).put(options.path)
} else {
req = request(options.url).post(options.path)
}
req.set('Accept', 'application/json')
if (options.token) req.set('Authorization', 'Bearer ' + options.token)
@ -70,7 +77,7 @@ function makePostUploadRequest (options: {
Object.keys(options.attaches).forEach(attach => {
const value = options.attaches[attach]
req.attach(attach, value)
req.attach(attach, buildAbsoluteFixturePath(value))
})
return req.expect(options.statusCodeExpected)
@ -119,7 +126,7 @@ function makePutBodyRequest (options: {
export {
makeGetRequest,
makePostUploadRequest,
makeUploadRequest,
makePostBodyRequest,
makePutBodyRequest,
makeDeleteRequest

View File

@ -1,6 +1,6 @@
import { isAbsolute, join } from 'path'
import * as request from 'supertest'
import { makePostBodyRequest, makePostUploadRequest, makePutBodyRequest } from '../'
import { makePostBodyRequest, makeUploadRequest, makePutBodyRequest } from '../'
import { UserRole } from '../../../../shared/index'
@ -162,7 +162,7 @@ function updateMyAvatar (options: {
filePath = join(__dirname, '..', '..', 'api', 'fixtures', options.fixture)
}
return makePostUploadRequest({
return makeUploadRequest({
url: options.url,
path,
token: options.accessToken,

View File

@ -5,7 +5,16 @@ import { existsSync, readFile } from 'fs'
import * as parseTorrent from 'parse-torrent'
import { extname, isAbsolute, join } from 'path'
import * as request from 'supertest'
import { getMyUserInformation, makeGetRequest, root, ServerInfo, testImage } from '../'
import {
buildAbsoluteFixturePath,
getMyUserInformation,
makeGetRequest,
makePutBodyRequest,
makeUploadRequest,
root,
ServerInfo,
testImage
} from '../'
import { VideoPrivacy } from '../../../../shared/models/videos'
import { readdirPromise } from '../../../helpers/core-utils'
import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers'
@ -23,6 +32,8 @@ type VideoAttributes = {
channelId?: number
privacy?: VideoPrivacy
fixture?: string
thumbnailfile?: string
previewfile?: string
}
function getVideoCategories (url: string) {
@ -228,8 +239,8 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg
defaultChannelId = res.body.videoChannels[0].id
} catch (e) { /* empty */ }
// Default attributes
let attributes = {
// Override default attributes
const attributes = Object.assign({
name: 'my super video',
category: 5,
licence: 4,
@ -241,8 +252,7 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg
privacy: VideoPrivacy.PUBLIC,
commentsEnabled: true,
fixture: 'video_short.webm'
}
attributes = Object.assign(attributes, videoAttributesArg)
}, videoAttributesArg)
const req = request(url)
.post(path)
@ -267,22 +277,22 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg
req.field('licence', attributes.licence.toString())
}
if (attributes.thumbnailfile !== undefined) {
req.attach('thumbnailfile', buildAbsoluteFixturePath(attributes.thumbnailfile))
}
if (attributes.previewfile !== undefined) {
req.attach('previewfile', buildAbsoluteFixturePath(attributes.previewfile))
}
for (let i = 0; i < attributes.tags.length; i++) {
req.field('tags[' + i + ']', attributes.tags[i])
}
let filePath = ''
if (isAbsolute(attributes.fixture)) {
filePath = attributes.fixture
} else {
filePath = join(__dirname, '..', '..', 'api', 'fixtures', attributes.fixture)
}
return req.attach('videofile', filePath)
return req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
.expect(specialStatus)
}
function updateVideo (url: string, accessToken: string, id: number | string, attributes: VideoAttributes, specialStatus = 204) {
function updateVideo (url: string, accessToken: string, id: number | string, attributes: VideoAttributes, statusCodeExpected = 204) {
const path = '/api/v1/videos/' + id
const body = {}
@ -296,12 +306,30 @@ function updateVideo (url: string, accessToken: string, id: number | string, att
if (attributes.tags) body['tags'] = attributes.tags
if (attributes.privacy) body['privacy'] = attributes.privacy
return request(url)
.put(path)
.send(body)
.set('Accept', 'application/json')
.set('Authorization', 'Bearer ' + accessToken)
.expect(specialStatus)
// Upload request
if (attributes.thumbnailfile || attributes.previewfile) {
const attaches: any = {}
if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
if (attributes.previewfile) attaches.previewfile = attributes.previewfile
return makeUploadRequest({
url,
method: 'PUT',
path,
token: accessToken,
fields: body,
attaches,
statusCodeExpected
})
}
return makePutBodyRequest({
url,
path,
fields: body,
token: accessToken,
statusCodeExpected
})
}
function rateVideo (url: string, accessToken: string, id: number, rating: string, specialStatus = 204) {
@ -355,7 +383,9 @@ async function completeVideoCheck (
files: {
resolution: number
size: number
}[]
}[],
thumbnailfile?: string
previewfile?: string
}
) {
if (!attributes.likes) attributes.likes = 0
@ -414,8 +444,15 @@ async function completeVideoCheck (
const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
expect(file.size).to.be.above(minSize).and.below(maxSize)
const test = await testImage(url, attributes.fixture, videoDetails.thumbnailPath)
expect(test).to.equal(true)
{
const test = await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
expect(test).to.equal(true)
}
if (attributes.previewfile) {
const test = await testImage(url, attributes.previewfile, videoDetails.previewPath)
expect(test).to.equal(true)
}
const torrent = await webtorrentAdd(magnetUri, true)
expect(torrent.files).to.be.an('array')

View File

@ -1,7 +1,5 @@
import * as program from 'commander'
import { createWriteStream } from 'fs'
import { join } from 'path'
import { cursorTo } from 'readline'
import * as youtubeDL from 'youtube-dl'
import { VideoPrivacy } from '../../shared/models/videos'
import { unlinkPromise } from '../helpers/core-utils'