From 302eba0d898e38dca14739486441c27c0be6c62f Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 22 Apr 2021 10:55:28 +0200 Subject: [PATCH] Add data directory for plugins and some helpers --- server/lib/plugins/plugin-helpers-builder.ts | 46 +++++++---- server/lib/plugins/plugin-manager.ts | 5 +- .../peertube-plugin-test-four/main.js | 23 +++++- .../fixtures/peertube-plugin-test-six/main.js | 18 ++++- server/tests/plugins/plugin-helpers.ts | 40 ++++++++++ server/tests/plugins/plugin-storage.ts | 80 ++++++++++++++++++- .../plugins/register-server-option.model.ts | 17 +++- support/doc/plugins/guide.md | 40 +++++++++- 8 files changed, 245 insertions(+), 24 deletions(-) diff --git a/server/lib/plugins/plugin-helpers-builder.ts b/server/lib/plugins/plugin-helpers-builder.ts index cbd849742..d57c69ef0 100644 --- a/server/lib/plugins/plugin-helpers-builder.ts +++ b/server/lib/plugins/plugin-helpers-builder.ts @@ -1,19 +1,22 @@ -import { PeerTubeHelpers } from '@server/types/plugins' -import { sequelizeTypescript } from '@server/initializers/database' +import * as express from 'express' +import { join } from 'path' import { buildLogger } from '@server/helpers/logger' -import { VideoModel } from '@server/models/video/video' +import { CONFIG } from '@server/initializers/config' import { WEBSERVER } from '@server/initializers/constants' -import { ServerModel } from '@server/models/server/server' -import { getServerActor } from '@server/models/application/application' -import { addServerInBlocklist, removeServerFromBlocklist, addAccountInBlocklist, removeAccountFromBlocklist } from '../blocklist' -import { ServerBlocklistModel } from '@server/models/server/server-blocklist' +import { sequelizeTypescript } from '@server/initializers/database' import { AccountModel } from '@server/models/account/account' -import { VideoBlacklistCreate } from '@shared/models' -import { blacklistVideo, unblacklistVideo } from '../video-blacklist' -import { VideoBlacklistModel } from '@server/models/video/video-blacklist' import { AccountBlocklistModel } from '@server/models/account/account-blocklist' -import { getServerConfig } from '../config' +import { getServerActor } from '@server/models/application/application' +import { ServerModel } from '@server/models/server/server' +import { ServerBlocklistModel } from '@server/models/server/server-blocklist' +import { VideoModel } from '@server/models/video/video' +import { VideoBlacklistModel } from '@server/models/video/video-blacklist' import { MPlugin } from '@server/types/models' +import { PeerTubeHelpers } from '@server/types/plugins' +import { VideoBlacklistCreate } from '@shared/models' +import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../blocklist' +import { getServerConfig } from '../config' +import { blacklistVideo, unblacklistVideo } from '../video-blacklist' function buildPluginHelpers (pluginModel: MPlugin, npmName: string): PeerTubeHelpers { const logger = buildPluginLogger(npmName) @@ -27,7 +30,9 @@ function buildPluginHelpers (pluginModel: MPlugin, npmName: string): PeerTubeHel const moderation = buildModerationHelpers() - const plugin = buildPluginRelatedHelpers(pluginModel) + const plugin = buildPluginRelatedHelpers(pluginModel, npmName) + + const user = buildUserHelpers() return { logger, @@ -36,7 +41,8 @@ function buildPluginHelpers (pluginModel: MPlugin, npmName: string): PeerTubeHel config, moderation, plugin, - server + server, + user } } @@ -145,8 +151,18 @@ function buildConfigHelpers () { } } -function buildPluginRelatedHelpers (plugin: MPlugin) { +function buildPluginRelatedHelpers (plugin: MPlugin, npmName: string) { return { - getBaseStaticRoute: () => `/plugins/${plugin.name}/${plugin.version}/static/` + getBaseStaticRoute: () => `/plugins/${plugin.name}/${plugin.version}/static/`, + + getBaseRouterRoute: () => `/plugins/${plugin.name}/${plugin.version}/router/`, + + getDataDirectoryPath: () => join(CONFIG.STORAGE.PLUGINS_DIR, 'data', npmName) + } +} + +function buildUserHelpers () { + return { + getAuthUser: (res: express.Response) => res.locals.oauth?.token?.User } } diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts index ae05af721..ba9814383 100644 --- a/server/lib/plugins/plugin-manager.ts +++ b/server/lib/plugins/plugin-manager.ts @@ -1,7 +1,7 @@ import decache from 'decache' import * as express from 'express' import { createReadStream, createWriteStream } from 'fs' -import { outputFile, readJSON } from 'fs-extra' +import { ensureDir, outputFile, readJSON } from 'fs-extra' import { basename, join } from 'path' import { MOAuthTokenUser, MUser } from '@server/types/models' import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model' @@ -428,6 +428,9 @@ export class PluginManager implements ServerHook { } const { registerOptions, registerStore } = this.getRegisterHelpers(npmName, plugin) + + await ensureDir(registerOptions.peertubeHelpers.plugin.getDataDirectoryPath()) + library.register(registerOptions) .catch(err => logger.error('Cannot register plugin %s.', npmName, { err })) diff --git a/server/tests/fixtures/peertube-plugin-test-four/main.js b/server/tests/fixtures/peertube-plugin-test-four/main.js index ea0599997..6930ac511 100644 --- a/server/tests/fixtures/peertube-plugin-test-four/main.js +++ b/server/tests/fixtures/peertube-plugin-test-four/main.js @@ -77,10 +77,31 @@ async function register ({ }) router.get('/static-route', async (req, res) => { - const staticRoute = await peertubeHelpers.plugin.getBaseStaticRoute() + const staticRoute = peertubeHelpers.plugin.getBaseStaticRoute() return res.json({ staticRoute }) }) + + router.get('/router-route', async (req, res) => { + const routerRoute = peertubeHelpers.plugin.getBaseRouterRoute() + + return res.json({ routerRoute }) + }) + + router.get('/user', async (req, res) => { + const user = peertubeHelpers.user.getAuthUser(res) + + const isAdmin = user.role === 0 + const isModerator = user.role === 1 + const isUser = user.role === 2 + + return res.json({ + username: user.username, + isAdmin, + isModerator, + isUser + }) + }) } } diff --git a/server/tests/fixtures/peertube-plugin-test-six/main.js b/server/tests/fixtures/peertube-plugin-test-six/main.js index bb9aaffa7..858bdb2df 100644 --- a/server/tests/fixtures/peertube-plugin-test-six/main.js +++ b/server/tests/fixtures/peertube-plugin-test-six/main.js @@ -1,6 +1,10 @@ +const fs = require('fs') +const path = require('path') + async function register ({ storageManager, - peertubeHelpers + peertubeHelpers, + getRouter }) { const { logger } = peertubeHelpers @@ -11,6 +15,18 @@ async function register ({ const result = await storageManager.getData('superkey') logger.info('superkey stored value is %s', result.value) } + + { + getRouter().get('/create-file', async (req, res) => { + const basePath = peertubeHelpers.plugin.getDataDirectoryPath() + + fs.writeFile(path.join(basePath, 'Aladdin.txt'), 'Prince Ali', function (err) { + if (err) return res.sendStatus(500) + + res.sendStatus(200) + }) + }) + } } async function unregister () { diff --git a/server/tests/plugins/plugin-helpers.ts b/server/tests/plugins/plugin-helpers.ts index 325d20e84..2ac070b41 100644 --- a/server/tests/plugins/plugin-helpers.ts +++ b/server/tests/plugins/plugin-helpers.ts @@ -100,6 +100,46 @@ describe('Test plugin helpers', function () { expect(res.body.staticRoute).to.equal('/plugins/test-four/0.0.1/static/') }) + + it('Should get the base static route', async function () { + const baseRouter = '/plugins/test-four/0.0.1/router/' + + const res = await makeGetRequest({ + url: servers[0].url, + path: baseRouter + 'router-route', + statusCodeExpected: HttpStatusCode.OK_200 + }) + + expect(res.body.routerRoute).to.equal(baseRouter) + }) + }) + + describe('User', function () { + + it('Should not get a user if not authenticated', async function () { + const res = await makeGetRequest({ + url: servers[0].url, + path: '/plugins/test-four/router/user', + statusCodeExpected: HttpStatusCode.OK_200 + }) + + expect(res.body.user).to.be.undefined + }) + + it('Should get a user if authenticated', async function () { + const res = await makeGetRequest({ + url: servers[0].url, + token: servers[0].accessToken, + path: '/plugins/test-four/router/user', + statusCodeExpected: HttpStatusCode.OK_200 + }) + + expect(res.body.user).to.exist + expect(res.body.username).to.equal('root') + expect(res.body.isAdmin).to.be.true + expect(res.body.isModerator).to.be.false + expect(res.body.isUser).to.be.false + }) }) describe('Moderation', function () { diff --git a/server/tests/plugins/plugin-storage.ts b/server/tests/plugins/plugin-storage.ts index 356692eb9..3c46b2585 100644 --- a/server/tests/plugins/plugin-storage.ts +++ b/server/tests/plugins/plugin-storage.ts @@ -1,7 +1,18 @@ /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ import 'mocha' -import { getPluginTestPath, installPlugin, setAccessTokensToServers } from '../../../shared/extra-utils' +import { expect } from 'chai' +import { pathExists, readdir, readFile } from 'fs-extra' +import { join } from 'path' +import { HttpStatusCode } from '@shared/core-utils' +import { + buildServerDirectory, + getPluginTestPath, + installPlugin, + makeGetRequest, + setAccessTokensToServers, + uninstallPlugin +} from '../../../shared/extra-utils' import { cleanupTests, flushAndRunServer, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers' describe('Test plugin storage', function () { @@ -20,8 +31,71 @@ describe('Test plugin storage', function () { }) }) - it('Should correctly store a subkey', async function () { - await waitUntilLog(server, 'superkey stored value is toto') + describe('DB storage', function () { + + it('Should correctly store a subkey', async function () { + await waitUntilLog(server, 'superkey stored value is toto') + }) + }) + + describe('Disk storage', function () { + let dataPath: string + let pluginDataPath: string + + async function getFileContent () { + const files = await readdir(pluginDataPath) + expect(files).to.have.lengthOf(1) + + return readFile(join(pluginDataPath, files[0]), 'utf8') + } + + before(function () { + dataPath = buildServerDirectory(server, 'plugins/data') + pluginDataPath = join(dataPath, 'peertube-plugin-test-six') + }) + + it('Should have created the directory on install', async function () { + const dataPath = buildServerDirectory(server, 'plugins/data') + const pluginDataPath = join(dataPath, 'peertube-plugin-test-six') + + expect(await pathExists(dataPath)).to.be.true + expect(await pathExists(pluginDataPath)).to.be.true + expect(await readdir(pluginDataPath)).to.have.lengthOf(0) + }) + + it('Should have created a file', async function () { + await makeGetRequest({ + url: server.url, + token: server.accessToken, + path: '/plugins/test-six/router/create-file', + statusCodeExpected: HttpStatusCode.OK_200 + }) + + const content = await getFileContent() + expect(content).to.equal('Prince Ali') + }) + + it('Should still have the file after an uninstallation', async function () { + await uninstallPlugin({ + url: server.url, + accessToken: server.accessToken, + npmName: 'peertube-plugin-test-six' + }) + + const content = await getFileContent() + expect(content).to.equal('Prince Ali') + }) + + it('Should still have the file after the reinstallation', async function () { + await installPlugin({ + url: server.url, + accessToken: server.accessToken, + path: getPluginTestPath('-six') + }) + + const content = await getFileContent() + expect(content).to.equal('Prince Ali') + }) }) after(async function () { diff --git a/server/types/plugins/register-server-option.model.ts b/server/types/plugins/register-server-option.model.ts index 391dcc3f9..1b9250ce4 100644 --- a/server/types/plugins/register-server-option.model.ts +++ b/server/types/plugins/register-server-option.model.ts @@ -1,4 +1,4 @@ -import { Router } from 'express' +import { Router, Response } from 'express' import { Logger } from 'winston' import { ActorModel } from '@server/models/activitypub/actor' import { @@ -13,6 +13,7 @@ import { RegisterServerHookOptions, RegisterServerSettingOptions, ServerConfig, + UserRole, VideoBlacklistCreate } from '@shared/models' import { MVideoThumbnail } from '../models' @@ -58,6 +59,20 @@ export type PeerTubeHelpers = { plugin: { getBaseStaticRoute: () => string + + getBaseRouterRoute: () => string + + getDataDirectoryPath: () => string + } + + user: { + getAuthUser: (response: Response) => { + id?: string + username: string + email: string + blocked: boolean + role: UserRole + } | undefined } } diff --git a/support/doc/plugins/guide.md b/support/doc/plugins/guide.md index 5b5a3065d..36ade117b 100644 --- a/support/doc/plugins/guide.md +++ b/support/doc/plugins/guide.md @@ -195,12 +195,30 @@ Plugins can store/load JSON data, that PeerTube will store in its database (so d Example: ```js -function register (...) { +function register ({ + storageManager +}) { const value = await storageManager.getData('mykey') await storageManager.storeData('mykey', { subkey: 'value' }) } ``` +You can also store files in the plugin data directory (`/{plugins-directory}/data/{npm-plugin-name}`). +This directory and its content won't be deleted when your plugin is uninstalled/upgraded. + +```js +function register ({ + storageManager, + peertubeHelpers +}) { + const basePath = peertubeHelpers.plugin.getDataDirectoryPath() + + fs.writeFile(path.join(basePath, 'filename.txt'), 'content of my file', function (err) { + ... + }) +} +``` + #### Update video constants You can add/delete video categories, licences or languages using the appropriate managers: @@ -226,9 +244,27 @@ function register (...) { You can create custom routes using an [express Router](https://expressjs.com/en/4x/api.html#router) for your plugin: ```js -function register (...) { +function register ({ + router +}) { const router = getRouter() router.get('/ping', (req, res) => res.json({ message: 'pong' })) + + // Users are automatically authenticated + router.get('/auth', (res, res) => { + const user = peertubeHelpers.user.getAuthUser(res) + + const isAdmin = user.role === 0 + const isModerator = user.role === 1 + const isUser = user.role === 2 + + res.json({ + username: user.username, + isAdmin, + isModerator, + isUser + }) + }) } ```