1
0
Fork 0
mirror of https://github.com/Chocobozzz/PeerTube.git synced 2025-10-05 19:42:24 +02:00

First version with PostgreSQL

This commit is contained in:
Chocobozzz 2016-12-11 21:50:51 +01:00
parent 108626609e
commit feb4bdfd9b
68 changed files with 1171 additions and 730 deletions

View file

@ -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
}
}
)
// ---------------------------------------------------------------------------
const ApplicationSchema = mongoose.Schema({
mongoSchemaVersion: {
type: Number,
default: 0
}
})
ApplicationSchema.statics = {
loadMongoSchemaVersion,
updateMongoSchemaVersion
return Application
}
mongoose.model('Application', ApplicationSchema)
// ---------------------------------------------------------------------------
function loadMongoSchemaVersion (callback) {
return this.findOne({}, { mongoSchemaVersion: 1 }, function (err, data) {
const version = data ? data.mongoSchemaVersion : 0
function loadSqlSchemaVersion (callback) {
const query = {
attributes: [ 'sqlSchemaVersion' ]
}
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)
}

28
server/models/author.js Normal file
View file

@ -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'
})
}

View file

@ -1,33 +1,63 @@
const mongoose = require('mongoose')
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
}
}
)
const OAuthClientSchema = mongoose.Schema({
clientSecret: String,
grants: Array,
redirectUris: Array
})
OAuthClientSchema.path('clientSecret').required(true)
OAuthClientSchema.statics = {
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)
}

View file

@ -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' }
})
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,
OAuthTokenSchema.path('accessToken').required(true)
OAuthTokenSchema.path('client').required(true)
OAuthTokenSchema.path('user').required(true)
getByRefreshTokenAndPopulateClient,
getByTokenAndPopulateUser,
getByRefreshTokenAndPopulateUser,
removeByUserId
}
}
)
OAuthTokenSchema.statics = {
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)
}

View file

@ -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
}
})
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,
PodSchema.path('host').validate(validator.isURL)
PodSchema.path('publicKey').required(true)
PodSchema.path('score').validate(function (value) { return !isNaN(value) })
countAll,
incrementScores,
list,
listAllIds,
listBadPods,
load,
loadByHost,
removeAll
},
instanceMethods: {
toFormatedJSON
}
}
)
PodSchema.methods = {
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)
}

View file

@ -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',
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Pod'
}
]
})
request: {
type: DataTypes.JSON
},
endpoint: {
// TODO: enum?
type: DataTypes.STRING
}
},
{
classMethods: {
associate,
RequestSchema.statics = {
activate,
deactivate,
flush,
forceSend,
list,
remainingMilliSeconds
activate,
countTotalRequests,
deactivate,
flush,
forceSend,
remainingMilliSeconds
}
}
)
return Request
}
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)
// ------------------------------ 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)
}

View file

@ -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)
}

View file

@ -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
})
module.exports = function (sequelize, DataTypes) {
const User = sequelize.define('User',
{
password: {
type: DataTypes.STRING
},
username: {
type: DataTypes.STRING
},
role: {
type: DataTypes.STRING
}
},
{
classMethods: {
associate,
UserSchema.path('password').required(customUsersValidators.isUserPasswordValid)
UserSchema.path('username').required(customUsersValidators.isUserUsernameValid)
UserSchema.path('role').validate(customUsersValidators.isUserRoleValid)
countTotal,
getByUsername,
list,
listForApi,
loadById,
loadByUsername
},
instanceMethods: {
isPasswordMatch,
toFormatedJSON
},
hooks: {
beforeCreate: beforeCreateOrUpdate,
beforeUpdate: beforeCreateOrUpdate
}
}
)
UserSchema.methods = {
isPasswordMatch,
toFormatedJSON
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)
}

View file

@ -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 ]
}
// ---------------------------------------------------------------------------

View file

@ -7,70 +7,134 @@ 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
const Video = sequelize.define('Video',
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
name: {
type: DataTypes.STRING
},
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
}
}
)
return Video
}
// TODO: Validation
// VideoSchema.path('name').validate(customVideosValidators.isVideoNameValid)
// VideoSchema.path('description').validate(customVideosValidators.isVideoDescriptionValid)
// VideoSchema.path('podHost').validate(customVideosValidators.isVideoPodHostValid)
// VideoSchema.path('author').validate(customVideosValidators.isVideoAuthorValid)
// VideoSchema.path('duration').validate(customVideosValidators.isVideoDurationValid)
// VideoSchema.path('tags').validate(customVideosValidators.isVideoTagsValid)
function beforeCreate (video, options, next) {
const tasks = []
if (video.isOwned()) {
const videoPath = pathUtils.join(constants.CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename())
tasks.push(
// TODO: refractoring
function (callback) {
const options = {
announceList: [
[ constants.CONFIG.WEBSERVER.WS + '://' + constants.CONFIG.WEBSERVER.HOSTNAME + ':' + constants.CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
],
urlList: [
constants.CONFIG.WEBSERVER.URL + constants.STATIC_PATHS.WEBSEED + video.getVideoFilename()
]
}
createTorrent(videoPath, options, function (err, torrent) {
if (err) return callback(err)
fs.writeFile(constants.CONFIG.STORAGE.TORRENTS_DIR + video.getTorrentName(), torrent, function (err) {
if (err) return callback(err)
const parsedTorrent = parseTorrent(torrent)
video.infoHash = parsedTorrent.infoHash
callback(null)
})
})
},
function (callback) {
createThumbnail(video, videoPath, callback)
},
function (callback) {
createPreview(video, videoPath, callback)
}
)
return parallel(tasks, next)
}
})
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
return next()
}
VideoSchema.statics = {
generateThumbnailFromBase64,
getDurationFromFile,
listForApi,
listByHostAndRemoteId,
listByHost,
listOwned,
listOwnedByAuthor,
listRemotes,
load,
search
}
VideoSchema.pre('remove', function (next) {
const video = this
function afterDestroy (video, options, next) {
const tasks = []
tasks.push(
@ -94,59 +158,20 @@ VideoSchema.pre('remove', function (next) {
}
parallel(tasks, next)
})
VideoSchema.pre('save', function (next) {
const video = this
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
function (callback) {
const options = {
announceList: [
[ constants.CONFIG.WEBSERVER.WS + '://' + constants.CONFIG.WEBSERVER.HOSTNAME + ':' + constants.CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
],
urlList: [
constants.CONFIG.WEBSERVER.URL + constants.STATIC_PATHS.WEBSEED + video.getVideoFilename()
]
}
createTorrent(videoPath, options, function (err, torrent) {
if (err) return callback(err)
fs.writeFile(constants.CONFIG.STORAGE.TORRENTS_DIR + video.getTorrentName(), torrent, function (err) {
if (err) return callback(err)
const parsedTorrent = parseTorrent(torrent)
video.magnet.infoHash = parsedTorrent.infoHash
callback(null)
})
})
},
function (callback) {
createThumbnail(video, videoPath, callback)
},
function (callback) {
createPreview(video, videoPath, callback)
}
)
return parallel(tasks, next)
}
return next()
})
mongoose.model('Video', VideoSchema)
}
// ------------------------------ 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
}
}
]
}
]
}
return this.findAll(query).asCallback(callback)
}
function listByHost (fromHost, callback) {
this.find({ podHost: fromHost }, 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)
})
}
// ---------------------------------------------------------------------------