diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/api/plugins.ts | 7 | ||||
-rw-r--r-- | server/controllers/api/videos/upload.ts | 9 | ||||
-rw-r--r-- | server/helpers/decache.ts | 78 | ||||
-rw-r--r-- | server/initializers/config.ts | 4 | ||||
-rw-r--r-- | server/initializers/constants.ts | 2 | ||||
-rw-r--r-- | server/lib/client-html.ts | 4 | ||||
-rw-r--r-- | server/lib/plugins/plugin-manager.ts | 4 | ||||
-rw-r--r-- | server/lib/schedulers/remove-dangling-resumable-uploads-scheduler.ts | 35 | ||||
-rw-r--r-- | server/lib/uploadx.ts | 10 | ||||
-rw-r--r-- | server/middlewares/validators/plugins.ts | 6 | ||||
-rw-r--r-- | server/tests/cli/peertube.ts | 19 | ||||
-rw-r--r-- | server/tools/peertube-plugins.ts | 3 |
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' | |||
8 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' | 8 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' |
9 | import { generateWebTorrentVideoFilename } from '@server/lib/paths' | 9 | import { generateWebTorrentVideoFilename } from '@server/lib/paths' |
10 | import { Redis } from '@server/lib/redis' | 10 | import { Redis } from '@server/lib/redis' |
11 | import { uploadx } from '@server/lib/uploadx' | ||
11 | import { | 12 | import { |
12 | addMoveToObjectStorageJob, | 13 | addMoveToObjectStorageJob, |
13 | addOptimizeOrMergeAudioJob, | 14 | addOptimizeOrMergeAudioJob, |
@@ -19,7 +20,6 @@ import { VideoPathManager } from '@server/lib/video-path-manager' | |||
19 | import { buildNextVideoState } from '@server/lib/video-state' | 20 | import { buildNextVideoState } from '@server/lib/video-state' |
20 | import { openapiOperationDoc } from '@server/middlewares/doc' | 21 | import { openapiOperationDoc } from '@server/middlewares/doc' |
21 | import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' | 22 | import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' |
22 | import { Uploadx } from '@uploadx/core' | ||
23 | import { VideoCreate, VideoState } from '../../../../shared' | 23 | import { VideoCreate, VideoState } from '../../../../shared' |
24 | import { HttpStatusCode } from '../../../../shared/models' | 24 | import { HttpStatusCode } from '../../../../shared/models' |
25 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | 25 | import { 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' |
47 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' | 47 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' |
48 | import { VideoModel } from '../../../models/video/video' | 48 | import { VideoModel } from '../../../models/video/video' |
@@ -52,9 +52,6 @@ const lTags = loggerTagsFactory('api', 'video') | |||
52 | const auditLogger = auditLoggerFactory('videos') | 52 | const auditLogger = auditLoggerFactory('videos') |
53 | const uploadRouter = express.Router() | 53 | const uploadRouter = express.Router() |
54 | 54 | ||
55 | const uploadx = new Uploadx({ directory: getResumableUploadPath() }) | ||
56 | uploadx.getUserId = (_, res: express.Response) => res.locals.oauth?.token.user.id | ||
57 | |||
58 | const reqVideoFileAdd = createReqFiles( | 55 | const 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 | |||
4 | import { extname } from 'path' | ||
5 | |||
6 | function 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 | |||
18 | function 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 | |||
32 | export { | ||
33 | decacheModule, | ||
34 | decachePlugin | ||
35 | } | ||
36 | |||
37 | // --------------------------------------------------------------------------- | ||
38 | |||
39 | function find (moduleName: string) { | ||
40 | try { | ||
41 | return require.resolve(moduleName) | ||
42 | } catch { | ||
43 | return '' | ||
44 | } | ||
45 | } | ||
46 | |||
47 | function 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 | |||
70 | function 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 @@ | |||
1 | import bytes from 'bytes' | 1 | import bytes from 'bytes' |
2 | import { IConfig } from 'config' | 2 | import { IConfig } from 'config' |
3 | import decache from 'decache' | ||
4 | import { dirname, join } from 'path' | 3 | import { dirname, join } from 'path' |
4 | import { decacheModule } from '@server/helpers/decache' | ||
5 | import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type' | 5 | import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type' |
6 | import { BroadcastMessageLevel } from '@shared/models/server' | 6 | import { BroadcastMessageLevel } from '@shared/models/server' |
7 | import { VideosRedundancyStrategy } from '../../shared/models' | 7 | import { 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 @@ | |||
1 | import decache from 'decache' | ||
2 | import express from 'express' | 1 | import express from 'express' |
3 | import { createReadStream, createWriteStream } from 'fs' | 2 | import { createReadStream, createWriteStream } from 'fs' |
4 | import { ensureDir, outputFile, readJSON } from 'fs-extra' | 3 | import { ensureDir, outputFile, readJSON } from 'fs-extra' |
5 | import { basename, join } from 'path' | 4 | import { basename, join } from 'path' |
5 | import { decachePlugin } from '@server/helpers/decache' | ||
6 | import { MOAuthTokenUser, MUser } from '@server/types/models' | 6 | import { MOAuthTokenUser, MUser } from '@server/types/models' |
7 | import { getCompleteLocale } from '@shared/core-utils' | 7 | import { getCompleteLocale } from '@shared/core-utils' |
8 | import { ClientScript, PluginPackageJson, PluginTranslation, PluginTranslationPaths, RegisterServerHookOptions } from '@shared/models' | 8 | import { 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 @@ | |||
1 | import { map } from 'bluebird' | 1 | |
2 | import { readdir, remove, stat } from 'fs-extra' | ||
3 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 2 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
4 | import { getResumableUploadPath } from '@server/helpers/upload' | ||
5 | import { SCHEDULER_INTERVALS_MS } from '@server/initializers/constants' | 3 | import { SCHEDULER_INTERVALS_MS } from '@server/initializers/constants' |
6 | import { METAFILE_EXTNAME } from '@uploadx/core' | 4 | import { uploadx } from '../uploadx' |
7 | import { AbstractScheduler } from './abstract-scheduler' | 5 | import { AbstractScheduler } from './abstract-scheduler' |
8 | 6 | ||
9 | const lTags = loggerTagsFactory('scheduler', 'resumable-upload', 'cleaner') | 7 | const 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 @@ | |||
1 | import express from 'express' | ||
2 | import { getResumableUploadPath } from '@server/helpers/upload' | ||
3 | import { Uploadx } from '@uploadx/core' | ||
4 | |||
5 | const uploadx = new Uploadx({ directory: getResumableUploadPath() }) | ||
6 | uploadx.getUserId = (_, res: express.Response) => res.locals.oauth?.token.user.id | ||
7 | |||
8 | export { | ||
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 | ||
36 | program | 37 | program |
@@ -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) |