aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
authorChocobozzz <florian.bigard@gmail.com>2017-01-12 15:20:03 +0100
committerChocobozzz <florian.bigard@gmail.com>2017-01-12 15:20:03 +0100
commit99fe265a5fc077cb66c322e7f3d191ff7110aea0 (patch)
treec9e04ccfcc5496d2300d7c26db5833e494b4cdad /server/models
parentfcc5f77b95d330bfcb439c172b7fcc58f3162e4d (diff)
parent91cc839af88730ba55f84997c56b85ea100070a7 (diff)
downloadPeerTube-99fe265a5fc077cb66c322e7f3d191ff7110aea0.tar.gz
PeerTube-99fe265a5fc077cb66c322e7f3d191ff7110aea0.tar.zst
PeerTube-99fe265a5fc077cb66c322e7f3d191ff7110aea0.zip
Merge branch 'postgresql'
Diffstat (limited to 'server/models')
-rw-r--r--server/models/application.js61
-rw-r--r--server/models/author.js85
-rw-r--r--server/models/oauth-client.js75
-rw-r--r--server/models/oauth-token.js134
-rw-r--r--server/models/pod.js200
-rw-r--r--server/models/pods.js119
-rw-r--r--server/models/request-to-pod.js42
-rw-r--r--server/models/request.js275
-rw-r--r--server/models/tag.js76
-rw-r--r--server/models/user.js158
-rw-r--r--server/models/utils.js31
-rw-r--r--server/models/video-abuse.js113
-rw-r--r--server/models/video-tag.js18
-rw-r--r--server/models/video.js532
14 files changed, 1421 insertions, 498 deletions
diff --git a/server/models/application.js b/server/models/application.js
index 452ac4283..46dcfde33 100644
--- a/server/models/application.js
+++ b/server/models/application.js
@@ -1,31 +1,52 @@
1const mongoose = require('mongoose') 1'use strict'
2
3module.exports = function (sequelize, DataTypes) {
4 const Application = sequelize.define('Application',
5 {
6 migrationVersion: {
7 type: DataTypes.INTEGER,
8 defaultValue: 0,
9 allowNull: false,
10 validate: {
11 isInt: true
12 }
13 }
14 },
15 {
16 classMethods: {
17 loadMigrationVersion,
18 updateMigrationVersion
19 }
20 }
21 )
22
23 return Application
24}
2 25
3// --------------------------------------------------------------------------- 26// ---------------------------------------------------------------------------
4 27
5const ApplicationSchema = mongoose.Schema({ 28function loadMigrationVersion (callback) {
6 mongoSchemaVersion: { 29 const query = {
7 type: Number, 30 attributes: [ 'migrationVersion' ]
8 default: 0
9 } 31 }
10})
11
12ApplicationSchema.statics = {
13 loadMongoSchemaVersion,
14 updateMongoSchemaVersion
15}
16
17mongoose.model('Application', ApplicationSchema)
18
19// ---------------------------------------------------------------------------
20 32
21function loadMongoSchemaVersion (callback) { 33 return this.findOne(query).asCallback(function (err, data) {
22 return this.findOne({}, { mongoSchemaVersion: 1 }, function (err, data) { 34 const version = data ? data.migrationVersion : 0
23 const version = data ? data.mongoSchemaVersion : 0
24 35
25 return callback(err, version) 36 return callback(err, version)
26 }) 37 })
27} 38}
28 39
29function updateMongoSchemaVersion (newVersion, callback) { 40function updateMigrationVersion (newVersion, transaction, callback) {
30 return this.update({}, { mongoSchemaVersion: newVersion }, callback) 41 const options = {
42 where: {}
43 }
44
45 if (!callback) {
46 transaction = callback
47 } else {
48 options.transaction = transaction
49 }
50
51 return this.update({ migrationVersion: newVersion }, options).asCallback(callback)
31} 52}
diff --git a/server/models/author.js b/server/models/author.js
new file mode 100644
index 000000000..7d15fb6ec
--- /dev/null
+++ b/server/models/author.js
@@ -0,0 +1,85 @@
1'use strict'
2
3const customUsersValidators = require('../helpers/custom-validators').users
4
5module.exports = function (sequelize, DataTypes) {
6 const Author = sequelize.define('Author',
7 {
8 name: {
9 type: DataTypes.STRING,
10 allowNull: false,
11 validate: {
12 usernameValid: function (value) {
13 const res = customUsersValidators.isUserUsernameValid(value)
14 if (res === false) throw new Error('Username is not valid.')
15 }
16 }
17 }
18 },
19 {
20 indexes: [
21 {
22 fields: [ 'name' ]
23 },
24 {
25 fields: [ 'podId' ]
26 },
27 {
28 fields: [ 'userId' ]
29 }
30 ],
31 classMethods: {
32 associate,
33
34 findOrCreateAuthor
35 }
36 }
37 )
38
39 return Author
40}
41
42// ---------------------------------------------------------------------------
43
44function associate (models) {
45 this.belongsTo(models.Pod, {
46 foreignKey: {
47 name: 'podId',
48 allowNull: true
49 },
50 onDelete: 'cascade'
51 })
52
53 this.belongsTo(models.User, {
54 foreignKey: {
55 name: 'userId',
56 allowNull: true
57 },
58 onDelete: 'cascade'
59 })
60}
61
62function findOrCreateAuthor (name, podId, userId, transaction, callback) {
63 if (!callback) {
64 callback = transaction
65 transaction = null
66 }
67
68 const author = {
69 name,
70 podId,
71 userId
72 }
73
74 const query = {
75 where: author,
76 defaults: author
77 }
78
79 if (transaction) query.transaction = transaction
80
81 this.findOrCreate(query).asCallback(function (err, result) {
82 // [ instance, wasCreated ]
83 return callback(err, result[0])
84 })
85}
diff --git a/server/models/oauth-client.js b/server/models/oauth-client.js
index a1aefa985..021a34007 100644
--- a/server/models/oauth-client.js
+++ b/server/models/oauth-client.js
@@ -1,33 +1,62 @@
1const mongoose = require('mongoose') 1'use strict'
2 2
3// --------------------------------------------------------------------------- 3module.exports = function (sequelize, DataTypes) {
4 4 const OAuthClient = sequelize.define('OAuthClient',
5const OAuthClientSchema = mongoose.Schema({ 5 {
6 clientSecret: String, 6 clientId: {
7 grants: Array, 7 type: DataTypes.STRING,
8 redirectUris: Array 8 allowNull: false
9}) 9 },
10 10 clientSecret: {
11OAuthClientSchema.path('clientSecret').required(true) 11 type: DataTypes.STRING,
12 12 allowNull: false
13OAuthClientSchema.statics = { 13 },
14 getByIdAndSecret, 14 grants: {
15 list, 15 type: DataTypes.ARRAY(DataTypes.STRING)
16 loadFirstClient 16 },
17 redirectUris: {
18 type: DataTypes.ARRAY(DataTypes.STRING)
19 }
20 },
21 {
22 indexes: [
23 {
24 fields: [ 'clientId' ],
25 unique: true
26 },
27 {
28 fields: [ 'clientId', 'clientSecret' ],
29 unique: true
30 }
31 ],
32 classMethods: {
33 countTotal,
34 getByIdAndSecret,
35 loadFirstClient
36 }
37 }
38 )
39
40 return OAuthClient
17} 41}
18 42
19mongoose.model('OAuthClient', OAuthClientSchema)
20
21// --------------------------------------------------------------------------- 43// ---------------------------------------------------------------------------
22 44
23function list (callback) { 45function countTotal (callback) {
24 return this.find(callback) 46 return this.count().asCallback(callback)
25} 47}
26 48
27function loadFirstClient (callback) { 49function loadFirstClient (callback) {
28 return this.findOne({}, callback) 50 return this.findOne().asCallback(callback)
29} 51}
30 52
31function getByIdAndSecret (id, clientSecret) { 53function getByIdAndSecret (clientId, clientSecret) {
32 return this.findOne({ _id: id, clientSecret: clientSecret }).exec() 54 const query = {
55 where: {
56 clientId: clientId,
57 clientSecret: clientSecret
58 }
59 }
60
61 return this.findOne(query)
33} 62}
diff --git a/server/models/oauth-token.js b/server/models/oauth-token.js
index aff73bfb1..68e7c9ff7 100644
--- a/server/models/oauth-token.js
+++ b/server/models/oauth-token.js
@@ -1,42 +1,96 @@
1const mongoose = require('mongoose') 1'use strict'
2 2
3const logger = require('../helpers/logger') 3const logger = require('../helpers/logger')
4 4
5// --------------------------------------------------------------------------- 5// ---------------------------------------------------------------------------
6 6
7const OAuthTokenSchema = mongoose.Schema({ 7module.exports = function (sequelize, DataTypes) {
8 accessToken: String, 8 const OAuthToken = sequelize.define('OAuthToken',
9 accessTokenExpiresAt: Date, 9 {
10 client: { type: mongoose.Schema.Types.ObjectId, ref: 'OAuthClient' }, 10 accessToken: {
11 refreshToken: String, 11 type: DataTypes.STRING,
12 refreshTokenExpiresAt: Date, 12 allowNull: false
13 user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' } 13 },
14}) 14 accessTokenExpiresAt: {
15 15 type: DataTypes.DATE,
16OAuthTokenSchema.path('accessToken').required(true) 16 allowNull: false
17OAuthTokenSchema.path('client').required(true) 17 },
18OAuthTokenSchema.path('user').required(true) 18 refreshToken: {
19 19 type: DataTypes.STRING,
20OAuthTokenSchema.statics = { 20 allowNull: false
21 getByRefreshTokenAndPopulateClient, 21 },
22 getByTokenAndPopulateUser, 22 refreshTokenExpiresAt: {
23 getByRefreshTokenAndPopulateUser, 23 type: DataTypes.DATE,
24 removeByUserId 24 allowNull: false
25} 25 }
26 },
27 {
28 indexes: [
29 {
30 fields: [ 'refreshToken' ],
31 unique: true
32 },
33 {
34 fields: [ 'accessToken' ],
35 unique: true
36 },
37 {
38 fields: [ 'userId' ]
39 },
40 {
41 fields: [ 'oAuthClientId' ]
42 }
43 ],
44 classMethods: {
45 associate,
46
47 getByRefreshTokenAndPopulateClient,
48 getByTokenAndPopulateUser,
49 getByRefreshTokenAndPopulateUser,
50 removeByUserId
51 }
52 }
53 )
26 54
27mongoose.model('OAuthToken', OAuthTokenSchema) 55 return OAuthToken
56}
28 57
29// --------------------------------------------------------------------------- 58// ---------------------------------------------------------------------------
30 59
60function associate (models) {
61 this.belongsTo(models.User, {
62 foreignKey: {
63 name: 'userId',
64 allowNull: false
65 },
66 onDelete: 'cascade'
67 })
68
69 this.belongsTo(models.OAuthClient, {
70 foreignKey: {
71 name: 'oAuthClientId',
72 allowNull: false
73 },
74 onDelete: 'cascade'
75 })
76}
77
31function getByRefreshTokenAndPopulateClient (refreshToken) { 78function getByRefreshTokenAndPopulateClient (refreshToken) {
32 return this.findOne({ refreshToken: refreshToken }).populate('client').exec().then(function (token) { 79 const query = {
80 where: {
81 refreshToken: refreshToken
82 },
83 include: [ this.associations.OAuthClient ]
84 }
85
86 return this.findOne(query).then(function (token) {
33 if (!token) return token 87 if (!token) return token
34 88
35 const tokenInfos = { 89 const tokenInfos = {
36 refreshToken: token.refreshToken, 90 refreshToken: token.refreshToken,
37 refreshTokenExpiresAt: token.refreshTokenExpiresAt, 91 refreshTokenExpiresAt: token.refreshTokenExpiresAt,
38 client: { 92 client: {
39 id: token.client._id.toString() 93 id: token.client.id
40 }, 94 },
41 user: { 95 user: {
42 id: token.user 96 id: token.user
@@ -50,13 +104,41 @@ function getByRefreshTokenAndPopulateClient (refreshToken) {
50} 104}
51 105
52function getByTokenAndPopulateUser (bearerToken) { 106function getByTokenAndPopulateUser (bearerToken) {
53 return this.findOne({ accessToken: bearerToken }).populate('user').exec() 107 const query = {
108 where: {
109 accessToken: bearerToken
110 },
111 include: [ this.sequelize.models.User ]
112 }
113
114 return this.findOne(query).then(function (token) {
115 if (token) token.user = token.User
116
117 return token
118 })
54} 119}
55 120
56function getByRefreshTokenAndPopulateUser (refreshToken) { 121function getByRefreshTokenAndPopulateUser (refreshToken) {
57 return this.findOne({ refreshToken: refreshToken }).populate('user').exec() 122 const query = {
123 where: {
124 refreshToken: refreshToken
125 },
126 include: [ this.sequelize.models.User ]
127 }
128
129 return this.findOne(query).then(function (token) {
130 token.user = token.User
131
132 return token
133 })
58} 134}
59 135
60function removeByUserId (userId, callback) { 136function removeByUserId (userId, callback) {
61 return this.remove({ user: userId }, callback) 137 const query = {
138 where: {
139 userId: userId
140 }
141 }
142
143 return this.destroy(query).asCallback(callback)
62} 144}
diff --git a/server/models/pod.js b/server/models/pod.js
new file mode 100644
index 000000000..b3c6db8e8
--- /dev/null
+++ b/server/models/pod.js
@@ -0,0 +1,200 @@
1'use strict'
2
3const map = require('lodash/map')
4
5const constants = require('../initializers/constants')
6const customPodsValidators = require('../helpers/custom-validators').pods
7
8// ---------------------------------------------------------------------------
9
10module.exports = function (sequelize, DataTypes) {
11 const Pod = sequelize.define('Pod',
12 {
13 host: {
14 type: DataTypes.STRING,
15 allowNull: false,
16 validate: {
17 isHost: function (value) {
18 const res = customPodsValidators.isHostValid(value)
19 if (res === false) throw new Error('Host not valid.')
20 }
21 }
22 },
23 publicKey: {
24 type: DataTypes.STRING(5000),
25 allowNull: false
26 },
27 score: {
28 type: DataTypes.INTEGER,
29 defaultValue: constants.FRIEND_SCORE.BASE,
30 allowNull: false,
31 validate: {
32 isInt: true,
33 max: constants.FRIEND_SCORE.MAX
34 }
35 }
36 },
37 {
38 indexes: [
39 {
40 fields: [ 'host' ]
41 },
42 {
43 fields: [ 'score' ]
44 }
45 ],
46 classMethods: {
47 associate,
48
49 countAll,
50 incrementScores,
51 list,
52 listAllIds,
53 listRandomPodIdsWithRequest,
54 listBadPods,
55 load,
56 loadByHost,
57 removeAll
58 },
59 instanceMethods: {
60 toFormatedJSON
61 }
62 }
63 )
64
65 return Pod
66}
67
68// ------------------------------ METHODS ------------------------------
69
70function toFormatedJSON () {
71 const json = {
72 id: this.id,
73 host: this.host,
74 score: this.score,
75 createdAt: this.createdAt
76 }
77
78 return json
79}
80
81// ------------------------------ Statics ------------------------------
82
83function associate (models) {
84 this.belongsToMany(models.Request, {
85 foreignKey: 'podId',
86 through: models.RequestToPod,
87 onDelete: 'cascade'
88 })
89}
90
91function countAll (callback) {
92 return this.count().asCallback(callback)
93}
94
95function incrementScores (ids, value, callback) {
96 if (!callback) callback = function () {}
97
98 const update = {
99 score: this.sequelize.literal('score +' + value)
100 }
101
102 const options = {
103 where: {
104 id: {
105 $in: ids
106 }
107 },
108 // In this case score is a literal and not an integer so we do not validate it
109 validate: false
110 }
111
112 return this.update(update, options).asCallback(callback)
113}
114
115function list (callback) {
116 return this.findAll().asCallback(callback)
117}
118
119function listAllIds (transaction, callback) {
120 if (!callback) {
121 callback = transaction
122 transaction = null
123 }
124
125 const query = {
126 attributes: [ 'id' ]
127 }
128
129 if (transaction) query.transaction = transaction
130
131 return this.findAll(query).asCallback(function (err, pods) {
132 if (err) return callback(err)
133
134 return callback(null, map(pods, 'id'))
135 })
136}
137
138function listRandomPodIdsWithRequest (limit, callback) {
139 const self = this
140
141 self.count().asCallback(function (err, count) {
142 if (err) return callback(err)
143
144 // Optimization...
145 if (count === 0) return callback(null, [])
146
147 let start = Math.floor(Math.random() * count) - limit
148 if (start < 0) start = 0
149
150 const query = {
151 attributes: [ 'id' ],
152 order: [
153 [ 'id', 'ASC' ]
154 ],
155 offset: start,
156 limit: limit,
157 where: {
158 id: {
159 $in: [
160 this.sequelize.literal('SELECT "podId" FROM "RequestToPods"')
161 ]
162 }
163 }
164 }
165
166 return this.findAll(query).asCallback(function (err, pods) {
167 if (err) return callback(err)
168
169 return callback(null, map(pods, 'id'))
170 })
171 })
172}
173
174function listBadPods (callback) {
175 const query = {
176 where: {
177 score: { $lte: 0 }
178 }
179 }
180
181 return this.findAll(query).asCallback(callback)
182}
183
184function load (id, callback) {
185 return this.findById(id).asCallback(callback)
186}
187
188function loadByHost (host, callback) {
189 const query = {
190 where: {
191 host: host
192 }
193 }
194
195 return this.findOne(query).asCallback(callback)
196}
197
198function removeAll (callback) {
199 return this.destroy().asCallback(callback)
200}
diff --git a/server/models/pods.js b/server/models/pods.js
deleted file mode 100644
index 49c73472a..000000000
--- a/server/models/pods.js
+++ /dev/null
@@ -1,119 +0,0 @@
1'use strict'
2
3const each = require('async/each')
4const mongoose = require('mongoose')
5const map = require('lodash/map')
6const validator = require('express-validator').validator
7
8const constants = require('../initializers/constants')
9
10const Video = mongoose.model('Video')
11
12// ---------------------------------------------------------------------------
13
14const PodSchema = mongoose.Schema({
15 host: String,
16 publicKey: String,
17 score: { type: Number, max: constants.FRIEND_SCORE.MAX },
18 createdDate: {
19 type: Date,
20 default: Date.now
21 }
22})
23
24PodSchema.path('host').validate(validator.isURL)
25PodSchema.path('publicKey').required(true)
26PodSchema.path('score').validate(function (value) { return !isNaN(value) })
27
28PodSchema.methods = {
29 toFormatedJSON
30}
31
32PodSchema.statics = {
33 countAll,
34 incrementScores,
35 list,
36 listAllIds,
37 listBadPods,
38 load,
39 loadByHost,
40 removeAll
41}
42
43PodSchema.pre('save', function (next) {
44 const self = this
45
46 Pod.loadByHost(this.host, function (err, pod) {
47 if (err) return next(err)
48
49 if (pod) return next(new Error('Pod already exists.'))
50
51 self.score = constants.FRIEND_SCORE.BASE
52 return next()
53 })
54})
55
56PodSchema.pre('remove', function (next) {
57 // Remove the videos owned by this pod too
58 Video.listByHost(this.host, function (err, videos) {
59 if (err) return next(err)
60
61 each(videos, function (video, callbackEach) {
62 video.remove(callbackEach)
63 }, next)
64 })
65})
66
67const Pod = mongoose.model('Pod', PodSchema)
68
69// ------------------------------ METHODS ------------------------------
70
71function toFormatedJSON () {
72 const json = {
73 id: this._id,
74 host: this.host,
75 score: this.score,
76 createdDate: this.createdDate
77 }
78
79 return json
80}
81
82// ------------------------------ Statics ------------------------------
83
84function countAll (callback) {
85 return this.count(callback)
86}
87
88function incrementScores (ids, value, callback) {
89 if (!callback) callback = function () {}
90 return this.update({ _id: { $in: ids } }, { $inc: { score: value } }, { multi: true }, callback)
91}
92
93function list (callback) {
94 return this.find(callback)
95}
96
97function listAllIds (callback) {
98 return this.find({}, { _id: 1 }, function (err, pods) {
99 if (err) return callback(err)
100
101 return callback(null, map(pods, '_id'))
102 })
103}
104
105function listBadPods (callback) {
106 return this.find({ score: 0 }, callback)
107}
108
109function load (id, callback) {
110 return this.findById(id, callback)
111}
112
113function loadByHost (host, callback) {
114 return this.findOne({ host }, callback)
115}
116
117function removeAll (callback) {
118 return this.remove({}, callback)
119}
diff --git a/server/models/request-to-pod.js b/server/models/request-to-pod.js
new file mode 100644
index 000000000..f42a53458
--- /dev/null
+++ b/server/models/request-to-pod.js
@@ -0,0 +1,42 @@
1'use strict'
2
3// ---------------------------------------------------------------------------
4
5module.exports = function (sequelize, DataTypes) {
6 const RequestToPod = sequelize.define('RequestToPod', {}, {
7 indexes: [
8 {
9 fields: [ 'requestId' ]
10 },
11 {
12 fields: [ 'podId' ]
13 },
14 {
15 fields: [ 'requestId', 'podId' ],
16 unique: true
17 }
18 ],
19 classMethods: {
20 removePodOf
21 }
22 })
23
24 return RequestToPod
25}
26
27// ---------------------------------------------------------------------------
28
29function removePodOf (requestsIds, podId, callback) {
30 if (!callback) callback = function () {}
31
32 const query = {
33 where: {
34 requestId: {
35 $in: requestsIds
36 },
37 podId: podId
38 }
39 }
40
41 this.destroy(query).asCallback(callback)
42}
diff --git a/server/models/request.js b/server/models/request.js
index c2cfe83ce..cd52ea767 100644
--- a/server/models/request.js
+++ b/server/models/request.js
@@ -2,66 +2,60 @@
2 2
3const each = require('async/each') 3const each = require('async/each')
4const eachLimit = require('async/eachLimit') 4const eachLimit = require('async/eachLimit')
5const values = require('lodash/values')
6const mongoose = require('mongoose')
7const waterfall = require('async/waterfall') 5const waterfall = require('async/waterfall')
6const values = require('lodash/values')
8 7
9const constants = require('../initializers/constants') 8const constants = require('../initializers/constants')
10const logger = require('../helpers/logger') 9const logger = require('../helpers/logger')
11const requests = require('../helpers/requests') 10const requests = require('../helpers/requests')
12 11
13const Pod = mongoose.model('Pod')
14
15let timer = null 12let timer = null
16let lastRequestTimestamp = 0 13let lastRequestTimestamp = 0
17 14
18// --------------------------------------------------------------------------- 15// ---------------------------------------------------------------------------
19 16
20const RequestSchema = mongoose.Schema({ 17module.exports = function (sequelize, DataTypes) {
21 request: mongoose.Schema.Types.Mixed, 18 const Request = sequelize.define('Request',
22 endpoint: { 19 {
23 type: String, 20 request: {
24 enum: [ values(constants.REQUEST_ENDPOINTS) ] 21 type: DataTypes.JSON,
25 }, 22 allowNull: false
26 to: [ 23 },
24 endpoint: {
25 type: DataTypes.ENUM(values(constants.REQUEST_ENDPOINTS)),
26 allowNull: false
27 }
28 },
27 { 29 {
28 type: mongoose.Schema.Types.ObjectId, 30 classMethods: {
29 ref: 'Pod' 31 associate,
32
33 activate,
34 countTotalRequests,
35 deactivate,
36 flush,
37 forceSend,
38 remainingMilliSeconds
39 }
30 } 40 }
31 ] 41 )
32})
33
34RequestSchema.statics = {
35 activate,
36 deactivate,
37 flush,
38 forceSend,
39 list,
40 remainingMilliSeconds
41}
42
43RequestSchema.pre('save', function (next) {
44 const self = this
45
46 if (self.to.length === 0) {
47 Pod.listAllIds(function (err, podIds) {
48 if (err) return next(err)
49 42
50 // No friends 43 return Request
51 if (podIds.length === 0) return 44}
52
53 self.to = podIds
54 return next()
55 })
56 } else {
57 return next()
58 }
59})
60
61mongoose.model('Request', RequestSchema)
62 45
63// ------------------------------ STATICS ------------------------------ 46// ------------------------------ STATICS ------------------------------
64 47
48function associate (models) {
49 this.belongsToMany(models.Pod, {
50 foreignKey: {
51 name: 'requestId',
52 allowNull: false
53 },
54 through: models.RequestToPod,
55 onDelete: 'CASCADE'
56 })
57}
58
65function activate () { 59function activate () {
66 logger.info('Requests scheduler activated.') 60 logger.info('Requests scheduler activated.')
67 lastRequestTimestamp = Date.now() 61 lastRequestTimestamp = Date.now()
@@ -73,15 +67,25 @@ function activate () {
73 }, constants.REQUESTS_INTERVAL) 67 }, constants.REQUESTS_INTERVAL)
74} 68}
75 69
70function countTotalRequests (callback) {
71 const query = {
72 include: [ this.sequelize.models.Pod ]
73 }
74
75 return this.count(query).asCallback(callback)
76}
77
76function deactivate () { 78function deactivate () {
77 logger.info('Requests scheduler deactivated.') 79 logger.info('Requests scheduler deactivated.')
78 clearInterval(timer) 80 clearInterval(timer)
79 timer = null 81 timer = null
80} 82}
81 83
82function flush () { 84function flush (callback) {
83 removeAll.call(this, function (err) { 85 removeAll.call(this, function (err) {
84 if (err) logger.error('Cannot flush the requests.', { error: err }) 86 if (err) logger.error('Cannot flush the requests.', { error: err })
87
88 return callback(err)
85 }) 89 })
86} 90}
87 91
@@ -90,10 +94,6 @@ function forceSend () {
90 makeRequests.call(this) 94 makeRequests.call(this)
91} 95}
92 96
93function list (callback) {
94 this.find({ }, callback)
95}
96
97function remainingMilliSeconds () { 97function remainingMilliSeconds () {
98 if (timer === null) return -1 98 if (timer === null) return -1
99 99
@@ -122,7 +122,7 @@ function makeRequest (toPod, requestEndpoint, requestsToMake, callback) {
122 'Error sending secure request to %s pod.', 122 'Error sending secure request to %s pod.',
123 toPod.host, 123 toPod.host,
124 { 124 {
125 error: err || new Error('Status code not 20x : ' + res.statusCode) 125 error: err ? err.message : 'Status code not 20x : ' + res.statusCode
126 } 126 }
127 ) 127 )
128 128
@@ -136,10 +136,11 @@ function makeRequest (toPod, requestEndpoint, requestsToMake, callback) {
136// Make all the requests of the scheduler 136// Make all the requests of the scheduler
137function makeRequests () { 137function makeRequests () {
138 const self = this 138 const self = this
139 const RequestToPod = this.sequelize.models.RequestToPod
139 140
140 // We limit the size of the requests (REQUESTS_LIMIT) 141 // We limit the size of the requests
141 // We don't want to stuck with the same failing requests so we get a random list 142 // We don't want to stuck with the same failing requests so we get a random list
142 listWithLimitAndRandom.call(self, constants.REQUESTS_LIMIT, function (err, requests) { 143 listWithLimitAndRandom.call(self, constants.REQUESTS_LIMIT_PODS, constants.REQUESTS_LIMIT_PER_POD, function (err, requests) {
143 if (err) { 144 if (err) {
144 logger.error('Cannot get the list of requests.', { err: err }) 145 logger.error('Cannot get the list of requests.', { err: err })
145 return // Abort 146 return // Abort
@@ -151,78 +152,77 @@ function makeRequests () {
151 return 152 return
152 } 153 }
153 154
154 logger.info('Making requests to friends.')
155
156 // We want to group requests by destinations pod and endpoint 155 // We want to group requests by destinations pod and endpoint
157 const requestsToMakeGrouped = {} 156 const requestsToMakeGrouped = {}
157 Object.keys(requests).forEach(function (toPodId) {
158 requests[toPodId].forEach(function (data) {
159 const request = data.request
160 const pod = data.pod
161 const hashKey = toPodId + request.endpoint
158 162
159 requests.forEach(function (poolRequest) {
160 poolRequest.to.forEach(function (toPodId) {
161 const hashKey = toPodId + poolRequest.endpoint
162 if (!requestsToMakeGrouped[hashKey]) { 163 if (!requestsToMakeGrouped[hashKey]) {
163 requestsToMakeGrouped[hashKey] = { 164 requestsToMakeGrouped[hashKey] = {
164 toPodId, 165 toPod: pod,
165 endpoint: poolRequest.endpoint, 166 endpoint: request.endpoint,
166 ids: [], // pool request ids, to delete them from the DB in the future 167 ids: [], // request ids, to delete them from the DB in the future
167 datas: [] // requests data, 168 datas: [] // requests data,
168 } 169 }
169 } 170 }
170 171
171 requestsToMakeGrouped[hashKey].ids.push(poolRequest._id) 172 requestsToMakeGrouped[hashKey].ids.push(request.id)
172 requestsToMakeGrouped[hashKey].datas.push(poolRequest.request) 173 requestsToMakeGrouped[hashKey].datas.push(request.request)
173 }) 174 })
174 }) 175 })
175 176
177 logger.info('Making requests to friends.')
178
176 const goodPods = [] 179 const goodPods = []
177 const badPods = [] 180 const badPods = []
178 181
179 eachLimit(Object.keys(requestsToMakeGrouped), constants.REQUESTS_IN_PARALLEL, function (hashKey, callbackEach) { 182 eachLimit(Object.keys(requestsToMakeGrouped), constants.REQUESTS_IN_PARALLEL, function (hashKey, callbackEach) {
180 const requestToMake = requestsToMakeGrouped[hashKey] 183 const requestToMake = requestsToMakeGrouped[hashKey]
184 const toPod = requestToMake.toPod
181 185
182 // FIXME: mongodb request inside a loop :/ 186 // Maybe the pod is not our friend anymore so simply remove it
183 Pod.load(requestToMake.toPodId, function (err, toPod) { 187 if (!toPod) {
184 if (err) { 188 const requestIdsToDelete = requestToMake.ids
185 logger.error('Error finding pod by id.', { err: err })
186 return callbackEach()
187 }
188
189 // Maybe the pod is not our friend anymore so simply remove it
190 if (!toPod) {
191 const requestIdsToDelete = requestToMake.ids
192 189
193 logger.info('Removing %d requests of unexisting pod %s.', requestIdsToDelete.length, requestToMake.toPodId) 190 logger.info('Removing %d requests of unexisting pod %s.', requestIdsToDelete.length, requestToMake.toPod.id)
194 removePodOf.call(self, requestIdsToDelete, requestToMake.toPodId) 191 RequestToPod.removePodOf.call(self, requestIdsToDelete, requestToMake.toPod.id)
195 return callbackEach() 192 return callbackEach()
196 } 193 }
197 194
198 makeRequest(toPod, requestToMake.endpoint, requestToMake.datas, function (success) { 195 makeRequest(toPod, requestToMake.endpoint, requestToMake.datas, function (success) {
199 if (success === true) { 196 if (success === true) {
200 logger.debug('Removing requests for %s pod.', requestToMake.toPodId, { requestsIds: requestToMake.ids }) 197 logger.debug('Removing requests for pod %s.', requestToMake.toPod.id, { requestsIds: requestToMake.ids })
201 198
202 goodPods.push(requestToMake.toPodId) 199 goodPods.push(requestToMake.toPod.id)
203 200
204 // Remove the pod id of these request ids 201 // Remove the pod id of these request ids
205 removePodOf.call(self, requestToMake.ids, requestToMake.toPodId, callbackEach) 202 RequestToPod.removePodOf(requestToMake.ids, requestToMake.toPod.id, callbackEach)
206 } else { 203 } else {
207 badPods.push(requestToMake.toPodId) 204 badPods.push(requestToMake.toPod.id)
208 callbackEach() 205 callbackEach()
209 } 206 }
210 })
211 }) 207 })
212 }, function () { 208 }, function () {
213 // All the requests were made, we update the pods score 209 // All the requests were made, we update the pods score
214 updatePodsScore(goodPods, badPods) 210 updatePodsScore.call(self, goodPods, badPods)
215 // Flush requests with no pod 211 // Flush requests with no pod
216 removeWithEmptyTo.call(self) 212 removeWithEmptyTo.call(self, function (err) {
213 if (err) logger.error('Error when removing requests with no pods.', { error: err })
214 })
217 }) 215 })
218 }) 216 })
219} 217}
220 218
221// Remove pods with a score of 0 (too many requests where they were unreachable) 219// Remove pods with a score of 0 (too many requests where they were unreachable)
222function removeBadPods () { 220function removeBadPods () {
221 const self = this
222
223 waterfall([ 223 waterfall([
224 function findBadPods (callback) { 224 function findBadPods (callback) {
225 Pod.listBadPods(function (err, pods) { 225 self.sequelize.models.Pod.listBadPods(function (err, pods) {
226 if (err) { 226 if (err) {
227 logger.error('Cannot find bad pods.', { error: err }) 227 logger.error('Cannot find bad pods.', { error: err })
228 return callback(err) 228 return callback(err)
@@ -233,10 +233,8 @@ function removeBadPods () {
233 }, 233 },
234 234
235 function removeTheseBadPods (pods, callback) { 235 function removeTheseBadPods (pods, callback) {
236 if (pods.length === 0) return callback(null, 0)
237
238 each(pods, function (pod, callbackEach) { 236 each(pods, function (pod, callbackEach) {
239 pod.remove(callbackEach) 237 pod.destroy().asCallback(callbackEach)
240 }, function (err) { 238 }, function (err) {
241 return callback(err, pods.length) 239 return callback(err, pods.length)
242 }) 240 })
@@ -253,43 +251,98 @@ function removeBadPods () {
253} 251}
254 252
255function updatePodsScore (goodPods, badPods) { 253function updatePodsScore (goodPods, badPods) {
254 const self = this
255 const Pod = this.sequelize.models.Pod
256
256 logger.info('Updating %d good pods and %d bad pods scores.', goodPods.length, badPods.length) 257 logger.info('Updating %d good pods and %d bad pods scores.', goodPods.length, badPods.length)
257 258
258 Pod.incrementScores(goodPods, constants.PODS_SCORE.BONUS, function (err) { 259 if (goodPods.length !== 0) {
259 if (err) logger.error('Cannot increment scores of good pods.') 260 Pod.incrementScores(goodPods, constants.PODS_SCORE.BONUS, function (err) {
260 }) 261 if (err) logger.error('Cannot increment scores of good pods.', { error: err })
262 })
263 }
261 264
262 Pod.incrementScores(badPods, constants.PODS_SCORE.MALUS, function (err) { 265 if (badPods.length !== 0) {
263 if (err) logger.error('Cannot decrement scores of bad pods.') 266 Pod.incrementScores(badPods, constants.PODS_SCORE.MALUS, function (err) {
264 removeBadPods() 267 if (err) logger.error('Cannot decrement scores of bad pods.', { error: err })
265 }) 268 removeBadPods.call(self)
269 })
270 }
266} 271}
267 272
268function listWithLimitAndRandom (limit, callback) { 273function listWithLimitAndRandom (limitPods, limitRequestsPerPod, callback) {
269 const self = this 274 const self = this
275 const Pod = this.sequelize.models.Pod
270 276
271 self.count(function (err, count) { 277 Pod.listRandomPodIdsWithRequest(limitPods, function (err, podIds) {
272 if (err) return callback(err) 278 if (err) return callback(err)
273 279
274 let start = Math.floor(Math.random() * count) - limit 280 // We don't have friends that have requests
275 if (start < 0) start = 0 281 if (podIds.length === 0) return callback(null, [])
282
283 // The the first x requests of these pods
284 // It is very important to sort by id ASC to keep the requests order!
285 const query = {
286 order: [
287 [ 'id', 'ASC' ]
288 ],
289 include: [
290 {
291 model: self.sequelize.models.Pod,
292 where: {
293 id: {
294 $in: podIds
295 }
296 }
297 }
298 ]
299 }
300
301 self.findAll(query).asCallback(function (err, requests) {
302 if (err) return callback(err)
276 303
277 self.find().sort({ _id: 1 }).skip(start).limit(limit).exec(callback) 304 const requestsGrouped = groupAndTruncateRequests(requests, limitRequestsPerPod)
305 return callback(err, requestsGrouped)
306 })
278 }) 307 })
279} 308}
280 309
281function removeAll (callback) { 310function groupAndTruncateRequests (requests, limitRequestsPerPod) {
282 this.remove({ }, callback) 311 const requestsGrouped = {}
283}
284 312
285function removePodOf (requestsIds, podId, callback) { 313 requests.forEach(function (request) {
286 if (!callback) callback = function () {} 314 request.Pods.forEach(function (pod) {
315 if (!requestsGrouped[pod.id]) requestsGrouped[pod.id] = []
287 316
288 this.update({ _id: { $in: requestsIds } }, { $pull: { to: podId } }, { multi: true }, callback) 317 if (requestsGrouped[pod.id].length < limitRequestsPerPod) {
318 requestsGrouped[pod.id].push({
319 request,
320 pod
321 })
322 }
323 })
324 })
325
326 return requestsGrouped
327}
328
329function removeAll (callback) {
330 // Delete all requests
331 this.truncate({ cascade: true }).asCallback(callback)
289} 332}
290 333
291function removeWithEmptyTo (callback) { 334function removeWithEmptyTo (callback) {
292 if (!callback) callback = function () {} 335 if (!callback) callback = function () {}
293 336
294 this.remove({ to: { $size: 0 } }, callback) 337 const query = {
338 where: {
339 id: {
340 $notIn: [
341 this.sequelize.literal('SELECT "requestId" FROM "RequestToPods"')
342 ]
343 }
344 }
345 }
346
347 this.destroy(query).asCallback(callback)
295} 348}
diff --git a/server/models/tag.js b/server/models/tag.js
new file mode 100644
index 000000000..145e090c1
--- /dev/null
+++ b/server/models/tag.js
@@ -0,0 +1,76 @@
1'use strict'
2
3const each = require('async/each')
4
5// ---------------------------------------------------------------------------
6
7module.exports = function (sequelize, DataTypes) {
8 const Tag = sequelize.define('Tag',
9 {
10 name: {
11 type: DataTypes.STRING,
12 allowNull: false
13 }
14 },
15 {
16 timestamps: false,
17 indexes: [
18 {
19 fields: [ 'name' ],
20 unique: true
21 }
22 ],
23 classMethods: {
24 associate,
25
26 findOrCreateTags
27 }
28 }
29 )
30
31 return Tag
32}
33
34// ---------------------------------------------------------------------------
35
36function associate (models) {
37 this.belongsToMany(models.Video, {
38 foreignKey: 'tagId',
39 through: models.VideoTag,
40 onDelete: 'cascade'
41 })
42}
43
44function findOrCreateTags (tags, transaction, callback) {
45 if (!callback) {
46 callback = transaction
47 transaction = null
48 }
49
50 const self = this
51 const tagInstances = []
52
53 each(tags, function (tag, callbackEach) {
54 const query = {
55 where: {
56 name: tag
57 },
58 defaults: {
59 name: tag
60 }
61 }
62
63 if (transaction) query.transaction = transaction
64
65 self.findOrCreate(query).asCallback(function (err, res) {
66 if (err) return callbackEach(err)
67
68 // res = [ tag, isCreated ]
69 const tag = res[0]
70 tagInstances.push(tag)
71 return callbackEach()
72 })
73 }, function (err) {
74 return callback(err, tagInstances)
75 })
76}
diff --git a/server/models/user.js b/server/models/user.js
index a19de7072..36ed723cc 100644
--- a/server/models/user.js
+++ b/server/models/user.js
@@ -1,60 +1,81 @@
1const mongoose = require('mongoose') 1'use strict'
2
3const values = require('lodash/values')
2 4
3const customUsersValidators = require('../helpers/custom-validators').users
4const modelUtils = require('./utils') 5const modelUtils = require('./utils')
6const constants = require('../initializers/constants')
5const peertubeCrypto = require('../helpers/peertube-crypto') 7const peertubeCrypto = require('../helpers/peertube-crypto')
6 8const customUsersValidators = require('../helpers/custom-validators').users
7const OAuthToken = mongoose.model('OAuthToken')
8 9
9// --------------------------------------------------------------------------- 10// ---------------------------------------------------------------------------
10 11
11const UserSchema = mongoose.Schema({ 12module.exports = function (sequelize, DataTypes) {
12 createdDate: { 13 const User = sequelize.define('User',
13 type: Date, 14 {
14 default: Date.now 15 password: {
15 }, 16 type: DataTypes.STRING,
16 password: String, 17 allowNull: false,
17 username: String, 18 validate: {
18 role: String 19 passwordValid: function (value) {
19}) 20 const res = customUsersValidators.isUserPasswordValid(value)
20 21 if (res === false) throw new Error('Password not valid.')
21UserSchema.path('password').required(customUsersValidators.isUserPasswordValid) 22 }
22UserSchema.path('username').required(customUsersValidators.isUserUsernameValid) 23 }
23UserSchema.path('role').validate(customUsersValidators.isUserRoleValid) 24 },
24 25 username: {
25UserSchema.methods = { 26 type: DataTypes.STRING,
26 isPasswordMatch, 27 allowNull: false,
27 toFormatedJSON 28 validate: {
28} 29 usernameValid: function (value) {
29 30 const res = customUsersValidators.isUserUsernameValid(value)
30UserSchema.statics = { 31 if (res === false) throw new Error('Username not valid.')
31 countTotal, 32 }
32 getByUsername, 33 }
33 list, 34 },
34 listForApi, 35 role: {
35 loadById, 36 type: DataTypes.ENUM(values(constants.USER_ROLES)),
36 loadByUsername 37 allowNull: false
38 }
39 },
40 {
41 indexes: [
42 {
43 fields: [ 'username' ]
44 }
45 ],
46 classMethods: {
47 associate,
48
49 countTotal,
50 getByUsername,
51 list,
52 listForApi,
53 loadById,
54 loadByUsername
55 },
56 instanceMethods: {
57 isPasswordMatch,
58 toFormatedJSON
59 },
60 hooks: {
61 beforeCreate: beforeCreateOrUpdate,
62 beforeUpdate: beforeCreateOrUpdate
63 }
64 }
65 )
66
67 return User
37} 68}
38 69
39UserSchema.pre('save', function (next) { 70function beforeCreateOrUpdate (user, options, next) {
40 const user = this 71 peertubeCrypto.cryptPassword(user.password, function (err, hash) {
41
42 peertubeCrypto.cryptPassword(this.password, function (err, hash) {
43 if (err) return next(err) 72 if (err) return next(err)
44 73
45 user.password = hash 74 user.password = hash
46 75
47 return next() 76 return next()
48 }) 77 })
49}) 78}
50
51UserSchema.pre('remove', function (next) {
52 const user = this
53
54 OAuthToken.removeByUserId(user._id, next)
55})
56
57mongoose.model('User', UserSchema)
58 79
59// ------------------------------ METHODS ------------------------------ 80// ------------------------------ METHODS ------------------------------
60 81
@@ -64,35 +85,68 @@ function isPasswordMatch (password, callback) {
64 85
65function toFormatedJSON () { 86function toFormatedJSON () {
66 return { 87 return {
67 id: this._id, 88 id: this.id,
68 username: this.username, 89 username: this.username,
69 role: this.role, 90 role: this.role,
70 createdDate: this.createdDate 91 createdAt: this.createdAt
71 } 92 }
72} 93}
73// ------------------------------ STATICS ------------------------------ 94// ------------------------------ STATICS ------------------------------
74 95
96function associate (models) {
97 this.hasOne(models.Author, {
98 foreignKey: 'userId',
99 onDelete: 'cascade'
100 })
101
102 this.hasMany(models.OAuthToken, {
103 foreignKey: 'userId',
104 onDelete: 'cascade'
105 })
106}
107
75function countTotal (callback) { 108function countTotal (callback) {
76 return this.count(callback) 109 return this.count().asCallback(callback)
77} 110}
78 111
79function getByUsername (username) { 112function getByUsername (username) {
80 return this.findOne({ username: username }) 113 const query = {
114 where: {
115 username: username
116 }
117 }
118
119 return this.findOne(query)
81} 120}
82 121
83function list (callback) { 122function list (callback) {
84 return this.find(callback) 123 return this.find().asCallback(callback)
85} 124}
86 125
87function listForApi (start, count, sort, callback) { 126function listForApi (start, count, sort, callback) {
88 const query = {} 127 const query = {
89 return modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback) 128 offset: start,
129 limit: count,
130 order: [ modelUtils.getSort(sort) ]
131 }
132
133 return this.findAndCountAll(query).asCallback(function (err, result) {
134 if (err) return callback(err)
135
136 return callback(null, result.rows, result.count)
137 })
90} 138}
91 139
92function loadById (id, callback) { 140function loadById (id, callback) {
93 return this.findById(id, callback) 141 return this.findById(id).asCallback(callback)
94} 142}
95 143
96function loadByUsername (username, callback) { 144function loadByUsername (username, callback) {
97 return this.findOne({ username: username }, callback) 145 const query = {
146 where: {
147 username: username
148 }
149 }
150
151 return this.findOne(query).asCallback(callback)
98} 152}
diff --git a/server/models/utils.js b/server/models/utils.js
index e798aabe6..49636b3d8 100644
--- a/server/models/utils.js
+++ b/server/models/utils.js
@@ -1,28 +1,23 @@
1'use strict' 1'use strict'
2 2
3const parallel = require('async/parallel')
4
5const utils = { 3const utils = {
6 listForApiWithCount 4 getSort
7} 5}
8 6
9function listForApiWithCount (query, start, count, sort, callback) { 7// Translate for example "-name" to [ 'name', 'DESC' ]
10 const self = this 8function getSort (value) {
9 let field
10 let direction
11 11
12 parallel([ 12 if (value.substring(0, 1) === '-') {
13 function (asyncCallback) { 13 direction = 'DESC'
14 self.find(query).skip(start).limit(count).sort(sort).exec(asyncCallback) 14 field = value.substring(1)
15 }, 15 } else {
16 function (asyncCallback) { 16 direction = 'ASC'
17 self.count(query, asyncCallback) 17 field = value
18 } 18 }
19 ], function (err, results) {
20 if (err) return callback(err)
21 19
22 const data = results[0] 20 return [ field, direction ]
23 const total = results[1]
24 return callback(null, data, total)
25 })
26} 21}
27 22
28// --------------------------------------------------------------------------- 23// ---------------------------------------------------------------------------
diff --git a/server/models/video-abuse.js b/server/models/video-abuse.js
new file mode 100644
index 000000000..766a7568d
--- /dev/null
+++ b/server/models/video-abuse.js
@@ -0,0 +1,113 @@
1'use strict'
2
3const constants = require('../initializers/constants')
4const modelUtils = require('./utils')
5const customVideosValidators = require('../helpers/custom-validators').videos
6
7module.exports = function (sequelize, DataTypes) {
8 const VideoAbuse = sequelize.define('VideoAbuse',
9 {
10 reporterUsername: {
11 type: DataTypes.STRING,
12 allowNull: false,
13 validate: {
14 reporterUsernameValid: function (value) {
15 const res = customVideosValidators.isVideoAbuseReporterUsernameValid(value)
16 if (res === false) throw new Error('Video abuse reporter username is not valid.')
17 }
18 }
19 },
20 reason: {
21 type: DataTypes.STRING,
22 allowNull: false,
23 validate: {
24 reasonValid: function (value) {
25 const res = customVideosValidators.isVideoAbuseReasonValid(value)
26 if (res === false) throw new Error('Video abuse reason is not valid.')
27 }
28 }
29 }
30 },
31 {
32 indexes: [
33 {
34 fields: [ 'videoId' ]
35 },
36 {
37 fields: [ 'reporterPodId' ]
38 }
39 ],
40 classMethods: {
41 associate,
42
43 listForApi
44 },
45 instanceMethods: {
46 toFormatedJSON
47 }
48 }
49 )
50
51 return VideoAbuse
52}
53
54// ---------------------------------------------------------------------------
55
56function associate (models) {
57 this.belongsTo(models.Pod, {
58 foreignKey: {
59 name: 'reporterPodId',
60 allowNull: true
61 },
62 onDelete: 'cascade'
63 })
64
65 this.belongsTo(models.Video, {
66 foreignKey: {
67 name: 'videoId',
68 allowNull: false
69 },
70 onDelete: 'cascade'
71 })
72}
73
74function listForApi (start, count, sort, callback) {
75 const query = {
76 offset: start,
77 limit: count,
78 order: [ modelUtils.getSort(sort) ],
79 include: [
80 {
81 model: this.sequelize.models.Pod,
82 required: false
83 }
84 ]
85 }
86
87 return this.findAndCountAll(query).asCallback(function (err, result) {
88 if (err) return callback(err)
89
90 return callback(null, result.rows, result.count)
91 })
92}
93
94function toFormatedJSON () {
95 let reporterPodHost
96
97 if (this.Pod) {
98 reporterPodHost = this.Pod.host
99 } else {
100 // It means it's our video
101 reporterPodHost = constants.CONFIG.WEBSERVER.HOST
102 }
103
104 const json = {
105 id: this.id,
106 reporterPodHost,
107 reason: this.reason,
108 reporterUsername: this.reporterUsername,
109 videoId: this.videoId
110 }
111
112 return json
113}
diff --git a/server/models/video-tag.js b/server/models/video-tag.js
new file mode 100644
index 000000000..cd9277a6e
--- /dev/null
+++ b/server/models/video-tag.js
@@ -0,0 +1,18 @@
1'use strict'
2
3// ---------------------------------------------------------------------------
4
5module.exports = function (sequelize, DataTypes) {
6 const VideoTag = sequelize.define('VideoTag', {}, {
7 indexes: [
8 {
9 fields: [ 'videoId' ]
10 },
11 {
12 fields: [ 'tagId' ]
13 }
14 ]
15 })
16
17 return VideoTag
18}
diff --git a/server/models/video.js b/server/models/video.js
index 330067cdf..17eff6428 100644
--- a/server/models/video.js
+++ b/server/models/video.js
@@ -1,108 +1,160 @@
1'use strict' 1'use strict'
2 2
3const Buffer = require('safe-buffer').Buffer
3const createTorrent = require('create-torrent') 4const createTorrent = require('create-torrent')
4const ffmpeg = require('fluent-ffmpeg') 5const ffmpeg = require('fluent-ffmpeg')
5const fs = require('fs') 6const fs = require('fs')
6const magnetUtil = require('magnet-uri') 7const magnetUtil = require('magnet-uri')
8const map = require('lodash/map')
7const parallel = require('async/parallel') 9const parallel = require('async/parallel')
8const parseTorrent = require('parse-torrent') 10const parseTorrent = require('parse-torrent')
9const pathUtils = require('path') 11const pathUtils = require('path')
10const mongoose = require('mongoose') 12const values = require('lodash/values')
11 13
12const constants = require('../initializers/constants') 14const constants = require('../initializers/constants')
13const customVideosValidators = require('../helpers/custom-validators').videos
14const logger = require('../helpers/logger') 15const logger = require('../helpers/logger')
16const friends = require('../lib/friends')
15const modelUtils = require('./utils') 17const modelUtils = require('./utils')
18const customVideosValidators = require('../helpers/custom-validators').videos
16 19
17// --------------------------------------------------------------------------- 20// ---------------------------------------------------------------------------
18 21
19// TODO: add indexes on searchable columns 22module.exports = function (sequelize, DataTypes) {
20const VideoSchema = mongoose.Schema({ 23 const Video = sequelize.define('Video',
21 name: String, 24 {
22 extname: { 25 id: {
23 type: String, 26 type: DataTypes.UUID,
24 enum: [ '.mp4', '.webm', '.ogv' ] 27 defaultValue: DataTypes.UUIDV4,
25 }, 28 primaryKey: true,
26 remoteId: mongoose.Schema.Types.ObjectId, 29 validate: {
27 description: String, 30 isUUID: 4
28 magnet: { 31 }
29 infoHash: String
30 },
31 podHost: String,
32 author: String,
33 duration: Number,
34 tags: [ String ],
35 createdDate: {
36 type: Date,
37 default: Date.now
38 }
39})
40
41VideoSchema.path('name').validate(customVideosValidators.isVideoNameValid)
42VideoSchema.path('description').validate(customVideosValidators.isVideoDescriptionValid)
43VideoSchema.path('podHost').validate(customVideosValidators.isVideoPodHostValid)
44VideoSchema.path('author').validate(customVideosValidators.isVideoAuthorValid)
45VideoSchema.path('duration').validate(customVideosValidators.isVideoDurationValid)
46VideoSchema.path('tags').validate(customVideosValidators.isVideoTagsValid)
47
48VideoSchema.methods = {
49 generateMagnetUri,
50 getVideoFilename,
51 getThumbnailName,
52 getPreviewName,
53 getTorrentName,
54 isOwned,
55 toFormatedJSON,
56 toRemoteJSON
57}
58
59VideoSchema.statics = {
60 generateThumbnailFromBase64,
61 getDurationFromFile,
62 listForApi,
63 listByHostAndRemoteId,
64 listByHost,
65 listOwned,
66 listOwnedByAuthor,
67 listRemotes,
68 load,
69 search
70}
71
72VideoSchema.pre('remove', function (next) {
73 const video = this
74 const tasks = []
75
76 tasks.push(
77 function (callback) {
78 removeThumbnail(video, callback)
79 }
80 )
81
82 if (video.isOwned()) {
83 tasks.push(
84 function (callback) {
85 removeFile(video, callback)
86 }, 32 },
87 function (callback) { 33 name: {
88 removeTorrent(video, callback) 34 type: DataTypes.STRING,
35 allowNull: false,
36 validate: {
37 nameValid: function (value) {
38 const res = customVideosValidators.isVideoNameValid(value)
39 if (res === false) throw new Error('Video name is not valid.')
40 }
41 }
89 }, 42 },
90 function (callback) { 43 extname: {
91 removePreview(video, callback) 44 type: DataTypes.ENUM(values(constants.CONSTRAINTS_FIELDS.VIDEOS.EXTNAME)),
45 allowNull: false
46 },
47 remoteId: {
48 type: DataTypes.UUID,
49 allowNull: true,
50 validate: {
51 isUUID: 4
52 }
53 },
54 description: {
55 type: DataTypes.STRING,
56 allowNull: false,
57 validate: {
58 descriptionValid: function (value) {
59 const res = customVideosValidators.isVideoDescriptionValid(value)
60 if (res === false) throw new Error('Video description is not valid.')
61 }
62 }
63 },
64 infoHash: {
65 type: DataTypes.STRING,
66 allowNull: false,
67 validate: {
68 infoHashValid: function (value) {
69 const res = customVideosValidators.isVideoInfoHashValid(value)
70 if (res === false) throw new Error('Video info hash is not valid.')
71 }
72 }
73 },
74 duration: {
75 type: DataTypes.INTEGER,
76 allowNull: false,
77 validate: {
78 durationValid: function (value) {
79 const res = customVideosValidators.isVideoDurationValid(value)
80 if (res === false) throw new Error('Video duration is not valid.')
81 }
82 }
92 } 83 }
93 ) 84 },
85 {
86 indexes: [
87 {
88 fields: [ 'authorId' ]
89 },
90 {
91 fields: [ 'remoteId' ]
92 },
93 {
94 fields: [ 'name' ]
95 },
96 {
97 fields: [ 'createdAt' ]
98 },
99 {
100 fields: [ 'duration' ]
101 },
102 {
103 fields: [ 'infoHash' ]
104 }
105 ],
106 classMethods: {
107 associate,
108
109 generateThumbnailFromData,
110 getDurationFromFile,
111 list,
112 listForApi,
113 listOwnedAndPopulateAuthorAndTags,
114 listOwnedByAuthor,
115 load,
116 loadByHostAndRemoteId,
117 loadAndPopulateAuthor,
118 loadAndPopulateAuthorAndPodAndTags,
119 searchAndPopulateAuthorAndPodAndTags
120 },
121 instanceMethods: {
122 generateMagnetUri,
123 getVideoFilename,
124 getThumbnailName,
125 getPreviewName,
126 getTorrentName,
127 isOwned,
128 toFormatedJSON,
129 toAddRemoteJSON,
130 toUpdateRemoteJSON
131 },
132 hooks: {
133 beforeValidate,
134 beforeCreate,
135 afterDestroy
136 }
137 }
138 )
139
140 return Video
141}
142
143function beforeValidate (video, options, next) {
144 // Put a fake infoHash if it does not exists yet
145 if (video.isOwned() && !video.infoHash) {
146 // 40 hexa length
147 video.infoHash = '0123456789abcdef0123456789abcdef01234567'
94 } 148 }
95 149
96 parallel(tasks, next) 150 return next(null)
97}) 151}
98 152
99VideoSchema.pre('save', function (next) { 153function beforeCreate (video, options, next) {
100 const video = this
101 const tasks = [] 154 const tasks = []
102 155
103 if (video.isOwned()) { 156 if (video.isOwned()) {
104 const videoPath = pathUtils.join(constants.CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) 157 const videoPath = pathUtils.join(constants.CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename())
105 this.podHost = constants.CONFIG.WEBSERVER.HOST
106 158
107 tasks.push( 159 tasks.push(
108 // TODO: refractoring 160 // TODO: refractoring
@@ -123,9 +175,8 @@ VideoSchema.pre('save', function (next) {
123 if (err) return callback(err) 175 if (err) return callback(err)
124 176
125 const parsedTorrent = parseTorrent(torrent) 177 const parsedTorrent = parseTorrent(torrent)
126 video.magnet.infoHash = parsedTorrent.infoHash 178 video.set('infoHash', parsedTorrent.infoHash)
127 179 video.validate().asCallback(callback)
128 callback(null)
129 }) 180 })
130 }) 181 })
131 }, 182 },
@@ -141,12 +192,72 @@ VideoSchema.pre('save', function (next) {
141 } 192 }
142 193
143 return next() 194 return next()
144}) 195}
196
197function afterDestroy (video, options, next) {
198 const tasks = []
145 199
146mongoose.model('Video', VideoSchema) 200 tasks.push(
201 function (callback) {
202 removeThumbnail(video, callback)
203 }
204 )
205
206 if (video.isOwned()) {
207 tasks.push(
208 function (callback) {
209 removeFile(video, callback)
210 },
211
212 function (callback) {
213 removeTorrent(video, callback)
214 },
215
216 function (callback) {
217 removePreview(video, callback)
218 },
219
220 function (callback) {
221 const params = {
222 remoteId: video.id
223 }
224
225 friends.removeVideoToFriends(params)
226
227 return callback()
228 }
229 )
230 }
231
232 parallel(tasks, next)
233}
147 234
148// ------------------------------ METHODS ------------------------------ 235// ------------------------------ METHODS ------------------------------
149 236
237function associate (models) {
238 this.belongsTo(models.Author, {
239 foreignKey: {
240 name: 'authorId',
241 allowNull: false
242 },
243 onDelete: 'cascade'
244 })
245
246 this.belongsToMany(models.Tag, {
247 foreignKey: 'videoId',
248 through: models.VideoTag,
249 onDelete: 'cascade'
250 })
251
252 this.hasMany(models.VideoAbuse, {
253 foreignKey: {
254 name: 'videoId',
255 allowNull: false
256 },
257 onDelete: 'cascade'
258 })
259}
260
150function generateMagnetUri () { 261function generateMagnetUri () {
151 let baseUrlHttp, baseUrlWs 262 let baseUrlHttp, baseUrlWs
152 263
@@ -154,8 +265,8 @@ function generateMagnetUri () {
154 baseUrlHttp = constants.CONFIG.WEBSERVER.URL 265 baseUrlHttp = constants.CONFIG.WEBSERVER.URL
155 baseUrlWs = constants.CONFIG.WEBSERVER.WS + '://' + constants.CONFIG.WEBSERVER.HOSTNAME + ':' + constants.CONFIG.WEBSERVER.PORT 266 baseUrlWs = constants.CONFIG.WEBSERVER.WS + '://' + constants.CONFIG.WEBSERVER.HOSTNAME + ':' + constants.CONFIG.WEBSERVER.PORT
156 } else { 267 } else {
157 baseUrlHttp = constants.REMOTE_SCHEME.HTTP + '://' + this.podHost 268 baseUrlHttp = constants.REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host
158 baseUrlWs = constants.REMOTE_SCHEME.WS + '://' + this.podHost 269 baseUrlWs = constants.REMOTE_SCHEME.WS + '://' + this.Author.Pod.host
159 } 270 }
160 271
161 const xs = baseUrlHttp + constants.STATIC_PATHS.TORRENTS + this.getTorrentName() 272 const xs = baseUrlHttp + constants.STATIC_PATHS.TORRENTS + this.getTorrentName()
@@ -166,7 +277,7 @@ function generateMagnetUri () {
166 xs, 277 xs,
167 announce, 278 announce,
168 urlList, 279 urlList,
169 infoHash: this.magnet.infoHash, 280 infoHash: this.infoHash,
170 name: this.name 281 name: this.name
171 } 282 }
172 283
@@ -174,20 +285,20 @@ function generateMagnetUri () {
174} 285}
175 286
176function getVideoFilename () { 287function getVideoFilename () {
177 if (this.isOwned()) return this._id + this.extname 288 if (this.isOwned()) return this.id + this.extname
178 289
179 return this.remoteId + this.extname 290 return this.remoteId + this.extname
180} 291}
181 292
182function getThumbnailName () { 293function getThumbnailName () {
183 // We always have a copy of the thumbnail 294 // We always have a copy of the thumbnail
184 return this._id + '.jpg' 295 return this.id + '.jpg'
185} 296}
186 297
187function getPreviewName () { 298function getPreviewName () {
188 const extension = '.jpg' 299 const extension = '.jpg'
189 300
190 if (this.isOwned()) return this._id + extension 301 if (this.isOwned()) return this.id + extension
191 302
192 return this.remoteId + extension 303 return this.remoteId + extension
193} 304}
@@ -195,7 +306,7 @@ function getPreviewName () {
195function getTorrentName () { 306function getTorrentName () {
196 const extension = '.torrent' 307 const extension = '.torrent'
197 308
198 if (this.isOwned()) return this._id + extension 309 if (this.isOwned()) return this.id + extension
199 310
200 return this.remoteId + extension 311 return this.remoteId + extension
201} 312}
@@ -205,27 +316,37 @@ function isOwned () {
205} 316}
206 317
207function toFormatedJSON () { 318function toFormatedJSON () {
319 let podHost
320
321 if (this.Author.Pod) {
322 podHost = this.Author.Pod.host
323 } else {
324 // It means it's our video
325 podHost = constants.CONFIG.WEBSERVER.HOST
326 }
327
208 const json = { 328 const json = {
209 id: this._id, 329 id: this.id,
210 name: this.name, 330 name: this.name,
211 description: this.description, 331 description: this.description,
212 podHost: this.podHost, 332 podHost,
213 isLocal: this.isOwned(), 333 isLocal: this.isOwned(),
214 magnetUri: this.generateMagnetUri(), 334 magnetUri: this.generateMagnetUri(),
215 author: this.author, 335 author: this.Author.name,
216 duration: this.duration, 336 duration: this.duration,
217 tags: this.tags, 337 tags: map(this.Tags, 'name'),
218 thumbnailPath: constants.STATIC_PATHS.THUMBNAILS + '/' + this.getThumbnailName(), 338 thumbnailPath: pathUtils.join(constants.STATIC_PATHS.THUMBNAILS, this.getThumbnailName()),
219 createdDate: this.createdDate 339 createdAt: this.createdAt,
340 updatedAt: this.updatedAt
220 } 341 }
221 342
222 return json 343 return json
223} 344}
224 345
225function toRemoteJSON (callback) { 346function toAddRemoteJSON (callback) {
226 const self = this 347 const self = this
227 348
228 // Convert thumbnail to base64 349 // Get thumbnail data to send to the other pod
229 const thumbnailPath = pathUtils.join(constants.CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) 350 const thumbnailPath = pathUtils.join(constants.CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
230 fs.readFile(thumbnailPath, function (err, thumbnailData) { 351 fs.readFile(thumbnailPath, function (err, thumbnailData) {
231 if (err) { 352 if (err) {
@@ -236,13 +357,14 @@ function toRemoteJSON (callback) {
236 const remoteVideo = { 357 const remoteVideo = {
237 name: self.name, 358 name: self.name,
238 description: self.description, 359 description: self.description,
239 magnet: self.magnet, 360 infoHash: self.infoHash,
240 remoteId: self._id, 361 remoteId: self.id,
241 author: self.author, 362 author: self.Author.name,
242 duration: self.duration, 363 duration: self.duration,
243 thumbnailBase64: new Buffer(thumbnailData).toString('base64'), 364 thumbnailData: thumbnailData.toString('binary'),
244 tags: self.tags, 365 tags: map(self.Tags, 'name'),
245 createdDate: self.createdDate, 366 createdAt: self.createdAt,
367 updatedAt: self.updatedAt,
246 extname: self.extname 368 extname: self.extname
247 } 369 }
248 370
@@ -250,14 +372,31 @@ function toRemoteJSON (callback) {
250 }) 372 })
251} 373}
252 374
375function toUpdateRemoteJSON (callback) {
376 const json = {
377 name: this.name,
378 description: this.description,
379 infoHash: this.infoHash,
380 remoteId: this.id,
381 author: this.Author.name,
382 duration: this.duration,
383 tags: map(this.Tags, 'name'),
384 createdAt: this.createdAt,
385 updatedAt: this.updatedAt,
386 extname: this.extname
387 }
388
389 return json
390}
391
253// ------------------------------ STATICS ------------------------------ 392// ------------------------------ STATICS ------------------------------
254 393
255function generateThumbnailFromBase64 (video, thumbnailData, callback) { 394function generateThumbnailFromData (video, thumbnailData, callback) {
256 // Creating the thumbnail for a remote video 395 // Creating the thumbnail for a remote video
257 396
258 const thumbnailName = video.getThumbnailName() 397 const thumbnailName = video.getThumbnailName()
259 const thumbnailPath = constants.CONFIG.STORAGE.THUMBNAILS_DIR + thumbnailName 398 const thumbnailPath = constants.CONFIG.STORAGE.THUMBNAILS_DIR + thumbnailName
260 fs.writeFile(thumbnailPath, thumbnailData, { encoding: 'base64' }, function (err) { 399 fs.writeFile(thumbnailPath, Buffer.from(thumbnailData, 'binary'), function (err) {
261 if (err) return callback(err) 400 if (err) return callback(err)
262 401
263 return callback(null, thumbnailName) 402 return callback(null, thumbnailName)
@@ -272,51 +411,186 @@ function getDurationFromFile (videoPath, callback) {
272 }) 411 })
273} 412}
274 413
275function listForApi (start, count, sort, callback) { 414function list (callback) {
276 const query = {} 415 return this.find().asCallback()
277 return modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback)
278} 416}
279 417
280function listByHostAndRemoteId (fromHost, remoteId, callback) { 418function listForApi (start, count, sort, callback) {
281 this.find({ podHost: fromHost, remoteId: remoteId }, callback) 419 const query = {
420 offset: start,
421 limit: count,
422 distinct: true, // For the count, a video can have many tags
423 order: [ modelUtils.getSort(sort), [ this.sequelize.models.Tag, 'name', 'ASC' ] ],
424 include: [
425 {
426 model: this.sequelize.models.Author,
427 include: [ { model: this.sequelize.models.Pod, required: false } ]
428 },
429
430 this.sequelize.models.Tag
431 ]
432 }
433
434 return this.findAndCountAll(query).asCallback(function (err, result) {
435 if (err) return callback(err)
436
437 return callback(null, result.rows, result.count)
438 })
282} 439}
283 440
284function listByHost (fromHost, callback) { 441function loadByHostAndRemoteId (fromHost, remoteId, callback) {
285 this.find({ podHost: fromHost }, callback) 442 const query = {
443 where: {
444 remoteId: remoteId
445 },
446 include: [
447 {
448 model: this.sequelize.models.Author,
449 include: [
450 {
451 model: this.sequelize.models.Pod,
452 required: true,
453 where: {
454 host: fromHost
455 }
456 }
457 ]
458 }
459 ]
460 }
461
462 return this.findOne(query).asCallback(callback)
286} 463}
287 464
288function listOwned (callback) { 465function listOwnedAndPopulateAuthorAndTags (callback) {
289 // If remoteId is null this is *our* video 466 // If remoteId is null this is *our* video
290 this.find({ remoteId: null }, callback) 467 const query = {
468 where: {
469 remoteId: null
470 },
471 include: [ this.sequelize.models.Author, this.sequelize.models.Tag ]
472 }
473
474 return this.findAll(query).asCallback(callback)
291} 475}
292 476
293function listOwnedByAuthor (author, callback) { 477function listOwnedByAuthor (author, callback) {
294 this.find({ remoteId: null, author: author }, callback) 478 const query = {
295} 479 where: {
480 remoteId: null
481 },
482 include: [
483 {
484 model: this.sequelize.models.Author,
485 where: {
486 name: author
487 }
488 }
489 ]
490 }
296 491
297function listRemotes (callback) { 492 return this.findAll(query).asCallback(callback)
298 this.find({ remoteId: { $ne: null } }, callback)
299} 493}
300 494
301function load (id, callback) { 495function load (id, callback) {
302 this.findById(id, callback) 496 return this.findById(id).asCallback(callback)
497}
498
499function loadAndPopulateAuthor (id, callback) {
500 const options = {
501 include: [ this.sequelize.models.Author ]
502 }
503
504 return this.findById(id, options).asCallback(callback)
505}
506
507function loadAndPopulateAuthorAndPodAndTags (id, callback) {
508 const options = {
509 include: [
510 {
511 model: this.sequelize.models.Author,
512 include: [ { model: this.sequelize.models.Pod, required: false } ]
513 },
514 this.sequelize.models.Tag
515 ]
516 }
517
518 return this.findById(id, options).asCallback(callback)
303} 519}
304 520
305function search (value, field, start, count, sort, callback) { 521function searchAndPopulateAuthorAndPodAndTags (value, field, start, count, sort, callback) {
306 const query = {} 522 const podInclude = {
523 model: this.sequelize.models.Pod,
524 required: false
525 }
526
527 const authorInclude = {
528 model: this.sequelize.models.Author,
529 include: [
530 podInclude
531 ]
532 }
533
534 const tagInclude = {
535 model: this.sequelize.models.Tag
536 }
537
538 const query = {
539 where: {},
540 offset: start,
541 limit: count,
542 distinct: true, // For the count, a video can have many tags
543 order: [ modelUtils.getSort(sort), [ this.sequelize.models.Tag, 'name', 'ASC' ] ]
544 }
545
307 // Make an exact search with the magnet 546 // Make an exact search with the magnet
308 if (field === 'magnetUri') { 547 if (field === 'magnetUri') {
309 const infoHash = magnetUtil.decode(value).infoHash 548 const infoHash = magnetUtil.decode(value).infoHash
310 query.magnet = { 549 query.where.infoHash = infoHash
311 infoHash
312 }
313 } else if (field === 'tags') { 550 } else if (field === 'tags') {
314 query[field] = value 551 const escapedValue = this.sequelize.escape('%' + value + '%')
552 query.where = {
553 id: {
554 $in: this.sequelize.literal(
555 '(SELECT "VideoTags"."videoId" FROM "Tags" INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" WHERE name LIKE ' + escapedValue + ')'
556 )
557 }
558 }
559 } else if (field === 'host') {
560 // FIXME: Include our pod? (not stored in the database)
561 podInclude.where = {
562 host: {
563 $like: '%' + value + '%'
564 }
565 }
566 podInclude.required = true
567 } else if (field === 'author') {
568 authorInclude.where = {
569 name: {
570 $like: '%' + value + '%'
571 }
572 }
573
574 // authorInclude.or = true
315 } else { 575 } else {
316 query[field] = new RegExp(value, 'i') 576 query.where[field] = {
577 $like: '%' + value + '%'
578 }
317 } 579 }
318 580
319 modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback) 581 query.include = [
582 authorInclude, tagInclude
583 ]
584
585 if (tagInclude.where) {
586 // query.include.push([ this.sequelize.models.Tag ])
587 }
588
589 return this.findAndCountAll(query).asCallback(function (err, result) {
590 if (err) return callback(err)
591
592 return callback(null, result.rows, result.count)
593 })
320} 594}
321 595
322// --------------------------------------------------------------------------- 596// ---------------------------------------------------------------------------