First version with PostgreSQL

pull/30/head
Chocobozzz 2016-12-11 21:50:51 +01:00
parent 108626609e
commit feb4bdfd9b
68 changed files with 1171 additions and 730 deletions

View File

@ -15,7 +15,7 @@
<td>{{ friend.id }}</td>
<td>{{ friend.host }}</td>
<td>{{ friend.score }}</td>
<td>{{ friend.createdDate | date: 'medium' }}</td>
<td>{{ friend.createdAt | date: 'medium' }}</td>
</tr>
</tbody>
</table>

View File

@ -2,5 +2,5 @@ export interface Friend {
id: string;
host: string;
score: number;
createdDate: Date;
createdAt: Date;
}

View File

@ -18,6 +18,6 @@
<div>
<span class="label-description">Remaining requests:</span>
{{ stats.requests.length }}
{{ stats.totalRequests }}
</div>
</div>

View File

@ -19,7 +19,7 @@ export class RequestStatsComponent implements OnInit, OnDestroy {
}
ngOnDestroy() {
if (this.stats.secondsInterval !== null) {
if (this.stats !== null && this.stats.secondsInterval !== null) {
clearInterval(this.interval);
}
}

View File

@ -7,18 +7,18 @@ export class RequestStats {
maxRequestsInParallel: number;
milliSecondsInterval: number;
remainingMilliSeconds: number;
requests: Request[];
totalRequests: number;
constructor(hash: {
maxRequestsInParallel: number,
milliSecondsInterval: number,
remainingMilliSeconds: number,
requests: Request[];
totalRequests: number;
}) {
this.maxRequestsInParallel = hash.maxRequestsInParallel;
this.milliSecondsInterval = hash.milliSecondsInterval;
this.remainingMilliSeconds = hash.remainingMilliSeconds;
this.requests = hash.requests;
this.totalRequests = hash.totalRequests;
}
get remainingSeconds() {

View File

@ -14,7 +14,7 @@
<tr *ngFor="let user of users">
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>{{ user.createdDate | date: 'medium' }}</td>
<td>{{ user.createdAt | date: 'medium' }}</td>
<td class="text-right">
<span class="glyphicon glyphicon-remove" *ngIf="!user.isAdmin()" (click)="removeUser(user)"></span>
</td>

View File

@ -7,9 +7,6 @@ export class AuthUser extends User {
USERNAME: 'username'
};
id: string;
role: string;
username: string;
tokens: Tokens;
static load() {
@ -17,7 +14,7 @@ export class AuthUser extends User {
if (usernameLocalStorage) {
return new AuthUser(
{
id: localStorage.getItem(this.KEYS.ID),
id: parseInt(localStorage.getItem(this.KEYS.ID)),
username: localStorage.getItem(this.KEYS.USERNAME),
role: localStorage.getItem(this.KEYS.ROLE)
},
@ -35,7 +32,7 @@ export class AuthUser extends User {
Tokens.flush();
}
constructor(userHash: { id: string, username: string, role: string }, hashTokens: any) {
constructor(userHash: { id: number, username: string, role: string }, hashTokens: any) {
super(userHash);
this.tokens = new Tokens(hashTokens);
}
@ -58,7 +55,7 @@ export class AuthUser extends User {
}
save() {
localStorage.setItem(AuthUser.KEYS.ID, this.id);
localStorage.setItem(AuthUser.KEYS.ID, this.id.toString());
localStorage.setItem(AuthUser.KEYS.USERNAME, this.username);
localStorage.setItem(AuthUser.KEYS.ROLE, this.role);
this.tokens.save();

View File

@ -1 +1 @@
export type SearchField = "name" | "author" | "podUrl" | "magnetUri" | "tags";
export type SearchField = "name" | "author" | "host" | "magnetUri" | "tags";

View File

@ -14,8 +14,8 @@ export class SearchComponent implements OnInit {
fieldChoices = {
name: 'Name',
author: 'Author',
podUrl: 'Pod Url',
magnetUri: 'Magnet Uri',
host: 'Pod Host',
magnetUri: 'Magnet URI',
tags: 'Tags'
};
searchCriterias: Search = {

View File

@ -1,16 +1,16 @@
export class User {
id: string;
id: number;
username: string;
role: string;
createdDate: Date;
createdAt: Date;
constructor(hash: { id: string, username: string, role: string, createdDate?: Date }) {
constructor(hash: { id: number, username: string, role: string, createdAt?: Date }) {
this.id = hash.id;
this.username = hash.username;
this.role = hash.role;
if (hash.createdDate) {
this.createdDate = hash.createdDate;
if (hash.createdAt) {
this.createdAt = hash.createdAt;
}
}

View File

@ -1,3 +1,3 @@
export type SortField = "name" | "-name"
| "duration" | "-duration"
| "createdDate" | "-createdDate";
| "createdAt" | "-createdAt";

View File

@ -1,7 +1,7 @@
export class Video {
author: string;
by: string;
createdDate: Date;
createdAt: Date;
description: string;
duration: string;
id: string;
@ -27,7 +27,7 @@ export class Video {
constructor(hash: {
author: string,
createdDate: string,
createdAt: string,
description: string,
duration: number;
id: string,
@ -39,7 +39,7 @@ export class Video {
thumbnailPath: string
}) {
this.author = hash.author;
this.createdDate = new Date(hash.createdDate);
this.createdAt = new Date(hash.createdAt);
this.description = hash.description;
this.duration = Video.createDurationString(hash.duration);
this.id = hash.id;

View File

@ -145,7 +145,7 @@ export class VideoListComponent implements OnInit, OnDestroy {
};
}
this.sort = <SortField>routeParams['sort'] || '-createdDate';
this.sort = <SortField>routeParams['sort'] || '-createdAt';
if (routeParams['page'] !== undefined) {
this.pagination.currentPage = parseInt(routeParams['page']);

View File

@ -23,6 +23,6 @@
</span>
<a [routerLink]="['/videos/list', { field: 'author', search: video.author, sort: currentSort }]" class="video-miniature-author">{{ video.by }}</a>
<span class="video-miniature-created-date">{{ video.createdDate | date:'short' }}</span>
<span class="video-miniature-created-at">{{ video.createdAt | date:'short' }}</span>
</div>
</div>

View File

@ -79,7 +79,7 @@
}
}
.video-miniature-author, .video-miniature-created-date {
.video-miniature-author, .video-miniature-created-at {
display: block;
margin-left: 1px;
font-size: 12px;

View File

@ -17,8 +17,8 @@ export class VideoSortComponent {
'-name': 'Name - Desc',
'duration': 'Duration - Asc',
'-duration': 'Duration - Desc',
'createdDate': 'Created Date - Asc',
'-createdDate': 'Created Date - Desc'
'createdAt': 'Created Date - Asc',
'-createdAt': 'Created Date - Desc'
};
get choiceKeys() {

View File

@ -47,7 +47,7 @@
{{ video.by }}
</a>
</span>
<span id="video-date">on {{ video.createdDate | date:'short' }}</span>
<span id="video-date">on {{ video.createdAt | date:'short' }}</span>
</div>
</div>
</div>

View File

@ -8,8 +8,8 @@ webserver:
database:
hostname: 'localhost'
port: 27017
suffix: '-dev'
port: 5432
suffix: '_dev'
# From the project root directory
storage:

View File

@ -5,4 +5,4 @@ webserver:
port: 80
database:
suffix: '-prod'
suffix: '_prod'

View File

@ -6,7 +6,7 @@ webserver:
port: 9001
database:
suffix: '-test1'
suffix: '_test1'
# From the project root directory
storage:

View File

@ -6,7 +6,7 @@ webserver:
port: 9002
database:
suffix: '-test2'
suffix: '_test2'
# From the project root directory
storage:

View File

@ -6,7 +6,7 @@ webserver:
port: 9003
database:
suffix: '-test3'
suffix: '_test3'
# From the project root directory
storage:

View File

@ -6,7 +6,7 @@ webserver:
port: 9004
database:
suffix: '-test4'
suffix: '_test4'
# From the project root directory
storage:

View File

@ -6,7 +6,7 @@ webserver:
port: 9005
database:
suffix: '-test5'
suffix: '_test5'
# From the project root directory
storage:

View File

@ -6,7 +6,7 @@ webserver:
port: 9006
database:
suffix: '-test6'
suffix: '_test6'
# From the project root directory
storage:

View File

@ -6,4 +6,4 @@ webserver:
database:
hostname: 'localhost'
port: 27017
port: 5432

View File

@ -56,16 +56,18 @@
"lodash": "^4.11.1",
"magnet-uri": "^5.1.4",
"mkdirp": "^0.5.1",
"mongoose": "^4.0.5",
"morgan": "^1.5.3",
"multer": "^1.1.0",
"openssl-wrapper": "^0.3.4",
"parse-torrent": "^5.8.0",
"password-generator": "^2.0.2",
"pg": "^6.1.0",
"pg-hstore": "^2.3.2",
"request": "^2.57.0",
"request-replay": "^1.0.2",
"rimraf": "^2.5.4",
"scripty": "^1.5.0",
"sequelize": "^3.27.0",
"ursa": "^0.9.1",
"winston": "^2.1.1",
"ws": "^1.1.1"

View File

@ -1,6 +1,7 @@
#!/usr/bin/env sh
for i in $(seq 1 6); do
printf "use peertube-test%s;\ndb.dropDatabase();" "$i" | mongo
dropdb "peertube_test$i"
rm -rf "./test$i"
createdb "peertube_test$i"
done

View File

@ -17,10 +17,9 @@ const app = express()
// ----------- Database -----------
const constants = require('./server/initializers/constants')
const database = require('./server/initializers/database')
const logger = require('./server/helpers/logger')
database.connect()
// Initialize database and models
const db = require('./server/initializers/database')
// ----------- Checker -----------
const checker = require('./server/initializers/checker')
@ -39,9 +38,7 @@ if (errorMessage !== null) {
const customValidators = require('./server/helpers/custom-validators')
const installer = require('./server/initializers/installer')
const migrator = require('./server/initializers/migrator')
const mongoose = require('mongoose')
const routes = require('./server/controllers')
const Request = mongoose.model('Request')
// ----------- Command line -----------
@ -130,7 +127,7 @@ installer.installApplication(function (err) {
// ----------- Make the server listening -----------
server.listen(port, function () {
// Activate the pool requests
Request.activate()
db.Request.activate()
logger.info('Server listening on port %d', port)
logger.info('Webserver: %s', constants.CONFIG.WEBSERVER.URL)

View File

@ -1,13 +1,11 @@
'use strict'
const express = require('express')
const mongoose = require('mongoose')
const constants = require('../../initializers/constants')
const db = require('../../initializers/database')
const logger = require('../../helpers/logger')
const Client = mongoose.model('OAuthClient')
const router = express.Router()
router.get('/local', getLocalClient)
@ -27,12 +25,12 @@ function getLocalClient (req, res, next) {
return res.type('json').status(403).end()
}
Client.loadFirstClient(function (err, client) {
db.OAuthClient.loadFirstClient(function (err, client) {
if (err) return next(err)
if (!client) return next(new Error('No client available.'))
res.json({
client_id: client._id,
client_id: client.clientId,
client_secret: client.clientSecret
})
})

View File

@ -1,9 +1,9 @@
'use strict'
const express = require('express')
const mongoose = require('mongoose')
const waterfall = require('async/waterfall')
const db = require('../../initializers/database')
const logger = require('../../helpers/logger')
const friends = require('../../lib/friends')
const middlewares = require('../../middlewares')
@ -15,7 +15,6 @@ const validators = middlewares.validators.pods
const signatureValidator = middlewares.validators.remote.signature
const router = express.Router()
const Pod = mongoose.model('Pod')
router.get('/', listPods)
router.post('/',
@ -53,15 +52,15 @@ function addPods (req, res, next) {
waterfall([
function addPod (callback) {
const pod = new Pod(informations)
pod.save(function (err, podCreated) {
const pod = db.Pod.build(informations)
pod.save().asCallback(function (err, podCreated) {
// Be sure about the number of parameters for the callback
return callback(err, podCreated)
})
},
function sendMyVideos (podCreated, callback) {
friends.sendOwnedVideosToPod(podCreated._id)
friends.sendOwnedVideosToPod(podCreated.id)
callback(null)
},
@ -84,7 +83,7 @@ function addPods (req, res, next) {
}
function listPods (req, res, next) {
Pod.list(function (err, podsList) {
db.Pod.list(function (err, podsList) {
if (err) return next(err)
res.json(getFormatedPods(podsList))
@ -111,11 +110,11 @@ function removePods (req, res, next) {
waterfall([
function loadPod (callback) {
Pod.loadByHost(host, callback)
db.Pod.loadByHost(host, callback)
},
function removePod (pod, callback) {
pod.remove(callback)
pod.destroy().asCallback(callback)
}
], function (err) {
if (err) return next(err)

View File

@ -3,15 +3,15 @@
const each = require('async/each')
const eachSeries = require('async/eachSeries')
const express = require('express')
const mongoose = require('mongoose')
const waterfall = require('async/waterfall')
const db = require('../../initializers/database')
const middlewares = require('../../middlewares')
const secureMiddleware = middlewares.secure
const validators = middlewares.validators.remote
const logger = require('../../helpers/logger')
const router = express.Router()
const Video = mongoose.model('Video')
router.post('/videos',
validators.signature,
@ -53,34 +53,99 @@ function remoteVideos (req, res, next) {
function addRemoteVideo (videoToCreateData, fromHost, callback) {
logger.debug('Adding remote video "%s".', videoToCreateData.name)
const video = new Video(videoToCreateData)
video.podHost = fromHost
Video.generateThumbnailFromBase64(video, videoToCreateData.thumbnailBase64, function (err) {
if (err) {
logger.error('Cannot generate thumbnail from base 64 data.', { error: err })
return callback(err)
waterfall([
function findOrCreatePod (callback) {
fromHost
const query = {
where: {
host: fromHost
},
defaults: {
host: fromHost
}
}
db.Pod.findOrCreate(query).asCallback(function (err, result) {
// [ instance, wasCreated ]
return callback(err, result[0])
})
},
function findOrCreateAuthor (pod, callback) {
const username = videoToCreateData.author
const query = {
where: {
name: username,
podId: pod.id
},
defaults: {
name: username,
podId: pod.id
}
}
db.Author.findOrCreate(query).asCallback(function (err, result) {
// [ instance, wasCreated ]
return callback(err, result[0])
})
},
function createVideoObject (author, callback) {
const videoData = {
name: videoToCreateData.name,
remoteId: videoToCreateData.remoteId,
extname: videoToCreateData.extname,
infoHash: videoToCreateData.infoHash,
description: videoToCreateData.description,
authorId: author.id,
duration: videoToCreateData.duration,
tags: videoToCreateData.tags
}
const video = db.Video.build(videoData)
return callback(null, video)
},
function generateThumbnail (video, callback) {
db.Video.generateThumbnailFromBase64(video, videoToCreateData.thumbnailBase64, function (err) {
if (err) {
logger.error('Cannot generate thumbnail from base 64 data.', { error: err })
return callback(err)
}
video.save().asCallback(callback)
})
},
function insertIntoDB (video, callback) {
video.save().asCallback(callback)
}
video.save(callback)
})
], callback)
}
function removeRemoteVideo (videoToRemoveData, fromHost, callback) {
// TODO: use bulkDestroy?
// We need the list because we have to remove some other stuffs (thumbnail etc)
Video.listByHostAndRemoteId(fromHost, videoToRemoveData.remoteId, function (err, videosList) {
db.Video.listByHostAndRemoteId(fromHost, videoToRemoveData.remoteId, function (err, videosList) {
if (err) {
logger.error('Cannot list videos from host and magnets.', { error: err })
logger.error('Cannot list videos from host and remote id.', { error: err.message })
return callback(err)
}
if (videosList.length === 0) {
logger.error('No remote video was found for this pod.', { magnetUri: videoToRemoveData.magnetUri, podHost: fromHost })
logger.error('No remote video was found for this pod.', { remoteId: videoToRemoveData.remoteId, podHost: fromHost })
}
each(videosList, function (video, callbackEach) {
logger.debug('Removing remote video %s.', video.magnetUri)
logger.debug('Removing remote video %s.', video.remoteId)
video.remove(callbackEach)
video.destroy().asCallback(callbackEach)
}, callback)
})
}

View File

@ -1,15 +1,13 @@
'use strict'
const express = require('express')
const mongoose = require('mongoose')
const constants = require('../../initializers/constants')
const db = require('../../initializers/database')
const middlewares = require('../../middlewares')
const admin = middlewares.admin
const oAuth = middlewares.oauth
const Request = mongoose.model('Request')
const router = express.Router()
router.get('/stats',
@ -25,13 +23,13 @@ module.exports = router
// ---------------------------------------------------------------------------
function getStatsRequests (req, res, next) {
Request.list(function (err, requests) {
db.Request.countTotalRequests(function (err, totalRequests) {
if (err) return next(err)
return res.json({
requests: requests,
totalRequests: totalRequests,
maxRequestsInParallel: constants.REQUESTS_IN_PARALLEL,
remainingMilliSeconds: Request.remainingMilliSeconds(),
remainingMilliSeconds: db.Request.remainingMilliSeconds(),
milliSecondsInterval: constants.REQUESTS_INTERVAL
})
})

View File

@ -2,10 +2,10 @@
const each = require('async/each')
const express = require('express')
const mongoose = require('mongoose')
const waterfall = require('async/waterfall')
const constants = require('../../initializers/constants')
const db = require('../../initializers/database')
const friends = require('../../lib/friends')
const logger = require('../../helpers/logger')
const middlewares = require('../../middlewares')
@ -17,9 +17,6 @@ const validatorsPagination = middlewares.validators.pagination
const validatorsSort = middlewares.validators.sort
const validatorsUsers = middlewares.validators.users
const User = mongoose.model('User')
const Video = mongoose.model('Video')
const router = express.Router()
router.get('/me', oAuth.authenticate, getUserInformation)
@ -62,13 +59,13 @@ module.exports = router
// ---------------------------------------------------------------------------
function createUser (req, res, next) {
const user = new User({
const user = db.User.build({
username: req.body.username,
password: req.body.password,
role: constants.USER_ROLES.USER
})
user.save(function (err, createdUser) {
user.save().asCallback(function (err, createdUser) {
if (err) return next(err)
return res.type('json').status(204).end()
@ -76,7 +73,7 @@ function createUser (req, res, next) {
}
function getUserInformation (req, res, next) {
User.loadByUsername(res.locals.oauth.token.user.username, function (err, user) {
db.User.loadByUsername(res.locals.oauth.token.user.username, function (err, user) {
if (err) return next(err)
return res.json(user.toFormatedJSON())
@ -84,7 +81,7 @@ function getUserInformation (req, res, next) {
}
function listUsers (req, res, next) {
User.listForApi(req.query.start, req.query.count, req.query.sort, function (err, usersList, usersTotal) {
db.User.listForApi(req.query.start, req.query.count, req.query.sort, function (err, usersList, usersTotal) {
if (err) return next(err)
res.json(getFormatedUsers(usersList, usersTotal))
@ -94,18 +91,19 @@ function listUsers (req, res, next) {
function removeUser (req, res, next) {
waterfall([
function getUser (callback) {
User.loadById(req.params.id, callback)
db.User.loadById(req.params.id, callback)
},
// TODO: use foreignkey?
function getVideos (user, callback) {
Video.listOwnedByAuthor(user.username, function (err, videos) {
db.Video.listOwnedByAuthor(user.username, function (err, videos) {
return callback(err, user, videos)
})
},
function removeVideosFromDB (user, videos, callback) {
each(videos, function (video, callbackEach) {
video.remove(callbackEach)
video.destroy().asCallback(callbackEach)
}, function (err) {
return callback(err, user, videos)
})
@ -115,7 +113,7 @@ function removeUser (req, res, next) {
videos.forEach(function (video) {
const params = {
name: video.name,
magnetUri: video.magnetUri
remoteId: video.id
}
friends.removeVideoToFriends(params)
@ -125,7 +123,7 @@ function removeUser (req, res, next) {
},
function removeUserFromDB (user, callback) {
user.remove(callback)
user.destroy().asCallback(callback)
}
], function andFinally (err) {
if (err) {
@ -138,11 +136,11 @@ function removeUser (req, res, next) {
}
function updateUser (req, res, next) {
User.loadByUsername(res.locals.oauth.token.user.username, function (err, user) {
db.User.loadByUsername(res.locals.oauth.token.user.username, function (err, user) {
if (err) return next(err)
user.password = req.body.password
user.save(function (err) {
user.save().asCallback(function (err) {
if (err) return next(err)
return res.sendStatus(204)

View File

@ -2,12 +2,12 @@
const express = require('express')
const fs = require('fs')
const mongoose = require('mongoose')
const multer = require('multer')
const path = require('path')
const waterfall = require('async/waterfall')
const constants = require('../../initializers/constants')
const db = require('../../initializers/database')
const logger = require('../../helpers/logger')
const friends = require('../../lib/friends')
const middlewares = require('../../middlewares')
@ -22,7 +22,6 @@ const sort = middlewares.sort
const utils = require('../../helpers/utils')
const router = express.Router()
const Video = mongoose.model('Video')
// multer configuration
const storage = multer.diskStorage({
@ -87,40 +86,60 @@ function addVideo (req, res, next) {
const videoInfos = req.body
waterfall([
function createVideoObject (callback) {
const id = mongoose.Types.ObjectId()
function findOrCreateAuthor (callback) {
const username = res.locals.oauth.token.user.username
const query = {
where: {
name: username,
podId: null
},
defaults: {
name: username,
podId: null // null because it is OUR pod
}
}
db.Author.findOrCreate(query).asCallback(function (err, result) {
// [ instance, wasCreated ]
return callback(err, result[0])
})
},
function createVideoObject (author, callback) {
const videoData = {
_id: id,
name: videoInfos.name,
remoteId: null,
extname: path.extname(videoFile.filename),
description: videoInfos.description,
author: res.locals.oauth.token.user.username,
duration: videoFile.duration,
tags: videoInfos.tags
tags: videoInfos.tags,
authorId: author.id
}
const video = new Video(videoData)
const video = db.Video.build(videoData)
return callback(null, video)
return callback(null, author, video)
},
// Set the videoname the same as the MongoDB id
function renameVideoFile (video, callback) {
// Set the videoname the same as the id
function renameVideoFile (author, video, callback) {
const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR
const source = path.join(videoDir, videoFile.filename)
const destination = path.join(videoDir, video.getVideoFilename())
fs.rename(source, destination, function (err) {
return callback(err, video)
return callback(err, author, video)
})
},
function insertIntoDB (video, callback) {
video.save(function (err, video) {
// Assert there are only one argument sent to the next function (video)
return callback(err, video)
function insertIntoDB (author, video, callback) {
video.save().asCallback(function (err, videoCreated) {
// Do not forget to add Author informations to the created video
videoCreated.Author = author
return callback(err, videoCreated)
})
},
@ -147,7 +166,7 @@ function addVideo (req, res, next) {
}
function getVideo (req, res, next) {
Video.load(req.params.id, function (err, video) {
db.Video.loadAndPopulateAuthorAndPod(req.params.id, function (err, video) {
if (err) return next(err)
if (!video) {
@ -159,7 +178,7 @@ function getVideo (req, res, next) {
}
function listVideos (req, res, next) {
Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
if (err) return next(err)
res.json(getFormatedVideos(videosList, videosTotal))
@ -171,11 +190,11 @@ function removeVideo (req, res, next) {
waterfall([
function getVideo (callback) {
Video.load(videoId, callback)
db.Video.load(videoId, callback)
},
function removeFromDB (video, callback) {
video.remove(function (err) {
video.destroy().asCallback(function (err) {
if (err) return callback(err)
return callback(null, video)
@ -185,7 +204,7 @@ function removeVideo (req, res, next) {
function sendInformationToFriends (video, callback) {
const params = {
name: video.name,
remoteId: video._id
remoteId: video.id
}
friends.removeVideoToFriends(params)
@ -203,7 +222,7 @@ function removeVideo (req, res, next) {
}
function searchVideos (req, res, next) {
Video.search(req.params.value, req.query.field, req.query.start, req.query.count, req.query.sort,
db.Video.searchAndPopulateAuthorAndPod(req.params.value, req.query.field, req.query.start, req.query.count, req.query.sort,
function (err, videosList, videosTotal) {
if (err) return next(err)

View File

@ -3,13 +3,12 @@
const parallel = require('async/parallel')
const express = require('express')
const fs = require('fs')
const mongoose = require('mongoose')
const path = require('path')
const validator = require('express-validator').validator
const constants = require('../initializers/constants')
const db = require('../initializers/database')
const Video = mongoose.model('Video')
const router = express.Router()
const opengraphComment = '<!-- opengraph tags -->'
@ -45,14 +44,14 @@ function addOpenGraphTags (htmlStringPage, video) {
if (video.isOwned()) {
basePreviewUrlHttp = constants.CONFIG.WEBSERVER.URL
} else {
basePreviewUrlHttp = constants.REMOTE_SCHEME.HTTP + '://' + video.podHost
basePreviewUrlHttp = constants.REMOTE_SCHEME.HTTP + '://' + video.Author.Pod.host
}
// We fetch the remote preview (bigger than the thumbnail)
// This should not overhead the remote server since social websites put in a cache the OpenGraph tags
// We can't use the thumbnail because these social websites want bigger images (> 200x200 for Facebook for example)
const previewUrl = basePreviewUrlHttp + constants.STATIC_PATHS.PREVIEWS + video.getPreviewName()
const videoUrl = constants.CONFIG.WEBSERVER.URL + '/videos/watch/' + video._id
const videoUrl = constants.CONFIG.WEBSERVER.URL + '/videos/watch/' + video.id
const metaTags = {
'og:type': 'video',
@ -86,7 +85,7 @@ function generateWatchHtmlPage (req, res, next) {
const videoId = req.params.id
// Let Angular application handle errors
if (!validator.isMongoId(videoId)) return res.sendFile(indexPath)
if (!validator.isUUID(videoId, 4)) return res.sendFile(indexPath)
parallel({
file: function (callback) {
@ -94,7 +93,7 @@ function generateWatchHtmlPage (req, res, next) {
},
video: function (callback) {
Video.load(videoId, callback)
db.Video.loadAndPopulateAuthorAndPod(videoId, callback)
}
}, function (err, results) {
if (err) return next(err)

View File

@ -13,7 +13,7 @@ const videosValidators = {
isVideoDateValid,
isVideoDescriptionValid,
isVideoDurationValid,
isVideoMagnetValid,
isVideoInfoHashValid,
isVideoNameValid,
isVideoPodHostValid,
isVideoTagsValid,
@ -28,14 +28,15 @@ function isEachRemoteVideosValid (requests) {
return (
isRequestTypeAddValid(request.type) &&
isVideoAuthorValid(video.author) &&
isVideoDateValid(video.createdDate) &&
isVideoDateValid(video.createdAt) &&
isVideoDescriptionValid(video.description) &&
isVideoDurationValid(video.duration) &&
isVideoMagnetValid(video.magnet) &&
isVideoInfoHashValid(video.infoHash) &&
isVideoNameValid(video.name) &&
isVideoTagsValid(video.tags) &&
isVideoThumbnail64Valid(video.thumbnailBase64) &&
isVideoRemoteIdValid(video.remoteId)
isVideoRemoteIdValid(video.remoteId) &&
isVideoExtnameValid(video.extname)
) ||
(
isRequestTypeRemoveValid(request.type) &&
@ -61,8 +62,12 @@ function isVideoDurationValid (value) {
return validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.DURATION)
}
function isVideoMagnetValid (value) {
return validator.isLength(value.infoHash, VIDEOS_CONSTRAINTS_FIELDS.MAGNET.INFO_HASH)
function isVideoExtnameValid (value) {
return VIDEOS_CONSTRAINTS_FIELDS.EXTNAME.indexOf(value) !== -1
}
function isVideoInfoHashValid (value) {
return validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.INFO_HASH)
}
function isVideoNameValid (value) {
@ -93,7 +98,7 @@ function isVideoThumbnail64Valid (value) {
}
function isVideoRemoteIdValid (value) {
return validator.isMongoId(value)
return validator.isUUID(value, 4)
}
// ---------------------------------------------------------------------------

View File

@ -22,7 +22,8 @@ const logger = new winston.Logger({
json: true,
maxsize: 5242880,
maxFiles: 5,
colorize: false
colorize: false,
prettyPrint: true
}),
new winston.transports.Console({
level: 'debug',
@ -30,7 +31,8 @@ const logger = new winston.Logger({
handleExceptions: true,
humanReadableUnhandledException: true,
json: false,
colorize: true
colorize: true,
prettyPrint: true
})
],
exitOnError: true

View File

@ -1,10 +1,8 @@
'use strict'
const config = require('config')
const mongoose = require('mongoose')
const Client = mongoose.model('OAuthClient')
const User = mongoose.model('User')
const db = require('./database')
const checker = {
checkConfig,
@ -44,7 +42,7 @@ function checkMissedConfig () {
}
function clientsExist (callback) {
Client.list(function (err, clients) {
db.OAuthClient.list(function (err, clients) {
if (err) return callback(err)
return callback(null, clients.length !== 0)
@ -52,7 +50,7 @@ function clientsExist (callback) {
}
function usersExist (callback) {
User.countTotal(function (err, totalUsers) {
db.User.countTotal(function (err, totalUsers) {
if (err) return callback(err)
return callback(null, totalUsers !== 0)

View File

@ -14,13 +14,13 @@ const PAGINATION_COUNT_DEFAULT = 15
// Sortable columns per schema
const SEARCHABLE_COLUMNS = {
VIDEOS: [ 'name', 'magnetUri', 'podHost', 'author', 'tags' ]
VIDEOS: [ 'name', 'magnetUri', 'host', 'author', 'tags' ]
}
// Sortable columns per schema
const SORTABLE_COLUMNS = {
USERS: [ 'username', '-username', 'createdDate', '-createdDate' ],
VIDEOS: [ 'name', '-name', 'duration', '-duration', 'createdDate', '-createdDate' ]
USERS: [ 'username', '-username', 'createdAt', '-createdAt' ],
VIDEOS: [ 'name', '-name', 'duration', '-duration', 'createdAt', '-createdAt' ]
}
const OAUTH_LIFETIME = {
@ -67,9 +67,8 @@ const CONSTRAINTS_FIELDS = {
VIDEOS: {
NAME: { min: 3, max: 50 }, // Length
DESCRIPTION: { min: 3, max: 250 }, // Length
MAGNET: {
INFO_HASH: { min: 10, max: 50 } // Length
},
EXTNAME: [ '.mp4', '.ogv', '.webm' ],
INFO_HASH: { min: 10, max: 50 }, // Length
DURATION: { min: 1, max: 7200 }, // Number
TAGS: { min: 1, max: 3 }, // Number of total tags
TAG: { min: 2, max: 10 }, // Length
@ -88,7 +87,7 @@ const FRIEND_SCORE = {
// ---------------------------------------------------------------------------
const MONGO_MIGRATION_SCRIPTS = [
const MIGRATION_SCRIPTS = [
{
script: '0005-create-application',
version: 5
@ -122,7 +121,7 @@ const MONGO_MIGRATION_SCRIPTS = [
version: 40
}
]
const LAST_MONGO_SCHEMA_VERSION = (maxBy(MONGO_MIGRATION_SCRIPTS, 'version'))['version']
const LAST_SQL_SCHEMA_VERSION = (maxBy(MIGRATION_SCRIPTS, 'version'))['version']
// ---------------------------------------------------------------------------
@ -198,8 +197,8 @@ module.exports = {
CONFIG,
CONSTRAINTS_FIELDS,
FRIEND_SCORE,
LAST_MONGO_SCHEMA_VERSION,
MONGO_MIGRATION_SCRIPTS,
LAST_SQL_SCHEMA_VERSION,
MIGRATION_SCRIPTS,
OAUTH_LIFETIME,
PAGINATION_COUNT_DEFAULT,
PODS_SCORE,

View File

@ -1,36 +1,46 @@
'use strict'
const mongoose = require('mongoose')
const fs = require('fs')
const path = require('path')
const Sequelize = require('sequelize')
const constants = require('../initializers/constants')
const logger = require('../helpers/logger')
// Bootstrap models
require('../models/application')
require('../models/oauth-token')
require('../models/user')
require('../models/oauth-client')
require('../models/video')
// Request model needs Video model
require('../models/pods')
// Request model needs Pod model
require('../models/request')
const database = {}
const database = {
connect: connect
}
const sequelize = new Sequelize(constants.CONFIG.DATABASE.DBNAME, 'peertube', 'peertube', {
dialect: 'postgres',
host: constants.CONFIG.DATABASE.HOSTNAME,
port: constants.CONFIG.DATABASE.PORT
})
function connect () {
mongoose.Promise = global.Promise
mongoose.connect('mongodb://' + constants.CONFIG.DATABASE.HOSTNAME + ':' + constants.CONFIG.DATABASE.PORT + '/' + constants.CONFIG.DATABASE.DBNAME)
mongoose.connection.on('error', function () {
throw new Error('Mongodb connection error.')
const modelDirectory = path.join(__dirname, '..', 'models')
fs.readdir(modelDirectory, function (err, files) {
if (err) throw err
files.filter(function (file) {
if (file === 'utils.js') return false
return true
})
.forEach(function (file) {
const model = sequelize.import(path.join(modelDirectory, file))
database[model.name] = model
})
mongoose.connection.on('open', function () {
logger.info('Connected to mongodb.')
Object.keys(database).forEach(function (modelName) {
if ('associate' in database[modelName]) {
database[modelName].associate(database)
}
})
}
logger.info('Database is ready.')
})
database.sequelize = sequelize
database.Sequelize = Sequelize
// ---------------------------------------------------------------------------

View File

@ -3,26 +3,27 @@
const config = require('config')
const each = require('async/each')
const mkdirp = require('mkdirp')
const mongoose = require('mongoose')
const passwordGenerator = require('password-generator')
const path = require('path')
const series = require('async/series')
const checker = require('./checker')
const constants = require('./constants')
const db = require('./database')
const logger = require('../helpers/logger')
const peertubeCrypto = require('../helpers/peertube-crypto')
const Application = mongoose.model('Application')
const Client = mongoose.model('OAuthClient')
const User = mongoose.model('User')
const installer = {
installApplication
}
function installApplication (callback) {
series([
function createDatabase (callbackAsync) {
db.sequelize.sync().asCallback(callbackAsync)
// db.sequelize.sync({ force: true }).asCallback(callbackAsync)
},
function createDirectories (callbackAsync) {
createDirectoriesIfNotExist(callbackAsync)
},
@ -65,16 +66,18 @@ function createOAuthClientIfNotExist (callback) {
logger.info('Creating a default OAuth Client.')
const secret = passwordGenerator(32, false)
const client = new Client({
const id = passwordGenerator(32, false, /[a-z0-9]/)
const secret = passwordGenerator(32, false, /[a-zA-Z0-9]/)
const client = db.OAuthClient.build({
clientId: id,
clientSecret: secret,
grants: [ 'password', 'refresh_token' ]
})
client.save(function (err, createdClient) {
client.save().asCallback(function (err, createdClient) {
if (err) return callback(err)
logger.info('Client id: ' + createdClient._id)
logger.info('Client id: ' + createdClient.clientId)
logger.info('Client secret: ' + createdClient.clientSecret)
return callback(null)
@ -106,21 +109,21 @@ function createOAuthAdminIfNotExist (callback) {
password = passwordGenerator(8, true)
}
const user = new User({
const user = db.User.build({
username,
password,
role
})
user.save(function (err, createdUser) {
user.save().asCallback(function (err, createdUser) {
if (err) return callback(err)
logger.info('Username: ' + username)
logger.info('User password: ' + password)
logger.info('Creating Application collection.')
const application = new Application({ mongoSchemaVersion: constants.LAST_MONGO_SCHEMA_VERSION })
application.save(callback)
const application = db.Application.build({ sqlSchemaVersion: constants.LAST_SQL_SCHEMA_VERSION })
application.save().asCallback(callback)
})
})
}

View File

@ -1,24 +1,22 @@
'use strict'
const eachSeries = require('async/eachSeries')
const mongoose = require('mongoose')
const path = require('path')
const constants = require('./constants')
const db = require('./database')
const logger = require('../helpers/logger')
const Application = mongoose.model('Application')
const migrator = {
migrate: migrate
}
function migrate (callback) {
Application.loadMongoSchemaVersion(function (err, actualVersion) {
db.Application.loadSqlSchemaVersion(function (err, actualVersion) {
if (err) return callback(err)
// If there are a new mongo schemas
if (!actualVersion || actualVersion < constants.LAST_MONGO_SCHEMA_VERSION) {
if (!actualVersion || actualVersion < constants.LAST_SQL_SCHEMA_VERSION) {
logger.info('Begin migrations.')
eachSeries(constants.MONGO_MIGRATION_SCRIPTS, function (entity, callbackEach) {
@ -36,12 +34,12 @@ function migrate (callback) {
if (err) return callbackEach(err)
// Update the new mongo version schema
Application.updateMongoSchemaVersion(versionScript, callbackEach)
db.Application.updateSqlSchemaVersion(versionScript, callbackEach)
})
}, function (err) {
if (err) return callback(err)
logger.info('Migrations finished. New mongo version schema: %s', constants.LAST_MONGO_SCHEMA_VERSION)
logger.info('Migrations finished. New SQL version schema: %s', constants.LAST_SQL_SCHEMA_VERSION)
return callback(null)
})
} else {

View File

@ -4,18 +4,14 @@ const each = require('async/each')
const eachLimit = require('async/eachLimit')
const eachSeries = require('async/eachSeries')
const fs = require('fs')
const mongoose = require('mongoose')
const request = require('request')
const waterfall = require('async/waterfall')
const constants = require('../initializers/constants')
const db = require('../initializers/database')
const logger = require('../helpers/logger')
const requests = require('../helpers/requests')
const Pod = mongoose.model('Pod')
const Request = mongoose.model('Request')
const Video = mongoose.model('Video')
const friends = {
addVideoToFriends,
hasFriends,
@ -31,7 +27,7 @@ function addVideoToFriends (video) {
}
function hasFriends (callback) {
Pod.countAll(function (err, count) {
db.Pod.countAll(function (err, count) {
if (err) return callback(err)
const hasFriends = (count !== 0)
@ -69,13 +65,13 @@ function makeFriends (hosts, callback) {
function quitFriends (callback) {
// Stop pool requests
Request.deactivate()
db.Request.deactivate()
// Flush pool requests
Request.flush()
db.Request.flush()
waterfall([
function getPodsList (callbackAsync) {
return Pod.list(callbackAsync)
return db.Pod.list(callbackAsync)
},
function announceIQuitMyFriends (pods, callbackAsync) {
@ -103,12 +99,12 @@ function quitFriends (callback) {
function removePodsFromDB (pods, callbackAsync) {
each(pods, function (pod, callbackEach) {
pod.remove(callbackEach)
pod.destroy().asCallback(callbackEach)
}, callbackAsync)
}
], function (err) {
// Don't forget to re activate the scheduler, even if there was an error
Request.activate()
db.Request.activate()
if (err) return callback(err)
@ -122,7 +118,7 @@ function removeVideoToFriends (videoParams) {
}
function sendOwnedVideosToPod (podId) {
Video.listOwned(function (err, videosList) {
db.Video.listOwnedAndPopulateAuthor(function (err, videosList) {
if (err) {
logger.error('Cannot get the list of videos we own.')
return
@ -200,9 +196,9 @@ function getForeignPodsList (host, callback) {
function makeRequestsToWinningPods (cert, podsList, callback) {
// Stop pool requests
Request.deactivate()
db.Request.deactivate()
// Flush pool requests
Request.forceSend()
db.Request.forceSend()
eachLimit(podsList, constants.REQUESTS_IN_PARALLEL, function (pod, callbackEach) {
const params = {
@ -222,8 +218,8 @@ function makeRequestsToWinningPods (cert, podsList, callback) {
}
if (res.statusCode === 200) {
const podObj = new Pod({ host: pod.host, publicKey: body.cert })
podObj.save(function (err, podCreated) {
const podObj = db.Pod.build({ host: pod.host, publicKey: body.cert })
podObj.save().asCallback(function (err, podCreated) {
if (err) {
logger.error('Cannot add friend %s pod.', pod.host, { error: err })
return callbackEach()
@ -242,28 +238,57 @@ function makeRequestsToWinningPods (cert, podsList, callback) {
}, function endRequests () {
// Final callback, we've ended all the requests
// Now we made new friends, we can re activate the pool of requests
Request.activate()
db.Request.activate()
logger.debug('makeRequestsToWinningPods finished.')
return callback()
})
}
// Wrapper that populate "to" argument with all our friends if it is not specified
function createRequest (type, endpoint, data, to) {
const req = new Request({
if (to) return _createRequest(type, endpoint, data, to)
// If the "to" pods is not specified, we send the request to all our friends
db.Pod.listAllIds(function (err, podIds) {
if (err) {
logger.error('Cannot get pod ids', { error: err })
return
}
return _createRequest(type, endpoint, data, podIds)
})
}
function _createRequest (type, endpoint, data, to) {
const pods = []
// If there are no destination pods abort
if (to.length === 0) return
to.forEach(function (toPod) {
pods.push(db.Pod.build({ id: toPod }))
})
const createQuery = {
endpoint,
request: {
type: type,
data: data
}
})
if (to) {
req.to = to
}
req.save(function (err) {
if (err) logger.error('Cannot save the request.', { error: err })
// We run in transaction to keep coherency between Request and RequestToPod tables
db.sequelize.transaction(function (t) {
const dbRequestOptions = {
transaction: t
}
return db.Request.create(createQuery, dbRequestOptions).then(function (request) {
return request.setPods(pods, dbRequestOptions)
})
}).asCallback(function (err) {
if (err) logger.error('Error in createRequest transaction.', { error: err })
})
}

View File

@ -1,11 +1,6 @@
const mongoose = require('mongoose')
const db = require('../initializers/database')
const logger = require('../helpers/logger')
const OAuthClient = mongoose.model('OAuthClient')
const OAuthToken = mongoose.model('OAuthToken')
const User = mongoose.model('User')
// See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications
const OAuthModel = {
getAccessToken,
@ -21,27 +16,25 @@ const OAuthModel = {
function getAccessToken (bearerToken) {
logger.debug('Getting access token (bearerToken: ' + bearerToken + ').')
return OAuthToken.getByTokenAndPopulateUser(bearerToken)
return db.OAuthToken.getByTokenAndPopulateUser(bearerToken)
}
function getClient (clientId, clientSecret) {
logger.debug('Getting Client (clientId: ' + clientId + ', clientSecret: ' + clientSecret + ').')
// TODO req validator
const mongoId = new mongoose.mongo.ObjectID(clientId)
return OAuthClient.getByIdAndSecret(mongoId, clientSecret)
return db.OAuthClient.getByIdAndSecret(clientId, clientSecret)
}
function getRefreshToken (refreshToken) {
logger.debug('Getting RefreshToken (refreshToken: ' + refreshToken + ').')
return OAuthToken.getByRefreshTokenAndPopulateClient(refreshToken)
return db.OAuthToken.getByRefreshTokenAndPopulateClient(refreshToken)
}
function getUser (username, password) {
logger.debug('Getting User (username: ' + username + ', password: ' + password + ').')
return User.getByUsername(username).then(function (user) {
return db.User.getByUsername(username).then(function (user) {
if (!user) return null
// We need to return a promise
@ -60,8 +53,8 @@ function getUser (username, password) {
}
function revokeToken (token) {
return OAuthToken.getByRefreshTokenAndPopulateUser(token.refreshToken).then(function (tokenDB) {
if (tokenDB) tokenDB.remove()
return db.OAuthToken.getByRefreshTokenAndPopulateUser(token.refreshToken).then(function (tokenDB) {
if (tokenDB) tokenDB.destroy()
/*
* Thanks to https://github.com/manjeshpv/node-oauth2-server-implementation/blob/master/components/oauth/mongo-models.js
@ -80,18 +73,19 @@ function revokeToken (token) {
function saveToken (token, client, user) {
logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.')
const tokenObj = new OAuthToken({
const tokenToCreate = {
accessToken: token.accessToken,
accessTokenExpiresAt: token.accessTokenExpiresAt,
client: client.id,
refreshToken: token.refreshToken,
refreshTokenExpiresAt: token.refreshTokenExpiresAt,
user: user.id
})
oAuthClientId: client.id,
userId: user.id
}
return tokenObj.save().then(function (tokenCreated) {
return db.OAuthToken.create(tokenToCreate).then(function (tokenCreated) {
tokenCreated.client = client
tokenCreated.user = user
return tokenCreated
}).catch(function (err) {
throw err

View File

@ -44,7 +44,6 @@ module.exports = podsMiddleware
function getHostWithPort (host) {
const splitted = host.split(':')
console.log(splitted)
// The port was not specified
if (splitted.length === 1) {
if (constants.REMOTE_SCHEME.HTTP === 'https') return host + ':443'

View File

@ -1,18 +1,16 @@
'use strict'
const db = require('../initializers/database')
const logger = require('../helpers/logger')
const mongoose = require('mongoose')
const peertubeCrypto = require('../helpers/peertube-crypto')
const Pod = mongoose.model('Pod')
const secureMiddleware = {
checkSignature
}
function checkSignature (req, res, next) {
const host = req.body.signature.host
Pod.loadByHost(host, function (err, pod) {
db.Pod.loadByHost(host, function (err, pod) {
if (err) {
logger.error('Cannot get signed host in body.', { error: err })
return res.sendStatus(500)

View File

@ -6,13 +6,13 @@ const sortMiddleware = {
}
function setUsersSort (req, res, next) {
if (!req.query.sort) req.query.sort = '-createdDate'
if (!req.query.sort) req.query.sort = '-createdAt'
return next()
}
function setVideosSort (req, res, next) {
if (!req.query.sort) req.query.sort = '-createdDate'
if (!req.query.sort) req.query.sort = '-createdAt'
return next()
}

View File

@ -1,12 +1,9 @@
'use strict'
const mongoose = require('mongoose')
const checkErrors = require('./utils').checkErrors
const db = require('../../initializers/database')
const logger = require('../../helpers/logger')
const User = mongoose.model('User')
const validatorsUsers = {
usersAdd,
usersRemove,
@ -20,7 +17,7 @@ function usersAdd (req, res, next) {
logger.debug('Checking usersAdd parameters', { parameters: req.body })
checkErrors(req, res, function () {
User.loadByUsername(req.body.username, function (err, user) {
db.User.loadByUsername(req.body.username, function (err, user) {
if (err) {
logger.error('Error in usersAdd request validator.', { error: err })
return res.sendStatus(500)
@ -34,12 +31,12 @@ function usersAdd (req, res, next) {
}
function usersRemove (req, res, next) {
req.checkParams('id', 'Should have a valid id').notEmpty().isMongoId()
req.checkParams('id', 'Should have a valid id').notEmpty().isInt()
logger.debug('Checking usersRemove parameters', { parameters: req.params })
checkErrors(req, res, function () {
User.loadById(req.params.id, function (err, user) {
db.User.loadById(req.params.id, function (err, user) {
if (err) {
logger.error('Error in usersRemove request validator.', { error: err })
return res.sendStatus(500)
@ -55,7 +52,7 @@ function usersRemove (req, res, next) {
}
function usersUpdate (req, res, next) {
req.checkParams('id', 'Should have a valid id').notEmpty().isMongoId()
req.checkParams('id', 'Should have a valid id').notEmpty().isInt()
// Add old password verification
req.checkBody('password', 'Should have a valid password').isUserPasswordValid()

View File

@ -1,14 +1,11 @@
'use strict'
const mongoose = require('mongoose')
const checkErrors = require('./utils').checkErrors
const constants = require('../../initializers/constants')
const customVideosValidators = require('../../helpers/custom-validators').videos
const db = require('../../initializers/database')
const logger = require('../../helpers/logger')
const Video = mongoose.model('Video')
const validatorsVideos = {
videosAdd,
videosGet,
@ -29,7 +26,7 @@ function videosAdd (req, res, next) {
checkErrors(req, res, function () {
const videoFile = req.files.videofile[0]
Video.getDurationFromFile(videoFile.path, function (err, duration) {
db.Video.getDurationFromFile(videoFile.path, function (err, duration) {
if (err) {
return res.status(400).send('Cannot retrieve metadata of the file.')
}
@ -45,12 +42,12 @@ function videosAdd (req, res, next) {
}
function videosGet (req, res, next) {
req.checkParams('id', 'Should have a valid id').notEmpty().isMongoId()
req.checkParams('id', 'Should have a valid id').notEmpty().isUUID(4)
logger.debug('Checking videosGet parameters', { parameters: req.params })
checkErrors(req, res, function () {
Video.load(req.params.id, function (err, video) {
db.Video.load(req.params.id, function (err, video) {
if (err) {
logger.error('Error in videosGet request validator.', { error: err })
return res.sendStatus(500)
@ -64,12 +61,12 @@ function videosGet (req, res, next) {
}
function videosRemove (req, res, next) {
req.checkParams('id', 'Should have a valid id').notEmpty().isMongoId()
req.checkParams('id', 'Should have a valid id').notEmpty().isUUID(4)
logger.debug('Checking videosRemove parameters', { parameters: req.params })
checkErrors(req, res, function () {
Video.load(req.params.id, function (err, video) {
db.Video.loadAndPopulateAuthor(req.params.id, function (err, video) {
if (err) {
logger.error('Error in videosRemove request validator.', { error: err })
return res.sendStatus(500)
@ -77,7 +74,7 @@ function videosRemove (req, res, next) {
if (!video) return res.status(404).send('Video not found')
else if (video.isOwned() === false) return res.status(403).send('Cannot remove video of another pod')
else if (video.author !== res.locals.oauth.token.user.username) return res.status(403).send('Cannot remove video of another user')
else if (video.Author.name !== res.locals.oauth.token.user.username) return res.status(403).send('Cannot remove video of another user')
next()
})

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

View File

@ -465,7 +465,7 @@ describe('Test parameters validator', function () {
it('Should return 404 with an incorrect video', function (done) {
request(server.url)
.get(path + '123456789012345678901234')
.get(path + '4da6fde3-88f7-4d16-b119-108df5630b06')
.set('Accept', 'application/json')
.expect(404, done)
})
@ -490,7 +490,7 @@ describe('Test parameters validator', function () {
it('Should fail with a video which does not exist', function (done) {
request(server.url)
.delete(path + '123456789012345678901234')
.delete(path + '4da6fde3-88f7-4d16-b119-108df5630b06')
.set('Authorization', 'Bearer ' + server.accessToken)
.expect(404, done)
})
@ -711,7 +711,7 @@ describe('Test parameters validator', function () {
it('Should return 404 with a non existing id', function (done) {
request(server.url)
.delete(path + '579f982228c99c221d8092b8')
.delete(path + '45')
.set('Authorization', 'Bearer ' + server.accessToken)
.expect(404, done)
})

View File

@ -97,7 +97,7 @@ describe('Test basic friends', function () {
const pod = result[0]
expect(pod.host).to.equal(servers[2].host)
expect(pod.score).to.equal(20)
expect(miscsUtils.dateIsValid(pod.createdDate)).to.be.true
expect(miscsUtils.dateIsValid(pod.createdAt)).to.be.true
next()
})
@ -114,7 +114,7 @@ describe('Test basic friends', function () {
const pod = result[0]
expect(pod.host).to.equal(servers[1].host)
expect(pod.score).to.equal(20)
expect(miscsUtils.dateIsValid(pod.createdDate)).to.be.true
expect(miscsUtils.dateIsValid(pod.createdAt)).to.be.true
next()
})

View File

@ -104,7 +104,7 @@ describe('Test multiple pods', function () {
expect(video.magnetUri).to.exist
expect(video.duration).to.equal(10)
expect(video.tags).to.deep.equal([ 'tag1p1', 'tag2p1' ])
expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
expect(video.author).to.equal('root')
if (server.url !== 'http://localhost:9001') {
@ -166,7 +166,7 @@ describe('Test multiple pods', function () {
expect(video.magnetUri).to.exist
expect(video.duration).to.equal(5)
expect(video.tags).to.deep.equal([ 'tag1p2', 'tag2p2', 'tag3p2' ])
expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
expect(video.author).to.equal('root')
if (server.url !== 'http://localhost:9002') {
@ -246,7 +246,7 @@ describe('Test multiple pods', function () {
expect(video1.duration).to.equal(5)
expect(video1.tags).to.deep.equal([ 'tag1p3' ])
expect(video1.author).to.equal('root')
expect(miscsUtils.dateIsValid(video1.createdDate)).to.be.true
expect(miscsUtils.dateIsValid(video1.createdAt)).to.be.true
expect(video2.name).to.equal('my super name for pod 3-2')
expect(video2.description).to.equal('my super description for pod 3-2')
@ -255,7 +255,7 @@ describe('Test multiple pods', function () {
expect(video2.duration).to.equal(5)
expect(video2.tags).to.deep.equal([ 'tag2p3', 'tag3p3', 'tag4p3' ])
expect(video2.author).to.equal('root')
expect(miscsUtils.dateIsValid(video2.createdDate)).to.be.true
expect(miscsUtils.dateIsValid(video2.createdAt)).to.be.true
if (server.url !== 'http://localhost:9003') {
expect(video1.isLocal).to.be.false

View File

@ -69,7 +69,7 @@ describe('Test requests stats', function () {
})
})
it('Should have the correct request', function (done) {
it('Should have the correct total request', function (done) {
this.timeout(15000)
const server = servers[0]
@ -83,11 +83,7 @@ describe('Test requests stats', function () {
if (err) throw err
const body = res.body
expect(body.requests).to.have.lengthOf(1)
const request = body.requests[0]
expect(request.to).to.have.lengthOf(1)
expect(request.request.type).to.equal('add')
expect(body.totalRequests).to.equal(1)
// Wait one cycle
setTimeout(done, 10000)
@ -95,27 +91,6 @@ describe('Test requests stats', function () {
})
})
it('Should have the correct requests', function (done) {
const server = servers[0]
uploadVideo(server, function (err) {
if (err) throw err
getRequestsStats(server, function (err, res) {
if (err) throw err
const body = res.body
expect(body.requests).to.have.lengthOf(2)
const request = body.requests[1]
expect(request.to).to.have.lengthOf(1)
expect(request.request.type).to.equal('add')
done()
})
})
})
after(function (done) {
process.kill(-servers[0].app.pid)

View File

@ -82,7 +82,7 @@ describe('Test a single pod', function () {
expect(video.author).to.equal('root')
expect(video.isLocal).to.be.true
expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
if (err) throw err
@ -116,7 +116,7 @@ describe('Test a single pod', function () {
expect(video.author).to.equal('root')
expect(video.isLocal).to.be.true
expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
if (err) throw err
@ -142,7 +142,7 @@ describe('Test a single pod', function () {
expect(video.author).to.equal('root')
expect(video.isLocal).to.be.true
expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
if (err) throw err
@ -154,7 +154,7 @@ describe('Test a single pod', function () {
})
it('Should search the video by podHost', function (done) {
videosUtils.searchVideo(server.url, '9001', 'podHost', function (err, res) {
videosUtils.searchVideo(server.url, '9001', 'host', function (err, res) {
if (err) throw err
expect(res.body.total).to.equal(1)
@ -168,7 +168,7 @@ describe('Test a single pod', function () {
expect(video.author).to.equal('root')
expect(video.isLocal).to.be.true
expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
if (err) throw err
@ -194,7 +194,7 @@ describe('Test a single pod', function () {
expect(video.author).to.equal('root')
expect(video.isLocal).to.be.true
expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
expect(miscsUtils.dateIsValid(video.createdDate)).to.be.true
expect(miscsUtils.dateIsValid(video.createdAt)).to.be.true
videosUtils.testVideoImage(server.url, 'video_short.webm', video.thumbnailPath, function (err, test) {
if (err) throw err
@ -425,7 +425,7 @@ describe('Test a single pod', function () {
})
it('Should search all the 9001 port videos', function (done) {
videosUtils.searchVideoWithPagination(server.url, '9001', 'podHost', 0, 15, function (err, res) {
videosUtils.searchVideoWithPagination(server.url, '9001', 'host', 0, 15, function (err, res) {
if (err) throw err
const videos = res.body.data
@ -437,7 +437,7 @@ describe('Test a single pod', function () {
})
it('Should search all the localhost videos', function (done) {
videosUtils.searchVideoWithPagination(server.url, 'localhost', 'podHost', 0, 15, function (err, res) {
videosUtils.searchVideoWithPagination(server.url, 'localhost', 'host', 0, 15, function (err, res) {
if (err) throw err
const videos = res.body.data

View File

@ -261,8 +261,8 @@ describe('Test users', function () {
})
})
it('Should list only the second user by createdDate desc', function (done) {
usersUtils.getUsersListPaginationAndSort(server.url, 0, 1, '-createdDate', function (err, res) {
it('Should list only the second user by createdAt desc', function (done) {
usersUtils.getUsersListPaginationAndSort(server.url, 0, 1, '-createdAt', function (err, res) {
if (err) throw err
const result = res.body
@ -279,8 +279,8 @@ describe('Test users', function () {
})
})
it('Should list all the users by createdDate asc', function (done) {
usersUtils.getUsersListPaginationAndSort(server.url, 0, 2, 'createdDate', function (err, res) {
it('Should list all the users by createdAt asc', function (done) {
usersUtils.getUsersListPaginationAndSort(server.url, 0, 2, 'createdAt', function (err, res) {
if (err) throw err
const result = res.body

View File

@ -60,12 +60,12 @@ function runServer (number, callback) {
// These actions are async so we need to be sure that they have both been done
const serverRunString = {
'Connected to mongodb': false,
'Database is ready': false,
'Server listening on port': false
}
const regexps = {
client_id: 'Client id: ([a-f0-9]+)',
client_id: 'Client id: (.+)',
client_secret: 'Client secret: (.+)',
user_username: 'Username: (.+)',
user_password: 'User password: (.+)'

View File

@ -25,7 +25,7 @@ function getAllVideosListBy (url, end) {
request(url)
.get(path)
.query({ sort: 'createdDate' })
.query({ sort: 'createdAt' })
.query({ start: 0 })
.query({ count: 10000 })
.set('Accept', 'application/json')