aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/plugins.ts7
-rw-r--r--server/controllers/api/videos/upload.ts9
-rw-r--r--server/helpers/decache.ts78
-rw-r--r--server/initializers/config.ts4
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/lib/client-html.ts4
-rw-r--r--server/lib/plugins/plugin-manager.ts4
-rw-r--r--server/lib/schedulers/remove-dangling-resumable-uploads-scheduler.ts35
-rw-r--r--server/lib/uploadx.ts10
-rw-r--r--server/middlewares/validators/plugins.ts6
-rw-r--r--server/tests/cli/peertube.ts19
-rw-r--r--server/tools/peertube-plugins.ts3
12 files changed, 136 insertions, 45 deletions
diff --git a/server/controllers/api/plugins.ts b/server/controllers/api/plugins.ts
index 2de7fe41f..de9e055dc 100644
--- a/server/controllers/api/plugins.ts
+++ b/server/controllers/api/plugins.ts
@@ -144,8 +144,13 @@ async function installPlugin (req: express.Request, res: express.Response) {
144 144
145 const fromDisk = !!body.path 145 const fromDisk = !!body.path
146 const toInstall = body.npmName || body.path 146 const toInstall = body.npmName || body.path
147
148 const pluginVersion = body.pluginVersion && body.npmName
149 ? body.pluginVersion
150 : undefined
151
147 try { 152 try {
148 const plugin = await PluginManager.Instance.install(toInstall, undefined, fromDisk) 153 const plugin = await PluginManager.Instance.install(toInstall, pluginVersion, fromDisk)
149 154
150 return res.json(plugin.toFormattedJSON()) 155 return res.json(plugin.toFormattedJSON())
151 } catch (err) { 156 } catch (err) {
diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts
index 6773b500f..c827f6bf0 100644
--- a/server/controllers/api/videos/upload.ts
+++ b/server/controllers/api/videos/upload.ts
@@ -8,6 +8,7 @@ import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
8import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' 8import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
9import { generateWebTorrentVideoFilename } from '@server/lib/paths' 9import { generateWebTorrentVideoFilename } from '@server/lib/paths'
10import { Redis } from '@server/lib/redis' 10import { Redis } from '@server/lib/redis'
11import { uploadx } from '@server/lib/uploadx'
11import { 12import {
12 addMoveToObjectStorageJob, 13 addMoveToObjectStorageJob,
13 addOptimizeOrMergeAudioJob, 14 addOptimizeOrMergeAudioJob,
@@ -19,7 +20,6 @@ import { VideoPathManager } from '@server/lib/video-path-manager'
19import { buildNextVideoState } from '@server/lib/video-state' 20import { buildNextVideoState } from '@server/lib/video-state'
20import { openapiOperationDoc } from '@server/middlewares/doc' 21import { openapiOperationDoc } from '@server/middlewares/doc'
21import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' 22import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
22import { Uploadx } from '@uploadx/core'
23import { VideoCreate, VideoState } from '../../../../shared' 23import { VideoCreate, VideoState } from '../../../../shared'
24import { HttpStatusCode } from '../../../../shared/models' 24import { HttpStatusCode } from '../../../../shared/models'
25import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' 25import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
@@ -41,8 +41,8 @@ import {
41 authenticate, 41 authenticate,
42 videosAddLegacyValidator, 42 videosAddLegacyValidator,
43 videosAddResumableInitValidator, 43 videosAddResumableInitValidator,
44 videosResumableUploadIdValidator, 44 videosAddResumableValidator,
45 videosAddResumableValidator 45 videosResumableUploadIdValidator
46} from '../../../middlewares' 46} from '../../../middlewares'
47import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' 47import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
48import { VideoModel } from '../../../models/video/video' 48import { VideoModel } from '../../../models/video/video'
@@ -52,9 +52,6 @@ const lTags = loggerTagsFactory('api', 'video')
52const auditLogger = auditLoggerFactory('videos') 52const auditLogger = auditLoggerFactory('videos')
53const uploadRouter = express.Router() 53const uploadRouter = express.Router()
54 54
55const uploadx = new Uploadx({ directory: getResumableUploadPath() })
56uploadx.getUserId = (_, res: express.Response) => res.locals.oauth?.token.user.id
57
58const reqVideoFileAdd = createReqFiles( 55const reqVideoFileAdd = createReqFiles(
59 [ 'videofile', 'thumbnailfile', 'previewfile' ], 56 [ 'videofile', 'thumbnailfile', 'previewfile' ],
60 Object.assign({}, MIMETYPES.VIDEO.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT), 57 Object.assign({}, MIMETYPES.VIDEO.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT),
diff --git a/server/helpers/decache.ts b/server/helpers/decache.ts
new file mode 100644
index 000000000..e31973b7a
--- /dev/null
+++ b/server/helpers/decache.ts
@@ -0,0 +1,78 @@
1// Thanks: https://github.com/dwyl/decache
2// We reuse this file to also uncache plugin base path
3
4import { extname } from 'path'
5
6function decachePlugin (pluginPath: string, libraryPath: string) {
7 const moduleName = find(libraryPath)
8
9 if (!moduleName) return
10
11 searchCache(moduleName, function (mod) {
12 delete require.cache[mod.id]
13 })
14
15 removeCachedPath(pluginPath)
16}
17
18function decacheModule (name: string) {
19 const moduleName = find(name)
20
21 if (!moduleName) return
22
23 searchCache(moduleName, function (mod) {
24 delete require.cache[mod.id]
25 })
26
27 removeCachedPath(moduleName)
28}
29
30// ---------------------------------------------------------------------------
31
32export {
33 decacheModule,
34 decachePlugin
35}
36
37// ---------------------------------------------------------------------------
38
39function find (moduleName: string) {
40 try {
41 return require.resolve(moduleName)
42 } catch {
43 return ''
44 }
45}
46
47function searchCache (moduleName: string, callback: (current: NodeModule) => void) {
48 const resolvedModule = require.resolve(moduleName)
49 let mod: NodeModule
50 const visited = {}
51
52 if (resolvedModule && ((mod = require.cache[resolvedModule]) !== undefined)) {
53 // Recursively go over the results
54 (function run (current) {
55 visited[current.id] = true
56
57 current.children.forEach(function (child) {
58 if (extname(child.filename) !== '.node' && !visited[child.id]) {
59 run(child)
60 }
61 })
62
63 // Call the specified callback providing the
64 // found module
65 callback(current)
66 })(mod)
67 }
68};
69
70function removeCachedPath (pluginPath: string) {
71 const pathCache = (module.constructor as any)._pathCache
72
73 Object.keys(pathCache).forEach(function (cacheKey) {
74 if (cacheKey.includes(pluginPath)) {
75 delete pathCache[cacheKey]
76 }
77 })
78}
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
index dadda2a77..f3a7c6b6b 100644
--- a/server/initializers/config.ts
+++ b/server/initializers/config.ts
@@ -1,7 +1,7 @@
1import bytes from 'bytes' 1import bytes from 'bytes'
2import { IConfig } from 'config' 2import { IConfig } from 'config'
3import decache from 'decache'
4import { dirname, join } from 'path' 3import { dirname, join } from 'path'
4import { decacheModule } from '@server/helpers/decache'
5import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type' 5import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
6import { BroadcastMessageLevel } from '@shared/models/server' 6import { BroadcastMessageLevel } from '@shared/models/server'
7import { VideosRedundancyStrategy } from '../../shared/models' 7import { VideosRedundancyStrategy } from '../../shared/models'
@@ -497,7 +497,7 @@ export function reloadConfig () {
497 delete require.cache[fileName] 497 delete require.cache[fileName]
498 } 498 }
499 499
500 decache('config') 500 decacheModule('config')
501 } 501 }
502 502
503 purge() 503 purge()
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index b8633e83e..c61c01d62 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -223,7 +223,7 @@ const SCHEDULER_INTERVALS_MS = {
223 REMOVE_OLD_VIEWS: 60000 * 60 * 24, // 1 day 223 REMOVE_OLD_VIEWS: 60000 * 60 * 24, // 1 day
224 REMOVE_OLD_HISTORY: 60000 * 60 * 24, // 1 day 224 REMOVE_OLD_HISTORY: 60000 * 60 * 24, // 1 day
225 UPDATE_INBOX_STATS: 1000 * 60, // 1 minute 225 UPDATE_INBOX_STATS: 1000 * 60, // 1 minute
226 REMOVE_DANGLING_RESUMABLE_UPLOADS: 60000 * 60 * 16 // 16 hours 226 REMOVE_DANGLING_RESUMABLE_UPLOADS: 60000 * 60 // 1 hour
227} 227}
228 228
229// --------------------------------------------------------------------------- 229// ---------------------------------------------------------------------------
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts
index 360b4667f..adc3d712e 100644
--- a/server/lib/client-html.ts
+++ b/server/lib/client-html.ts
@@ -350,10 +350,6 @@ class ClientHtml {
350 return join(__dirname, '../../../client/dist/standalone/videos/embed.html') 350 return join(__dirname, '../../../client/dist/standalone/videos/embed.html')
351 } 351 }
352 352
353 private static addHtmlLang (htmlStringPage: string, paramLang: string) {
354 return htmlStringPage.replace('<html>', `<html lang="${paramLang}">`)
355 }
356
357 private static addManifestContentHash (htmlStringPage: string) { 353 private static addManifestContentHash (htmlStringPage: string) {
358 return htmlStringPage.replace('[manifestContentHash]', FILES_CONTENT_HASH.MANIFEST) 354 return htmlStringPage.replace('[manifestContentHash]', FILES_CONTENT_HASH.MANIFEST)
359 } 355 }
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts
index d4d2a7edc..6c2f4764e 100644
--- a/server/lib/plugins/plugin-manager.ts
+++ b/server/lib/plugins/plugin-manager.ts
@@ -1,8 +1,8 @@
1import decache from 'decache'
2import express from 'express' 1import express from 'express'
3import { createReadStream, createWriteStream } from 'fs' 2import { createReadStream, createWriteStream } from 'fs'
4import { ensureDir, outputFile, readJSON } from 'fs-extra' 3import { ensureDir, outputFile, readJSON } from 'fs-extra'
5import { basename, join } from 'path' 4import { basename, join } from 'path'
5import { decachePlugin } from '@server/helpers/decache'
6import { MOAuthTokenUser, MUser } from '@server/types/models' 6import { MOAuthTokenUser, MUser } from '@server/types/models'
7import { getCompleteLocale } from '@shared/core-utils' 7import { getCompleteLocale } from '@shared/core-utils'
8import { ClientScript, PluginPackageJson, PluginTranslation, PluginTranslationPaths, RegisterServerHookOptions } from '@shared/models' 8import { ClientScript, PluginPackageJson, PluginTranslation, PluginTranslationPaths, RegisterServerHookOptions } from '@shared/models'
@@ -420,7 +420,7 @@ export class PluginManager implements ServerHook {
420 420
421 // Delete cache if needed 421 // Delete cache if needed
422 const modulePath = join(pluginPath, packageJSON.library) 422 const modulePath = join(pluginPath, packageJSON.library)
423 decache(modulePath) 423 decachePlugin(pluginPath, modulePath)
424 const library: PluginLibrary = require(modulePath) 424 const library: PluginLibrary = require(modulePath)
425 425
426 if (!isLibraryCodeValid(library)) { 426 if (!isLibraryCodeValid(library)) {
diff --git a/server/lib/schedulers/remove-dangling-resumable-uploads-scheduler.ts b/server/lib/schedulers/remove-dangling-resumable-uploads-scheduler.ts
index d6e561cad..61e93eafa 100644
--- a/server/lib/schedulers/remove-dangling-resumable-uploads-scheduler.ts
+++ b/server/lib/schedulers/remove-dangling-resumable-uploads-scheduler.ts
@@ -1,9 +1,7 @@
1import { map } from 'bluebird' 1
2import { readdir, remove, stat } from 'fs-extra'
3import { logger, loggerTagsFactory } from '@server/helpers/logger' 2import { logger, loggerTagsFactory } from '@server/helpers/logger'
4import { getResumableUploadPath } from '@server/helpers/upload'
5import { SCHEDULER_INTERVALS_MS } from '@server/initializers/constants' 3import { SCHEDULER_INTERVALS_MS } from '@server/initializers/constants'
6import { METAFILE_EXTNAME } from '@uploadx/core' 4import { uploadx } from '../uploadx'
7import { AbstractScheduler } from './abstract-scheduler' 5import { AbstractScheduler } from './abstract-scheduler'
8 6
9const lTags = loggerTagsFactory('scheduler', 'resumable-upload', 'cleaner') 7const lTags = loggerTagsFactory('scheduler', 'resumable-upload', 'cleaner')
@@ -22,36 +20,17 @@ export class RemoveDanglingResumableUploadsScheduler extends AbstractScheduler {
22 } 20 }
23 21
24 protected async internalExecute () { 22 protected async internalExecute () {
25 const path = getResumableUploadPath() 23 logger.debug('Removing dangling resumable uploads', lTags())
26 const files = await readdir(path)
27
28 const metafiles = files.filter(f => f.endsWith(METAFILE_EXTNAME))
29 24
30 if (metafiles.length === 0) return 25 const now = new Date().getTime()
31
32 logger.debug('Reading resumable video upload folder %s with %d files', path, metafiles.length, lTags())
33 26
34 try { 27 try {
35 await map(metafiles, metafile => { 28 // Remove files that were not updated since the last execution
36 return this.deleteIfOlderThan(metafile, this.lastExecutionTimeMs) 29 await uploadx.storage.purge(now - this.lastExecutionTimeMs)
37 }, { concurrency: 5 })
38 } catch (error) { 30 } catch (error) {
39 logger.error('Failed to handle file during resumable video upload folder cleanup', { error, ...lTags() }) 31 logger.error('Failed to handle file during resumable video upload folder cleanup', { error, ...lTags() })
40 } finally { 32 } finally {
41 this.lastExecutionTimeMs = new Date().getTime() 33 this.lastExecutionTimeMs = now
42 }
43 }
44
45 private async deleteIfOlderThan (metafile: string, olderThan: number) {
46 const metafilePath = getResumableUploadPath(metafile)
47 const statResult = await stat(metafilePath)
48
49 // Delete uploads that started since a long time
50 if (statResult.ctimeMs < olderThan) {
51 await remove(metafilePath)
52
53 const datafile = metafilePath.replace(new RegExp(`${METAFILE_EXTNAME}$`), '')
54 await remove(datafile)
55 } 34 }
56 } 35 }
57 36
diff --git a/server/lib/uploadx.ts b/server/lib/uploadx.ts
new file mode 100644
index 000000000..11b1044db
--- /dev/null
+++ b/server/lib/uploadx.ts
@@ -0,0 +1,10 @@
1import express from 'express'
2import { getResumableUploadPath } from '@server/helpers/upload'
3import { Uploadx } from '@uploadx/core'
4
5const uploadx = new Uploadx({ directory: getResumableUploadPath() })
6uploadx.getUserId = (_, res: express.Response) => res.locals.oauth?.token.user.id
7
8export {
9 uploadx
10}
diff --git a/server/middlewares/validators/plugins.ts b/server/middlewares/validators/plugins.ts
index 21171af23..c1e9ebefb 100644
--- a/server/middlewares/validators/plugins.ts
+++ b/server/middlewares/validators/plugins.ts
@@ -116,6 +116,9 @@ const installOrUpdatePluginValidator = [
116 body('npmName') 116 body('npmName')
117 .optional() 117 .optional()
118 .custom(isNpmPluginNameValid).withMessage('Should have a valid npm name'), 118 .custom(isNpmPluginNameValid).withMessage('Should have a valid npm name'),
119 body('pluginVersion')
120 .optional()
121 .custom(isPluginVersionValid).withMessage('Should have a valid plugin version'),
119 body('path') 122 body('path')
120 .optional() 123 .optional()
121 .custom(isSafePath).withMessage('Should have a valid safe path'), 124 .custom(isSafePath).withMessage('Should have a valid safe path'),
@@ -129,6 +132,9 @@ const installOrUpdatePluginValidator = [
129 if (!body.path && !body.npmName) { 132 if (!body.path && !body.npmName) {
130 return res.fail({ message: 'Should have either a npmName or a path' }) 133 return res.fail({ message: 'Should have either a npmName or a path' })
131 } 134 }
135 if (body.pluginVersion && !body.npmName) {
136 return res.fail({ message: 'Should have a npmName when specifying a pluginVersion' })
137 }
132 138
133 return next() 139 return next()
134 } 140 }
diff --git a/server/tests/cli/peertube.ts b/server/tests/cli/peertube.ts
index f2a984962..3ac440f84 100644
--- a/server/tests/cli/peertube.ts
+++ b/server/tests/cli/peertube.ts
@@ -207,6 +207,25 @@ describe('Test CLI wrapper', function () {
207 207
208 expect(res).to.not.contain('peertube-plugin-hello-world') 208 expect(res).to.not.contain('peertube-plugin-hello-world')
209 }) 209 })
210
211 it('Should install a plugin in requested version', async function () {
212 this.timeout(60000)
213
214 await cliCommand.execWithEnv(`${cmd} plugins install --npm-name peertube-plugin-hello-world --plugin-version 0.0.17`)
215 })
216
217 it('Should list installed plugins, in correct version', async function () {
218 const res = await cliCommand.execWithEnv(`${cmd} plugins list`)
219
220 expect(res).to.contain('peertube-plugin-hello-world')
221 expect(res).to.contain('0.0.17')
222 })
223
224 it('Should uninstall the plugin again', async function () {
225 const res = await cliCommand.execWithEnv(`${cmd} plugins uninstall --npm-name peertube-plugin-hello-world`)
226
227 expect(res).to.not.contain('peertube-plugin-hello-world')
228 })
210 }) 229 })
211 230
212 describe('Manage video redundancies', function () { 231 describe('Manage video redundancies', function () {
diff --git a/server/tools/peertube-plugins.ts b/server/tools/peertube-plugins.ts
index ae625114d..9dd3f08c9 100644
--- a/server/tools/peertube-plugins.ts
+++ b/server/tools/peertube-plugins.ts
@@ -31,6 +31,7 @@ program
31 .option('-p, --password <token>', 'Password') 31 .option('-p, --password <token>', 'Password')
32 .option('-P --path <path>', 'Install from a path') 32 .option('-P --path <path>', 'Install from a path')
33 .option('-n, --npm-name <npmName>', 'Install from npm') 33 .option('-n, --npm-name <npmName>', 'Install from npm')
34 .option('--plugin-version <pluginVersion>', 'Specify the plugin version to install (only available when installing from npm)')
34 .action((options, command) => installPluginCLI(command, options)) 35 .action((options, command) => installPluginCLI(command, options))
35 36
36program 37program
@@ -109,7 +110,7 @@ async function installPluginCLI (command: Command, options: OptionValues) {
109 await assignToken(server, username, password) 110 await assignToken(server, username, password)
110 111
111 try { 112 try {
112 await server.plugins.install({ npmName: options.npmName, path: options.path }) 113 await server.plugins.install({ npmName: options.npmName, path: options.path, pluginVersion: options.pluginVersion })
113 } catch (err) { 114 } catch (err) {
114 console.error('Cannot install plugin.', err) 115 console.error('Cannot install plugin.', err)
115 process.exit(-1) 116 process.exit(-1)