]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/models/video.js
Server: add database field validations
[github/Chocobozzz/PeerTube.git] / server / models / video.js
index 8ef07c9e68bda59383b6b6f8f77c6656d2e2c243..3ebc48ad4d66ec5bc4faa8a370c5178e1abedf8c 100644 (file)
@@ -4,46 +4,81 @@ const createTorrent = require('create-torrent')
 const ffmpeg = require('fluent-ffmpeg')
 const fs = require('fs')
 const magnetUtil = require('magnet-uri')
+const map = require('lodash/map')
 const parallel = require('async/parallel')
 const parseTorrent = require('parse-torrent')
 const pathUtils = require('path')
+const values = require('lodash/values')
 
 const constants = require('../initializers/constants')
 const logger = require('../helpers/logger')
 const modelUtils = require('./utils')
+const customVideosValidators = require('../helpers/custom-validators').videos
 
 // ---------------------------------------------------------------------------
 
 module.exports = function (sequelize, DataTypes) {
-// TODO: add indexes on searchable columns
+  // TODO: add indexes on searchable columns
   const Video = sequelize.define('Video',
     {
       id: {
         type: DataTypes.UUID,
         defaultValue: DataTypes.UUIDV4,
-        primaryKey: true
+        primaryKey: true,
+        validate: {
+          isUUID: 4
+        }
       },
       name: {
-        type: DataTypes.STRING
+        type: DataTypes.STRING,
+        allowNull: false,
+        validate: {
+          nameValid: function (value) {
+            const res = customVideosValidators.isVideoNameValid(value)
+            if (res === false) throw new Error('Video name is not valid.')
+          }
+        }
       },
       extname: {
-        // TODO: enum?
-        type: DataTypes.STRING
+        type: DataTypes.ENUM(values(constants.CONSTRAINTS_FIELDS.VIDEOS.EXTNAME)),
+        allowNull: false
       },
       remoteId: {
-        type: DataTypes.UUID
+        type: DataTypes.UUID,
+        allowNull: true,
+        validate: {
+          isUUID: 4
+        }
       },
       description: {
-        type: DataTypes.STRING
+        type: DataTypes.STRING,
+        allowNull: false,
+        validate: {
+          descriptionValid: function (value) {
+            const res = customVideosValidators.isVideoDescriptionValid(value)
+            if (res === false) throw new Error('Video description is not valid.')
+          }
+        }
       },
       infoHash: {
-        type: DataTypes.STRING
+        type: DataTypes.STRING,
+        allowNull: false,
+        validate: {
+          infoHashValid: function (value) {
+            const res = customVideosValidators.isVideoInfoHashValid(value)
+            if (res === false) throw new Error('Video info hash is not valid.')
+          }
+        }
       },
       duration: {
-        type: DataTypes.INTEGER
-      },
-      tags: {
-        type: DataTypes.ARRAY(DataTypes.STRING)
+        type: DataTypes.INTEGER,
+        allowNull: false,
+        validate: {
+          durationValid: function (value) {
+            const res = customVideosValidators.isVideoDurationValid(value)
+            if (res === false) throw new Error('Video duration is not valid.')
+          }
+        }
       }
     },
     {
@@ -52,14 +87,15 @@ module.exports = function (sequelize, DataTypes) {
 
         generateThumbnailFromBase64,
         getDurationFromFile,
+        list,
         listForApi,
         listByHostAndRemoteId,
-        listOwnedAndPopulateAuthor,
+        listOwnedAndPopulateAuthorAndTags,
         listOwnedByAuthor,
         load,
         loadAndPopulateAuthor,
-        loadAndPopulateAuthorAndPod,
-        searchAndPopulateAuthorAndPod
+        loadAndPopulateAuthorAndPodAndTags,
+        searchAndPopulateAuthorAndPodAndTags
       },
       instanceMethods: {
         generateMagnetUri,
@@ -72,6 +108,7 @@ module.exports = function (sequelize, DataTypes) {
         toRemoteJSON
       },
       hooks: {
+        beforeValidate,
         beforeCreate,
         afterDestroy
       }
@@ -81,13 +118,14 @@ module.exports = function (sequelize, DataTypes) {
   return Video
 }
 
-// TODO: Validation
-// VideoSchema.path('name').validate(customVideosValidators.isVideoNameValid)
-// VideoSchema.path('description').validate(customVideosValidators.isVideoDescriptionValid)
-// VideoSchema.path('podHost').validate(customVideosValidators.isVideoPodHostValid)
-// VideoSchema.path('author').validate(customVideosValidators.isVideoAuthorValid)
-// VideoSchema.path('duration').validate(customVideosValidators.isVideoDurationValid)
-// VideoSchema.path('tags').validate(customVideosValidators.isVideoTagsValid)
+function beforeValidate (video, options, next) {
+  if (video.isOwned()) {
+    // 40 hexa length
+    video.infoHash = '0123456789abcdef0123456789abcdef01234567'
+  }
+
+  return next(null)
+}
 
 function beforeCreate (video, options, next) {
   const tasks = []
@@ -114,9 +152,8 @@ function beforeCreate (video, options, next) {
             if (err) return callback(err)
 
             const parsedTorrent = parseTorrent(torrent)
-            video.infoHash = parsedTorrent.infoHash
-
-            callback(null)
+            video.set('infoHash', parsedTorrent.infoHash)
+            video.validate().asCallback(callback)
           })
         })
       },
@@ -170,6 +207,12 @@ function associate (models) {
     },
     onDelete: 'cascade'
   })
+
+  this.belongsToMany(models.Tag, {
+    foreignKey: 'videoId',
+    through: models.VideoTag,
+    onDelete: 'cascade'
+  })
 }
 
 function generateMagnetUri () {
@@ -248,7 +291,7 @@ function toFormatedJSON () {
     magnetUri: this.generateMagnetUri(),
     author: this.Author.name,
     duration: this.duration,
-    tags: this.tags,
+    tags: map(this.Tags, 'name'),
     thumbnailPath: constants.STATIC_PATHS.THUMBNAILS + '/' + this.getThumbnailName(),
     createdAt: this.createdAt
   }
@@ -275,7 +318,7 @@ function toRemoteJSON (callback) {
       author: self.Author.name,
       duration: self.duration,
       thumbnailBase64: new Buffer(thumbnailData).toString('base64'),
-      tags: self.tags,
+      tags: map(self.Tags, 'name'),
       createdAt: self.createdAt,
       extname: self.extname
     }
@@ -306,16 +349,23 @@ function getDurationFromFile (videoPath, callback) {
   })
 }
 
+function list (callback) {
+  return this.find().asCallback()
+}
+
 function listForApi (start, count, sort, callback) {
   const query = {
     offset: start,
     limit: count,
-    order: [ modelUtils.getSort(sort) ],
+    distinct: true, // For the count, a video can have many tags
+    order: [ modelUtils.getSort(sort), [ this.sequelize.models.Tag, 'name', 'ASC' ] ],
     include: [
       {
         model: this.sequelize.models.Author,
-        include: [ this.sequelize.models.Pod ]
-      }
+        include: [ { model: this.sequelize.models.Pod, required: false } ]
+      },
+
+      this.sequelize.models.Tag
     ]
   }
 
@@ -337,6 +387,7 @@ function listByHostAndRemoteId (fromHost, remoteId, callback) {
         include: [
           {
             model: this.sequelize.models.Pod,
+            required: true,
             where: {
               host: fromHost
             }
@@ -349,13 +400,13 @@ function listByHostAndRemoteId (fromHost, remoteId, callback) {
   return this.findAll(query).asCallback(callback)
 }
 
-function listOwnedAndPopulateAuthor (callback) {
+function listOwnedAndPopulateAuthorAndTags (callback) {
   // If remoteId is null this is *our* video
   const query = {
     where: {
       remoteId: null
     },
-    include: [ this.sequelize.models.Author ]
+    include: [ this.sequelize.models.Author, this.sequelize.models.Tag ]
   }
 
   return this.findAll(query).asCallback(callback)
@@ -391,23 +442,26 @@ function loadAndPopulateAuthor (id, callback) {
   return this.findById(id, options).asCallback(callback)
 }
 
-function loadAndPopulateAuthorAndPod (id, callback) {
+function loadAndPopulateAuthorAndPodAndTags (id, callback) {
   const options = {
     include: [
       {
         model: this.sequelize.models.Author,
-        include: [ this.sequelize.models.Pod ]
-      }
+        include: [ { model: this.sequelize.models.Pod, required: false } ]
+      },
+      this.sequelize.models.Tag
     ]
   }
 
   return this.findById(id, options).asCallback(callback)
 }
 
-function searchAndPopulateAuthorAndPod (value, field, start, count, sort, callback) {
+function searchAndPopulateAuthorAndPodAndTags (value, field, start, count, sort, callback) {
   const podInclude = {
-    model: this.sequelize.models.Pod
+    model: this.sequelize.models.Pod,
+    required: false
   }
+
   const authorInclude = {
     model: this.sequelize.models.Author,
     include: [
@@ -415,55 +469,61 @@ function searchAndPopulateAuthorAndPod (value, field, start, count, sort, callba
     ]
   }
 
+  const tagInclude = {
+    model: this.sequelize.models.Tag
+  }
+
   const query = {
     where: {},
-    include: [
-      authorInclude
-    ],
     offset: start,
     limit: count,
-    order: [ modelUtils.getSort(sort) ]
+    distinct: true, // For the count, a video can have many tags
+    order: [ modelUtils.getSort(sort), [ this.sequelize.models.Tag, 'name', 'ASC' ] ]
   }
 
-  // TODO: include our pod for podHost searches (we are not stored in the database)
   // Make an exact search with the magnet
   if (field === 'magnetUri') {
     const infoHash = magnetUtil.decode(value).infoHash
     query.where.infoHash = infoHash
   } else if (field === 'tags') {
-    query.where[field] = value
-  } else if (field === 'host') {
-    const whereQuery = {
-      '$Author.Pod.host$': {
-        $like: '%' + value + '%'
+    const escapedValue = this.sequelize.escape('%' + value + '%')
+    query.where = {
+      id: {
+        $in: this.sequelize.literal(
+          '(SELECT "VideoTags"."videoId" FROM "Tags" INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" WHERE name LIKE ' + escapedValue + ')'
+        )
       }
     }
-
-    // Include our pod? (not stored in the database)
-    if (constants.CONFIG.WEBSERVER.HOST.indexOf(value) !== -1) {
-      query.where = {
-        $or: [
-          whereQuery,
-          {
-            remoteId: null
-          }
-        ]
+  } else if (field === 'host') {
+    // FIXME: Include our pod? (not stored in the database)
+    podInclude.where = {
+      host: {
+        $like: '%' + value + '%'
       }
-    } else {
-      query.where = whereQuery
     }
+    podInclude.required = true
   } else if (field === 'author') {
-    query.where = {
-      '$Author.name$': {
+    authorInclude.where = {
+      name: {
         $like: '%' + value + '%'
       }
     }
+
+    // authorInclude.or = true
   } else {
     query.where[field] = {
       $like: '%' + value + '%'
     }
   }
 
+  query.include = [
+    authorInclude, tagInclude
+  ]
+
+  if (tagInclude.where) {
+    // query.include.push([ this.sequelize.models.Tag ])
+  }
+
   return this.findAndCountAll(query).asCallback(function (err, result) {
     if (err) return callback(err)