diff options
author | Chocobozzz <florian.bigard@gmail.com> | 2016-12-11 21:50:51 +0100 |
---|---|---|
committer | Chocobozzz <florian.bigard@gmail.com> | 2016-12-19 21:22:28 +0100 |
commit | feb4bdfd9b46e87aadfa7c0d5338cde887d1f58c (patch) | |
tree | 2abc9fbc9569760e218fd52835850b757344b420 /server/models | |
parent | 108626609eda75e4ecc0a83a650a4d53c46220e0 (diff) | |
download | PeerTube-feb4bdfd9b46e87aadfa7c0d5338cde887d1f58c.tar.gz PeerTube-feb4bdfd9b46e87aadfa7c0d5338cde887d1f58c.tar.zst PeerTube-feb4bdfd9b46e87aadfa7c0d5338cde887d1f58c.zip |
First version with PostgreSQL
Diffstat (limited to 'server/models')
-rw-r--r-- | server/models/application.js | 45 | ||||
-rw-r--r-- | server/models/author.js | 28 | ||||
-rw-r--r-- | server/models/oauth-client.js | 72 | ||||
-rw-r--r-- | server/models/oauth-token.js | 109 | ||||
-rw-r--r-- | server/models/pods.js | 156 | ||||
-rw-r--r-- | server/models/request.js | 187 | ||||
-rw-r--r-- | server/models/requestToPod.js | 30 | ||||
-rw-r--r-- | server/models/user.js | 132 | ||||
-rw-r--r-- | server/models/utils.js | 31 | ||||
-rw-r--r-- | server/models/video.js | 388 |
10 files changed, 773 insertions, 405 deletions
diff --git a/server/models/application.js b/server/models/application.js index 452ac4283..ec1d7b122 100644 --- a/server/models/application.js +++ b/server/models/application.js | |||
@@ -1,31 +1,36 @@ | |||
1 | const mongoose = require('mongoose') | 1 | module.exports = function (sequelize, DataTypes) { |
2 | const Application = sequelize.define('Application', | ||
3 | { | ||
4 | sqlSchemaVersion: { | ||
5 | type: DataTypes.INTEGER, | ||
6 | defaultValue: 0 | ||
7 | } | ||
8 | }, | ||
9 | { | ||
10 | classMethods: { | ||
11 | loadSqlSchemaVersion, | ||
12 | updateSqlSchemaVersion | ||
13 | } | ||
14 | } | ||
15 | ) | ||
16 | |||
17 | return Application | ||
18 | } | ||
2 | 19 | ||
3 | // --------------------------------------------------------------------------- | 20 | // --------------------------------------------------------------------------- |
4 | 21 | ||
5 | const ApplicationSchema = mongoose.Schema({ | 22 | function loadSqlSchemaVersion (callback) { |
6 | mongoSchemaVersion: { | 23 | const query = { |
7 | type: Number, | 24 | attributes: [ 'sqlSchemaVersion' ] |
8 | default: 0 | ||
9 | } | 25 | } |
10 | }) | ||
11 | |||
12 | ApplicationSchema.statics = { | ||
13 | loadMongoSchemaVersion, | ||
14 | updateMongoSchemaVersion | ||
15 | } | ||
16 | |||
17 | mongoose.model('Application', ApplicationSchema) | ||
18 | |||
19 | // --------------------------------------------------------------------------- | ||
20 | 26 | ||
21 | function loadMongoSchemaVersion (callback) { | 27 | return this.findOne(query).asCallback(function (err, data) { |
22 | return this.findOne({}, { mongoSchemaVersion: 1 }, function (err, data) { | 28 | const version = data ? data.sqlSchemaVersion : 0 |
23 | const version = data ? data.mongoSchemaVersion : 0 | ||
24 | 29 | ||
25 | return callback(err, version) | 30 | return callback(err, version) |
26 | }) | 31 | }) |
27 | } | 32 | } |
28 | 33 | ||
29 | function updateMongoSchemaVersion (newVersion, callback) { | 34 | function updateSqlSchemaVersion (newVersion, callback) { |
30 | return this.update({}, { mongoSchemaVersion: newVersion }, callback) | 35 | return this.update({ sqlSchemaVersion: newVersion }).asCallback(callback) |
31 | } | 36 | } |
diff --git a/server/models/author.js b/server/models/author.js new file mode 100644 index 000000000..493c2ca11 --- /dev/null +++ b/server/models/author.js | |||
@@ -0,0 +1,28 @@ | |||
1 | module.exports = function (sequelize, DataTypes) { | ||
2 | const Author = sequelize.define('Author', | ||
3 | { | ||
4 | name: { | ||
5 | type: DataTypes.STRING | ||
6 | } | ||
7 | }, | ||
8 | { | ||
9 | classMethods: { | ||
10 | associate | ||
11 | } | ||
12 | } | ||
13 | ) | ||
14 | |||
15 | return Author | ||
16 | } | ||
17 | |||
18 | // --------------------------------------------------------------------------- | ||
19 | |||
20 | function associate (models) { | ||
21 | this.belongsTo(models.Pod, { | ||
22 | foreignKey: { | ||
23 | name: 'podId', | ||
24 | allowNull: true | ||
25 | }, | ||
26 | onDelete: 'cascade' | ||
27 | }) | ||
28 | } | ||
diff --git a/server/models/oauth-client.js b/server/models/oauth-client.js index a1aefa985..15118591a 100644 --- a/server/models/oauth-client.js +++ b/server/models/oauth-client.js | |||
@@ -1,33 +1,63 @@ | |||
1 | const mongoose = require('mongoose') | 1 | module.exports = function (sequelize, DataTypes) { |
2 | 2 | const OAuthClient = sequelize.define('OAuthClient', | |
3 | // --------------------------------------------------------------------------- | 3 | { |
4 | 4 | clientId: { | |
5 | const OAuthClientSchema = mongoose.Schema({ | 5 | type: DataTypes.STRING |
6 | clientSecret: String, | 6 | }, |
7 | grants: Array, | 7 | clientSecret: { |
8 | redirectUris: Array | 8 | type: DataTypes.STRING |
9 | }) | 9 | }, |
10 | 10 | grants: { | |
11 | OAuthClientSchema.path('clientSecret').required(true) | 11 | type: DataTypes.ARRAY(DataTypes.STRING) |
12 | 12 | }, | |
13 | OAuthClientSchema.statics = { | 13 | redirectUris: { |
14 | getByIdAndSecret, | 14 | type: DataTypes.ARRAY(DataTypes.STRING) |
15 | list, | 15 | } |
16 | loadFirstClient | 16 | }, |
17 | { | ||
18 | classMethods: { | ||
19 | associate, | ||
20 | |||
21 | getByIdAndSecret, | ||
22 | list, | ||
23 | loadFirstClient | ||
24 | } | ||
25 | } | ||
26 | ) | ||
27 | |||
28 | return OAuthClient | ||
17 | } | 29 | } |
18 | 30 | ||
19 | mongoose.model('OAuthClient', OAuthClientSchema) | 31 | // TODO: validation |
32 | // OAuthClientSchema.path('clientSecret').required(true) | ||
20 | 33 | ||
21 | // --------------------------------------------------------------------------- | 34 | // --------------------------------------------------------------------------- |
22 | 35 | ||
36 | function associate (models) { | ||
37 | this.hasMany(models.OAuthToken, { | ||
38 | foreignKey: { | ||
39 | name: 'oAuthClientId', | ||
40 | allowNull: false | ||
41 | }, | ||
42 | onDelete: 'cascade' | ||
43 | }) | ||
44 | } | ||
45 | |||
23 | function list (callback) { | 46 | function list (callback) { |
24 | return this.find(callback) | 47 | return this.findAll().asCallback(callback) |
25 | } | 48 | } |
26 | 49 | ||
27 | function loadFirstClient (callback) { | 50 | function loadFirstClient (callback) { |
28 | return this.findOne({}, callback) | 51 | return this.findOne().asCallback(callback) |
29 | } | 52 | } |
30 | 53 | ||
31 | function getByIdAndSecret (id, clientSecret) { | 54 | function getByIdAndSecret (clientId, clientSecret) { |
32 | return this.findOne({ _id: id, clientSecret: clientSecret }).exec() | 55 | const query = { |
56 | where: { | ||
57 | clientId: clientId, | ||
58 | clientSecret: clientSecret | ||
59 | } | ||
60 | } | ||
61 | |||
62 | return this.findOne(query) | ||
33 | } | 63 | } |
diff --git a/server/models/oauth-token.js b/server/models/oauth-token.js index aff73bfb1..c9108bf95 100644 --- a/server/models/oauth-token.js +++ b/server/models/oauth-token.js | |||
@@ -1,42 +1,71 @@ | |||
1 | const mongoose = require('mongoose') | ||
2 | |||
3 | const logger = require('../helpers/logger') | 1 | const logger = require('../helpers/logger') |
4 | 2 | ||
5 | // --------------------------------------------------------------------------- | 3 | // --------------------------------------------------------------------------- |
6 | 4 | ||
7 | const OAuthTokenSchema = mongoose.Schema({ | 5 | module.exports = function (sequelize, DataTypes) { |
8 | accessToken: String, | 6 | const OAuthToken = sequelize.define('OAuthToken', |
9 | accessTokenExpiresAt: Date, | 7 | { |
10 | client: { type: mongoose.Schema.Types.ObjectId, ref: 'OAuthClient' }, | 8 | accessToken: { |
11 | refreshToken: String, | 9 | type: DataTypes.STRING |
12 | refreshTokenExpiresAt: Date, | 10 | }, |
13 | user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' } | 11 | accessTokenExpiresAt: { |
14 | }) | 12 | type: DataTypes.DATE |
15 | 13 | }, | |
16 | OAuthTokenSchema.path('accessToken').required(true) | 14 | refreshToken: { |
17 | OAuthTokenSchema.path('client').required(true) | 15 | type: DataTypes.STRING |
18 | OAuthTokenSchema.path('user').required(true) | 16 | }, |
19 | 17 | refreshTokenExpiresAt: { | |
20 | OAuthTokenSchema.statics = { | 18 | type: DataTypes.DATE |
21 | getByRefreshTokenAndPopulateClient, | 19 | } |
22 | getByTokenAndPopulateUser, | 20 | }, |
23 | getByRefreshTokenAndPopulateUser, | 21 | { |
24 | removeByUserId | 22 | classMethods: { |
23 | associate, | ||
24 | |||
25 | getByRefreshTokenAndPopulateClient, | ||
26 | getByTokenAndPopulateUser, | ||
27 | getByRefreshTokenAndPopulateUser, | ||
28 | removeByUserId | ||
29 | } | ||
30 | } | ||
31 | ) | ||
32 | |||
33 | return OAuthToken | ||
25 | } | 34 | } |
26 | 35 | ||
27 | mongoose.model('OAuthToken', OAuthTokenSchema) | 36 | // TODO: validation |
37 | // OAuthTokenSchema.path('accessToken').required(true) | ||
38 | // OAuthTokenSchema.path('client').required(true) | ||
39 | // OAuthTokenSchema.path('user').required(true) | ||
28 | 40 | ||
29 | // --------------------------------------------------------------------------- | 41 | // --------------------------------------------------------------------------- |
30 | 42 | ||
43 | function associate (models) { | ||
44 | this.belongsTo(models.User, { | ||
45 | foreignKey: { | ||
46 | name: 'userId', | ||
47 | allowNull: false | ||
48 | }, | ||
49 | onDelete: 'cascade' | ||
50 | }) | ||
51 | } | ||
52 | |||
31 | function getByRefreshTokenAndPopulateClient (refreshToken) { | 53 | function getByRefreshTokenAndPopulateClient (refreshToken) { |
32 | return this.findOne({ refreshToken: refreshToken }).populate('client').exec().then(function (token) { | 54 | const query = { |
55 | where: { | ||
56 | refreshToken: refreshToken | ||
57 | }, | ||
58 | include: [ this.associations.OAuthClient ] | ||
59 | } | ||
60 | |||
61 | return this.findOne(query).then(function (token) { | ||
33 | if (!token) return token | 62 | if (!token) return token |
34 | 63 | ||
35 | const tokenInfos = { | 64 | const tokenInfos = { |
36 | refreshToken: token.refreshToken, | 65 | refreshToken: token.refreshToken, |
37 | refreshTokenExpiresAt: token.refreshTokenExpiresAt, | 66 | refreshTokenExpiresAt: token.refreshTokenExpiresAt, |
38 | client: { | 67 | client: { |
39 | id: token.client._id.toString() | 68 | id: token.client.id |
40 | }, | 69 | }, |
41 | user: { | 70 | user: { |
42 | id: token.user | 71 | id: token.user |
@@ -50,13 +79,41 @@ function getByRefreshTokenAndPopulateClient (refreshToken) { | |||
50 | } | 79 | } |
51 | 80 | ||
52 | function getByTokenAndPopulateUser (bearerToken) { | 81 | function getByTokenAndPopulateUser (bearerToken) { |
53 | return this.findOne({ accessToken: bearerToken }).populate('user').exec() | 82 | const query = { |
83 | where: { | ||
84 | accessToken: bearerToken | ||
85 | }, | ||
86 | include: [ this.sequelize.models.User ] | ||
87 | } | ||
88 | |||
89 | return this.findOne(query).then(function (token) { | ||
90 | if (token) token.user = token.User | ||
91 | |||
92 | return token | ||
93 | }) | ||
54 | } | 94 | } |
55 | 95 | ||
56 | function getByRefreshTokenAndPopulateUser (refreshToken) { | 96 | function getByRefreshTokenAndPopulateUser (refreshToken) { |
57 | return this.findOne({ refreshToken: refreshToken }).populate('user').exec() | 97 | const query = { |
98 | where: { | ||
99 | refreshToken: refreshToken | ||
100 | }, | ||
101 | include: [ this.sequelize.models.User ] | ||
102 | } | ||
103 | |||
104 | return this.findOne(query).then(function (token) { | ||
105 | token.user = token.User | ||
106 | |||
107 | return token | ||
108 | }) | ||
58 | } | 109 | } |
59 | 110 | ||
60 | function removeByUserId (userId, callback) { | 111 | function removeByUserId (userId, callback) { |
61 | return this.remove({ user: userId }, callback) | 112 | const query = { |
113 | where: { | ||
114 | userId: userId | ||
115 | } | ||
116 | } | ||
117 | |||
118 | return this.destroy(query).asCallback(callback) | ||
62 | } | 119 | } |
diff --git a/server/models/pods.js b/server/models/pods.js index 49c73472a..2c1f56203 100644 --- a/server/models/pods.js +++ b/server/models/pods.js | |||
@@ -1,79 +1,62 @@ | |||
1 | 'use strict' | 1 | 'use strict' |
2 | 2 | ||
3 | const each = require('async/each') | ||
4 | const mongoose = require('mongoose') | ||
5 | const map = require('lodash/map') | 3 | const map = require('lodash/map') |
6 | const validator = require('express-validator').validator | ||
7 | 4 | ||
8 | const constants = require('../initializers/constants') | 5 | const constants = require('../initializers/constants') |
9 | 6 | ||
10 | const Video = mongoose.model('Video') | ||
11 | |||
12 | // --------------------------------------------------------------------------- | 7 | // --------------------------------------------------------------------------- |
13 | 8 | ||
14 | const PodSchema = mongoose.Schema({ | 9 | module.exports = function (sequelize, DataTypes) { |
15 | host: String, | 10 | const Pod = sequelize.define('Pod', |
16 | publicKey: String, | 11 | { |
17 | score: { type: Number, max: constants.FRIEND_SCORE.MAX }, | 12 | host: { |
18 | createdDate: { | 13 | type: DataTypes.STRING |
19 | type: Date, | 14 | }, |
20 | default: Date.now | 15 | publicKey: { |
21 | } | 16 | type: DataTypes.STRING(5000) |
22 | }) | 17 | }, |
23 | 18 | score: { | |
24 | PodSchema.path('host').validate(validator.isURL) | 19 | type: DataTypes.INTEGER, |
25 | PodSchema.path('publicKey').required(true) | 20 | defaultValue: constants.FRIEND_SCORE.BASE |
26 | PodSchema.path('score').validate(function (value) { return !isNaN(value) }) | 21 | } |
27 | 22 | // Check createdAt | |
28 | PodSchema.methods = { | 23 | }, |
29 | toFormatedJSON | 24 | { |
25 | classMethods: { | ||
26 | associate, | ||
27 | |||
28 | countAll, | ||
29 | incrementScores, | ||
30 | list, | ||
31 | listAllIds, | ||
32 | listBadPods, | ||
33 | load, | ||
34 | loadByHost, | ||
35 | removeAll | ||
36 | }, | ||
37 | instanceMethods: { | ||
38 | toFormatedJSON | ||
39 | } | ||
40 | } | ||
41 | ) | ||
42 | |||
43 | return Pod | ||
30 | } | 44 | } |
31 | 45 | ||
32 | PodSchema.statics = { | 46 | // TODO: max score -> constants.FRIENDS_SCORE.MAX |
33 | countAll, | 47 | // TODO: validation |
34 | incrementScores, | 48 | // PodSchema.path('host').validate(validator.isURL) |
35 | list, | 49 | // PodSchema.path('publicKey').required(true) |
36 | listAllIds, | 50 | // PodSchema.path('score').validate(function (value) { return !isNaN(value) }) |
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 | 51 | ||
69 | // ------------------------------ METHODS ------------------------------ | 52 | // ------------------------------ METHODS ------------------------------ |
70 | 53 | ||
71 | function toFormatedJSON () { | 54 | function toFormatedJSON () { |
72 | const json = { | 55 | const json = { |
73 | id: this._id, | 56 | id: this.id, |
74 | host: this.host, | 57 | host: this.host, |
75 | score: this.score, | 58 | score: this.score, |
76 | createdDate: this.createdDate | 59 | createdAt: this.createdAt |
77 | } | 60 | } |
78 | 61 | ||
79 | return json | 62 | return json |
@@ -81,39 +64,76 @@ function toFormatedJSON () { | |||
81 | 64 | ||
82 | // ------------------------------ Statics ------------------------------ | 65 | // ------------------------------ Statics ------------------------------ |
83 | 66 | ||
67 | function associate (models) { | ||
68 | this.belongsToMany(models.Request, { | ||
69 | foreignKey: 'podId', | ||
70 | through: models.RequestToPod, | ||
71 | onDelete: 'CASCADE' | ||
72 | }) | ||
73 | } | ||
74 | |||
84 | function countAll (callback) { | 75 | function countAll (callback) { |
85 | return this.count(callback) | 76 | return this.count().asCallback(callback) |
86 | } | 77 | } |
87 | 78 | ||
88 | function incrementScores (ids, value, callback) { | 79 | function incrementScores (ids, value, callback) { |
89 | if (!callback) callback = function () {} | 80 | if (!callback) callback = function () {} |
90 | return this.update({ _id: { $in: ids } }, { $inc: { score: value } }, { multi: true }, callback) | 81 | |
82 | const update = { | ||
83 | score: this.sequelize.literal('score +' + value) | ||
84 | } | ||
85 | |||
86 | const query = { | ||
87 | where: { | ||
88 | id: { | ||
89 | $in: ids | ||
90 | } | ||
91 | } | ||
92 | } | ||
93 | |||
94 | return this.update(update, query).asCallback(callback) | ||
91 | } | 95 | } |
92 | 96 | ||
93 | function list (callback) { | 97 | function list (callback) { |
94 | return this.find(callback) | 98 | return this.findAll().asCallback(callback) |
95 | } | 99 | } |
96 | 100 | ||
97 | function listAllIds (callback) { | 101 | function listAllIds (callback) { |
98 | return this.find({}, { _id: 1 }, function (err, pods) { | 102 | const query = { |
103 | attributes: [ 'id' ] | ||
104 | } | ||
105 | |||
106 | return this.findAll(query).asCallback(function (err, pods) { | ||
99 | if (err) return callback(err) | 107 | if (err) return callback(err) |
100 | 108 | ||
101 | return callback(null, map(pods, '_id')) | 109 | return callback(null, map(pods, 'id')) |
102 | }) | 110 | }) |
103 | } | 111 | } |
104 | 112 | ||
105 | function listBadPods (callback) { | 113 | function listBadPods (callback) { |
106 | return this.find({ score: 0 }, callback) | 114 | const query = { |
115 | where: { | ||
116 | score: { $lte: 0 } | ||
117 | } | ||
118 | } | ||
119 | |||
120 | return this.findAll(query).asCallback(callback) | ||
107 | } | 121 | } |
108 | 122 | ||
109 | function load (id, callback) { | 123 | function load (id, callback) { |
110 | return this.findById(id, callback) | 124 | return this.findById(id).asCallback(callback) |
111 | } | 125 | } |
112 | 126 | ||
113 | function loadByHost (host, callback) { | 127 | function loadByHost (host, callback) { |
114 | return this.findOne({ host }, callback) | 128 | const query = { |
129 | where: { | ||
130 | host: host | ||
131 | } | ||
132 | } | ||
133 | |||
134 | return this.findOne(query).asCallback(callback) | ||
115 | } | 135 | } |
116 | 136 | ||
117 | function removeAll (callback) { | 137 | function removeAll (callback) { |
118 | return this.remove({}, callback) | 138 | return this.destroy().asCallback(callback) |
119 | } | 139 | } |
diff --git a/server/models/request.js b/server/models/request.js index c2cfe83ce..882f747b7 100644 --- a/server/models/request.js +++ b/server/models/request.js | |||
@@ -2,66 +2,58 @@ | |||
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') |
8 | 6 | ||
9 | const constants = require('../initializers/constants') | 7 | const constants = require('../initializers/constants') |
10 | const logger = require('../helpers/logger') | 8 | const logger = require('../helpers/logger') |
11 | const requests = require('../helpers/requests') | 9 | const requests = require('../helpers/requests') |
12 | 10 | ||
13 | const Pod = mongoose.model('Pod') | ||
14 | |||
15 | let timer = null | 11 | let timer = null |
16 | let lastRequestTimestamp = 0 | 12 | let lastRequestTimestamp = 0 |
17 | 13 | ||
18 | // --------------------------------------------------------------------------- | 14 | // --------------------------------------------------------------------------- |
19 | 15 | ||
20 | const RequestSchema = mongoose.Schema({ | 16 | module.exports = function (sequelize, DataTypes) { |
21 | request: mongoose.Schema.Types.Mixed, | 17 | const Request = sequelize.define('Request', |
22 | endpoint: { | 18 | { |
23 | type: String, | 19 | request: { |
24 | enum: [ values(constants.REQUEST_ENDPOINTS) ] | 20 | type: DataTypes.JSON |
25 | }, | 21 | }, |
26 | to: [ | 22 | endpoint: { |
23 | // TODO: enum? | ||
24 | type: DataTypes.STRING | ||
25 | } | ||
26 | }, | ||
27 | { | 27 | { |
28 | type: mongoose.Schema.Types.ObjectId, | 28 | classMethods: { |
29 | ref: 'Pod' | 29 | associate, |
30 | |||
31 | activate, | ||
32 | countTotalRequests, | ||
33 | deactivate, | ||
34 | flush, | ||
35 | forceSend, | ||
36 | remainingMilliSeconds | ||
37 | } | ||
30 | } | 38 | } |
31 | ] | 39 | ) |
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 | |||
50 | // No friends | ||
51 | if (podIds.length === 0) return | ||
52 | |||
53 | self.to = podIds | ||
54 | return next() | ||
55 | }) | ||
56 | } else { | ||
57 | return next() | ||
58 | } | ||
59 | }) | ||
60 | 40 | ||
61 | mongoose.model('Request', RequestSchema) | 41 | return Request |
42 | } | ||
62 | 43 | ||
63 | // ------------------------------ STATICS ------------------------------ | 44 | // ------------------------------ STATICS ------------------------------ |
64 | 45 | ||
46 | function associate (models) { | ||
47 | this.belongsToMany(models.Pod, { | ||
48 | foreignKey: { | ||
49 | name: 'requestId', | ||
50 | allowNull: false | ||
51 | }, | ||
52 | through: models.RequestToPod, | ||
53 | onDelete: 'CASCADE' | ||
54 | }) | ||
55 | } | ||
56 | |||
65 | function activate () { | 57 | function activate () { |
66 | logger.info('Requests scheduler activated.') | 58 | logger.info('Requests scheduler activated.') |
67 | lastRequestTimestamp = Date.now() | 59 | lastRequestTimestamp = Date.now() |
@@ -73,6 +65,14 @@ function activate () { | |||
73 | }, constants.REQUESTS_INTERVAL) | 65 | }, constants.REQUESTS_INTERVAL) |
74 | } | 66 | } |
75 | 67 | ||
68 | function countTotalRequests (callback) { | ||
69 | const query = { | ||
70 | include: [ this.sequelize.models.Pod ] | ||
71 | } | ||
72 | |||
73 | return this.count(query).asCallback(callback) | ||
74 | } | ||
75 | |||
76 | function deactivate () { | 76 | function deactivate () { |
77 | logger.info('Requests scheduler deactivated.') | 77 | logger.info('Requests scheduler deactivated.') |
78 | clearInterval(timer) | 78 | clearInterval(timer) |
@@ -90,10 +90,6 @@ function forceSend () { | |||
90 | makeRequests.call(this) | 90 | makeRequests.call(this) |
91 | } | 91 | } |
92 | 92 | ||
93 | function list (callback) { | ||
94 | this.find({ }, callback) | ||
95 | } | ||
96 | |||
97 | function remainingMilliSeconds () { | 93 | function remainingMilliSeconds () { |
98 | if (timer === null) return -1 | 94 | if (timer === null) return -1 |
99 | 95 | ||
@@ -136,6 +132,7 @@ function makeRequest (toPod, requestEndpoint, requestsToMake, callback) { | |||
136 | // Make all the requests of the scheduler | 132 | // Make all the requests of the scheduler |
137 | function makeRequests () { | 133 | function makeRequests () { |
138 | const self = this | 134 | const self = this |
135 | const RequestToPod = this.sequelize.models.RequestToPod | ||
139 | 136 | ||
140 | // We limit the size of the requests (REQUESTS_LIMIT) | 137 | // We limit the size of the requests (REQUESTS_LIMIT) |
141 | // We don't want to stuck with the same failing requests so we get a random list | 138 | // We don't want to stuck with the same failing requests so we get a random list |
@@ -156,20 +153,20 @@ function makeRequests () { | |||
156 | // We want to group requests by destinations pod and endpoint | 153 | // We want to group requests by destinations pod and endpoint |
157 | const requestsToMakeGrouped = {} | 154 | const requestsToMakeGrouped = {} |
158 | 155 | ||
159 | requests.forEach(function (poolRequest) { | 156 | requests.forEach(function (request) { |
160 | poolRequest.to.forEach(function (toPodId) { | 157 | request.Pods.forEach(function (toPod) { |
161 | const hashKey = toPodId + poolRequest.endpoint | 158 | const hashKey = toPod.id + request.endpoint |
162 | if (!requestsToMakeGrouped[hashKey]) { | 159 | if (!requestsToMakeGrouped[hashKey]) { |
163 | requestsToMakeGrouped[hashKey] = { | 160 | requestsToMakeGrouped[hashKey] = { |
164 | toPodId, | 161 | toPodId: toPod.id, |
165 | endpoint: poolRequest.endpoint, | 162 | endpoint: request.endpoint, |
166 | ids: [], // pool request ids, to delete them from the DB in the future | 163 | ids: [], // request ids, to delete them from the DB in the future |
167 | datas: [] // requests data, | 164 | datas: [] // requests data, |
168 | } | 165 | } |
169 | } | 166 | } |
170 | 167 | ||
171 | requestsToMakeGrouped[hashKey].ids.push(poolRequest._id) | 168 | requestsToMakeGrouped[hashKey].ids.push(request.id) |
172 | requestsToMakeGrouped[hashKey].datas.push(poolRequest.request) | 169 | requestsToMakeGrouped[hashKey].datas.push(request.request) |
173 | }) | 170 | }) |
174 | }) | 171 | }) |
175 | 172 | ||
@@ -179,8 +176,8 @@ function makeRequests () { | |||
179 | eachLimit(Object.keys(requestsToMakeGrouped), constants.REQUESTS_IN_PARALLEL, function (hashKey, callbackEach) { | 176 | eachLimit(Object.keys(requestsToMakeGrouped), constants.REQUESTS_IN_PARALLEL, function (hashKey, callbackEach) { |
180 | const requestToMake = requestsToMakeGrouped[hashKey] | 177 | const requestToMake = requestsToMakeGrouped[hashKey] |
181 | 178 | ||
182 | // FIXME: mongodb request inside a loop :/ | 179 | // FIXME: SQL request inside a loop :/ |
183 | Pod.load(requestToMake.toPodId, function (err, toPod) { | 180 | self.sequelize.models.Pod.load(requestToMake.toPodId, function (err, toPod) { |
184 | if (err) { | 181 | if (err) { |
185 | logger.error('Error finding pod by id.', { err: err }) | 182 | logger.error('Error finding pod by id.', { err: err }) |
186 | return callbackEach() | 183 | return callbackEach() |
@@ -191,7 +188,7 @@ function makeRequests () { | |||
191 | const requestIdsToDelete = requestToMake.ids | 188 | 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.toPodId) |
194 | removePodOf.call(self, requestIdsToDelete, requestToMake.toPodId) | 191 | RequestToPod.removePodOf.call(self, requestIdsToDelete, requestToMake.toPodId) |
195 | return callbackEach() | 192 | return callbackEach() |
196 | } | 193 | } |
197 | 194 | ||
@@ -202,7 +199,7 @@ function makeRequests () { | |||
202 | goodPods.push(requestToMake.toPodId) | 199 | goodPods.push(requestToMake.toPodId) |
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.toPodId, callbackEach) |
206 | } else { | 203 | } else { |
207 | badPods.push(requestToMake.toPodId) | 204 | badPods.push(requestToMake.toPodId) |
208 | callbackEach() | 205 | callbackEach() |
@@ -211,18 +208,22 @@ function makeRequests () { | |||
211 | }) | 208 | }) |
212 | }, function () { | 209 | }, function () { |
213 | // All the requests were made, we update the pods score | 210 | // All the requests were made, we update the pods score |
214 | updatePodsScore(goodPods, badPods) | 211 | updatePodsScore.call(self, goodPods, badPods) |
215 | // Flush requests with no pod | 212 | // Flush requests with no pod |
216 | removeWithEmptyTo.call(self) | 213 | removeWithEmptyTo.call(self, function (err) { |
214 | if (err) logger.error('Error when removing requests with no pods.', { error: err }) | ||
215 | }) | ||
217 | }) | 216 | }) |
218 | }) | 217 | }) |
219 | } | 218 | } |
220 | 219 | ||
221 | // Remove pods with a score of 0 (too many requests where they were unreachable) | 220 | // Remove pods with a score of 0 (too many requests where they were unreachable) |
222 | function removeBadPods () { | 221 | function removeBadPods () { |
222 | const self = this | ||
223 | |||
223 | waterfall([ | 224 | waterfall([ |
224 | function findBadPods (callback) { | 225 | function findBadPods (callback) { |
225 | Pod.listBadPods(function (err, pods) { | 226 | self.sequelize.models.Pod.listBadPods(function (err, pods) { |
226 | if (err) { | 227 | if (err) { |
227 | logger.error('Cannot find bad pods.', { error: err }) | 228 | logger.error('Cannot find bad pods.', { error: err }) |
228 | return callback(err) | 229 | return callback(err) |
@@ -233,10 +234,8 @@ function removeBadPods () { | |||
233 | }, | 234 | }, |
234 | 235 | ||
235 | function removeTheseBadPods (pods, callback) { | 236 | function removeTheseBadPods (pods, callback) { |
236 | if (pods.length === 0) return callback(null, 0) | ||
237 | |||
238 | each(pods, function (pod, callbackEach) { | 237 | each(pods, function (pod, callbackEach) { |
239 | pod.remove(callbackEach) | 238 | pod.destroy().asCallback(callbackEach) |
240 | }, function (err) { | 239 | }, function (err) { |
241 | return callback(err, pods.length) | 240 | return callback(err, pods.length) |
242 | }) | 241 | }) |
@@ -253,43 +252,67 @@ function removeBadPods () { | |||
253 | } | 252 | } |
254 | 253 | ||
255 | function updatePodsScore (goodPods, badPods) { | 254 | function updatePodsScore (goodPods, badPods) { |
255 | const self = this | ||
256 | const Pod = this.sequelize.models.Pod | ||
257 | |||
256 | logger.info('Updating %d good pods and %d bad pods scores.', goodPods.length, badPods.length) | 258 | logger.info('Updating %d good pods and %d bad pods scores.', goodPods.length, badPods.length) |
257 | 259 | ||
258 | Pod.incrementScores(goodPods, constants.PODS_SCORE.BONUS, function (err) { | 260 | if (goodPods.length !== 0) { |
259 | if (err) logger.error('Cannot increment scores of good pods.') | 261 | Pod.incrementScores(goodPods, constants.PODS_SCORE.BONUS, function (err) { |
260 | }) | 262 | if (err) logger.error('Cannot increment scores of good pods.') |
263 | }) | ||
264 | } | ||
261 | 265 | ||
262 | Pod.incrementScores(badPods, constants.PODS_SCORE.MALUS, function (err) { | 266 | if (badPods.length !== 0) { |
263 | if (err) logger.error('Cannot decrement scores of bad pods.') | 267 | Pod.incrementScores(badPods, constants.PODS_SCORE.MALUS, function (err) { |
264 | removeBadPods() | 268 | if (err) logger.error('Cannot decrement scores of bad pods.') |
265 | }) | 269 | removeBadPods.call(self) |
270 | }) | ||
271 | } | ||
266 | } | 272 | } |
267 | 273 | ||
268 | function listWithLimitAndRandom (limit, callback) { | 274 | function listWithLimitAndRandom (limit, callback) { |
269 | const self = this | 275 | const self = this |
270 | 276 | ||
271 | self.count(function (err, count) { | 277 | self.count().asCallback(function (err, count) { |
272 | if (err) return callback(err) | 278 | if (err) return callback(err) |
273 | 279 | ||
280 | // Optimization... | ||
281 | if (count === 0) return callback(null, []) | ||
282 | |||
274 | let start = Math.floor(Math.random() * count) - limit | 283 | let start = Math.floor(Math.random() * count) - limit |
275 | if (start < 0) start = 0 | 284 | if (start < 0) start = 0 |
276 | 285 | ||
277 | self.find().sort({ _id: 1 }).skip(start).limit(limit).exec(callback) | 286 | const query = { |
287 | order: [ | ||
288 | [ 'id', 'ASC' ] | ||
289 | ], | ||
290 | offset: start, | ||
291 | limit: limit, | ||
292 | include: [ this.sequelize.models.Pod ] | ||
293 | } | ||
294 | |||
295 | self.findAll(query).asCallback(callback) | ||
278 | }) | 296 | }) |
279 | } | 297 | } |
280 | 298 | ||
281 | function removeAll (callback) { | 299 | function removeAll (callback) { |
282 | this.remove({ }, callback) | 300 | // Delete all requests |
283 | } | 301 | this.destroy({ truncate: true }).asCallback(callback) |
284 | |||
285 | function removePodOf (requestsIds, podId, callback) { | ||
286 | if (!callback) callback = function () {} | ||
287 | |||
288 | this.update({ _id: { $in: requestsIds } }, { $pull: { to: podId } }, { multi: true }, callback) | ||
289 | } | 302 | } |
290 | 303 | ||
291 | function removeWithEmptyTo (callback) { | 304 | function removeWithEmptyTo (callback) { |
292 | if (!callback) callback = function () {} | 305 | if (!callback) callback = function () {} |
293 | 306 | ||
294 | this.remove({ to: { $size: 0 } }, callback) | 307 | const query = { |
308 | where: { | ||
309 | id: { | ||
310 | $notIn: [ | ||
311 | this.sequelize.literal('SELECT "requestId" FROM "RequestToPods"') | ||
312 | ] | ||
313 | } | ||
314 | } | ||
315 | } | ||
316 | |||
317 | this.destroy(query).asCallback(callback) | ||
295 | } | 318 | } |
diff --git a/server/models/requestToPod.js b/server/models/requestToPod.js new file mode 100644 index 000000000..378c2bdcf --- /dev/null +++ b/server/models/requestToPod.js | |||
@@ -0,0 +1,30 @@ | |||
1 | 'use strict' | ||
2 | |||
3 | // --------------------------------------------------------------------------- | ||
4 | |||
5 | module.exports = function (sequelize, DataTypes) { | ||
6 | const RequestToPod = sequelize.define('RequestToPod', {}, { | ||
7 | classMethods: { | ||
8 | removePodOf | ||
9 | } | ||
10 | }) | ||
11 | |||
12 | return RequestToPod | ||
13 | } | ||
14 | |||
15 | // --------------------------------------------------------------------------- | ||
16 | |||
17 | function removePodOf (requestsIds, podId, callback) { | ||
18 | if (!callback) callback = function () {} | ||
19 | |||
20 | const query = { | ||
21 | where: { | ||
22 | requestId: { | ||
23 | $in: requestsIds | ||
24 | }, | ||
25 | podId: podId | ||
26 | } | ||
27 | } | ||
28 | |||
29 | this.destroy(query).asCallback(callback) | ||
30 | } | ||
diff --git a/server/models/user.js b/server/models/user.js index a19de7072..e50eb96ea 100644 --- a/server/models/user.js +++ b/server/models/user.js | |||
@@ -1,60 +1,60 @@ | |||
1 | const mongoose = require('mongoose') | ||
2 | |||
3 | const customUsersValidators = require('../helpers/custom-validators').users | ||
4 | const modelUtils = require('./utils') | 1 | const modelUtils = require('./utils') |
5 | const peertubeCrypto = require('../helpers/peertube-crypto') | 2 | const peertubeCrypto = require('../helpers/peertube-crypto') |
6 | 3 | ||
7 | const OAuthToken = mongoose.model('OAuthToken') | ||
8 | |||
9 | // --------------------------------------------------------------------------- | 4 | // --------------------------------------------------------------------------- |
10 | 5 | ||
11 | const UserSchema = mongoose.Schema({ | 6 | module.exports = function (sequelize, DataTypes) { |
12 | createdDate: { | 7 | const User = sequelize.define('User', |
13 | type: Date, | 8 | { |
14 | default: Date.now | 9 | password: { |
15 | }, | 10 | type: DataTypes.STRING |
16 | password: String, | 11 | }, |
17 | username: String, | 12 | username: { |
18 | role: String | 13 | type: DataTypes.STRING |
19 | }) | 14 | }, |
20 | 15 | role: { | |
21 | UserSchema.path('password').required(customUsersValidators.isUserPasswordValid) | 16 | type: DataTypes.STRING |
22 | UserSchema.path('username').required(customUsersValidators.isUserUsernameValid) | 17 | } |
23 | UserSchema.path('role').validate(customUsersValidators.isUserRoleValid) | 18 | }, |
24 | 19 | { | |
25 | UserSchema.methods = { | 20 | classMethods: { |
26 | isPasswordMatch, | 21 | associate, |
27 | toFormatedJSON | 22 | |
23 | countTotal, | ||
24 | getByUsername, | ||
25 | list, | ||
26 | listForApi, | ||
27 | loadById, | ||
28 | loadByUsername | ||
29 | }, | ||
30 | instanceMethods: { | ||
31 | isPasswordMatch, | ||
32 | toFormatedJSON | ||
33 | }, | ||
34 | hooks: { | ||
35 | beforeCreate: beforeCreateOrUpdate, | ||
36 | beforeUpdate: beforeCreateOrUpdate | ||
37 | } | ||
38 | } | ||
39 | ) | ||
40 | |||
41 | return User | ||
28 | } | 42 | } |
29 | 43 | ||
30 | UserSchema.statics = { | 44 | // TODO: Validation |
31 | countTotal, | 45 | // UserSchema.path('password').required(customUsersValidators.isUserPasswordValid) |
32 | getByUsername, | 46 | // UserSchema.path('username').required(customUsersValidators.isUserUsernameValid) |
33 | list, | 47 | // UserSchema.path('role').validate(customUsersValidators.isUserRoleValid) |
34 | listForApi, | ||
35 | loadById, | ||
36 | loadByUsername | ||
37 | } | ||
38 | 48 | ||
39 | UserSchema.pre('save', function (next) { | 49 | function beforeCreateOrUpdate (user, options, next) { |
40 | const user = this | 50 | peertubeCrypto.cryptPassword(user.password, function (err, hash) { |
41 | |||
42 | peertubeCrypto.cryptPassword(this.password, function (err, hash) { | ||
43 | if (err) return next(err) | 51 | if (err) return next(err) |
44 | 52 | ||
45 | user.password = hash | 53 | user.password = hash |
46 | 54 | ||
47 | return next() | 55 | return next() |
48 | }) | 56 | }) |
49 | }) | 57 | } |
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 | 58 | ||
59 | // ------------------------------ METHODS ------------------------------ | 59 | // ------------------------------ METHODS ------------------------------ |
60 | 60 | ||
@@ -64,35 +64,63 @@ function isPasswordMatch (password, callback) { | |||
64 | 64 | ||
65 | function toFormatedJSON () { | 65 | function toFormatedJSON () { |
66 | return { | 66 | return { |
67 | id: this._id, | 67 | id: this.id, |
68 | username: this.username, | 68 | username: this.username, |
69 | role: this.role, | 69 | role: this.role, |
70 | createdDate: this.createdDate | 70 | createdAt: this.createdAt |
71 | } | 71 | } |
72 | } | 72 | } |
73 | // ------------------------------ STATICS ------------------------------ | 73 | // ------------------------------ STATICS ------------------------------ |
74 | 74 | ||
75 | function associate (models) { | ||
76 | this.hasMany(models.OAuthToken, { | ||
77 | foreignKey: 'userId', | ||
78 | onDelete: 'cascade' | ||
79 | }) | ||
80 | } | ||
81 | |||
75 | function countTotal (callback) { | 82 | function countTotal (callback) { |
76 | return this.count(callback) | 83 | return this.count().asCallback(callback) |
77 | } | 84 | } |
78 | 85 | ||
79 | function getByUsername (username) { | 86 | function getByUsername (username) { |
80 | return this.findOne({ username: username }) | 87 | const query = { |
88 | where: { | ||
89 | username: username | ||
90 | } | ||
91 | } | ||
92 | |||
93 | return this.findOne(query) | ||
81 | } | 94 | } |
82 | 95 | ||
83 | function list (callback) { | 96 | function list (callback) { |
84 | return this.find(callback) | 97 | return this.find().asCallback(callback) |
85 | } | 98 | } |
86 | 99 | ||
87 | function listForApi (start, count, sort, callback) { | 100 | function listForApi (start, count, sort, callback) { |
88 | const query = {} | 101 | const query = { |
89 | return modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback) | 102 | offset: start, |
103 | limit: count, | ||
104 | order: [ modelUtils.getSort(sort) ] | ||
105 | } | ||
106 | |||
107 | return this.findAndCountAll(query).asCallback(function (err, result) { | ||
108 | if (err) return callback(err) | ||
109 | |||
110 | return callback(null, result.rows, result.count) | ||
111 | }) | ||
90 | } | 112 | } |
91 | 113 | ||
92 | function loadById (id, callback) { | 114 | function loadById (id, callback) { |
93 | return this.findById(id, callback) | 115 | return this.findById(id).asCallback(callback) |
94 | } | 116 | } |
95 | 117 | ||
96 | function loadByUsername (username, callback) { | 118 | function loadByUsername (username, callback) { |
97 | return this.findOne({ username: username }, callback) | 119 | const query = { |
120 | where: { | ||
121 | username: username | ||
122 | } | ||
123 | } | ||
124 | |||
125 | return this.findOne(query).asCallback(callback) | ||
98 | } | 126 | } |
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.js b/server/models/video.js index 330067cdf..8ef07c9e6 100644 --- a/server/models/video.js +++ b/server/models/video.js | |||
@@ -7,102 +7,93 @@ const magnetUtil = require('magnet-uri') | |||
7 | const parallel = require('async/parallel') | 7 | const parallel = require('async/parallel') |
8 | const parseTorrent = require('parse-torrent') | 8 | const parseTorrent = require('parse-torrent') |
9 | const pathUtils = require('path') | 9 | const pathUtils = require('path') |
10 | const mongoose = require('mongoose') | ||
11 | 10 | ||
12 | const constants = require('../initializers/constants') | 11 | const constants = require('../initializers/constants') |
13 | const customVideosValidators = require('../helpers/custom-validators').videos | ||
14 | const logger = require('../helpers/logger') | 12 | const logger = require('../helpers/logger') |
15 | const modelUtils = require('./utils') | 13 | const modelUtils = require('./utils') |
16 | 14 | ||
17 | // --------------------------------------------------------------------------- | 15 | // --------------------------------------------------------------------------- |
18 | 16 | ||
17 | module.exports = function (sequelize, DataTypes) { | ||
19 | // TODO: add indexes on searchable columns | 18 | // TODO: add indexes on searchable columns |
20 | const VideoSchema = mongoose.Schema({ | 19 | const Video = sequelize.define('Video', |
21 | name: String, | 20 | { |
22 | extname: { | 21 | id: { |
23 | type: String, | 22 | type: DataTypes.UUID, |
24 | enum: [ '.mp4', '.webm', '.ogv' ] | 23 | defaultValue: DataTypes.UUIDV4, |
25 | }, | 24 | primaryKey: true |
26 | remoteId: mongoose.Schema.Types.ObjectId, | ||
27 | description: String, | ||
28 | magnet: { | ||
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 | }, | 25 | }, |
87 | function (callback) { | 26 | name: { |
88 | removeTorrent(video, callback) | 27 | type: DataTypes.STRING |
89 | }, | 28 | }, |
90 | function (callback) { | 29 | extname: { |
91 | removePreview(video, callback) | 30 | // TODO: enum? |
31 | type: DataTypes.STRING | ||
32 | }, | ||
33 | remoteId: { | ||
34 | type: DataTypes.UUID | ||
35 | }, | ||
36 | description: { | ||
37 | type: DataTypes.STRING | ||
38 | }, | ||
39 | infoHash: { | ||
40 | type: DataTypes.STRING | ||
41 | }, | ||
42 | duration: { | ||
43 | type: DataTypes.INTEGER | ||
44 | }, | ||
45 | tags: { | ||
46 | type: DataTypes.ARRAY(DataTypes.STRING) | ||
92 | } | 47 | } |
93 | ) | 48 | }, |
94 | } | 49 | { |
50 | classMethods: { | ||
51 | associate, | ||
52 | |||
53 | generateThumbnailFromBase64, | ||
54 | getDurationFromFile, | ||
55 | listForApi, | ||
56 | listByHostAndRemoteId, | ||
57 | listOwnedAndPopulateAuthor, | ||
58 | listOwnedByAuthor, | ||
59 | load, | ||
60 | loadAndPopulateAuthor, | ||
61 | loadAndPopulateAuthorAndPod, | ||
62 | searchAndPopulateAuthorAndPod | ||
63 | }, | ||
64 | instanceMethods: { | ||
65 | generateMagnetUri, | ||
66 | getVideoFilename, | ||
67 | getThumbnailName, | ||
68 | getPreviewName, | ||
69 | getTorrentName, | ||
70 | isOwned, | ||
71 | toFormatedJSON, | ||
72 | toRemoteJSON | ||
73 | }, | ||
74 | hooks: { | ||
75 | beforeCreate, | ||
76 | afterDestroy | ||
77 | } | ||
78 | } | ||
79 | ) | ||
95 | 80 | ||
96 | parallel(tasks, next) | 81 | return Video |
97 | }) | 82 | } |
98 | 83 | ||
99 | VideoSchema.pre('save', function (next) { | 84 | // TODO: Validation |
100 | const video = this | 85 | // VideoSchema.path('name').validate(customVideosValidators.isVideoNameValid) |
86 | // VideoSchema.path('description').validate(customVideosValidators.isVideoDescriptionValid) | ||
87 | // VideoSchema.path('podHost').validate(customVideosValidators.isVideoPodHostValid) | ||
88 | // VideoSchema.path('author').validate(customVideosValidators.isVideoAuthorValid) | ||
89 | // VideoSchema.path('duration').validate(customVideosValidators.isVideoDurationValid) | ||
90 | // VideoSchema.path('tags').validate(customVideosValidators.isVideoTagsValid) | ||
91 | |||
92 | function beforeCreate (video, options, next) { | ||
101 | const tasks = [] | 93 | const tasks = [] |
102 | 94 | ||
103 | if (video.isOwned()) { | 95 | if (video.isOwned()) { |
104 | const videoPath = pathUtils.join(constants.CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) | 96 | const videoPath = pathUtils.join(constants.CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) |
105 | this.podHost = constants.CONFIG.WEBSERVER.HOST | ||
106 | 97 | ||
107 | tasks.push( | 98 | tasks.push( |
108 | // TODO: refractoring | 99 | // TODO: refractoring |
@@ -123,7 +114,7 @@ VideoSchema.pre('save', function (next) { | |||
123 | if (err) return callback(err) | 114 | if (err) return callback(err) |
124 | 115 | ||
125 | const parsedTorrent = parseTorrent(torrent) | 116 | const parsedTorrent = parseTorrent(torrent) |
126 | video.magnet.infoHash = parsedTorrent.infoHash | 117 | video.infoHash = parsedTorrent.infoHash |
127 | 118 | ||
128 | callback(null) | 119 | callback(null) |
129 | }) | 120 | }) |
@@ -141,12 +132,46 @@ VideoSchema.pre('save', function (next) { | |||
141 | } | 132 | } |
142 | 133 | ||
143 | return next() | 134 | return next() |
144 | }) | 135 | } |
136 | |||
137 | function afterDestroy (video, options, next) { | ||
138 | const tasks = [] | ||
145 | 139 | ||
146 | mongoose.model('Video', VideoSchema) | 140 | tasks.push( |
141 | function (callback) { | ||
142 | removeThumbnail(video, callback) | ||
143 | } | ||
144 | ) | ||
145 | |||
146 | if (video.isOwned()) { | ||
147 | tasks.push( | ||
148 | function (callback) { | ||
149 | removeFile(video, callback) | ||
150 | }, | ||
151 | function (callback) { | ||
152 | removeTorrent(video, callback) | ||
153 | }, | ||
154 | function (callback) { | ||
155 | removePreview(video, callback) | ||
156 | } | ||
157 | ) | ||
158 | } | ||
159 | |||
160 | parallel(tasks, next) | ||
161 | } | ||
147 | 162 | ||
148 | // ------------------------------ METHODS ------------------------------ | 163 | // ------------------------------ METHODS ------------------------------ |
149 | 164 | ||
165 | function associate (models) { | ||
166 | this.belongsTo(models.Author, { | ||
167 | foreignKey: { | ||
168 | name: 'authorId', | ||
169 | allowNull: false | ||
170 | }, | ||
171 | onDelete: 'cascade' | ||
172 | }) | ||
173 | } | ||
174 | |||
150 | function generateMagnetUri () { | 175 | function generateMagnetUri () { |
151 | let baseUrlHttp, baseUrlWs | 176 | let baseUrlHttp, baseUrlWs |
152 | 177 | ||
@@ -154,8 +179,8 @@ function generateMagnetUri () { | |||
154 | baseUrlHttp = constants.CONFIG.WEBSERVER.URL | 179 | baseUrlHttp = constants.CONFIG.WEBSERVER.URL |
155 | baseUrlWs = constants.CONFIG.WEBSERVER.WS + '://' + constants.CONFIG.WEBSERVER.HOSTNAME + ':' + constants.CONFIG.WEBSERVER.PORT | 180 | baseUrlWs = constants.CONFIG.WEBSERVER.WS + '://' + constants.CONFIG.WEBSERVER.HOSTNAME + ':' + constants.CONFIG.WEBSERVER.PORT |
156 | } else { | 181 | } else { |
157 | baseUrlHttp = constants.REMOTE_SCHEME.HTTP + '://' + this.podHost | 182 | baseUrlHttp = constants.REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host |
158 | baseUrlWs = constants.REMOTE_SCHEME.WS + '://' + this.podHost | 183 | baseUrlWs = constants.REMOTE_SCHEME.WS + '://' + this.Author.Pod.host |
159 | } | 184 | } |
160 | 185 | ||
161 | const xs = baseUrlHttp + constants.STATIC_PATHS.TORRENTS + this.getTorrentName() | 186 | const xs = baseUrlHttp + constants.STATIC_PATHS.TORRENTS + this.getTorrentName() |
@@ -166,7 +191,7 @@ function generateMagnetUri () { | |||
166 | xs, | 191 | xs, |
167 | announce, | 192 | announce, |
168 | urlList, | 193 | urlList, |
169 | infoHash: this.magnet.infoHash, | 194 | infoHash: this.infoHash, |
170 | name: this.name | 195 | name: this.name |
171 | } | 196 | } |
172 | 197 | ||
@@ -174,20 +199,20 @@ function generateMagnetUri () { | |||
174 | } | 199 | } |
175 | 200 | ||
176 | function getVideoFilename () { | 201 | function getVideoFilename () { |
177 | if (this.isOwned()) return this._id + this.extname | 202 | if (this.isOwned()) return this.id + this.extname |
178 | 203 | ||
179 | return this.remoteId + this.extname | 204 | return this.remoteId + this.extname |
180 | } | 205 | } |
181 | 206 | ||
182 | function getThumbnailName () { | 207 | function getThumbnailName () { |
183 | // We always have a copy of the thumbnail | 208 | // We always have a copy of the thumbnail |
184 | return this._id + '.jpg' | 209 | return this.id + '.jpg' |
185 | } | 210 | } |
186 | 211 | ||
187 | function getPreviewName () { | 212 | function getPreviewName () { |
188 | const extension = '.jpg' | 213 | const extension = '.jpg' |
189 | 214 | ||
190 | if (this.isOwned()) return this._id + extension | 215 | if (this.isOwned()) return this.id + extension |
191 | 216 | ||
192 | return this.remoteId + extension | 217 | return this.remoteId + extension |
193 | } | 218 | } |
@@ -195,7 +220,7 @@ function getPreviewName () { | |||
195 | function getTorrentName () { | 220 | function getTorrentName () { |
196 | const extension = '.torrent' | 221 | const extension = '.torrent' |
197 | 222 | ||
198 | if (this.isOwned()) return this._id + extension | 223 | if (this.isOwned()) return this.id + extension |
199 | 224 | ||
200 | return this.remoteId + extension | 225 | return this.remoteId + extension |
201 | } | 226 | } |
@@ -205,18 +230,27 @@ function isOwned () { | |||
205 | } | 230 | } |
206 | 231 | ||
207 | function toFormatedJSON () { | 232 | function toFormatedJSON () { |
233 | let podHost | ||
234 | |||
235 | if (this.Author.Pod) { | ||
236 | podHost = this.Author.Pod.host | ||
237 | } else { | ||
238 | // It means it's our video | ||
239 | podHost = constants.CONFIG.WEBSERVER.HOST | ||
240 | } | ||
241 | |||
208 | const json = { | 242 | const json = { |
209 | id: this._id, | 243 | id: this.id, |
210 | name: this.name, | 244 | name: this.name, |
211 | description: this.description, | 245 | description: this.description, |
212 | podHost: this.podHost, | 246 | podHost, |
213 | isLocal: this.isOwned(), | 247 | isLocal: this.isOwned(), |
214 | magnetUri: this.generateMagnetUri(), | 248 | magnetUri: this.generateMagnetUri(), |
215 | author: this.author, | 249 | author: this.Author.name, |
216 | duration: this.duration, | 250 | duration: this.duration, |
217 | tags: this.tags, | 251 | tags: this.tags, |
218 | thumbnailPath: constants.STATIC_PATHS.THUMBNAILS + '/' + this.getThumbnailName(), | 252 | thumbnailPath: constants.STATIC_PATHS.THUMBNAILS + '/' + this.getThumbnailName(), |
219 | createdDate: this.createdDate | 253 | createdAt: this.createdAt |
220 | } | 254 | } |
221 | 255 | ||
222 | return json | 256 | return json |
@@ -236,13 +270,13 @@ function toRemoteJSON (callback) { | |||
236 | const remoteVideo = { | 270 | const remoteVideo = { |
237 | name: self.name, | 271 | name: self.name, |
238 | description: self.description, | 272 | description: self.description, |
239 | magnet: self.magnet, | 273 | infoHash: self.infoHash, |
240 | remoteId: self._id, | 274 | remoteId: self.id, |
241 | author: self.author, | 275 | author: self.Author.name, |
242 | duration: self.duration, | 276 | duration: self.duration, |
243 | thumbnailBase64: new Buffer(thumbnailData).toString('base64'), | 277 | thumbnailBase64: new Buffer(thumbnailData).toString('base64'), |
244 | tags: self.tags, | 278 | tags: self.tags, |
245 | createdDate: self.createdDate, | 279 | createdAt: self.createdAt, |
246 | extname: self.extname | 280 | extname: self.extname |
247 | } | 281 | } |
248 | 282 | ||
@@ -273,50 +307,168 @@ function getDurationFromFile (videoPath, callback) { | |||
273 | } | 307 | } |
274 | 308 | ||
275 | function listForApi (start, count, sort, callback) { | 309 | function listForApi (start, count, sort, callback) { |
276 | const query = {} | 310 | const query = { |
277 | return modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback) | 311 | offset: start, |
312 | limit: count, | ||
313 | order: [ modelUtils.getSort(sort) ], | ||
314 | include: [ | ||
315 | { | ||
316 | model: this.sequelize.models.Author, | ||
317 | include: [ this.sequelize.models.Pod ] | ||
318 | } | ||
319 | ] | ||
320 | } | ||
321 | |||
322 | return this.findAndCountAll(query).asCallback(function (err, result) { | ||
323 | if (err) return callback(err) | ||
324 | |||
325 | return callback(null, result.rows, result.count) | ||
326 | }) | ||
278 | } | 327 | } |
279 | 328 | ||
280 | function listByHostAndRemoteId (fromHost, remoteId, callback) { | 329 | function listByHostAndRemoteId (fromHost, remoteId, callback) { |
281 | this.find({ podHost: fromHost, remoteId: remoteId }, callback) | 330 | const query = { |
282 | } | 331 | where: { |
332 | remoteId: remoteId | ||
333 | }, | ||
334 | include: [ | ||
335 | { | ||
336 | model: this.sequelize.models.Author, | ||
337 | include: [ | ||
338 | { | ||
339 | model: this.sequelize.models.Pod, | ||
340 | where: { | ||
341 | host: fromHost | ||
342 | } | ||
343 | } | ||
344 | ] | ||
345 | } | ||
346 | ] | ||
347 | } | ||
283 | 348 | ||
284 | function listByHost (fromHost, callback) { | 349 | return this.findAll(query).asCallback(callback) |
285 | this.find({ podHost: fromHost }, callback) | ||
286 | } | 350 | } |
287 | 351 | ||
288 | function listOwned (callback) { | 352 | function listOwnedAndPopulateAuthor (callback) { |
289 | // If remoteId is null this is *our* video | 353 | // If remoteId is null this is *our* video |
290 | this.find({ remoteId: null }, callback) | 354 | const query = { |
355 | where: { | ||
356 | remoteId: null | ||
357 | }, | ||
358 | include: [ this.sequelize.models.Author ] | ||
359 | } | ||
360 | |||
361 | return this.findAll(query).asCallback(callback) | ||
291 | } | 362 | } |
292 | 363 | ||
293 | function listOwnedByAuthor (author, callback) { | 364 | function listOwnedByAuthor (author, callback) { |
294 | this.find({ remoteId: null, author: author }, callback) | 365 | const query = { |
295 | } | 366 | where: { |
367 | remoteId: null | ||
368 | }, | ||
369 | include: [ | ||
370 | { | ||
371 | model: this.sequelize.models.Author, | ||
372 | where: { | ||
373 | name: author | ||
374 | } | ||
375 | } | ||
376 | ] | ||
377 | } | ||
296 | 378 | ||
297 | function listRemotes (callback) { | 379 | return this.findAll(query).asCallback(callback) |
298 | this.find({ remoteId: { $ne: null } }, callback) | ||
299 | } | 380 | } |
300 | 381 | ||
301 | function load (id, callback) { | 382 | function load (id, callback) { |
302 | this.findById(id, callback) | 383 | return this.findById(id).asCallback(callback) |
303 | } | 384 | } |
304 | 385 | ||
305 | function search (value, field, start, count, sort, callback) { | 386 | function loadAndPopulateAuthor (id, callback) { |
306 | const query = {} | 387 | const options = { |
388 | include: [ this.sequelize.models.Author ] | ||
389 | } | ||
390 | |||
391 | return this.findById(id, options).asCallback(callback) | ||
392 | } | ||
393 | |||
394 | function loadAndPopulateAuthorAndPod (id, callback) { | ||
395 | const options = { | ||
396 | include: [ | ||
397 | { | ||
398 | model: this.sequelize.models.Author, | ||
399 | include: [ this.sequelize.models.Pod ] | ||
400 | } | ||
401 | ] | ||
402 | } | ||
403 | |||
404 | return this.findById(id, options).asCallback(callback) | ||
405 | } | ||
406 | |||
407 | function searchAndPopulateAuthorAndPod (value, field, start, count, sort, callback) { | ||
408 | const podInclude = { | ||
409 | model: this.sequelize.models.Pod | ||
410 | } | ||
411 | const authorInclude = { | ||
412 | model: this.sequelize.models.Author, | ||
413 | include: [ | ||
414 | podInclude | ||
415 | ] | ||
416 | } | ||
417 | |||
418 | const query = { | ||
419 | where: {}, | ||
420 | include: [ | ||
421 | authorInclude | ||
422 | ], | ||
423 | offset: start, | ||
424 | limit: count, | ||
425 | order: [ modelUtils.getSort(sort) ] | ||
426 | } | ||
427 | |||
428 | // TODO: include our pod for podHost searches (we are not stored in the database) | ||
307 | // Make an exact search with the magnet | 429 | // Make an exact search with the magnet |
308 | if (field === 'magnetUri') { | 430 | if (field === 'magnetUri') { |
309 | const infoHash = magnetUtil.decode(value).infoHash | 431 | const infoHash = magnetUtil.decode(value).infoHash |
310 | query.magnet = { | 432 | query.where.infoHash = infoHash |
311 | infoHash | ||
312 | } | ||
313 | } else if (field === 'tags') { | 433 | } else if (field === 'tags') { |
314 | query[field] = value | 434 | query.where[field] = value |
435 | } else if (field === 'host') { | ||
436 | const whereQuery = { | ||
437 | '$Author.Pod.host$': { | ||
438 | $like: '%' + value + '%' | ||
439 | } | ||
440 | } | ||
441 | |||
442 | // Include our pod? (not stored in the database) | ||
443 | if (constants.CONFIG.WEBSERVER.HOST.indexOf(value) !== -1) { | ||
444 | query.where = { | ||
445 | $or: [ | ||
446 | whereQuery, | ||
447 | { | ||
448 | remoteId: null | ||
449 | } | ||
450 | ] | ||
451 | } | ||
452 | } else { | ||
453 | query.where = whereQuery | ||
454 | } | ||
455 | } else if (field === 'author') { | ||
456 | query.where = { | ||
457 | '$Author.name$': { | ||
458 | $like: '%' + value + '%' | ||
459 | } | ||
460 | } | ||
315 | } else { | 461 | } else { |
316 | query[field] = new RegExp(value, 'i') | 462 | query.where[field] = { |
463 | $like: '%' + value + '%' | ||
464 | } | ||
317 | } | 465 | } |
318 | 466 | ||
319 | modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback) | 467 | return this.findAndCountAll(query).asCallback(function (err, result) { |
468 | if (err) return callback(err) | ||
469 | |||
470 | return callback(null, result.rows, result.count) | ||
471 | }) | ||
320 | } | 472 | } |
321 | 473 | ||
322 | // --------------------------------------------------------------------------- | 474 | // --------------------------------------------------------------------------- |