From f981dae8617271a2dc713bb683951730b306e0c5 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 12 Jul 2017 11:56:02 +0200 Subject: [PATCH] Add previews cache system between pods --- .gitignore | 1 + config/default.yaml | 5 ++ config/production.yaml.example | 1 + config/test-1.yaml | 2 + config/test-2.yaml | 2 + config/test-3.yaml | 2 + config/test-4.yaml | 2 + config/test-5.yaml | 2 + config/test-6.yaml | 2 + package.json | 1 + server.ts | 4 +- server/controllers/static.ts | 16 +++- server/helpers/core-utils.ts | 5 +- server/initializers/constants.ts | 16 +++- server/initializers/installer.ts | 30 ++++++- server/lib/cache/index.ts | 1 + server/lib/cache/videos-preview-cache.ts | 74 ++++++++++++++++++ server/lib/friends.ts | 14 +++- server/lib/index.ts | 1 + server/models/oauth/oauth-token-interface.ts | 2 + server/models/oauth/oauth-token.ts | 4 +- server/models/video/video.ts | 1 + .../fixtures/video_short1-preview.webm.jpg | Bin 0 -> 31725 bytes server/tests/api/multiple-pods.js | 19 ++++- server/tests/utils/videos.js | 4 +- shared/models/videos/video.model.ts | 1 + yarn.lock | 6 ++ 27 files changed, 202 insertions(+), 16 deletions(-) create mode 100644 server/lib/cache/index.ts create mode 100644 server/lib/cache/videos-preview-cache.ts create mode 100644 server/tests/api/fixtures/video_short1-preview.webm.jpg diff --git a/.gitignore b/.gitignore index 6caee2e4c..169027c36 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ /certs/ /logs/ /torrents/ +/cache/ /config/production.yaml /ffmpeg/ /*.sublime-project diff --git a/config/default.yaml b/config/default.yaml index e03bf1aea..b4e7606cf 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -22,6 +22,11 @@ storage: previews: 'previews/' thumbnails: 'thumbnails/' torrents: 'torrents/' + cache: 'cache/' + +cache: + previews: + size: 1 # Max number of previews you want to cache admin: email: 'admin@example.com' diff --git a/config/production.yaml.example b/config/production.yaml.example index c18457df6..0857aa3ca 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example @@ -23,6 +23,7 @@ storage: previews: 'previews/' thumbnails: 'thumbnails/' torrents: 'torrents/' + cache: 'cache/' admin: email: 'admin@example.com' diff --git a/config/test-1.yaml b/config/test-1.yaml index dbe408a8c..e244a8797 100644 --- a/config/test-1.yaml +++ b/config/test-1.yaml @@ -13,8 +13,10 @@ storage: certs: 'test1/certs/' videos: 'test1/videos/' logs: 'test1/logs/' + previews: 'test1/previews/' thumbnails: 'test1/thumbnails/' torrents: 'test1/torrents/' + cache: 'test1/cache/' admin: email: 'admin1@example.com' diff --git a/config/test-2.yaml b/config/test-2.yaml index c95b9c229..236dcb10d 100644 --- a/config/test-2.yaml +++ b/config/test-2.yaml @@ -13,8 +13,10 @@ storage: certs: 'test2/certs/' videos: 'test2/videos/' logs: 'test2/logs/' + previews: 'test2/previews/' thumbnails: 'test2/thumbnails/' torrents: 'test2/torrents/' + cache: 'test2/cache/' admin: email: 'admin2@example.com' diff --git a/config/test-3.yaml b/config/test-3.yaml index 2eb984692..a29225a44 100644 --- a/config/test-3.yaml +++ b/config/test-3.yaml @@ -13,8 +13,10 @@ storage: certs: 'test3/certs/' videos: 'test3/videos/' logs: 'test3/logs/' + previews: 'test3/previews/' thumbnails: 'test3/thumbnails/' torrents: 'test3/torrents/' + cache: 'test3/cache/' admin: email: 'admin3@example.com' diff --git a/config/test-4.yaml b/config/test-4.yaml index a0a9bde21..da93e128d 100644 --- a/config/test-4.yaml +++ b/config/test-4.yaml @@ -13,8 +13,10 @@ storage: certs: 'test4/certs/' videos: 'test4/videos/' logs: 'test4/logs/' + previews: 'test4/previews/' thumbnails: 'test4/thumbnails/' torrents: 'test4/torrents/' + cache: 'test4/cache/' admin: email: 'admin4@example.com' diff --git a/config/test-5.yaml b/config/test-5.yaml index af8654e14..f95e25eb8 100644 --- a/config/test-5.yaml +++ b/config/test-5.yaml @@ -13,8 +13,10 @@ storage: certs: 'test5/certs/' videos: 'test5/videos/' logs: 'test5/logs/' + previews: 'test5/previews/' thumbnails: 'test5/thumbnails/' torrents: 'test5/torrents/' + cache: 'test5/cache/' admin: email: 'admin5@example.com' diff --git a/config/test-6.yaml b/config/test-6.yaml index d74d3b052..87d054439 100644 --- a/config/test-6.yaml +++ b/config/test-6.yaml @@ -13,8 +13,10 @@ storage: certs: 'test6/certs/' videos: 'test6/videos/' logs: 'test6/logs/' + previews: 'test6/previews/' thumbnails: 'test6/thumbnails/' torrents: 'test6/torrents/' + cache: 'test6/cache/' admin: email: 'admin6@example.com' diff --git a/package.json b/package.json index b875f5c26..d6da61975 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ }, "dependencies": { "async": "^2.0.0", + "async-lru": "^1.1.1", "bcrypt": "^1.0.2", "bittorrent-tracker": "^9.0.0", "bluebird": "^3.5.0", diff --git a/server.ts b/server.ts index e7fa99c90..a6a9fcb02 100644 --- a/server.ts +++ b/server.ts @@ -47,7 +47,7 @@ if (errorMessage !== null) { // ----------- PeerTube modules ----------- import { migrate, installApplication } from './server/initializers' -import { JobScheduler, activateSchedulers } from './server/lib' +import { JobScheduler, activateSchedulers, VideosPreviewCache } from './server/lib' import * as customValidators from './server/helpers/custom-validators' import { apiRouter, clientsRouter, staticRouter } from './server/controllers' @@ -147,6 +147,8 @@ function onDatabaseInitDone () { // Activate job scheduler JobScheduler.Instance.activate() + VideosPreviewCache.Instance.init(CONFIG.CACHE.PREVIEWS.SIZE) + logger.info('Server listening on port %d', port) logger.info('Webserver: %s', CONFIG.WEBSERVER.URL) }) diff --git a/server/controllers/static.ts b/server/controllers/static.ts index e65282339..2fd14131e 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts @@ -6,6 +6,7 @@ import { STATIC_MAX_AGE, STATIC_PATHS } from '../initializers' +import { VideosPreviewCache } from '../lib' const staticRouter = express.Router() @@ -38,8 +39,8 @@ staticRouter.use( // Video previews path for express const previewsPhysicalPath = CONFIG.STORAGE.PREVIEWS_DIR staticRouter.use( - STATIC_PATHS.PREVIEWS, - express.static(previewsPhysicalPath, { maxAge: STATIC_MAX_AGE }) + STATIC_PATHS.PREVIEWS + ':uuid.jpg', + getPreview ) // --------------------------------------------------------------------------- @@ -47,3 +48,14 @@ staticRouter.use( export { staticRouter } + +// --------------------------------------------------------------------------- + +function getPreview (req: express.Request, res: express.Response, next: express.NextFunction) { + VideosPreviewCache.Instance.getPreviewPath(req.params.uuid) + .then(path => { + if (!path) return res.sendStatus(404) + + return res.sendFile(path, { maxAge: STATIC_MAX_AGE }) + }) +} diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts index 1e92049f1..d28c97f09 100644 --- a/server/helpers/core-utils.ts +++ b/server/helpers/core-utils.ts @@ -16,6 +16,7 @@ import { import * as mkdirp from 'mkdirp' import * as bcrypt from 'bcrypt' import * as createTorrent from 'create-torrent' +import * as rimraf from 'rimraf' import * as openssl from 'openssl-wrapper' import * as Promise from 'bluebird' @@ -83,6 +84,7 @@ const bcryptComparePromise = promisify2(bcrypt.compare) const bcryptGenSaltPromise = promisify1(bcrypt.genSalt) const bcryptHashPromise = promisify2(bcrypt.hash) const createTorrentPromise = promisify2(createTorrent) +const rimrafPromise = promisify1WithVoid(rimraf) // --------------------------------------------------------------------------- @@ -105,5 +107,6 @@ export { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, - createTorrentPromise + createTorrentPromise, + rimrafPromise } diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index f087b7476..928a3f570 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -61,7 +61,8 @@ const CONFIG = { VIDEOS_DIR: join(root(), config.get('storage.videos')), THUMBNAILS_DIR: join(root(), config.get('storage.thumbnails')), PREVIEWS_DIR: join(root(), config.get('storage.previews')), - TORRENTS_DIR: join(root(), config.get('storage.torrents')) + TORRENTS_DIR: join(root(), config.get('storage.torrents')), + CACHE_DIR: join(root(), config.get('storage.cache')) }, WEBSERVER: { SCHEME: config.get('webserver.https') === true ? 'https' : 'http', @@ -80,6 +81,11 @@ const CONFIG = { TRANSCODING: { ENABLED: config.get('transcoding.enabled'), THREADS: config.get('transcoding.threads') + }, + CACHE: { + PREVIEWS: { + SIZE: config.get('cache.previews.size') + } } } CONFIG.WEBSERVER.URL = CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT @@ -278,6 +284,13 @@ let STATIC_MAX_AGE = '30d' const THUMBNAILS_SIZE = '200x110' const PREVIEWS_SIZE = '640x480' +// Subfolders of cache directory +const CACHE = { + DIRECTORIES: { + PREVIEWS: join(CONFIG.STORAGE.CACHE_DIR, 'previews') + } +} + // --------------------------------------------------------------------------- const USER_ROLES: { [ id: string ]: UserRole } = { @@ -307,6 +320,7 @@ if (isTestInstance() === true) { export { API_VERSION, BCRYPT_SALT_SIZE, + CACHE, CONFIG, CONSTRAINTS_FIELDS, FRIEND_SCORE, diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts index 1ec24c4ad..3c5a77df9 100644 --- a/server/initializers/installer.ts +++ b/server/initializers/installer.ts @@ -4,12 +4,13 @@ import * as passwordGenerator from 'password-generator' import * as Promise from 'bluebird' import { database as db } from './database' -import { USER_ROLES, CONFIG, LAST_MIGRATION_VERSION } from './constants' +import { USER_ROLES, CONFIG, LAST_MIGRATION_VERSION, CACHE } from './constants' import { clientsExist, usersExist } from './checker' -import { logger, createCertsIfNotExist, root, mkdirpPromise } from '../helpers' +import { logger, createCertsIfNotExist, root, mkdirpPromise, rimrafPromise } from '../helpers' function installApplication () { return db.sequelize.sync() + .then(() => removeCacheDirectories()) .then(() => createDirectoriesIfNotExist()) .then(() => createCertsIfNotExist()) .then(() => createOAuthClientIfNotExist()) @@ -24,13 +25,34 @@ export { // --------------------------------------------------------------------------- +function removeCacheDirectories () { + const cacheDirectories = CACHE.DIRECTORIES + + const tasks = [] + + // Cache directories + Object.keys(cacheDirectories).forEach(key => { + const dir = cacheDirectories[key] + tasks.push(rimrafPromise(dir)) + }) + + return Promise.all(tasks) +} + function createDirectoriesIfNotExist () { - const storages = config.get('storage') + const storages = CONFIG.STORAGE + const cacheDirectories = CACHE.DIRECTORIES const tasks = [] Object.keys(storages).forEach(key => { const dir = storages[key] - tasks.push(mkdirpPromise(join(root(), dir))) + tasks.push(mkdirpPromise(dir)) + }) + + // Cache directories + Object.keys(cacheDirectories).forEach(key => { + const dir = cacheDirectories[key] + tasks.push(mkdirpPromise(dir)) }) return Promise.all(tasks) diff --git a/server/lib/cache/index.ts b/server/lib/cache/index.ts new file mode 100644 index 000000000..7bf63790a --- /dev/null +++ b/server/lib/cache/index.ts @@ -0,0 +1 @@ +export * from './videos-preview-cache' diff --git a/server/lib/cache/videos-preview-cache.ts b/server/lib/cache/videos-preview-cache.ts new file mode 100644 index 000000000..9d365e496 --- /dev/null +++ b/server/lib/cache/videos-preview-cache.ts @@ -0,0 +1,74 @@ +import * as request from 'request' +import * as asyncLRU from 'async-lru' +import { join } from 'path' +import { createWriteStream } from 'fs' +import * as Promise from 'bluebird' + +import { database as db, CONFIG, CACHE } from '../../initializers' +import { logger, writeFilePromise, unlinkPromise } from '../../helpers' +import { VideoInstance } from '../../models' +import { fetchRemotePreview } from '../../lib' + +class VideosPreviewCache { + + private static instance: VideosPreviewCache + + private lru + + private constructor () { } + + static get Instance () { + return this.instance || (this.instance = new this()) + } + + init (max: number) { + this.lru = new asyncLRU({ + max, + load: (key, cb) => { + this.loadPreviews(key) + .then(res => cb(null, res)) + .catch(err => cb(err)) + } + }) + + this.lru.on('evict', (obj: { key: string, value: string }) => { + unlinkPromise(obj.value).then(() => logger.debug('%s evicted from VideosPreviewCache', obj.value)) + }) + } + + getPreviewPath (key: string) { + return new Promise((res, rej) => { + this.lru.get(key, (err, value) => { + err ? rej(err) : res(value) + }) + }) + } + + private loadPreviews (key: string) { + return db.Video.loadByUUIDAndPopulateAuthorAndPodAndTags(key) + .then(video => { + if (!video) return undefined + + if (video.isOwned()) return join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName()) + + return this.saveRemotePreviewAndReturnPath(video) + }) + } + + private saveRemotePreviewAndReturnPath (video: VideoInstance) { + const req = fetchRemotePreview(video.Author.Pod, video) + + return new Promise((res, rej) => { + const path = join(CACHE.DIRECTORIES.PREVIEWS, video.getPreviewName()) + const stream = createWriteStream(path) + + req.pipe(stream) + .on('finish', () => res(path)) + .on('error', (err) => rej(err)) + }) + } +} + +export { + VideosPreviewCache +} diff --git a/server/lib/friends.ts b/server/lib/friends.ts index 6ed0da013..50355d5d1 100644 --- a/server/lib/friends.ts +++ b/server/lib/friends.ts @@ -1,6 +1,7 @@ import * as request from 'request' import * as Sequelize from 'sequelize' import * as Promise from 'bluebird' +import { join } from 'path' import { database as db } from '../initializers/database' import { @@ -9,7 +10,8 @@ import { REQUESTS_IN_PARALLEL, REQUEST_ENDPOINTS, REQUEST_ENDPOINT_ACTIONS, - REMOTE_SCHEME + REMOTE_SCHEME, + STATIC_PATHS } from '../initializers' import { logger, @@ -233,6 +235,13 @@ function sendOwnedVideosToPod (podId: number) { }) } +function fetchRemotePreview (pod: PodInstance, video: VideoInstance) { + const host = video.Author.Pod.host + const path = join(STATIC_PATHS.PREVIEWS, video.getPreviewName()) + + return request.get(REMOTE_SCHEME.HTTP + '://' + host + path) +} + function getRequestScheduler () { return requestScheduler } @@ -263,7 +272,8 @@ export { sendOwnedVideosToPod, getRequestScheduler, getRequestVideoQaduScheduler, - getRequestVideoEventScheduler + getRequestVideoEventScheduler, + fetchRemotePreview } // --------------------------------------------------------------------------- diff --git a/server/lib/index.ts b/server/lib/index.ts index b8697fb96..8628da4dd 100644 --- a/server/lib/index.ts +++ b/server/lib/index.ts @@ -1,3 +1,4 @@ +export * from './cache' export * from './jobs' export * from './request' export * from './friends' diff --git a/server/models/oauth/oauth-token-interface.ts b/server/models/oauth/oauth-token-interface.ts index f2ddafa54..97af3c815 100644 --- a/server/models/oauth/oauth-token-interface.ts +++ b/server/models/oauth/oauth-token-interface.ts @@ -35,6 +35,8 @@ export interface OAuthTokenAttributes { refreshToken: string refreshTokenExpiresAt: Date + userId?: number + oAuthClientId?: number User?: UserModel } diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts index 5c3781394..e3de9468e 100644 --- a/server/models/oauth/oauth-token.ts +++ b/server/models/oauth/oauth-token.ts @@ -106,10 +106,10 @@ getByRefreshTokenAndPopulateClient = function (refreshToken: string) { refreshToken: token.refreshToken, refreshTokenExpiresAt: token.refreshTokenExpiresAt, client: { - id: token['client'].id + id: token.oAuthClientId }, user: { - id: token['user'] + id: token.userId } } diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 650025205..b7eb24c4a 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -451,6 +451,7 @@ toFormatedJSON = function (this: VideoInstance) { dislikes: this.dislikes, tags: map(this.Tags, 'name'), thumbnailPath: join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()), + previewPath: join(STATIC_PATHS.PREVIEWS, this.getPreviewName()), createdAt: this.createdAt, updatedAt: this.updatedAt } diff --git a/server/tests/api/fixtures/video_short1-preview.webm.jpg b/server/tests/api/fixtures/video_short1-preview.webm.jpg new file mode 100644 index 0000000000000000000000000000000000000000..69c100c4e4f5b2fd17c5d24416eaf96e3539f7b3 GIT binary patch literal 31725 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 Q)&%7wXxIS)=XdM>1I*mA*#H0l literal 0 HcmV?d00001 diff --git a/server/tests/api/multiple-pods.js b/server/tests/api/multiple-pods.js index 1bc6157e8..7753e6f2d 100644 --- a/server/tests/api/multiple-pods.js +++ b/server/tests/api/multiple-pods.js @@ -747,7 +747,7 @@ describe('Test multiple pods', function () { expect(videos[0].name).not.to.equal(toRemove[1].name) expect(videos[1].name).not.to.equal(toRemove[1].name) - videoUUID = videos[0].uuid + videoUUID = videos.find(video => video.name === 'my super name for pod 1').uuid callback() }) @@ -781,6 +781,23 @@ describe('Test multiple pods', function () { }) }, done) }) + + it('Should get the preview from each pod', function (done) { + each(servers, function (server, callback) { + videosUtils.getVideo(server.url, videoUUID, function (err, res) { + if (err) throw err + + const video = res.body + + videosUtils.testVideoImage(server.url, 'video_short1-preview.webm', video.previewPath, function (err, test) { + if (err) throw err + expect(test).to.equal(true) + + callback() + }) + }) + }, done) + }) }) after(function (done) { diff --git a/server/tests/utils/videos.js b/server/tests/utils/videos.js index 6e7aabc5d..cb3be6897 100644 --- a/server/tests/utils/videos.js +++ b/server/tests/utils/videos.js @@ -195,7 +195,7 @@ function searchVideoWithSort (url, search, sort, end) { .end(end) } -function testVideoImage (url, videoName, imagePath, callback) { +function testVideoImage (url, imageName, imagePath, callback) { // Don't test images if the node env is not set // Because we need a special ffmpeg version for this test if (process.env.NODE_TEST_IMAGE) { @@ -205,7 +205,7 @@ function testVideoImage (url, videoName, imagePath, callback) { .end(function (err, res) { if (err) return callback(err) - fs.readFile(pathUtils.join(__dirname, '..', 'api', 'fixtures', videoName + '.jpg'), function (err, data) { + fs.readFile(pathUtils.join(__dirname, '..', 'api', 'fixtures', imageName + '.jpg'), function (err, data) { if (err) return callback(err) callback(null, data.equals(res.body)) diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts index d472cc8fb..8aa8ee448 100644 --- a/shared/models/videos/video.model.ts +++ b/shared/models/videos/video.model.ts @@ -17,6 +17,7 @@ export interface Video { podHost: string tags: string[] thumbnailPath: string + previewPath: string views: number likes: number dislikes: number diff --git a/yarn.lock b/yarn.lock index 5636db494..68187f684 100644 --- a/yarn.lock +++ b/yarn.lock @@ -285,6 +285,12 @@ async-each@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" +async-lru@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/async-lru/-/async-lru-1.1.1.tgz#3edbf7e96484d5c2dd852a8bf9794fc07f5e7274" + dependencies: + lru "^3.1.0" + async@>=0.2.9, async@^2.0.0: version "2.4.1" resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7"