diff options
author | Chocobozzz <florian.bigard@gmail.com> | 2017-01-12 15:20:03 +0100 |
---|---|---|
committer | Chocobozzz <florian.bigard@gmail.com> | 2017-01-12 15:20:03 +0100 |
commit | 99fe265a5fc077cb66c322e7f3d191ff7110aea0 (patch) | |
tree | c9e04ccfcc5496d2300d7c26db5833e494b4cdad /server/models | |
parent | fcc5f77b95d330bfcb439c172b7fcc58f3162e4d (diff) | |
parent | 91cc839af88730ba55f84997c56b85ea100070a7 (diff) | |
download | PeerTube-99fe265a5fc077cb66c322e7f3d191ff7110aea0.tar.gz PeerTube-99fe265a5fc077cb66c322e7f3d191ff7110aea0.tar.zst PeerTube-99fe265a5fc077cb66c322e7f3d191ff7110aea0.zip |
Merge branch 'postgresql'
Diffstat (limited to 'server/models')
-rw-r--r-- | server/models/application.js | 61 | ||||
-rw-r--r-- | server/models/author.js | 85 | ||||
-rw-r--r-- | server/models/oauth-client.js | 75 | ||||
-rw-r--r-- | server/models/oauth-token.js | 134 | ||||
-rw-r--r-- | server/models/pod.js | 200 | ||||
-rw-r--r-- | server/models/pods.js | 119 | ||||
-rw-r--r-- | server/models/request-to-pod.js | 42 | ||||
-rw-r--r-- | server/models/request.js | 275 | ||||
-rw-r--r-- | server/models/tag.js | 76 | ||||
-rw-r--r-- | server/models/user.js | 158 | ||||
-rw-r--r-- | server/models/utils.js | 31 | ||||
-rw-r--r-- | server/models/video-abuse.js | 113 | ||||
-rw-r--r-- | server/models/video-tag.js | 18 | ||||
-rw-r--r-- | server/models/video.js | 532 |
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 @@ | |||
1 | const mongoose = require('mongoose') | 1 | 'use strict' |
2 | |||
3 | module.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 | ||
5 | const ApplicationSchema = mongoose.Schema({ | 28 | function loadMigrationVersion (callback) { |
6 | mongoSchemaVersion: { | 29 | const query = { |
7 | type: Number, | 30 | attributes: [ 'migrationVersion' ] |
8 | default: 0 | ||
9 | } | 31 | } |
10 | }) | ||
11 | |||
12 | ApplicationSchema.statics = { | ||
13 | loadMongoSchemaVersion, | ||
14 | updateMongoSchemaVersion | ||
15 | } | ||
16 | |||
17 | mongoose.model('Application', ApplicationSchema) | ||
18 | |||
19 | // --------------------------------------------------------------------------- | ||
20 | 32 | ||
21 | function 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 | ||
29 | function updateMongoSchemaVersion (newVersion, callback) { | 40 | function 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 | |||
3 | const customUsersValidators = require('../helpers/custom-validators').users | ||
4 | |||
5 | module.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 | |||
44 | function 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 | |||
62 | function 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 @@ | |||
1 | const mongoose = require('mongoose') | 1 | 'use strict' |
2 | 2 | ||
3 | // --------------------------------------------------------------------------- | 3 | module.exports = function (sequelize, DataTypes) { |
4 | 4 | const OAuthClient = sequelize.define('OAuthClient', | |
5 | const 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: { | |
11 | OAuthClientSchema.path('clientSecret').required(true) | 11 | type: DataTypes.STRING, |
12 | 12 | allowNull: false | |
13 | OAuthClientSchema.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 | ||
19 | mongoose.model('OAuthClient', OAuthClientSchema) | ||
20 | |||
21 | // --------------------------------------------------------------------------- | 43 | // --------------------------------------------------------------------------- |
22 | 44 | ||
23 | function list (callback) { | 45 | function countTotal (callback) { |
24 | return this.find(callback) | 46 | return this.count().asCallback(callback) |
25 | } | 47 | } |
26 | 48 | ||
27 | function loadFirstClient (callback) { | 49 | function loadFirstClient (callback) { |
28 | return this.findOne({}, callback) | 50 | return this.findOne().asCallback(callback) |
29 | } | 51 | } |
30 | 52 | ||
31 | function getByIdAndSecret (id, clientSecret) { | 53 | function 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 @@ | |||
1 | const mongoose = require('mongoose') | 1 | 'use strict' |
2 | 2 | ||
3 | const logger = require('../helpers/logger') | 3 | const logger = require('../helpers/logger') |
4 | 4 | ||
5 | // --------------------------------------------------------------------------- | 5 | // --------------------------------------------------------------------------- |
6 | 6 | ||
7 | const OAuthTokenSchema = mongoose.Schema({ | 7 | module.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, | |
16 | OAuthTokenSchema.path('accessToken').required(true) | 16 | allowNull: false |
17 | OAuthTokenSchema.path('client').required(true) | 17 | }, |
18 | OAuthTokenSchema.path('user').required(true) | 18 | refreshToken: { |
19 | 19 | type: DataTypes.STRING, | |
20 | OAuthTokenSchema.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 | ||
27 | mongoose.model('OAuthToken', OAuthTokenSchema) | 55 | return OAuthToken |
56 | } | ||
28 | 57 | ||
29 | // --------------------------------------------------------------------------- | 58 | // --------------------------------------------------------------------------- |
30 | 59 | ||
60 | function 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 | |||
31 | function getByRefreshTokenAndPopulateClient (refreshToken) { | 78 | function 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 | ||
52 | function getByTokenAndPopulateUser (bearerToken) { | 106 | function 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 | ||
56 | function getByRefreshTokenAndPopulateUser (refreshToken) { | 121 | function 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 | ||
60 | function removeByUserId (userId, callback) { | 136 | function 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 | |||
3 | const map = require('lodash/map') | ||
4 | |||
5 | const constants = require('../initializers/constants') | ||
6 | const customPodsValidators = require('../helpers/custom-validators').pods | ||
7 | |||
8 | // --------------------------------------------------------------------------- | ||
9 | |||
10 | module.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 | |||
70 | function 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 | |||
83 | function associate (models) { | ||
84 | this.belongsToMany(models.Request, { | ||
85 | foreignKey: 'podId', | ||
86 | through: models.RequestToPod, | ||
87 | onDelete: 'cascade' | ||
88 | }) | ||
89 | } | ||
90 | |||
91 | function countAll (callback) { | ||
92 | return this.count().asCallback(callback) | ||
93 | } | ||
94 | |||
95 | function 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 | |||
115 | function list (callback) { | ||
116 | return this.findAll().asCallback(callback) | ||
117 | } | ||
118 | |||
119 | function 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 | |||
138 | function 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 | |||
174 | function listBadPods (callback) { | ||
175 | const query = { | ||
176 | where: { | ||
177 | score: { $lte: 0 } | ||
178 | } | ||
179 | } | ||
180 | |||
181 | return this.findAll(query).asCallback(callback) | ||
182 | } | ||
183 | |||
184 | function load (id, callback) { | ||
185 | return this.findById(id).asCallback(callback) | ||
186 | } | ||
187 | |||
188 | function loadByHost (host, callback) { | ||
189 | const query = { | ||
190 | where: { | ||
191 | host: host | ||
192 | } | ||
193 | } | ||
194 | |||
195 | return this.findOne(query).asCallback(callback) | ||
196 | } | ||
197 | |||
198 | function 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 | |||
3 | const each = require('async/each') | ||
4 | const mongoose = require('mongoose') | ||
5 | const map = require('lodash/map') | ||
6 | const validator = require('express-validator').validator | ||
7 | |||
8 | const constants = require('../initializers/constants') | ||
9 | |||
10 | const Video = mongoose.model('Video') | ||
11 | |||
12 | // --------------------------------------------------------------------------- | ||
13 | |||
14 | const 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 | |||
24 | PodSchema.path('host').validate(validator.isURL) | ||
25 | PodSchema.path('publicKey').required(true) | ||
26 | PodSchema.path('score').validate(function (value) { return !isNaN(value) }) | ||
27 | |||
28 | PodSchema.methods = { | ||
29 | toFormatedJSON | ||
30 | } | ||
31 | |||
32 | PodSchema.statics = { | ||
33 | countAll, | ||
34 | incrementScores, | ||
35 | list, | ||
36 | listAllIds, | ||
37 | listBadPods, | ||
38 | load, | ||
39 | loadByHost, | ||
40 | removeAll | ||
41 | } | ||
42 | |||
43 | PodSchema.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 | |||
56 | PodSchema.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 | |||
67 | const Pod = mongoose.model('Pod', PodSchema) | ||
68 | |||
69 | // ------------------------------ METHODS ------------------------------ | ||
70 | |||
71 | function 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 | |||
84 | function countAll (callback) { | ||
85 | return this.count(callback) | ||
86 | } | ||
87 | |||
88 | function incrementScores (ids, value, callback) { | ||
89 | if (!callback) callback = function () {} | ||
90 | return this.update({ _id: { $in: ids } }, { $inc: { score: value } }, { multi: true }, callback) | ||
91 | } | ||
92 | |||
93 | function list (callback) { | ||
94 | return this.find(callback) | ||
95 | } | ||
96 | |||
97 | function 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 | |||
105 | function listBadPods (callback) { | ||
106 | return this.find({ score: 0 }, callback) | ||
107 | } | ||
108 | |||
109 | function load (id, callback) { | ||
110 | return this.findById(id, callback) | ||
111 | } | ||
112 | |||
113 | function loadByHost (host, callback) { | ||
114 | return this.findOne({ host }, callback) | ||
115 | } | ||
116 | |||
117 | function 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 | |||
5 | module.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 | |||
29 | function 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 | ||
3 | const each = require('async/each') | 3 | const each = require('async/each') |
4 | const eachLimit = require('async/eachLimit') | 4 | const eachLimit = require('async/eachLimit') |
5 | const values = require('lodash/values') | ||
6 | const mongoose = require('mongoose') | ||
7 | const waterfall = require('async/waterfall') | 5 | const waterfall = require('async/waterfall') |
6 | const values = require('lodash/values') | ||
8 | 7 | ||
9 | const constants = require('../initializers/constants') | 8 | const constants = require('../initializers/constants') |
10 | const logger = require('../helpers/logger') | 9 | const logger = require('../helpers/logger') |
11 | const requests = require('../helpers/requests') | 10 | const requests = require('../helpers/requests') |
12 | 11 | ||
13 | const Pod = mongoose.model('Pod') | ||
14 | |||
15 | let timer = null | 12 | let timer = null |
16 | let lastRequestTimestamp = 0 | 13 | let lastRequestTimestamp = 0 |
17 | 14 | ||
18 | // --------------------------------------------------------------------------- | 15 | // --------------------------------------------------------------------------- |
19 | 16 | ||
20 | const RequestSchema = mongoose.Schema({ | 17 | module.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 | |||
34 | RequestSchema.statics = { | ||
35 | activate, | ||
36 | deactivate, | ||
37 | flush, | ||
38 | forceSend, | ||
39 | list, | ||
40 | remainingMilliSeconds | ||
41 | } | ||
42 | |||
43 | RequestSchema.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 | |||
61 | mongoose.model('Request', RequestSchema) | ||
62 | 45 | ||
63 | // ------------------------------ STATICS ------------------------------ | 46 | // ------------------------------ STATICS ------------------------------ |
64 | 47 | ||
48 | function 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 | |||
65 | function activate () { | 59 | function 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 | ||
70 | function countTotalRequests (callback) { | ||
71 | const query = { | ||
72 | include: [ this.sequelize.models.Pod ] | ||
73 | } | ||
74 | |||
75 | return this.count(query).asCallback(callback) | ||
76 | } | ||
77 | |||
76 | function deactivate () { | 78 | function 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 | ||
82 | function flush () { | 84 | function 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 | ||
93 | function list (callback) { | ||
94 | this.find({ }, callback) | ||
95 | } | ||
96 | |||
97 | function remainingMilliSeconds () { | 97 | function 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 |
137 | function makeRequests () { | 137 | function 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) |
222 | function removeBadPods () { | 220 | function 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 | ||
255 | function updatePodsScore (goodPods, badPods) { | 253 | function 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 | ||
268 | function listWithLimitAndRandom (limit, callback) { | 273 | function 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 | ||
281 | function removeAll (callback) { | 310 | function groupAndTruncateRequests (requests, limitRequestsPerPod) { |
282 | this.remove({ }, callback) | 311 | const requestsGrouped = {} |
283 | } | ||
284 | 312 | ||
285 | function 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 | |||
329 | function removeAll (callback) { | ||
330 | // Delete all requests | ||
331 | this.truncate({ cascade: true }).asCallback(callback) | ||
289 | } | 332 | } |
290 | 333 | ||
291 | function removeWithEmptyTo (callback) { | 334 | function 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 | |||
3 | const each = require('async/each') | ||
4 | |||
5 | // --------------------------------------------------------------------------- | ||
6 | |||
7 | module.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 | |||
36 | function associate (models) { | ||
37 | this.belongsToMany(models.Video, { | ||
38 | foreignKey: 'tagId', | ||
39 | through: models.VideoTag, | ||
40 | onDelete: 'cascade' | ||
41 | }) | ||
42 | } | ||
43 | |||
44 | function 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 @@ | |||
1 | const mongoose = require('mongoose') | 1 | 'use strict' |
2 | |||
3 | const values = require('lodash/values') | ||
2 | 4 | ||
3 | const customUsersValidators = require('../helpers/custom-validators').users | ||
4 | const modelUtils = require('./utils') | 5 | const modelUtils = require('./utils') |
6 | const constants = require('../initializers/constants') | ||
5 | const peertubeCrypto = require('../helpers/peertube-crypto') | 7 | const peertubeCrypto = require('../helpers/peertube-crypto') |
6 | 8 | const customUsersValidators = require('../helpers/custom-validators').users | |
7 | const OAuthToken = mongoose.model('OAuthToken') | ||
8 | 9 | ||
9 | // --------------------------------------------------------------------------- | 10 | // --------------------------------------------------------------------------- |
10 | 11 | ||
11 | const UserSchema = mongoose.Schema({ | 12 | module.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.') | |
21 | UserSchema.path('password').required(customUsersValidators.isUserPasswordValid) | 22 | } |
22 | UserSchema.path('username').required(customUsersValidators.isUserUsernameValid) | 23 | } |
23 | UserSchema.path('role').validate(customUsersValidators.isUserRoleValid) | 24 | }, |
24 | 25 | username: { | |
25 | UserSchema.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) | |
30 | UserSchema.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 | ||
39 | UserSchema.pre('save', function (next) { | 70 | function 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 | |||
51 | UserSchema.pre('remove', function (next) { | ||
52 | const user = this | ||
53 | |||
54 | OAuthToken.removeByUserId(user._id, next) | ||
55 | }) | ||
56 | |||
57 | mongoose.model('User', UserSchema) | ||
58 | 79 | ||
59 | // ------------------------------ METHODS ------------------------------ | 80 | // ------------------------------ METHODS ------------------------------ |
60 | 81 | ||
@@ -64,35 +85,68 @@ function isPasswordMatch (password, callback) { | |||
64 | 85 | ||
65 | function toFormatedJSON () { | 86 | function 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 | ||
96 | function 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 | |||
75 | function countTotal (callback) { | 108 | function countTotal (callback) { |
76 | return this.count(callback) | 109 | return this.count().asCallback(callback) |
77 | } | 110 | } |
78 | 111 | ||
79 | function getByUsername (username) { | 112 | function 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 | ||
83 | function list (callback) { | 122 | function list (callback) { |
84 | return this.find(callback) | 123 | return this.find().asCallback(callback) |
85 | } | 124 | } |
86 | 125 | ||
87 | function listForApi (start, count, sort, callback) { | 126 | function 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 | ||
92 | function loadById (id, callback) { | 140 | function loadById (id, callback) { |
93 | return this.findById(id, callback) | 141 | return this.findById(id).asCallback(callback) |
94 | } | 142 | } |
95 | 143 | ||
96 | function loadByUsername (username, callback) { | 144 | function 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 | ||
3 | const parallel = require('async/parallel') | ||
4 | |||
5 | const utils = { | 3 | const utils = { |
6 | listForApiWithCount | 4 | getSort |
7 | } | 5 | } |
8 | 6 | ||
9 | function listForApiWithCount (query, start, count, sort, callback) { | 7 | // Translate for example "-name" to [ 'name', 'DESC' ] |
10 | const self = this | 8 | function 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 | |||
3 | const constants = require('../initializers/constants') | ||
4 | const modelUtils = require('./utils') | ||
5 | const customVideosValidators = require('../helpers/custom-validators').videos | ||
6 | |||
7 | module.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 | |||
56 | function 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 | |||
74 | function 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 | |||
94 | function 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 | |||
5 | module.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 | ||
3 | const Buffer = require('safe-buffer').Buffer | ||
3 | const createTorrent = require('create-torrent') | 4 | const createTorrent = require('create-torrent') |
4 | const ffmpeg = require('fluent-ffmpeg') | 5 | const ffmpeg = require('fluent-ffmpeg') |
5 | const fs = require('fs') | 6 | const fs = require('fs') |
6 | const magnetUtil = require('magnet-uri') | 7 | const magnetUtil = require('magnet-uri') |
8 | const map = require('lodash/map') | ||
7 | const parallel = require('async/parallel') | 9 | const parallel = require('async/parallel') |
8 | const parseTorrent = require('parse-torrent') | 10 | const parseTorrent = require('parse-torrent') |
9 | const pathUtils = require('path') | 11 | const pathUtils = require('path') |
10 | const mongoose = require('mongoose') | 12 | const values = require('lodash/values') |
11 | 13 | ||
12 | const constants = require('../initializers/constants') | 14 | const constants = require('../initializers/constants') |
13 | const customVideosValidators = require('../helpers/custom-validators').videos | ||
14 | const logger = require('../helpers/logger') | 15 | const logger = require('../helpers/logger') |
16 | const friends = require('../lib/friends') | ||
15 | const modelUtils = require('./utils') | 17 | const modelUtils = require('./utils') |
18 | const customVideosValidators = require('../helpers/custom-validators').videos | ||
16 | 19 | ||
17 | // --------------------------------------------------------------------------- | 20 | // --------------------------------------------------------------------------- |
18 | 21 | ||
19 | // TODO: add indexes on searchable columns | 22 | module.exports = function (sequelize, DataTypes) { |
20 | const 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 | |||
41 | VideoSchema.path('name').validate(customVideosValidators.isVideoNameValid) | ||
42 | VideoSchema.path('description').validate(customVideosValidators.isVideoDescriptionValid) | ||
43 | VideoSchema.path('podHost').validate(customVideosValidators.isVideoPodHostValid) | ||
44 | VideoSchema.path('author').validate(customVideosValidators.isVideoAuthorValid) | ||
45 | VideoSchema.path('duration').validate(customVideosValidators.isVideoDurationValid) | ||
46 | VideoSchema.path('tags').validate(customVideosValidators.isVideoTagsValid) | ||
47 | |||
48 | VideoSchema.methods = { | ||
49 | generateMagnetUri, | ||
50 | getVideoFilename, | ||
51 | getThumbnailName, | ||
52 | getPreviewName, | ||
53 | getTorrentName, | ||
54 | isOwned, | ||
55 | toFormatedJSON, | ||
56 | toRemoteJSON | ||
57 | } | ||
58 | |||
59 | VideoSchema.statics = { | ||
60 | generateThumbnailFromBase64, | ||
61 | getDurationFromFile, | ||
62 | listForApi, | ||
63 | listByHostAndRemoteId, | ||
64 | listByHost, | ||
65 | listOwned, | ||
66 | listOwnedByAuthor, | ||
67 | listRemotes, | ||
68 | load, | ||
69 | search | ||
70 | } | ||
71 | |||
72 | VideoSchema.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 | |||
143 | function 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 | ||
99 | VideoSchema.pre('save', function (next) { | 153 | function 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 | |||
197 | function afterDestroy (video, options, next) { | ||
198 | const tasks = [] | ||
145 | 199 | ||
146 | mongoose.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 | ||
237 | function 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 | |||
150 | function generateMagnetUri () { | 261 | function 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 | ||
176 | function getVideoFilename () { | 287 | function 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 | ||
182 | function getThumbnailName () { | 293 | function 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 | ||
187 | function getPreviewName () { | 298 | function 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 () { | |||
195 | function getTorrentName () { | 306 | function 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 | ||
207 | function toFormatedJSON () { | 318 | function 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 | ||
225 | function toRemoteJSON (callback) { | 346 | function 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 | ||
375 | function 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 | ||
255 | function generateThumbnailFromBase64 (video, thumbnailData, callback) { | 394 | function 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 | ||
275 | function listForApi (start, count, sort, callback) { | 414 | function 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 | ||
280 | function listByHostAndRemoteId (fromHost, remoteId, callback) { | 418 | function 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 | ||
284 | function listByHost (fromHost, callback) { | 441 | function 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 | ||
288 | function listOwned (callback) { | 465 | function 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 | ||
293 | function listOwnedByAuthor (author, callback) { | 477 | function 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 | ||
297 | function listRemotes (callback) { | 492 | return this.findAll(query).asCallback(callback) |
298 | this.find({ remoteId: { $ne: null } }, callback) | ||
299 | } | 493 | } |
300 | 494 | ||
301 | function load (id, callback) { | 495 | function load (id, callback) { |
302 | this.findById(id, callback) | 496 | return this.findById(id).asCallback(callback) |
497 | } | ||
498 | |||
499 | function loadAndPopulateAuthor (id, callback) { | ||
500 | const options = { | ||
501 | include: [ this.sequelize.models.Author ] | ||
502 | } | ||
503 | |||
504 | return this.findById(id, options).asCallback(callback) | ||
505 | } | ||
506 | |||
507 | function 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 | ||
305 | function search (value, field, start, count, sort, callback) { | 521 | function 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 | // --------------------------------------------------------------------------- |