From feb4bdfd9b46e87aadfa7c0d5338cde887d1f58c Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Sun, 11 Dec 2016 21:50:51 +0100 Subject: First version with PostgreSQL --- server/models/application.js | 45 ++--- server/models/author.js | 28 +++ server/models/oauth-client.js | 72 +++++--- server/models/oauth-token.js | 109 +++++++++--- server/models/pods.js | 156 +++++++++-------- server/models/request.js | 187 +++++++++++--------- server/models/requestToPod.js | 30 ++++ server/models/user.js | 132 ++++++++------ server/models/utils.js | 31 ++-- server/models/video.js | 388 +++++++++++++++++++++++++++++------------- 10 files changed, 773 insertions(+), 405 deletions(-) create mode 100644 server/models/author.js create mode 100644 server/models/requestToPod.js (limited to 'server/models') 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 @@ -const mongoose = require('mongoose') +module.exports = function (sequelize, DataTypes) { + const Application = sequelize.define('Application', + { + sqlSchemaVersion: { + type: DataTypes.INTEGER, + defaultValue: 0 + } + }, + { + classMethods: { + loadSqlSchemaVersion, + updateSqlSchemaVersion + } + } + ) + + return Application +} // --------------------------------------------------------------------------- -const ApplicationSchema = mongoose.Schema({ - mongoSchemaVersion: { - type: Number, - default: 0 +function loadSqlSchemaVersion (callback) { + const query = { + attributes: [ 'sqlSchemaVersion' ] } -}) - -ApplicationSchema.statics = { - loadMongoSchemaVersion, - updateMongoSchemaVersion -} - -mongoose.model('Application', ApplicationSchema) - -// --------------------------------------------------------------------------- -function loadMongoSchemaVersion (callback) { - return this.findOne({}, { mongoSchemaVersion: 1 }, function (err, data) { - const version = data ? data.mongoSchemaVersion : 0 + return this.findOne(query).asCallback(function (err, data) { + const version = data ? data.sqlSchemaVersion : 0 return callback(err, version) }) } -function updateMongoSchemaVersion (newVersion, callback) { - return this.update({}, { mongoSchemaVersion: newVersion }, callback) +function updateSqlSchemaVersion (newVersion, callback) { + return this.update({ sqlSchemaVersion: newVersion }).asCallback(callback) } 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 @@ +module.exports = function (sequelize, DataTypes) { + const Author = sequelize.define('Author', + { + name: { + type: DataTypes.STRING + } + }, + { + classMethods: { + associate + } + } + ) + + return Author +} + +// --------------------------------------------------------------------------- + +function associate (models) { + this.belongsTo(models.Pod, { + foreignKey: { + name: 'podId', + allowNull: true + }, + onDelete: 'cascade' + }) +} 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 @@ -const mongoose = require('mongoose') - -// --------------------------------------------------------------------------- - -const OAuthClientSchema = mongoose.Schema({ - clientSecret: String, - grants: Array, - redirectUris: Array -}) - -OAuthClientSchema.path('clientSecret').required(true) - -OAuthClientSchema.statics = { - getByIdAndSecret, - list, - loadFirstClient +module.exports = function (sequelize, DataTypes) { + const OAuthClient = sequelize.define('OAuthClient', + { + clientId: { + type: DataTypes.STRING + }, + clientSecret: { + type: DataTypes.STRING + }, + grants: { + type: DataTypes.ARRAY(DataTypes.STRING) + }, + redirectUris: { + type: DataTypes.ARRAY(DataTypes.STRING) + } + }, + { + classMethods: { + associate, + + getByIdAndSecret, + list, + loadFirstClient + } + } + ) + + return OAuthClient } -mongoose.model('OAuthClient', OAuthClientSchema) +// TODO: validation +// OAuthClientSchema.path('clientSecret').required(true) // --------------------------------------------------------------------------- +function associate (models) { + this.hasMany(models.OAuthToken, { + foreignKey: { + name: 'oAuthClientId', + allowNull: false + }, + onDelete: 'cascade' + }) +} + function list (callback) { - return this.find(callback) + return this.findAll().asCallback(callback) } function loadFirstClient (callback) { - return this.findOne({}, callback) + return this.findOne().asCallback(callback) } -function getByIdAndSecret (id, clientSecret) { - return this.findOne({ _id: id, clientSecret: clientSecret }).exec() +function getByIdAndSecret (clientId, clientSecret) { + const query = { + where: { + clientId: clientId, + clientSecret: clientSecret + } + } + + return this.findOne(query) } 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 @@ -const mongoose = require('mongoose') - const logger = require('../helpers/logger') // --------------------------------------------------------------------------- -const OAuthTokenSchema = mongoose.Schema({ - accessToken: String, - accessTokenExpiresAt: Date, - client: { type: mongoose.Schema.Types.ObjectId, ref: 'OAuthClient' }, - refreshToken: String, - refreshTokenExpiresAt: Date, - user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' } -}) - -OAuthTokenSchema.path('accessToken').required(true) -OAuthTokenSchema.path('client').required(true) -OAuthTokenSchema.path('user').required(true) - -OAuthTokenSchema.statics = { - getByRefreshTokenAndPopulateClient, - getByTokenAndPopulateUser, - getByRefreshTokenAndPopulateUser, - removeByUserId +module.exports = function (sequelize, DataTypes) { + const OAuthToken = sequelize.define('OAuthToken', + { + accessToken: { + type: DataTypes.STRING + }, + accessTokenExpiresAt: { + type: DataTypes.DATE + }, + refreshToken: { + type: DataTypes.STRING + }, + refreshTokenExpiresAt: { + type: DataTypes.DATE + } + }, + { + classMethods: { + associate, + + getByRefreshTokenAndPopulateClient, + getByTokenAndPopulateUser, + getByRefreshTokenAndPopulateUser, + removeByUserId + } + } + ) + + return OAuthToken } -mongoose.model('OAuthToken', OAuthTokenSchema) +// TODO: validation +// OAuthTokenSchema.path('accessToken').required(true) +// OAuthTokenSchema.path('client').required(true) +// OAuthTokenSchema.path('user').required(true) // --------------------------------------------------------------------------- +function associate (models) { + this.belongsTo(models.User, { + foreignKey: { + name: 'userId', + allowNull: false + }, + onDelete: 'cascade' + }) +} + function getByRefreshTokenAndPopulateClient (refreshToken) { - return this.findOne({ refreshToken: refreshToken }).populate('client').exec().then(function (token) { + const query = { + where: { + refreshToken: refreshToken + }, + include: [ this.associations.OAuthClient ] + } + + return this.findOne(query).then(function (token) { if (!token) return token const tokenInfos = { refreshToken: token.refreshToken, refreshTokenExpiresAt: token.refreshTokenExpiresAt, client: { - id: token.client._id.toString() + id: token.client.id }, user: { id: token.user @@ -50,13 +79,41 @@ function getByRefreshTokenAndPopulateClient (refreshToken) { } function getByTokenAndPopulateUser (bearerToken) { - return this.findOne({ accessToken: bearerToken }).populate('user').exec() + const query = { + where: { + accessToken: bearerToken + }, + include: [ this.sequelize.models.User ] + } + + return this.findOne(query).then(function (token) { + if (token) token.user = token.User + + return token + }) } function getByRefreshTokenAndPopulateUser (refreshToken) { - return this.findOne({ refreshToken: refreshToken }).populate('user').exec() + const query = { + where: { + refreshToken: refreshToken + }, + include: [ this.sequelize.models.User ] + } + + return this.findOne(query).then(function (token) { + token.user = token.User + + return token + }) } function removeByUserId (userId, callback) { - return this.remove({ user: userId }, callback) + const query = { + where: { + userId: userId + } + } + + return this.destroy(query).asCallback(callback) } 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 @@ 'use strict' -const each = require('async/each') -const mongoose = require('mongoose') const map = require('lodash/map') -const validator = require('express-validator').validator const constants = require('../initializers/constants') -const Video = mongoose.model('Video') - // --------------------------------------------------------------------------- -const PodSchema = mongoose.Schema({ - host: String, - publicKey: String, - score: { type: Number, max: constants.FRIEND_SCORE.MAX }, - createdDate: { - type: Date, - default: Date.now - } -}) - -PodSchema.path('host').validate(validator.isURL) -PodSchema.path('publicKey').required(true) -PodSchema.path('score').validate(function (value) { return !isNaN(value) }) - -PodSchema.methods = { - toFormatedJSON +module.exports = function (sequelize, DataTypes) { + const Pod = sequelize.define('Pod', + { + host: { + type: DataTypes.STRING + }, + publicKey: { + type: DataTypes.STRING(5000) + }, + score: { + type: DataTypes.INTEGER, + defaultValue: constants.FRIEND_SCORE.BASE + } + // Check createdAt + }, + { + classMethods: { + associate, + + countAll, + incrementScores, + list, + listAllIds, + listBadPods, + load, + loadByHost, + removeAll + }, + instanceMethods: { + toFormatedJSON + } + } + ) + + return Pod } -PodSchema.statics = { - countAll, - incrementScores, - list, - listAllIds, - listBadPods, - load, - loadByHost, - removeAll -} - -PodSchema.pre('save', function (next) { - const self = this - - Pod.loadByHost(this.host, function (err, pod) { - if (err) return next(err) - - if (pod) return next(new Error('Pod already exists.')) - - self.score = constants.FRIEND_SCORE.BASE - return next() - }) -}) - -PodSchema.pre('remove', function (next) { - // Remove the videos owned by this pod too - Video.listByHost(this.host, function (err, videos) { - if (err) return next(err) - - each(videos, function (video, callbackEach) { - video.remove(callbackEach) - }, next) - }) -}) - -const Pod = mongoose.model('Pod', PodSchema) +// TODO: max score -> constants.FRIENDS_SCORE.MAX +// TODO: validation +// PodSchema.path('host').validate(validator.isURL) +// PodSchema.path('publicKey').required(true) +// PodSchema.path('score').validate(function (value) { return !isNaN(value) }) // ------------------------------ METHODS ------------------------------ function toFormatedJSON () { const json = { - id: this._id, + id: this.id, host: this.host, score: this.score, - createdDate: this.createdDate + createdAt: this.createdAt } return json @@ -81,39 +64,76 @@ function toFormatedJSON () { // ------------------------------ Statics ------------------------------ +function associate (models) { + this.belongsToMany(models.Request, { + foreignKey: 'podId', + through: models.RequestToPod, + onDelete: 'CASCADE' + }) +} + function countAll (callback) { - return this.count(callback) + return this.count().asCallback(callback) } function incrementScores (ids, value, callback) { if (!callback) callback = function () {} - return this.update({ _id: { $in: ids } }, { $inc: { score: value } }, { multi: true }, callback) + + const update = { + score: this.sequelize.literal('score +' + value) + } + + const query = { + where: { + id: { + $in: ids + } + } + } + + return this.update(update, query).asCallback(callback) } function list (callback) { - return this.find(callback) + return this.findAll().asCallback(callback) } function listAllIds (callback) { - return this.find({}, { _id: 1 }, function (err, pods) { + const query = { + attributes: [ 'id' ] + } + + return this.findAll(query).asCallback(function (err, pods) { if (err) return callback(err) - return callback(null, map(pods, '_id')) + return callback(null, map(pods, 'id')) }) } function listBadPods (callback) { - return this.find({ score: 0 }, callback) + const query = { + where: { + score: { $lte: 0 } + } + } + + return this.findAll(query).asCallback(callback) } function load (id, callback) { - return this.findById(id, callback) + return this.findById(id).asCallback(callback) } function loadByHost (host, callback) { - return this.findOne({ host }, callback) + const query = { + where: { + host: host + } + } + + return this.findOne(query).asCallback(callback) } function removeAll (callback) { - return this.remove({}, callback) + return this.destroy().asCallback(callback) } 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 @@ const each = require('async/each') const eachLimit = require('async/eachLimit') -const values = require('lodash/values') -const mongoose = require('mongoose') const waterfall = require('async/waterfall') const constants = require('../initializers/constants') const logger = require('../helpers/logger') const requests = require('../helpers/requests') -const Pod = mongoose.model('Pod') - let timer = null let lastRequestTimestamp = 0 // --------------------------------------------------------------------------- -const RequestSchema = mongoose.Schema({ - request: mongoose.Schema.Types.Mixed, - endpoint: { - type: String, - enum: [ values(constants.REQUEST_ENDPOINTS) ] - }, - to: [ +module.exports = function (sequelize, DataTypes) { + const Request = sequelize.define('Request', + { + request: { + type: DataTypes.JSON + }, + endpoint: { + // TODO: enum? + type: DataTypes.STRING + } + }, { - type: mongoose.Schema.Types.ObjectId, - ref: 'Pod' + classMethods: { + associate, + + activate, + countTotalRequests, + deactivate, + flush, + forceSend, + remainingMilliSeconds + } } - ] -}) - -RequestSchema.statics = { - activate, - deactivate, - flush, - forceSend, - list, - remainingMilliSeconds -} - -RequestSchema.pre('save', function (next) { - const self = this - - if (self.to.length === 0) { - Pod.listAllIds(function (err, podIds) { - if (err) return next(err) - - // No friends - if (podIds.length === 0) return - - self.to = podIds - return next() - }) - } else { - return next() - } -}) + ) -mongoose.model('Request', RequestSchema) + return Request +} // ------------------------------ STATICS ------------------------------ +function associate (models) { + this.belongsToMany(models.Pod, { + foreignKey: { + name: 'requestId', + allowNull: false + }, + through: models.RequestToPod, + onDelete: 'CASCADE' + }) +} + function activate () { logger.info('Requests scheduler activated.') lastRequestTimestamp = Date.now() @@ -73,6 +65,14 @@ function activate () { }, constants.REQUESTS_INTERVAL) } +function countTotalRequests (callback) { + const query = { + include: [ this.sequelize.models.Pod ] + } + + return this.count(query).asCallback(callback) +} + function deactivate () { logger.info('Requests scheduler deactivated.') clearInterval(timer) @@ -90,10 +90,6 @@ function forceSend () { makeRequests.call(this) } -function list (callback) { - this.find({ }, callback) -} - function remainingMilliSeconds () { if (timer === null) return -1 @@ -136,6 +132,7 @@ function makeRequest (toPod, requestEndpoint, requestsToMake, callback) { // Make all the requests of the scheduler function makeRequests () { const self = this + const RequestToPod = this.sequelize.models.RequestToPod // We limit the size of the requests (REQUESTS_LIMIT) // We don't want to stuck with the same failing requests so we get a random list @@ -156,20 +153,20 @@ function makeRequests () { // We want to group requests by destinations pod and endpoint const requestsToMakeGrouped = {} - requests.forEach(function (poolRequest) { - poolRequest.to.forEach(function (toPodId) { - const hashKey = toPodId + poolRequest.endpoint + requests.forEach(function (request) { + request.Pods.forEach(function (toPod) { + const hashKey = toPod.id + request.endpoint if (!requestsToMakeGrouped[hashKey]) { requestsToMakeGrouped[hashKey] = { - toPodId, - endpoint: poolRequest.endpoint, - ids: [], // pool request ids, to delete them from the DB in the future + toPodId: toPod.id, + endpoint: request.endpoint, + ids: [], // request ids, to delete them from the DB in the future datas: [] // requests data, } } - requestsToMakeGrouped[hashKey].ids.push(poolRequest._id) - requestsToMakeGrouped[hashKey].datas.push(poolRequest.request) + requestsToMakeGrouped[hashKey].ids.push(request.id) + requestsToMakeGrouped[hashKey].datas.push(request.request) }) }) @@ -179,8 +176,8 @@ function makeRequests () { eachLimit(Object.keys(requestsToMakeGrouped), constants.REQUESTS_IN_PARALLEL, function (hashKey, callbackEach) { const requestToMake = requestsToMakeGrouped[hashKey] - // FIXME: mongodb request inside a loop :/ - Pod.load(requestToMake.toPodId, function (err, toPod) { + // FIXME: SQL request inside a loop :/ + self.sequelize.models.Pod.load(requestToMake.toPodId, function (err, toPod) { if (err) { logger.error('Error finding pod by id.', { err: err }) return callbackEach() @@ -191,7 +188,7 @@ function makeRequests () { const requestIdsToDelete = requestToMake.ids logger.info('Removing %d requests of unexisting pod %s.', requestIdsToDelete.length, requestToMake.toPodId) - removePodOf.call(self, requestIdsToDelete, requestToMake.toPodId) + RequestToPod.removePodOf.call(self, requestIdsToDelete, requestToMake.toPodId) return callbackEach() } @@ -202,7 +199,7 @@ function makeRequests () { goodPods.push(requestToMake.toPodId) // Remove the pod id of these request ids - removePodOf.call(self, requestToMake.ids, requestToMake.toPodId, callbackEach) + RequestToPod.removePodOf(requestToMake.ids, requestToMake.toPodId, callbackEach) } else { badPods.push(requestToMake.toPodId) callbackEach() @@ -211,18 +208,22 @@ function makeRequests () { }) }, function () { // All the requests were made, we update the pods score - updatePodsScore(goodPods, badPods) + updatePodsScore.call(self, goodPods, badPods) // Flush requests with no pod - removeWithEmptyTo.call(self) + removeWithEmptyTo.call(self, function (err) { + if (err) logger.error('Error when removing requests with no pods.', { error: err }) + }) }) }) } // Remove pods with a score of 0 (too many requests where they were unreachable) function removeBadPods () { + const self = this + waterfall([ function findBadPods (callback) { - Pod.listBadPods(function (err, pods) { + self.sequelize.models.Pod.listBadPods(function (err, pods) { if (err) { logger.error('Cannot find bad pods.', { error: err }) return callback(err) @@ -233,10 +234,8 @@ function removeBadPods () { }, function removeTheseBadPods (pods, callback) { - if (pods.length === 0) return callback(null, 0) - each(pods, function (pod, callbackEach) { - pod.remove(callbackEach) + pod.destroy().asCallback(callbackEach) }, function (err) { return callback(err, pods.length) }) @@ -253,43 +252,67 @@ function removeBadPods () { } function updatePodsScore (goodPods, badPods) { + const self = this + const Pod = this.sequelize.models.Pod + logger.info('Updating %d good pods and %d bad pods scores.', goodPods.length, badPods.length) - Pod.incrementScores(goodPods, constants.PODS_SCORE.BONUS, function (err) { - if (err) logger.error('Cannot increment scores of good pods.') - }) + if (goodPods.length !== 0) { + Pod.incrementScores(goodPods, constants.PODS_SCORE.BONUS, function (err) { + if (err) logger.error('Cannot increment scores of good pods.') + }) + } - Pod.incrementScores(badPods, constants.PODS_SCORE.MALUS, function (err) { - if (err) logger.error('Cannot decrement scores of bad pods.') - removeBadPods() - }) + if (badPods.length !== 0) { + Pod.incrementScores(badPods, constants.PODS_SCORE.MALUS, function (err) { + if (err) logger.error('Cannot decrement scores of bad pods.') + removeBadPods.call(self) + }) + } } function listWithLimitAndRandom (limit, callback) { const self = this - self.count(function (err, count) { + self.count().asCallback(function (err, count) { if (err) return callback(err) + // Optimization... + if (count === 0) return callback(null, []) + let start = Math.floor(Math.random() * count) - limit if (start < 0) start = 0 - self.find().sort({ _id: 1 }).skip(start).limit(limit).exec(callback) + const query = { + order: [ + [ 'id', 'ASC' ] + ], + offset: start, + limit: limit, + include: [ this.sequelize.models.Pod ] + } + + self.findAll(query).asCallback(callback) }) } function removeAll (callback) { - this.remove({ }, callback) -} - -function removePodOf (requestsIds, podId, callback) { - if (!callback) callback = function () {} - - this.update({ _id: { $in: requestsIds } }, { $pull: { to: podId } }, { multi: true }, callback) + // Delete all requests + this.destroy({ truncate: true }).asCallback(callback) } function removeWithEmptyTo (callback) { if (!callback) callback = function () {} - this.remove({ to: { $size: 0 } }, callback) + const query = { + where: { + id: { + $notIn: [ + this.sequelize.literal('SELECT "requestId" FROM "RequestToPods"') + ] + } + } + } + + this.destroy(query).asCallback(callback) } 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 @@ +'use strict' + +// --------------------------------------------------------------------------- + +module.exports = function (sequelize, DataTypes) { + const RequestToPod = sequelize.define('RequestToPod', {}, { + classMethods: { + removePodOf + } + }) + + return RequestToPod +} + +// --------------------------------------------------------------------------- + +function removePodOf (requestsIds, podId, callback) { + if (!callback) callback = function () {} + + const query = { + where: { + requestId: { + $in: requestsIds + }, + podId: podId + } + } + + this.destroy(query).asCallback(callback) +} 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 @@ -const mongoose = require('mongoose') - -const customUsersValidators = require('../helpers/custom-validators').users const modelUtils = require('./utils') const peertubeCrypto = require('../helpers/peertube-crypto') -const OAuthToken = mongoose.model('OAuthToken') - // --------------------------------------------------------------------------- -const UserSchema = mongoose.Schema({ - createdDate: { - type: Date, - default: Date.now - }, - password: String, - username: String, - role: String -}) - -UserSchema.path('password').required(customUsersValidators.isUserPasswordValid) -UserSchema.path('username').required(customUsersValidators.isUserUsernameValid) -UserSchema.path('role').validate(customUsersValidators.isUserRoleValid) - -UserSchema.methods = { - isPasswordMatch, - toFormatedJSON +module.exports = function (sequelize, DataTypes) { + const User = sequelize.define('User', + { + password: { + type: DataTypes.STRING + }, + username: { + type: DataTypes.STRING + }, + role: { + type: DataTypes.STRING + } + }, + { + classMethods: { + associate, + + countTotal, + getByUsername, + list, + listForApi, + loadById, + loadByUsername + }, + instanceMethods: { + isPasswordMatch, + toFormatedJSON + }, + hooks: { + beforeCreate: beforeCreateOrUpdate, + beforeUpdate: beforeCreateOrUpdate + } + } + ) + + return User } -UserSchema.statics = { - countTotal, - getByUsername, - list, - listForApi, - loadById, - loadByUsername -} +// TODO: Validation +// UserSchema.path('password').required(customUsersValidators.isUserPasswordValid) +// UserSchema.path('username').required(customUsersValidators.isUserUsernameValid) +// UserSchema.path('role').validate(customUsersValidators.isUserRoleValid) -UserSchema.pre('save', function (next) { - const user = this - - peertubeCrypto.cryptPassword(this.password, function (err, hash) { +function beforeCreateOrUpdate (user, options, next) { + peertubeCrypto.cryptPassword(user.password, function (err, hash) { if (err) return next(err) user.password = hash return next() }) -}) - -UserSchema.pre('remove', function (next) { - const user = this - - OAuthToken.removeByUserId(user._id, next) -}) - -mongoose.model('User', UserSchema) +} // ------------------------------ METHODS ------------------------------ @@ -64,35 +64,63 @@ function isPasswordMatch (password, callback) { function toFormatedJSON () { return { - id: this._id, + id: this.id, username: this.username, role: this.role, - createdDate: this.createdDate + createdAt: this.createdAt } } // ------------------------------ STATICS ------------------------------ +function associate (models) { + this.hasMany(models.OAuthToken, { + foreignKey: 'userId', + onDelete: 'cascade' + }) +} + function countTotal (callback) { - return this.count(callback) + return this.count().asCallback(callback) } function getByUsername (username) { - return this.findOne({ username: username }) + const query = { + where: { + username: username + } + } + + return this.findOne(query) } function list (callback) { - return this.find(callback) + return this.find().asCallback(callback) } function listForApi (start, count, sort, callback) { - const query = {} - return modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback) + const query = { + offset: start, + limit: count, + order: [ modelUtils.getSort(sort) ] + } + + return this.findAndCountAll(query).asCallback(function (err, result) { + if (err) return callback(err) + + return callback(null, result.rows, result.count) + }) } function loadById (id, callback) { - return this.findById(id, callback) + return this.findById(id).asCallback(callback) } function loadByUsername (username, callback) { - return this.findOne({ username: username }, callback) + const query = { + where: { + username: username + } + } + + return this.findOne(query).asCallback(callback) } 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 @@ 'use strict' -const parallel = require('async/parallel') - const utils = { - listForApiWithCount + getSort } -function listForApiWithCount (query, start, count, sort, callback) { - const self = this +// Translate for example "-name" to [ 'name', 'DESC' ] +function getSort (value) { + let field + let direction - parallel([ - function (asyncCallback) { - self.find(query).skip(start).limit(count).sort(sort).exec(asyncCallback) - }, - function (asyncCallback) { - self.count(query, asyncCallback) - } - ], function (err, results) { - if (err) return callback(err) + if (value.substring(0, 1) === '-') { + direction = 'DESC' + field = value.substring(1) + } else { + direction = 'ASC' + field = value + } - const data = results[0] - const total = results[1] - return callback(null, data, total) - }) + return [ field, direction ] } // --------------------------------------------------------------------------- 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') const parallel = require('async/parallel') const parseTorrent = require('parse-torrent') const pathUtils = require('path') -const mongoose = require('mongoose') const constants = require('../initializers/constants') -const customVideosValidators = require('../helpers/custom-validators').videos const logger = require('../helpers/logger') const modelUtils = require('./utils') // --------------------------------------------------------------------------- +module.exports = function (sequelize, DataTypes) { // TODO: add indexes on searchable columns -const VideoSchema = mongoose.Schema({ - name: String, - extname: { - type: String, - enum: [ '.mp4', '.webm', '.ogv' ] - }, - remoteId: mongoose.Schema.Types.ObjectId, - description: String, - magnet: { - infoHash: String - }, - podHost: String, - author: String, - duration: Number, - tags: [ String ], - createdDate: { - type: Date, - default: Date.now - } -}) - -VideoSchema.path('name').validate(customVideosValidators.isVideoNameValid) -VideoSchema.path('description').validate(customVideosValidators.isVideoDescriptionValid) -VideoSchema.path('podHost').validate(customVideosValidators.isVideoPodHostValid) -VideoSchema.path('author').validate(customVideosValidators.isVideoAuthorValid) -VideoSchema.path('duration').validate(customVideosValidators.isVideoDurationValid) -VideoSchema.path('tags').validate(customVideosValidators.isVideoTagsValid) - -VideoSchema.methods = { - generateMagnetUri, - getVideoFilename, - getThumbnailName, - getPreviewName, - getTorrentName, - isOwned, - toFormatedJSON, - toRemoteJSON -} - -VideoSchema.statics = { - generateThumbnailFromBase64, - getDurationFromFile, - listForApi, - listByHostAndRemoteId, - listByHost, - listOwned, - listOwnedByAuthor, - listRemotes, - load, - search -} - -VideoSchema.pre('remove', function (next) { - const video = this - const tasks = [] - - tasks.push( - function (callback) { - removeThumbnail(video, callback) - } - ) - - if (video.isOwned()) { - tasks.push( - function (callback) { - removeFile(video, callback) + const Video = sequelize.define('Video', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true }, - function (callback) { - removeTorrent(video, callback) + name: { + type: DataTypes.STRING }, - function (callback) { - removePreview(video, callback) + extname: { + // TODO: enum? + type: DataTypes.STRING + }, + remoteId: { + type: DataTypes.UUID + }, + description: { + type: DataTypes.STRING + }, + infoHash: { + type: DataTypes.STRING + }, + duration: { + type: DataTypes.INTEGER + }, + tags: { + type: DataTypes.ARRAY(DataTypes.STRING) } - ) - } + }, + { + classMethods: { + associate, + + generateThumbnailFromBase64, + getDurationFromFile, + listForApi, + listByHostAndRemoteId, + listOwnedAndPopulateAuthor, + listOwnedByAuthor, + load, + loadAndPopulateAuthor, + loadAndPopulateAuthorAndPod, + searchAndPopulateAuthorAndPod + }, + instanceMethods: { + generateMagnetUri, + getVideoFilename, + getThumbnailName, + getPreviewName, + getTorrentName, + isOwned, + toFormatedJSON, + toRemoteJSON + }, + hooks: { + beforeCreate, + afterDestroy + } + } + ) - parallel(tasks, next) -}) + return Video +} -VideoSchema.pre('save', function (next) { - const video = this +// TODO: Validation +// VideoSchema.path('name').validate(customVideosValidators.isVideoNameValid) +// VideoSchema.path('description').validate(customVideosValidators.isVideoDescriptionValid) +// VideoSchema.path('podHost').validate(customVideosValidators.isVideoPodHostValid) +// VideoSchema.path('author').validate(customVideosValidators.isVideoAuthorValid) +// VideoSchema.path('duration').validate(customVideosValidators.isVideoDurationValid) +// VideoSchema.path('tags').validate(customVideosValidators.isVideoTagsValid) + +function beforeCreate (video, options, next) { const tasks = [] if (video.isOwned()) { const videoPath = pathUtils.join(constants.CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) - this.podHost = constants.CONFIG.WEBSERVER.HOST tasks.push( // TODO: refractoring @@ -123,7 +114,7 @@ VideoSchema.pre('save', function (next) { if (err) return callback(err) const parsedTorrent = parseTorrent(torrent) - video.magnet.infoHash = parsedTorrent.infoHash + video.infoHash = parsedTorrent.infoHash callback(null) }) @@ -141,12 +132,46 @@ VideoSchema.pre('save', function (next) { } return next() -}) +} + +function afterDestroy (video, options, next) { + const tasks = [] -mongoose.model('Video', VideoSchema) + tasks.push( + function (callback) { + removeThumbnail(video, callback) + } + ) + + if (video.isOwned()) { + tasks.push( + function (callback) { + removeFile(video, callback) + }, + function (callback) { + removeTorrent(video, callback) + }, + function (callback) { + removePreview(video, callback) + } + ) + } + + parallel(tasks, next) +} // ------------------------------ METHODS ------------------------------ +function associate (models) { + this.belongsTo(models.Author, { + foreignKey: { + name: 'authorId', + allowNull: false + }, + onDelete: 'cascade' + }) +} + function generateMagnetUri () { let baseUrlHttp, baseUrlWs @@ -154,8 +179,8 @@ function generateMagnetUri () { baseUrlHttp = constants.CONFIG.WEBSERVER.URL baseUrlWs = constants.CONFIG.WEBSERVER.WS + '://' + constants.CONFIG.WEBSERVER.HOSTNAME + ':' + constants.CONFIG.WEBSERVER.PORT } else { - baseUrlHttp = constants.REMOTE_SCHEME.HTTP + '://' + this.podHost - baseUrlWs = constants.REMOTE_SCHEME.WS + '://' + this.podHost + baseUrlHttp = constants.REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host + baseUrlWs = constants.REMOTE_SCHEME.WS + '://' + this.Author.Pod.host } const xs = baseUrlHttp + constants.STATIC_PATHS.TORRENTS + this.getTorrentName() @@ -166,7 +191,7 @@ function generateMagnetUri () { xs, announce, urlList, - infoHash: this.magnet.infoHash, + infoHash: this.infoHash, name: this.name } @@ -174,20 +199,20 @@ function generateMagnetUri () { } function getVideoFilename () { - if (this.isOwned()) return this._id + this.extname + if (this.isOwned()) return this.id + this.extname return this.remoteId + this.extname } function getThumbnailName () { // We always have a copy of the thumbnail - return this._id + '.jpg' + return this.id + '.jpg' } function getPreviewName () { const extension = '.jpg' - if (this.isOwned()) return this._id + extension + if (this.isOwned()) return this.id + extension return this.remoteId + extension } @@ -195,7 +220,7 @@ function getPreviewName () { function getTorrentName () { const extension = '.torrent' - if (this.isOwned()) return this._id + extension + if (this.isOwned()) return this.id + extension return this.remoteId + extension } @@ -205,18 +230,27 @@ function isOwned () { } function toFormatedJSON () { + let podHost + + if (this.Author.Pod) { + podHost = this.Author.Pod.host + } else { + // It means it's our video + podHost = constants.CONFIG.WEBSERVER.HOST + } + const json = { - id: this._id, + id: this.id, name: this.name, description: this.description, - podHost: this.podHost, + podHost, isLocal: this.isOwned(), magnetUri: this.generateMagnetUri(), - author: this.author, + author: this.Author.name, duration: this.duration, tags: this.tags, thumbnailPath: constants.STATIC_PATHS.THUMBNAILS + '/' + this.getThumbnailName(), - createdDate: this.createdDate + createdAt: this.createdAt } return json @@ -236,13 +270,13 @@ function toRemoteJSON (callback) { const remoteVideo = { name: self.name, description: self.description, - magnet: self.magnet, - remoteId: self._id, - author: self.author, + infoHash: self.infoHash, + remoteId: self.id, + author: self.Author.name, duration: self.duration, thumbnailBase64: new Buffer(thumbnailData).toString('base64'), tags: self.tags, - createdDate: self.createdDate, + createdAt: self.createdAt, extname: self.extname } @@ -273,50 +307,168 @@ function getDurationFromFile (videoPath, callback) { } function listForApi (start, count, sort, callback) { - const query = {} - return modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback) + const query = { + offset: start, + limit: count, + order: [ modelUtils.getSort(sort) ], + include: [ + { + model: this.sequelize.models.Author, + include: [ this.sequelize.models.Pod ] + } + ] + } + + return this.findAndCountAll(query).asCallback(function (err, result) { + if (err) return callback(err) + + return callback(null, result.rows, result.count) + }) } function listByHostAndRemoteId (fromHost, remoteId, callback) { - this.find({ podHost: fromHost, remoteId: remoteId }, callback) -} + const query = { + where: { + remoteId: remoteId + }, + include: [ + { + model: this.sequelize.models.Author, + include: [ + { + model: this.sequelize.models.Pod, + where: { + host: fromHost + } + } + ] + } + ] + } -function listByHost (fromHost, callback) { - this.find({ podHost: fromHost }, callback) + return this.findAll(query).asCallback(callback) } -function listOwned (callback) { +function listOwnedAndPopulateAuthor (callback) { // If remoteId is null this is *our* video - this.find({ remoteId: null }, callback) + const query = { + where: { + remoteId: null + }, + include: [ this.sequelize.models.Author ] + } + + return this.findAll(query).asCallback(callback) } function listOwnedByAuthor (author, callback) { - this.find({ remoteId: null, author: author }, callback) -} + const query = { + where: { + remoteId: null + }, + include: [ + { + model: this.sequelize.models.Author, + where: { + name: author + } + } + ] + } -function listRemotes (callback) { - this.find({ remoteId: { $ne: null } }, callback) + return this.findAll(query).asCallback(callback) } function load (id, callback) { - this.findById(id, callback) + return this.findById(id).asCallback(callback) } -function search (value, field, start, count, sort, callback) { - const query = {} +function loadAndPopulateAuthor (id, callback) { + const options = { + include: [ this.sequelize.models.Author ] + } + + return this.findById(id, options).asCallback(callback) +} + +function loadAndPopulateAuthorAndPod (id, callback) { + const options = { + include: [ + { + model: this.sequelize.models.Author, + include: [ this.sequelize.models.Pod ] + } + ] + } + + return this.findById(id, options).asCallback(callback) +} + +function searchAndPopulateAuthorAndPod (value, field, start, count, sort, callback) { + const podInclude = { + model: this.sequelize.models.Pod + } + const authorInclude = { + model: this.sequelize.models.Author, + include: [ + podInclude + ] + } + + const query = { + where: {}, + include: [ + authorInclude + ], + offset: start, + limit: count, + order: [ modelUtils.getSort(sort) ] + } + + // TODO: include our pod for podHost searches (we are not stored in the database) // Make an exact search with the magnet if (field === 'magnetUri') { const infoHash = magnetUtil.decode(value).infoHash - query.magnet = { - infoHash - } + query.where.infoHash = infoHash } else if (field === 'tags') { - query[field] = value + query.where[field] = value + } else if (field === 'host') { + const whereQuery = { + '$Author.Pod.host$': { + $like: '%' + value + '%' + } + } + + // Include our pod? (not stored in the database) + if (constants.CONFIG.WEBSERVER.HOST.indexOf(value) !== -1) { + query.where = { + $or: [ + whereQuery, + { + remoteId: null + } + ] + } + } else { + query.where = whereQuery + } + } else if (field === 'author') { + query.where = { + '$Author.name$': { + $like: '%' + value + '%' + } + } } else { - query[field] = new RegExp(value, 'i') + query.where[field] = { + $like: '%' + value + '%' + } } - modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback) + return this.findAndCountAll(query).asCallback(function (err, result) { + if (err) return callback(err) + + return callback(null, result.rows, result.count) + }) } // --------------------------------------------------------------------------- -- cgit v1.2.3