diff options
Diffstat (limited to 'packages/server-commands/src/server')
16 files changed, 2183 insertions, 0 deletions
diff --git a/packages/server-commands/src/server/config-command.ts b/packages/server-commands/src/server/config-command.ts new file mode 100644 index 000000000..8fcf0bd51 --- /dev/null +++ b/packages/server-commands/src/server/config-command.ts | |||
@@ -0,0 +1,576 @@ | |||
1 | import merge from 'lodash-es/merge.js' | ||
2 | import { About, CustomConfig, HttpStatusCode, ServerConfig } from '@peertube/peertube-models' | ||
3 | import { DeepPartial } from '@peertube/peertube-typescript-utils' | ||
4 | import { AbstractCommand, OverrideCommandOptions } from '../shared/abstract-command.js' | ||
5 | |||
6 | export class ConfigCommand extends AbstractCommand { | ||
7 | |||
8 | static getCustomConfigResolutions (enabled: boolean, with0p = false) { | ||
9 | return { | ||
10 | '0p': enabled && with0p, | ||
11 | '144p': enabled, | ||
12 | '240p': enabled, | ||
13 | '360p': enabled, | ||
14 | '480p': enabled, | ||
15 | '720p': enabled, | ||
16 | '1080p': enabled, | ||
17 | '1440p': enabled, | ||
18 | '2160p': enabled | ||
19 | } | ||
20 | } | ||
21 | |||
22 | // --------------------------------------------------------------------------- | ||
23 | |||
24 | static getEmailOverrideConfig (emailPort: number) { | ||
25 | return { | ||
26 | smtp: { | ||
27 | hostname: '127.0.0.1', | ||
28 | port: emailPort | ||
29 | } | ||
30 | } | ||
31 | } | ||
32 | |||
33 | // --------------------------------------------------------------------------- | ||
34 | |||
35 | enableSignup (requiresApproval: boolean, limit = -1) { | ||
36 | return this.updateExistingSubConfig({ | ||
37 | newConfig: { | ||
38 | signup: { | ||
39 | enabled: true, | ||
40 | requiresApproval, | ||
41 | limit | ||
42 | } | ||
43 | } | ||
44 | }) | ||
45 | } | ||
46 | |||
47 | // --------------------------------------------------------------------------- | ||
48 | |||
49 | disableImports () { | ||
50 | return this.setImportsEnabled(false) | ||
51 | } | ||
52 | |||
53 | enableImports () { | ||
54 | return this.setImportsEnabled(true) | ||
55 | } | ||
56 | |||
57 | private setImportsEnabled (enabled: boolean) { | ||
58 | return this.updateExistingSubConfig({ | ||
59 | newConfig: { | ||
60 | import: { | ||
61 | videos: { | ||
62 | http: { | ||
63 | enabled | ||
64 | }, | ||
65 | |||
66 | torrent: { | ||
67 | enabled | ||
68 | } | ||
69 | } | ||
70 | } | ||
71 | } | ||
72 | }) | ||
73 | } | ||
74 | |||
75 | // --------------------------------------------------------------------------- | ||
76 | |||
77 | disableFileUpdate () { | ||
78 | return this.setFileUpdateEnabled(false) | ||
79 | } | ||
80 | |||
81 | enableFileUpdate () { | ||
82 | return this.setFileUpdateEnabled(true) | ||
83 | } | ||
84 | |||
85 | private setFileUpdateEnabled (enabled: boolean) { | ||
86 | return this.updateExistingSubConfig({ | ||
87 | newConfig: { | ||
88 | videoFile: { | ||
89 | update: { | ||
90 | enabled | ||
91 | } | ||
92 | } | ||
93 | } | ||
94 | }) | ||
95 | } | ||
96 | |||
97 | // --------------------------------------------------------------------------- | ||
98 | |||
99 | enableChannelSync () { | ||
100 | return this.setChannelSyncEnabled(true) | ||
101 | } | ||
102 | |||
103 | disableChannelSync () { | ||
104 | return this.setChannelSyncEnabled(false) | ||
105 | } | ||
106 | |||
107 | private setChannelSyncEnabled (enabled: boolean) { | ||
108 | return this.updateExistingSubConfig({ | ||
109 | newConfig: { | ||
110 | import: { | ||
111 | videoChannelSynchronization: { | ||
112 | enabled | ||
113 | } | ||
114 | } | ||
115 | } | ||
116 | }) | ||
117 | } | ||
118 | |||
119 | // --------------------------------------------------------------------------- | ||
120 | |||
121 | enableLive (options: { | ||
122 | allowReplay?: boolean | ||
123 | transcoding?: boolean | ||
124 | resolutions?: 'min' | 'max' // Default max | ||
125 | } = {}) { | ||
126 | const { allowReplay, transcoding, resolutions = 'max' } = options | ||
127 | |||
128 | return this.updateExistingSubConfig({ | ||
129 | newConfig: { | ||
130 | live: { | ||
131 | enabled: true, | ||
132 | allowReplay: allowReplay ?? true, | ||
133 | transcoding: { | ||
134 | enabled: transcoding ?? true, | ||
135 | resolutions: ConfigCommand.getCustomConfigResolutions(resolutions === 'max') | ||
136 | } | ||
137 | } | ||
138 | } | ||
139 | }) | ||
140 | } | ||
141 | |||
142 | disableTranscoding () { | ||
143 | return this.updateExistingSubConfig({ | ||
144 | newConfig: { | ||
145 | transcoding: { | ||
146 | enabled: false | ||
147 | }, | ||
148 | videoStudio: { | ||
149 | enabled: false | ||
150 | } | ||
151 | } | ||
152 | }) | ||
153 | } | ||
154 | |||
155 | enableTranscoding (options: { | ||
156 | webVideo?: boolean // default true | ||
157 | hls?: boolean // default true | ||
158 | with0p?: boolean // default false | ||
159 | } = {}) { | ||
160 | const { webVideo = true, hls = true, with0p = false } = options | ||
161 | |||
162 | return this.updateExistingSubConfig({ | ||
163 | newConfig: { | ||
164 | transcoding: { | ||
165 | enabled: true, | ||
166 | |||
167 | allowAudioFiles: true, | ||
168 | allowAdditionalExtensions: true, | ||
169 | |||
170 | resolutions: ConfigCommand.getCustomConfigResolutions(true, with0p), | ||
171 | |||
172 | webVideos: { | ||
173 | enabled: webVideo | ||
174 | }, | ||
175 | hls: { | ||
176 | enabled: hls | ||
177 | } | ||
178 | } | ||
179 | } | ||
180 | }) | ||
181 | } | ||
182 | |||
183 | enableMinimumTranscoding (options: { | ||
184 | webVideo?: boolean // default true | ||
185 | hls?: boolean // default true | ||
186 | } = {}) { | ||
187 | const { webVideo = true, hls = true } = options | ||
188 | |||
189 | return this.updateExistingSubConfig({ | ||
190 | newConfig: { | ||
191 | transcoding: { | ||
192 | enabled: true, | ||
193 | |||
194 | allowAudioFiles: true, | ||
195 | allowAdditionalExtensions: true, | ||
196 | |||
197 | resolutions: { | ||
198 | ...ConfigCommand.getCustomConfigResolutions(false), | ||
199 | |||
200 | '240p': true | ||
201 | }, | ||
202 | |||
203 | webVideos: { | ||
204 | enabled: webVideo | ||
205 | }, | ||
206 | hls: { | ||
207 | enabled: hls | ||
208 | } | ||
209 | } | ||
210 | } | ||
211 | }) | ||
212 | } | ||
213 | |||
214 | enableRemoteTranscoding () { | ||
215 | return this.updateExistingSubConfig({ | ||
216 | newConfig: { | ||
217 | transcoding: { | ||
218 | remoteRunners: { | ||
219 | enabled: true | ||
220 | } | ||
221 | }, | ||
222 | live: { | ||
223 | transcoding: { | ||
224 | remoteRunners: { | ||
225 | enabled: true | ||
226 | } | ||
227 | } | ||
228 | } | ||
229 | } | ||
230 | }) | ||
231 | } | ||
232 | |||
233 | enableRemoteStudio () { | ||
234 | return this.updateExistingSubConfig({ | ||
235 | newConfig: { | ||
236 | videoStudio: { | ||
237 | remoteRunners: { | ||
238 | enabled: true | ||
239 | } | ||
240 | } | ||
241 | } | ||
242 | }) | ||
243 | } | ||
244 | |||
245 | // --------------------------------------------------------------------------- | ||
246 | |||
247 | enableStudio () { | ||
248 | return this.updateExistingSubConfig({ | ||
249 | newConfig: { | ||
250 | videoStudio: { | ||
251 | enabled: true | ||
252 | } | ||
253 | } | ||
254 | }) | ||
255 | } | ||
256 | |||
257 | // --------------------------------------------------------------------------- | ||
258 | |||
259 | getConfig (options: OverrideCommandOptions = {}) { | ||
260 | const path = '/api/v1/config' | ||
261 | |||
262 | return this.getRequestBody<ServerConfig>({ | ||
263 | ...options, | ||
264 | |||
265 | path, | ||
266 | implicitToken: false, | ||
267 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
268 | }) | ||
269 | } | ||
270 | |||
271 | async getIndexHTMLConfig (options: OverrideCommandOptions = {}) { | ||
272 | const text = await this.getRequestText({ | ||
273 | ...options, | ||
274 | |||
275 | path: '/', | ||
276 | implicitToken: false, | ||
277 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
278 | }) | ||
279 | |||
280 | const match = text.match('<script type="application/javascript">window.PeerTubeServerConfig = (".+?")</script>') | ||
281 | |||
282 | // We parse the string twice, first to extract the string and then to extract the JSON | ||
283 | return JSON.parse(JSON.parse(match[1])) as ServerConfig | ||
284 | } | ||
285 | |||
286 | getAbout (options: OverrideCommandOptions = {}) { | ||
287 | const path = '/api/v1/config/about' | ||
288 | |||
289 | return this.getRequestBody<About>({ | ||
290 | ...options, | ||
291 | |||
292 | path, | ||
293 | implicitToken: false, | ||
294 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
295 | }) | ||
296 | } | ||
297 | |||
298 | getCustomConfig (options: OverrideCommandOptions = {}) { | ||
299 | const path = '/api/v1/config/custom' | ||
300 | |||
301 | return this.getRequestBody<CustomConfig>({ | ||
302 | ...options, | ||
303 | |||
304 | path, | ||
305 | implicitToken: true, | ||
306 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
307 | }) | ||
308 | } | ||
309 | |||
310 | updateCustomConfig (options: OverrideCommandOptions & { | ||
311 | newCustomConfig: CustomConfig | ||
312 | }) { | ||
313 | const path = '/api/v1/config/custom' | ||
314 | |||
315 | return this.putBodyRequest({ | ||
316 | ...options, | ||
317 | |||
318 | path, | ||
319 | fields: options.newCustomConfig, | ||
320 | implicitToken: true, | ||
321 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
322 | }) | ||
323 | } | ||
324 | |||
325 | deleteCustomConfig (options: OverrideCommandOptions = {}) { | ||
326 | const path = '/api/v1/config/custom' | ||
327 | |||
328 | return this.deleteRequest({ | ||
329 | ...options, | ||
330 | |||
331 | path, | ||
332 | implicitToken: true, | ||
333 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
334 | }) | ||
335 | } | ||
336 | |||
337 | async updateExistingSubConfig (options: OverrideCommandOptions & { | ||
338 | newConfig: DeepPartial<CustomConfig> | ||
339 | }) { | ||
340 | const existing = await this.getCustomConfig({ ...options, expectedStatus: HttpStatusCode.OK_200 }) | ||
341 | |||
342 | return this.updateCustomConfig({ ...options, newCustomConfig: merge({}, existing, options.newConfig) }) | ||
343 | } | ||
344 | |||
345 | updateCustomSubConfig (options: OverrideCommandOptions & { | ||
346 | newConfig: DeepPartial<CustomConfig> | ||
347 | }) { | ||
348 | const newCustomConfig: CustomConfig = { | ||
349 | instance: { | ||
350 | name: 'PeerTube updated', | ||
351 | shortDescription: 'my short description', | ||
352 | description: 'my super description', | ||
353 | terms: 'my super terms', | ||
354 | codeOfConduct: 'my super coc', | ||
355 | |||
356 | creationReason: 'my super creation reason', | ||
357 | moderationInformation: 'my super moderation information', | ||
358 | administrator: 'Kuja', | ||
359 | maintenanceLifetime: 'forever', | ||
360 | businessModel: 'my super business model', | ||
361 | hardwareInformation: '2vCore 3GB RAM', | ||
362 | |||
363 | languages: [ 'en', 'es' ], | ||
364 | categories: [ 1, 2 ], | ||
365 | |||
366 | isNSFW: true, | ||
367 | defaultNSFWPolicy: 'blur', | ||
368 | |||
369 | defaultClientRoute: '/videos/recently-added', | ||
370 | |||
371 | customizations: { | ||
372 | javascript: 'alert("coucou")', | ||
373 | css: 'body { background-color: red; }' | ||
374 | } | ||
375 | }, | ||
376 | theme: { | ||
377 | default: 'default' | ||
378 | }, | ||
379 | services: { | ||
380 | twitter: { | ||
381 | username: '@MySuperUsername', | ||
382 | whitelisted: true | ||
383 | } | ||
384 | }, | ||
385 | client: { | ||
386 | videos: { | ||
387 | miniature: { | ||
388 | preferAuthorDisplayName: false | ||
389 | } | ||
390 | }, | ||
391 | menu: { | ||
392 | login: { | ||
393 | redirectOnSingleExternalAuth: false | ||
394 | } | ||
395 | } | ||
396 | }, | ||
397 | cache: { | ||
398 | previews: { | ||
399 | size: 2 | ||
400 | }, | ||
401 | captions: { | ||
402 | size: 3 | ||
403 | }, | ||
404 | torrents: { | ||
405 | size: 4 | ||
406 | }, | ||
407 | storyboards: { | ||
408 | size: 5 | ||
409 | } | ||
410 | }, | ||
411 | signup: { | ||
412 | enabled: false, | ||
413 | limit: 5, | ||
414 | requiresApproval: true, | ||
415 | requiresEmailVerification: false, | ||
416 | minimumAge: 16 | ||
417 | }, | ||
418 | admin: { | ||
419 | email: 'superadmin1@example.com' | ||
420 | }, | ||
421 | contactForm: { | ||
422 | enabled: true | ||
423 | }, | ||
424 | user: { | ||
425 | history: { | ||
426 | videos: { | ||
427 | enabled: true | ||
428 | } | ||
429 | }, | ||
430 | videoQuota: 5242881, | ||
431 | videoQuotaDaily: 318742 | ||
432 | }, | ||
433 | videoChannels: { | ||
434 | maxPerUser: 20 | ||
435 | }, | ||
436 | transcoding: { | ||
437 | enabled: true, | ||
438 | remoteRunners: { | ||
439 | enabled: false | ||
440 | }, | ||
441 | allowAdditionalExtensions: true, | ||
442 | allowAudioFiles: true, | ||
443 | threads: 1, | ||
444 | concurrency: 3, | ||
445 | profile: 'default', | ||
446 | resolutions: { | ||
447 | '0p': false, | ||
448 | '144p': false, | ||
449 | '240p': false, | ||
450 | '360p': true, | ||
451 | '480p': true, | ||
452 | '720p': false, | ||
453 | '1080p': false, | ||
454 | '1440p': false, | ||
455 | '2160p': false | ||
456 | }, | ||
457 | alwaysTranscodeOriginalResolution: true, | ||
458 | webVideos: { | ||
459 | enabled: true | ||
460 | }, | ||
461 | hls: { | ||
462 | enabled: false | ||
463 | } | ||
464 | }, | ||
465 | live: { | ||
466 | enabled: true, | ||
467 | allowReplay: false, | ||
468 | latencySetting: { | ||
469 | enabled: false | ||
470 | }, | ||
471 | maxDuration: -1, | ||
472 | maxInstanceLives: -1, | ||
473 | maxUserLives: 50, | ||
474 | transcoding: { | ||
475 | enabled: true, | ||
476 | remoteRunners: { | ||
477 | enabled: false | ||
478 | }, | ||
479 | threads: 4, | ||
480 | profile: 'default', | ||
481 | resolutions: { | ||
482 | '144p': true, | ||
483 | '240p': true, | ||
484 | '360p': true, | ||
485 | '480p': true, | ||
486 | '720p': true, | ||
487 | '1080p': true, | ||
488 | '1440p': true, | ||
489 | '2160p': true | ||
490 | }, | ||
491 | alwaysTranscodeOriginalResolution: true | ||
492 | } | ||
493 | }, | ||
494 | videoStudio: { | ||
495 | enabled: false, | ||
496 | remoteRunners: { | ||
497 | enabled: false | ||
498 | } | ||
499 | }, | ||
500 | videoFile: { | ||
501 | update: { | ||
502 | enabled: false | ||
503 | } | ||
504 | }, | ||
505 | import: { | ||
506 | videos: { | ||
507 | concurrency: 3, | ||
508 | http: { | ||
509 | enabled: false | ||
510 | }, | ||
511 | torrent: { | ||
512 | enabled: false | ||
513 | } | ||
514 | }, | ||
515 | videoChannelSynchronization: { | ||
516 | enabled: false, | ||
517 | maxPerUser: 10 | ||
518 | } | ||
519 | }, | ||
520 | trending: { | ||
521 | videos: { | ||
522 | algorithms: { | ||
523 | enabled: [ 'hot', 'most-viewed', 'most-liked' ], | ||
524 | default: 'hot' | ||
525 | } | ||
526 | } | ||
527 | }, | ||
528 | autoBlacklist: { | ||
529 | videos: { | ||
530 | ofUsers: { | ||
531 | enabled: false | ||
532 | } | ||
533 | } | ||
534 | }, | ||
535 | followers: { | ||
536 | instance: { | ||
537 | enabled: true, | ||
538 | manualApproval: false | ||
539 | } | ||
540 | }, | ||
541 | followings: { | ||
542 | instance: { | ||
543 | autoFollowBack: { | ||
544 | enabled: false | ||
545 | }, | ||
546 | autoFollowIndex: { | ||
547 | indexUrl: 'https://instances.joinpeertube.org/api/v1/instances/hosts', | ||
548 | enabled: false | ||
549 | } | ||
550 | } | ||
551 | }, | ||
552 | broadcastMessage: { | ||
553 | enabled: true, | ||
554 | level: 'warning', | ||
555 | message: 'hello', | ||
556 | dismissable: true | ||
557 | }, | ||
558 | search: { | ||
559 | remoteUri: { | ||
560 | users: true, | ||
561 | anonymous: true | ||
562 | }, | ||
563 | searchIndex: { | ||
564 | enabled: true, | ||
565 | url: 'https://search.joinpeertube.org', | ||
566 | disableLocalSearch: true, | ||
567 | isDefaultSearch: true | ||
568 | } | ||
569 | } | ||
570 | } | ||
571 | |||
572 | merge(newCustomConfig, options.newConfig) | ||
573 | |||
574 | return this.updateCustomConfig({ ...options, newCustomConfig }) | ||
575 | } | ||
576 | } | ||
diff --git a/packages/server-commands/src/server/contact-form-command.ts b/packages/server-commands/src/server/contact-form-command.ts new file mode 100644 index 000000000..399e06d2f --- /dev/null +++ b/packages/server-commands/src/server/contact-form-command.ts | |||
@@ -0,0 +1,30 @@ | |||
1 | import { ContactForm, HttpStatusCode } from '@peertube/peertube-models' | ||
2 | import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js' | ||
3 | |||
4 | export class ContactFormCommand extends AbstractCommand { | ||
5 | |||
6 | send (options: OverrideCommandOptions & { | ||
7 | fromEmail: string | ||
8 | fromName: string | ||
9 | subject: string | ||
10 | body: string | ||
11 | }) { | ||
12 | const path = '/api/v1/server/contact' | ||
13 | |||
14 | const body: ContactForm = { | ||
15 | fromEmail: options.fromEmail, | ||
16 | fromName: options.fromName, | ||
17 | subject: options.subject, | ||
18 | body: options.body | ||
19 | } | ||
20 | |||
21 | return this.postBodyRequest({ | ||
22 | ...options, | ||
23 | |||
24 | path, | ||
25 | fields: body, | ||
26 | implicitToken: false, | ||
27 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
28 | }) | ||
29 | } | ||
30 | } | ||
diff --git a/packages/server-commands/src/server/debug-command.ts b/packages/server-commands/src/server/debug-command.ts new file mode 100644 index 000000000..9bb7fda10 --- /dev/null +++ b/packages/server-commands/src/server/debug-command.ts | |||
@@ -0,0 +1,33 @@ | |||
1 | import { Debug, HttpStatusCode, SendDebugCommand } from '@peertube/peertube-models' | ||
2 | import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js' | ||
3 | |||
4 | export class DebugCommand extends AbstractCommand { | ||
5 | |||
6 | getDebug (options: OverrideCommandOptions = {}) { | ||
7 | const path = '/api/v1/server/debug' | ||
8 | |||
9 | return this.getRequestBody<Debug>({ | ||
10 | ...options, | ||
11 | |||
12 | path, | ||
13 | implicitToken: true, | ||
14 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
15 | }) | ||
16 | } | ||
17 | |||
18 | sendCommand (options: OverrideCommandOptions & { | ||
19 | body: SendDebugCommand | ||
20 | }) { | ||
21 | const { body } = options | ||
22 | const path = '/api/v1/server/debug/run-command' | ||
23 | |||
24 | return this.postBodyRequest({ | ||
25 | ...options, | ||
26 | |||
27 | path, | ||
28 | fields: body, | ||
29 | implicitToken: true, | ||
30 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
31 | }) | ||
32 | } | ||
33 | } | ||
diff --git a/packages/server-commands/src/server/follows-command.ts b/packages/server-commands/src/server/follows-command.ts new file mode 100644 index 000000000..cdc263982 --- /dev/null +++ b/packages/server-commands/src/server/follows-command.ts | |||
@@ -0,0 +1,139 @@ | |||
1 | import { pick } from '@peertube/peertube-core-utils' | ||
2 | import { ActivityPubActorType, ActorFollow, FollowState, HttpStatusCode, ResultList, ServerFollowCreate } from '@peertube/peertube-models' | ||
3 | import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js' | ||
4 | import { PeerTubeServer } from './server.js' | ||
5 | |||
6 | export class FollowsCommand extends AbstractCommand { | ||
7 | |||
8 | getFollowers (options: OverrideCommandOptions & { | ||
9 | start?: number | ||
10 | count?: number | ||
11 | sort?: string | ||
12 | search?: string | ||
13 | actorType?: ActivityPubActorType | ||
14 | state?: FollowState | ||
15 | } = {}) { | ||
16 | const path = '/api/v1/server/followers' | ||
17 | |||
18 | const query = pick(options, [ 'start', 'count', 'sort', 'search', 'state', 'actorType' ]) | ||
19 | |||
20 | return this.getRequestBody<ResultList<ActorFollow>>({ | ||
21 | ...options, | ||
22 | |||
23 | path, | ||
24 | query, | ||
25 | implicitToken: false, | ||
26 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
27 | }) | ||
28 | } | ||
29 | |||
30 | getFollowings (options: OverrideCommandOptions & { | ||
31 | start?: number | ||
32 | count?: number | ||
33 | sort?: string | ||
34 | search?: string | ||
35 | actorType?: ActivityPubActorType | ||
36 | state?: FollowState | ||
37 | } = {}) { | ||
38 | const path = '/api/v1/server/following' | ||
39 | |||
40 | const query = pick(options, [ 'start', 'count', 'sort', 'search', 'state', 'actorType' ]) | ||
41 | |||
42 | return this.getRequestBody<ResultList<ActorFollow>>({ | ||
43 | ...options, | ||
44 | |||
45 | path, | ||
46 | query, | ||
47 | implicitToken: false, | ||
48 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
49 | }) | ||
50 | } | ||
51 | |||
52 | follow (options: OverrideCommandOptions & { | ||
53 | hosts?: string[] | ||
54 | handles?: string[] | ||
55 | }) { | ||
56 | const path = '/api/v1/server/following' | ||
57 | |||
58 | const fields: ServerFollowCreate = {} | ||
59 | |||
60 | if (options.hosts) { | ||
61 | fields.hosts = options.hosts.map(f => f.replace(/^http:\/\//, '')) | ||
62 | } | ||
63 | |||
64 | if (options.handles) { | ||
65 | fields.handles = options.handles | ||
66 | } | ||
67 | |||
68 | return this.postBodyRequest({ | ||
69 | ...options, | ||
70 | |||
71 | path, | ||
72 | fields, | ||
73 | implicitToken: true, | ||
74 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
75 | }) | ||
76 | } | ||
77 | |||
78 | async unfollow (options: OverrideCommandOptions & { | ||
79 | target: PeerTubeServer | string | ||
80 | }) { | ||
81 | const { target } = options | ||
82 | |||
83 | const handle = typeof target === 'string' | ||
84 | ? target | ||
85 | : target.host | ||
86 | |||
87 | const path = '/api/v1/server/following/' + handle | ||
88 | |||
89 | return this.deleteRequest({ | ||
90 | ...options, | ||
91 | |||
92 | path, | ||
93 | implicitToken: true, | ||
94 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
95 | }) | ||
96 | } | ||
97 | |||
98 | acceptFollower (options: OverrideCommandOptions & { | ||
99 | follower: string | ||
100 | }) { | ||
101 | const path = '/api/v1/server/followers/' + options.follower + '/accept' | ||
102 | |||
103 | return this.postBodyRequest({ | ||
104 | ...options, | ||
105 | |||
106 | path, | ||
107 | implicitToken: true, | ||
108 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
109 | }) | ||
110 | } | ||
111 | |||
112 | rejectFollower (options: OverrideCommandOptions & { | ||
113 | follower: string | ||
114 | }) { | ||
115 | const path = '/api/v1/server/followers/' + options.follower + '/reject' | ||
116 | |||
117 | return this.postBodyRequest({ | ||
118 | ...options, | ||
119 | |||
120 | path, | ||
121 | implicitToken: true, | ||
122 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
123 | }) | ||
124 | } | ||
125 | |||
126 | removeFollower (options: OverrideCommandOptions & { | ||
127 | follower: PeerTubeServer | ||
128 | }) { | ||
129 | const path = '/api/v1/server/followers/peertube@' + options.follower.host | ||
130 | |||
131 | return this.deleteRequest({ | ||
132 | ...options, | ||
133 | |||
134 | path, | ||
135 | implicitToken: true, | ||
136 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
137 | }) | ||
138 | } | ||
139 | } | ||
diff --git a/packages/server-commands/src/server/follows.ts b/packages/server-commands/src/server/follows.ts new file mode 100644 index 000000000..32304495a --- /dev/null +++ b/packages/server-commands/src/server/follows.ts | |||
@@ -0,0 +1,20 @@ | |||
1 | import { waitJobs } from './jobs.js' | ||
2 | import { PeerTubeServer } from './server.js' | ||
3 | |||
4 | async function doubleFollow (server1: PeerTubeServer, server2: PeerTubeServer) { | ||
5 | await Promise.all([ | ||
6 | server1.follows.follow({ hosts: [ server2.url ] }), | ||
7 | server2.follows.follow({ hosts: [ server1.url ] }) | ||
8 | ]) | ||
9 | |||
10 | // Wait request propagation | ||
11 | await waitJobs([ server1, server2 ]) | ||
12 | |||
13 | return true | ||
14 | } | ||
15 | |||
16 | // --------------------------------------------------------------------------- | ||
17 | |||
18 | export { | ||
19 | doubleFollow | ||
20 | } | ||
diff --git a/packages/server-commands/src/server/index.ts b/packages/server-commands/src/server/index.ts new file mode 100644 index 000000000..c13972eca --- /dev/null +++ b/packages/server-commands/src/server/index.ts | |||
@@ -0,0 +1,15 @@ | |||
1 | export * from './config-command.js' | ||
2 | export * from './contact-form-command.js' | ||
3 | export * from './debug-command.js' | ||
4 | export * from './follows-command.js' | ||
5 | export * from './follows.js' | ||
6 | export * from './jobs.js' | ||
7 | export * from './jobs-command.js' | ||
8 | export * from './metrics-command.js' | ||
9 | export * from './object-storage-command.js' | ||
10 | export * from './plugins-command.js' | ||
11 | export * from './redundancy-command.js' | ||
12 | export * from './server.js' | ||
13 | export * from './servers-command.js' | ||
14 | export * from './servers.js' | ||
15 | export * from './stats-command.js' | ||
diff --git a/packages/server-commands/src/server/jobs-command.ts b/packages/server-commands/src/server/jobs-command.ts new file mode 100644 index 000000000..18aa0cd95 --- /dev/null +++ b/packages/server-commands/src/server/jobs-command.ts | |||
@@ -0,0 +1,84 @@ | |||
1 | import { pick } from '@peertube/peertube-core-utils' | ||
2 | import { HttpStatusCode, Job, JobState, JobType, ResultList } from '@peertube/peertube-models' | ||
3 | import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js' | ||
4 | |||
5 | export class JobsCommand extends AbstractCommand { | ||
6 | |||
7 | async getLatest (options: OverrideCommandOptions & { | ||
8 | jobType: JobType | ||
9 | }) { | ||
10 | const { data } = await this.list({ ...options, start: 0, count: 1, sort: '-createdAt' }) | ||
11 | |||
12 | if (data.length === 0) return undefined | ||
13 | |||
14 | return data[0] | ||
15 | } | ||
16 | |||
17 | pauseJobQueue (options: OverrideCommandOptions = {}) { | ||
18 | const path = '/api/v1/jobs/pause' | ||
19 | |||
20 | return this.postBodyRequest({ | ||
21 | ...options, | ||
22 | |||
23 | path, | ||
24 | implicitToken: true, | ||
25 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
26 | }) | ||
27 | } | ||
28 | |||
29 | resumeJobQueue (options: OverrideCommandOptions = {}) { | ||
30 | const path = '/api/v1/jobs/resume' | ||
31 | |||
32 | return this.postBodyRequest({ | ||
33 | ...options, | ||
34 | |||
35 | path, | ||
36 | implicitToken: true, | ||
37 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
38 | }) | ||
39 | } | ||
40 | |||
41 | list (options: OverrideCommandOptions & { | ||
42 | state?: JobState | ||
43 | jobType?: JobType | ||
44 | start?: number | ||
45 | count?: number | ||
46 | sort?: string | ||
47 | } = {}) { | ||
48 | const path = this.buildJobsUrl(options.state) | ||
49 | |||
50 | const query = pick(options, [ 'start', 'count', 'sort', 'jobType' ]) | ||
51 | |||
52 | return this.getRequestBody<ResultList<Job>>({ | ||
53 | ...options, | ||
54 | |||
55 | path, | ||
56 | query, | ||
57 | implicitToken: true, | ||
58 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
59 | }) | ||
60 | } | ||
61 | |||
62 | listFailed (options: OverrideCommandOptions & { | ||
63 | jobType?: JobType | ||
64 | }) { | ||
65 | const path = this.buildJobsUrl('failed') | ||
66 | |||
67 | return this.getRequestBody<ResultList<Job>>({ | ||
68 | ...options, | ||
69 | |||
70 | path, | ||
71 | query: { start: 0, count: 50 }, | ||
72 | implicitToken: true, | ||
73 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
74 | }) | ||
75 | } | ||
76 | |||
77 | private buildJobsUrl (state?: JobState) { | ||
78 | let path = '/api/v1/jobs' | ||
79 | |||
80 | if (state) path += '/' + state | ||
81 | |||
82 | return path | ||
83 | } | ||
84 | } | ||
diff --git a/packages/server-commands/src/server/jobs.ts b/packages/server-commands/src/server/jobs.ts new file mode 100644 index 000000000..1f3b1f745 --- /dev/null +++ b/packages/server-commands/src/server/jobs.ts | |||
@@ -0,0 +1,117 @@ | |||
1 | import { expect } from 'chai' | ||
2 | import { wait } from '@peertube/peertube-core-utils' | ||
3 | import { JobState, JobType, RunnerJobState } from '@peertube/peertube-models' | ||
4 | import { PeerTubeServer } from './server.js' | ||
5 | |||
6 | async function waitJobs ( | ||
7 | serversArg: PeerTubeServer[] | PeerTubeServer, | ||
8 | options: { | ||
9 | skipDelayed?: boolean // default false | ||
10 | runnerJobs?: boolean // default false | ||
11 | } = {} | ||
12 | ) { | ||
13 | const { skipDelayed = false, runnerJobs = false } = options | ||
14 | |||
15 | const pendingJobWait = process.env.NODE_PENDING_JOB_WAIT | ||
16 | ? parseInt(process.env.NODE_PENDING_JOB_WAIT, 10) | ||
17 | : 250 | ||
18 | |||
19 | let servers: PeerTubeServer[] | ||
20 | |||
21 | if (Array.isArray(serversArg) === false) servers = [ serversArg as PeerTubeServer ] | ||
22 | else servers = serversArg as PeerTubeServer[] | ||
23 | |||
24 | const states: JobState[] = [ 'waiting', 'active' ] | ||
25 | if (!skipDelayed) states.push('delayed') | ||
26 | |||
27 | const repeatableJobs: JobType[] = [ 'videos-views-stats', 'activitypub-cleaner' ] | ||
28 | let pendingRequests: boolean | ||
29 | |||
30 | function tasksBuilder () { | ||
31 | const tasks: Promise<any>[] = [] | ||
32 | |||
33 | // Check if each server has pending request | ||
34 | for (const server of servers) { | ||
35 | if (process.env.DEBUG) console.log('Checking ' + server.url) | ||
36 | |||
37 | for (const state of states) { | ||
38 | |||
39 | const jobPromise = server.jobs.list({ | ||
40 | state, | ||
41 | start: 0, | ||
42 | count: 10, | ||
43 | sort: '-createdAt' | ||
44 | }).then(body => body.data) | ||
45 | .then(jobs => jobs.filter(j => !repeatableJobs.includes(j.type))) | ||
46 | .then(jobs => { | ||
47 | if (jobs.length !== 0) { | ||
48 | pendingRequests = true | ||
49 | |||
50 | if (process.env.DEBUG) { | ||
51 | console.log(jobs) | ||
52 | } | ||
53 | } | ||
54 | }) | ||
55 | |||
56 | tasks.push(jobPromise) | ||
57 | } | ||
58 | |||
59 | const debugPromise = server.debug.getDebug() | ||
60 | .then(obj => { | ||
61 | if (obj.activityPubMessagesWaiting !== 0) { | ||
62 | pendingRequests = true | ||
63 | |||
64 | if (process.env.DEBUG) { | ||
65 | console.log('AP messages waiting: ' + obj.activityPubMessagesWaiting) | ||
66 | } | ||
67 | } | ||
68 | }) | ||
69 | tasks.push(debugPromise) | ||
70 | |||
71 | if (runnerJobs) { | ||
72 | const runnerJobsPromise = server.runnerJobs.list({ count: 100 }) | ||
73 | .then(({ data }) => { | ||
74 | for (const job of data) { | ||
75 | if (job.state.id !== RunnerJobState.COMPLETED) { | ||
76 | pendingRequests = true | ||
77 | |||
78 | if (process.env.DEBUG) { | ||
79 | console.log(job) | ||
80 | } | ||
81 | } | ||
82 | } | ||
83 | }) | ||
84 | tasks.push(runnerJobsPromise) | ||
85 | } | ||
86 | } | ||
87 | |||
88 | return tasks | ||
89 | } | ||
90 | |||
91 | do { | ||
92 | pendingRequests = false | ||
93 | await Promise.all(tasksBuilder()) | ||
94 | |||
95 | // Retry, in case of new jobs were created | ||
96 | if (pendingRequests === false) { | ||
97 | await wait(pendingJobWait) | ||
98 | await Promise.all(tasksBuilder()) | ||
99 | } | ||
100 | |||
101 | if (pendingRequests) { | ||
102 | await wait(pendingJobWait) | ||
103 | } | ||
104 | } while (pendingRequests) | ||
105 | } | ||
106 | |||
107 | async function expectNoFailedTranscodingJob (server: PeerTubeServer) { | ||
108 | const { data } = await server.jobs.listFailed({ jobType: 'video-transcoding' }) | ||
109 | expect(data).to.have.lengthOf(0) | ||
110 | } | ||
111 | |||
112 | // --------------------------------------------------------------------------- | ||
113 | |||
114 | export { | ||
115 | waitJobs, | ||
116 | expectNoFailedTranscodingJob | ||
117 | } | ||
diff --git a/packages/server-commands/src/server/metrics-command.ts b/packages/server-commands/src/server/metrics-command.ts new file mode 100644 index 000000000..1f969a024 --- /dev/null +++ b/packages/server-commands/src/server/metrics-command.ts | |||
@@ -0,0 +1,18 @@ | |||
1 | import { HttpStatusCode, PlaybackMetricCreate } from '@peertube/peertube-models' | ||
2 | import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js' | ||
3 | |||
4 | export class MetricsCommand extends AbstractCommand { | ||
5 | |||
6 | addPlaybackMetric (options: OverrideCommandOptions & { metrics: PlaybackMetricCreate }) { | ||
7 | const path = '/api/v1/metrics/playback' | ||
8 | |||
9 | return this.postBodyRequest({ | ||
10 | ...options, | ||
11 | |||
12 | path, | ||
13 | fields: options.metrics, | ||
14 | implicitToken: false, | ||
15 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
16 | }) | ||
17 | } | ||
18 | } | ||
diff --git a/packages/server-commands/src/server/object-storage-command.ts b/packages/server-commands/src/server/object-storage-command.ts new file mode 100644 index 000000000..ff8d5d75c --- /dev/null +++ b/packages/server-commands/src/server/object-storage-command.ts | |||
@@ -0,0 +1,165 @@ | |||
1 | import { randomInt } from 'crypto' | ||
2 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
3 | import { makePostBodyRequest } from '../requests/index.js' | ||
4 | |||
5 | export class ObjectStorageCommand { | ||
6 | static readonly DEFAULT_SCALEWAY_BUCKET = 'peertube-ci-test' | ||
7 | |||
8 | private readonly bucketsCreated: string[] = [] | ||
9 | private readonly seed: number | ||
10 | |||
11 | // --------------------------------------------------------------------------- | ||
12 | |||
13 | constructor () { | ||
14 | this.seed = randomInt(0, 10000) | ||
15 | } | ||
16 | |||
17 | static getMockCredentialsConfig () { | ||
18 | return { | ||
19 | access_key_id: 'AKIAIOSFODNN7EXAMPLE', | ||
20 | secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' | ||
21 | } | ||
22 | } | ||
23 | |||
24 | static getMockEndpointHost () { | ||
25 | return 'localhost:9444' | ||
26 | } | ||
27 | |||
28 | static getMockRegion () { | ||
29 | return 'us-east-1' | ||
30 | } | ||
31 | |||
32 | getDefaultMockConfig () { | ||
33 | return { | ||
34 | object_storage: { | ||
35 | enabled: true, | ||
36 | endpoint: 'http://' + ObjectStorageCommand.getMockEndpointHost(), | ||
37 | region: ObjectStorageCommand.getMockRegion(), | ||
38 | |||
39 | credentials: ObjectStorageCommand.getMockCredentialsConfig(), | ||
40 | |||
41 | streaming_playlists: { | ||
42 | bucket_name: this.getMockStreamingPlaylistsBucketName() | ||
43 | }, | ||
44 | |||
45 | web_videos: { | ||
46 | bucket_name: this.getMockWebVideosBucketName() | ||
47 | } | ||
48 | } | ||
49 | } | ||
50 | } | ||
51 | |||
52 | getMockWebVideosBaseUrl () { | ||
53 | return `http://${this.getMockWebVideosBucketName()}.${ObjectStorageCommand.getMockEndpointHost()}/` | ||
54 | } | ||
55 | |||
56 | getMockPlaylistBaseUrl () { | ||
57 | return `http://${this.getMockStreamingPlaylistsBucketName()}.${ObjectStorageCommand.getMockEndpointHost()}/` | ||
58 | } | ||
59 | |||
60 | async prepareDefaultMockBuckets () { | ||
61 | await this.createMockBucket(this.getMockStreamingPlaylistsBucketName()) | ||
62 | await this.createMockBucket(this.getMockWebVideosBucketName()) | ||
63 | } | ||
64 | |||
65 | async createMockBucket (name: string) { | ||
66 | this.bucketsCreated.push(name) | ||
67 | |||
68 | await this.deleteMockBucket(name) | ||
69 | |||
70 | await makePostBodyRequest({ | ||
71 | url: ObjectStorageCommand.getMockEndpointHost(), | ||
72 | path: '/ui/' + name + '?create', | ||
73 | expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307 | ||
74 | }) | ||
75 | |||
76 | await makePostBodyRequest({ | ||
77 | url: ObjectStorageCommand.getMockEndpointHost(), | ||
78 | path: '/ui/' + name + '?make-public', | ||
79 | expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307 | ||
80 | }) | ||
81 | } | ||
82 | |||
83 | async cleanupMock () { | ||
84 | for (const name of this.bucketsCreated) { | ||
85 | await this.deleteMockBucket(name) | ||
86 | } | ||
87 | } | ||
88 | |||
89 | getMockStreamingPlaylistsBucketName (name = 'streaming-playlists') { | ||
90 | return this.getMockBucketName(name) | ||
91 | } | ||
92 | |||
93 | getMockWebVideosBucketName (name = 'web-videos') { | ||
94 | return this.getMockBucketName(name) | ||
95 | } | ||
96 | |||
97 | getMockBucketName (name: string) { | ||
98 | return `${this.seed}-${name}` | ||
99 | } | ||
100 | |||
101 | private async deleteMockBucket (name: string) { | ||
102 | await makePostBodyRequest({ | ||
103 | url: ObjectStorageCommand.getMockEndpointHost(), | ||
104 | path: '/ui/' + name + '?delete', | ||
105 | expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307 | ||
106 | }) | ||
107 | } | ||
108 | |||
109 | // --------------------------------------------------------------------------- | ||
110 | |||
111 | static getDefaultScalewayConfig (options: { | ||
112 | serverNumber: number | ||
113 | enablePrivateProxy?: boolean // default true | ||
114 | privateACL?: 'private' | 'public-read' // default 'private' | ||
115 | }) { | ||
116 | const { serverNumber, enablePrivateProxy = true, privateACL = 'private' } = options | ||
117 | |||
118 | return { | ||
119 | object_storage: { | ||
120 | enabled: true, | ||
121 | endpoint: this.getScalewayEndpointHost(), | ||
122 | region: this.getScalewayRegion(), | ||
123 | |||
124 | credentials: this.getScalewayCredentialsConfig(), | ||
125 | |||
126 | upload_acl: { | ||
127 | private: privateACL | ||
128 | }, | ||
129 | |||
130 | proxy: { | ||
131 | proxify_private_files: enablePrivateProxy | ||
132 | }, | ||
133 | |||
134 | streaming_playlists: { | ||
135 | bucket_name: this.DEFAULT_SCALEWAY_BUCKET, | ||
136 | prefix: `test:server-${serverNumber}-streaming-playlists:` | ||
137 | }, | ||
138 | |||
139 | web_videos: { | ||
140 | bucket_name: this.DEFAULT_SCALEWAY_BUCKET, | ||
141 | prefix: `test:server-${serverNumber}-web-videos:` | ||
142 | } | ||
143 | } | ||
144 | } | ||
145 | } | ||
146 | |||
147 | static getScalewayCredentialsConfig () { | ||
148 | return { | ||
149 | access_key_id: process.env.OBJECT_STORAGE_SCALEWAY_KEY_ID, | ||
150 | secret_access_key: process.env.OBJECT_STORAGE_SCALEWAY_ACCESS_KEY | ||
151 | } | ||
152 | } | ||
153 | |||
154 | static getScalewayEndpointHost () { | ||
155 | return 's3.fr-par.scw.cloud' | ||
156 | } | ||
157 | |||
158 | static getScalewayRegion () { | ||
159 | return 'fr-par' | ||
160 | } | ||
161 | |||
162 | static getScalewayBaseUrl () { | ||
163 | return `https://${this.DEFAULT_SCALEWAY_BUCKET}.${this.getScalewayEndpointHost()}/` | ||
164 | } | ||
165 | } | ||
diff --git a/packages/server-commands/src/server/plugins-command.ts b/packages/server-commands/src/server/plugins-command.ts new file mode 100644 index 000000000..f85ef0330 --- /dev/null +++ b/packages/server-commands/src/server/plugins-command.ts | |||
@@ -0,0 +1,258 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { readJSON, writeJSON } from 'fs-extra/esm' | ||
4 | import { join } from 'path' | ||
5 | import { | ||
6 | HttpStatusCode, | ||
7 | HttpStatusCodeType, | ||
8 | PeerTubePlugin, | ||
9 | PeerTubePluginIndex, | ||
10 | PeertubePluginIndexList, | ||
11 | PluginPackageJSON, | ||
12 | PluginTranslation, | ||
13 | PluginType_Type, | ||
14 | PublicServerSetting, | ||
15 | RegisteredServerSettings, | ||
16 | ResultList | ||
17 | } from '@peertube/peertube-models' | ||
18 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
19 | import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js' | ||
20 | |||
21 | export class PluginsCommand extends AbstractCommand { | ||
22 | |||
23 | static getPluginTestPath (suffix = '') { | ||
24 | return buildAbsoluteFixturePath('peertube-plugin-test' + suffix) | ||
25 | } | ||
26 | |||
27 | list (options: OverrideCommandOptions & { | ||
28 | start?: number | ||
29 | count?: number | ||
30 | sort?: string | ||
31 | pluginType?: PluginType_Type | ||
32 | uninstalled?: boolean | ||
33 | }) { | ||
34 | const { start, count, sort, pluginType, uninstalled } = options | ||
35 | const path = '/api/v1/plugins' | ||
36 | |||
37 | return this.getRequestBody<ResultList<PeerTubePlugin>>({ | ||
38 | ...options, | ||
39 | |||
40 | path, | ||
41 | query: { | ||
42 | start, | ||
43 | count, | ||
44 | sort, | ||
45 | pluginType, | ||
46 | uninstalled | ||
47 | }, | ||
48 | implicitToken: true, | ||
49 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
50 | }) | ||
51 | } | ||
52 | |||
53 | listAvailable (options: OverrideCommandOptions & { | ||
54 | start?: number | ||
55 | count?: number | ||
56 | sort?: string | ||
57 | pluginType?: PluginType_Type | ||
58 | currentPeerTubeEngine?: string | ||
59 | search?: string | ||
60 | expectedStatus?: HttpStatusCodeType | ||
61 | }) { | ||
62 | const { start, count, sort, pluginType, search, currentPeerTubeEngine } = options | ||
63 | const path = '/api/v1/plugins/available' | ||
64 | |||
65 | const query: PeertubePluginIndexList = { | ||
66 | start, | ||
67 | count, | ||
68 | sort, | ||
69 | pluginType, | ||
70 | currentPeerTubeEngine, | ||
71 | search | ||
72 | } | ||
73 | |||
74 | return this.getRequestBody<ResultList<PeerTubePluginIndex>>({ | ||
75 | ...options, | ||
76 | |||
77 | path, | ||
78 | query, | ||
79 | implicitToken: true, | ||
80 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
81 | }) | ||
82 | } | ||
83 | |||
84 | get (options: OverrideCommandOptions & { | ||
85 | npmName: string | ||
86 | }) { | ||
87 | const path = '/api/v1/plugins/' + options.npmName | ||
88 | |||
89 | return this.getRequestBody<PeerTubePlugin>({ | ||
90 | ...options, | ||
91 | |||
92 | path, | ||
93 | implicitToken: true, | ||
94 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
95 | }) | ||
96 | } | ||
97 | |||
98 | updateSettings (options: OverrideCommandOptions & { | ||
99 | npmName: string | ||
100 | settings: any | ||
101 | }) { | ||
102 | const { npmName, settings } = options | ||
103 | const path = '/api/v1/plugins/' + npmName + '/settings' | ||
104 | |||
105 | return this.putBodyRequest({ | ||
106 | ...options, | ||
107 | |||
108 | path, | ||
109 | fields: { settings }, | ||
110 | implicitToken: true, | ||
111 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
112 | }) | ||
113 | } | ||
114 | |||
115 | getRegisteredSettings (options: OverrideCommandOptions & { | ||
116 | npmName: string | ||
117 | }) { | ||
118 | const path = '/api/v1/plugins/' + options.npmName + '/registered-settings' | ||
119 | |||
120 | return this.getRequestBody<RegisteredServerSettings>({ | ||
121 | ...options, | ||
122 | |||
123 | path, | ||
124 | implicitToken: true, | ||
125 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
126 | }) | ||
127 | } | ||
128 | |||
129 | getPublicSettings (options: OverrideCommandOptions & { | ||
130 | npmName: string | ||
131 | }) { | ||
132 | const { npmName } = options | ||
133 | const path = '/api/v1/plugins/' + npmName + '/public-settings' | ||
134 | |||
135 | return this.getRequestBody<PublicServerSetting>({ | ||
136 | ...options, | ||
137 | |||
138 | path, | ||
139 | implicitToken: false, | ||
140 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
141 | }) | ||
142 | } | ||
143 | |||
144 | getTranslations (options: OverrideCommandOptions & { | ||
145 | locale: string | ||
146 | }) { | ||
147 | const { locale } = options | ||
148 | const path = '/plugins/translations/' + locale + '.json' | ||
149 | |||
150 | return this.getRequestBody<PluginTranslation>({ | ||
151 | ...options, | ||
152 | |||
153 | path, | ||
154 | implicitToken: false, | ||
155 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
156 | }) | ||
157 | } | ||
158 | |||
159 | install (options: OverrideCommandOptions & { | ||
160 | path?: string | ||
161 | npmName?: string | ||
162 | pluginVersion?: string | ||
163 | }) { | ||
164 | const { npmName, path, pluginVersion } = options | ||
165 | const apiPath = '/api/v1/plugins/install' | ||
166 | |||
167 | return this.postBodyRequest({ | ||
168 | ...options, | ||
169 | |||
170 | path: apiPath, | ||
171 | fields: { npmName, path, pluginVersion }, | ||
172 | implicitToken: true, | ||
173 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
174 | }) | ||
175 | } | ||
176 | |||
177 | update (options: OverrideCommandOptions & { | ||
178 | path?: string | ||
179 | npmName?: string | ||
180 | }) { | ||
181 | const { npmName, path } = options | ||
182 | const apiPath = '/api/v1/plugins/update' | ||
183 | |||
184 | return this.postBodyRequest({ | ||
185 | ...options, | ||
186 | |||
187 | path: apiPath, | ||
188 | fields: { npmName, path }, | ||
189 | implicitToken: true, | ||
190 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
191 | }) | ||
192 | } | ||
193 | |||
194 | uninstall (options: OverrideCommandOptions & { | ||
195 | npmName: string | ||
196 | }) { | ||
197 | const { npmName } = options | ||
198 | const apiPath = '/api/v1/plugins/uninstall' | ||
199 | |||
200 | return this.postBodyRequest({ | ||
201 | ...options, | ||
202 | |||
203 | path: apiPath, | ||
204 | fields: { npmName }, | ||
205 | implicitToken: true, | ||
206 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
207 | }) | ||
208 | } | ||
209 | |||
210 | getCSS (options: OverrideCommandOptions = {}) { | ||
211 | const path = '/plugins/global.css' | ||
212 | |||
213 | return this.getRequestText({ | ||
214 | ...options, | ||
215 | |||
216 | path, | ||
217 | implicitToken: false, | ||
218 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
219 | }) | ||
220 | } | ||
221 | |||
222 | getExternalAuth (options: OverrideCommandOptions & { | ||
223 | npmName: string | ||
224 | npmVersion: string | ||
225 | authName: string | ||
226 | query?: any | ||
227 | }) { | ||
228 | const { npmName, npmVersion, authName, query } = options | ||
229 | |||
230 | const path = '/plugins/' + npmName + '/' + npmVersion + '/auth/' + authName | ||
231 | |||
232 | return this.getRequest({ | ||
233 | ...options, | ||
234 | |||
235 | path, | ||
236 | query, | ||
237 | implicitToken: false, | ||
238 | defaultExpectedStatus: HttpStatusCode.OK_200, | ||
239 | redirects: 0 | ||
240 | }) | ||
241 | } | ||
242 | |||
243 | updatePackageJSON (npmName: string, json: any) { | ||
244 | const path = this.getPackageJSONPath(npmName) | ||
245 | |||
246 | return writeJSON(path, json) | ||
247 | } | ||
248 | |||
249 | getPackageJSON (npmName: string): Promise<PluginPackageJSON> { | ||
250 | const path = this.getPackageJSONPath(npmName) | ||
251 | |||
252 | return readJSON(path) | ||
253 | } | ||
254 | |||
255 | private getPackageJSONPath (npmName: string) { | ||
256 | return this.server.servers.buildDirectory(join('plugins', 'node_modules', npmName, 'package.json')) | ||
257 | } | ||
258 | } | ||
diff --git a/packages/server-commands/src/server/redundancy-command.ts b/packages/server-commands/src/server/redundancy-command.ts new file mode 100644 index 000000000..a0ec3e80e --- /dev/null +++ b/packages/server-commands/src/server/redundancy-command.ts | |||
@@ -0,0 +1,80 @@ | |||
1 | import { HttpStatusCode, ResultList, VideoRedundanciesTarget, VideoRedundancy } from '@peertube/peertube-models' | ||
2 | import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js' | ||
3 | |||
4 | export class RedundancyCommand extends AbstractCommand { | ||
5 | |||
6 | updateRedundancy (options: OverrideCommandOptions & { | ||
7 | host: string | ||
8 | redundancyAllowed: boolean | ||
9 | }) { | ||
10 | const { host, redundancyAllowed } = options | ||
11 | const path = '/api/v1/server/redundancy/' + host | ||
12 | |||
13 | return this.putBodyRequest({ | ||
14 | ...options, | ||
15 | |||
16 | path, | ||
17 | fields: { redundancyAllowed }, | ||
18 | implicitToken: true, | ||
19 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
20 | }) | ||
21 | } | ||
22 | |||
23 | listVideos (options: OverrideCommandOptions & { | ||
24 | target: VideoRedundanciesTarget | ||
25 | start?: number | ||
26 | count?: number | ||
27 | sort?: string | ||
28 | }) { | ||
29 | const path = '/api/v1/server/redundancy/videos' | ||
30 | |||
31 | const { target, start, count, sort } = options | ||
32 | |||
33 | return this.getRequestBody<ResultList<VideoRedundancy>>({ | ||
34 | ...options, | ||
35 | |||
36 | path, | ||
37 | |||
38 | query: { | ||
39 | start: start ?? 0, | ||
40 | count: count ?? 5, | ||
41 | sort: sort ?? 'name', | ||
42 | target | ||
43 | }, | ||
44 | |||
45 | implicitToken: true, | ||
46 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
47 | }) | ||
48 | } | ||
49 | |||
50 | addVideo (options: OverrideCommandOptions & { | ||
51 | videoId: number | ||
52 | }) { | ||
53 | const path = '/api/v1/server/redundancy/videos' | ||
54 | const { videoId } = options | ||
55 | |||
56 | return this.postBodyRequest({ | ||
57 | ...options, | ||
58 | |||
59 | path, | ||
60 | fields: { videoId }, | ||
61 | implicitToken: true, | ||
62 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
63 | }) | ||
64 | } | ||
65 | |||
66 | removeVideo (options: OverrideCommandOptions & { | ||
67 | redundancyId: number | ||
68 | }) { | ||
69 | const { redundancyId } = options | ||
70 | const path = '/api/v1/server/redundancy/videos/' + redundancyId | ||
71 | |||
72 | return this.deleteRequest({ | ||
73 | ...options, | ||
74 | |||
75 | path, | ||
76 | implicitToken: true, | ||
77 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
78 | }) | ||
79 | } | ||
80 | } | ||
diff --git a/packages/server-commands/src/server/server.ts b/packages/server-commands/src/server/server.ts new file mode 100644 index 000000000..57a897c17 --- /dev/null +++ b/packages/server-commands/src/server/server.ts | |||
@@ -0,0 +1,451 @@ | |||
1 | import { ChildProcess, fork } from 'child_process' | ||
2 | import { copy } from 'fs-extra/esm' | ||
3 | import { join } from 'path' | ||
4 | import { randomInt } from '@peertube/peertube-core-utils' | ||
5 | import { Video, VideoChannel, VideoChannelSync, VideoCreateResult, VideoDetails } from '@peertube/peertube-models' | ||
6 | import { parallelTests, root } from '@peertube/peertube-node-utils' | ||
7 | import { BulkCommand } from '../bulk/index.js' | ||
8 | import { CLICommand } from '../cli/index.js' | ||
9 | import { CustomPagesCommand } from '../custom-pages/index.js' | ||
10 | import { FeedCommand } from '../feeds/index.js' | ||
11 | import { LogsCommand } from '../logs/index.js' | ||
12 | import { AbusesCommand } from '../moderation/index.js' | ||
13 | import { OverviewsCommand } from '../overviews/index.js' | ||
14 | import { RunnerJobsCommand, RunnerRegistrationTokensCommand, RunnersCommand } from '../runners/index.js' | ||
15 | import { SearchCommand } from '../search/index.js' | ||
16 | import { SocketIOCommand } from '../socket/index.js' | ||
17 | import { | ||
18 | AccountsCommand, | ||
19 | BlocklistCommand, | ||
20 | LoginCommand, | ||
21 | NotificationsCommand, | ||
22 | RegistrationsCommand, | ||
23 | SubscriptionsCommand, | ||
24 | TwoFactorCommand, | ||
25 | UsersCommand | ||
26 | } from '../users/index.js' | ||
27 | import { | ||
28 | BlacklistCommand, | ||
29 | CaptionsCommand, | ||
30 | ChangeOwnershipCommand, | ||
31 | ChannelsCommand, | ||
32 | ChannelSyncsCommand, | ||
33 | CommentsCommand, | ||
34 | HistoryCommand, | ||
35 | ImportsCommand, | ||
36 | LiveCommand, | ||
37 | PlaylistsCommand, | ||
38 | ServicesCommand, | ||
39 | StoryboardCommand, | ||
40 | StreamingPlaylistsCommand, | ||
41 | VideoPasswordsCommand, | ||
42 | VideosCommand, | ||
43 | VideoStatsCommand, | ||
44 | VideoStudioCommand, | ||
45 | VideoTokenCommand, | ||
46 | ViewsCommand | ||
47 | } from '../videos/index.js' | ||
48 | import { ConfigCommand } from './config-command.js' | ||
49 | import { ContactFormCommand } from './contact-form-command.js' | ||
50 | import { DebugCommand } from './debug-command.js' | ||
51 | import { FollowsCommand } from './follows-command.js' | ||
52 | import { JobsCommand } from './jobs-command.js' | ||
53 | import { MetricsCommand } from './metrics-command.js' | ||
54 | import { PluginsCommand } from './plugins-command.js' | ||
55 | import { RedundancyCommand } from './redundancy-command.js' | ||
56 | import { ServersCommand } from './servers-command.js' | ||
57 | import { StatsCommand } from './stats-command.js' | ||
58 | |||
59 | export type RunServerOptions = { | ||
60 | hideLogs?: boolean | ||
61 | nodeArgs?: string[] | ||
62 | peertubeArgs?: string[] | ||
63 | env?: { [ id: string ]: string } | ||
64 | } | ||
65 | |||
66 | export class PeerTubeServer { | ||
67 | app?: ChildProcess | ||
68 | |||
69 | url: string | ||
70 | host?: string | ||
71 | hostname?: string | ||
72 | port?: number | ||
73 | |||
74 | rtmpPort?: number | ||
75 | rtmpsPort?: number | ||
76 | |||
77 | parallel?: boolean | ||
78 | internalServerNumber: number | ||
79 | |||
80 | serverNumber?: number | ||
81 | customConfigFile?: string | ||
82 | |||
83 | store?: { | ||
84 | client?: { | ||
85 | id?: string | ||
86 | secret?: string | ||
87 | } | ||
88 | |||
89 | user?: { | ||
90 | username: string | ||
91 | password: string | ||
92 | email?: string | ||
93 | } | ||
94 | |||
95 | channel?: VideoChannel | ||
96 | videoChannelSync?: Partial<VideoChannelSync> | ||
97 | |||
98 | video?: Video | ||
99 | videoCreated?: VideoCreateResult | ||
100 | videoDetails?: VideoDetails | ||
101 | |||
102 | videos?: { id: number, uuid: string }[] | ||
103 | } | ||
104 | |||
105 | accessToken?: string | ||
106 | refreshToken?: string | ||
107 | |||
108 | bulk?: BulkCommand | ||
109 | cli?: CLICommand | ||
110 | customPage?: CustomPagesCommand | ||
111 | feed?: FeedCommand | ||
112 | logs?: LogsCommand | ||
113 | abuses?: AbusesCommand | ||
114 | overviews?: OverviewsCommand | ||
115 | search?: SearchCommand | ||
116 | contactForm?: ContactFormCommand | ||
117 | debug?: DebugCommand | ||
118 | follows?: FollowsCommand | ||
119 | jobs?: JobsCommand | ||
120 | metrics?: MetricsCommand | ||
121 | plugins?: PluginsCommand | ||
122 | redundancy?: RedundancyCommand | ||
123 | stats?: StatsCommand | ||
124 | config?: ConfigCommand | ||
125 | socketIO?: SocketIOCommand | ||
126 | accounts?: AccountsCommand | ||
127 | blocklist?: BlocklistCommand | ||
128 | subscriptions?: SubscriptionsCommand | ||
129 | live?: LiveCommand | ||
130 | services?: ServicesCommand | ||
131 | blacklist?: BlacklistCommand | ||
132 | captions?: CaptionsCommand | ||
133 | changeOwnership?: ChangeOwnershipCommand | ||
134 | playlists?: PlaylistsCommand | ||
135 | history?: HistoryCommand | ||
136 | imports?: ImportsCommand | ||
137 | channelSyncs?: ChannelSyncsCommand | ||
138 | streamingPlaylists?: StreamingPlaylistsCommand | ||
139 | channels?: ChannelsCommand | ||
140 | comments?: CommentsCommand | ||
141 | notifications?: NotificationsCommand | ||
142 | servers?: ServersCommand | ||
143 | login?: LoginCommand | ||
144 | users?: UsersCommand | ||
145 | videoStudio?: VideoStudioCommand | ||
146 | videos?: VideosCommand | ||
147 | videoStats?: VideoStatsCommand | ||
148 | views?: ViewsCommand | ||
149 | twoFactor?: TwoFactorCommand | ||
150 | videoToken?: VideoTokenCommand | ||
151 | registrations?: RegistrationsCommand | ||
152 | videoPasswords?: VideoPasswordsCommand | ||
153 | |||
154 | storyboard?: StoryboardCommand | ||
155 | |||
156 | runners?: RunnersCommand | ||
157 | runnerRegistrationTokens?: RunnerRegistrationTokensCommand | ||
158 | runnerJobs?: RunnerJobsCommand | ||
159 | |||
160 | constructor (options: { serverNumber: number } | { url: string }) { | ||
161 | if ((options as any).url) { | ||
162 | this.setUrl((options as any).url) | ||
163 | } else { | ||
164 | this.setServerNumber((options as any).serverNumber) | ||
165 | } | ||
166 | |||
167 | this.store = { | ||
168 | client: { | ||
169 | id: null, | ||
170 | secret: null | ||
171 | }, | ||
172 | user: { | ||
173 | username: null, | ||
174 | password: null | ||
175 | } | ||
176 | } | ||
177 | |||
178 | this.assignCommands() | ||
179 | } | ||
180 | |||
181 | setServerNumber (serverNumber: number) { | ||
182 | this.serverNumber = serverNumber | ||
183 | |||
184 | this.parallel = parallelTests() | ||
185 | |||
186 | this.internalServerNumber = this.parallel ? this.randomServer() : this.serverNumber | ||
187 | this.rtmpPort = this.parallel ? this.randomRTMP() : 1936 | ||
188 | this.rtmpsPort = this.parallel ? this.randomRTMP() : 1937 | ||
189 | this.port = 9000 + this.internalServerNumber | ||
190 | |||
191 | this.url = `http://127.0.0.1:${this.port}` | ||
192 | this.host = `127.0.0.1:${this.port}` | ||
193 | this.hostname = '127.0.0.1' | ||
194 | } | ||
195 | |||
196 | setUrl (url: string) { | ||
197 | const parsed = new URL(url) | ||
198 | |||
199 | this.url = url | ||
200 | this.host = parsed.host | ||
201 | this.hostname = parsed.hostname | ||
202 | this.port = parseInt(parsed.port) | ||
203 | } | ||
204 | |||
205 | getDirectoryPath (directoryName: string) { | ||
206 | const testDirectory = 'test' + this.internalServerNumber | ||
207 | |||
208 | return join(root(), testDirectory, directoryName) | ||
209 | } | ||
210 | |||
211 | async flushAndRun (configOverride?: object, options: RunServerOptions = {}) { | ||
212 | await ServersCommand.flushTests(this.internalServerNumber) | ||
213 | |||
214 | return this.run(configOverride, options) | ||
215 | } | ||
216 | |||
217 | async run (configOverrideArg?: any, options: RunServerOptions = {}) { | ||
218 | // These actions are async so we need to be sure that they have both been done | ||
219 | const serverRunString = { | ||
220 | 'HTTP server listening': false | ||
221 | } | ||
222 | const key = 'Database peertube_test' + this.internalServerNumber + ' is ready' | ||
223 | serverRunString[key] = false | ||
224 | |||
225 | const regexps = { | ||
226 | client_id: 'Client id: (.+)', | ||
227 | client_secret: 'Client secret: (.+)', | ||
228 | user_username: 'Username: (.+)', | ||
229 | user_password: 'User password: (.+)' | ||
230 | } | ||
231 | |||
232 | await this.assignCustomConfigFile() | ||
233 | |||
234 | const configOverride = this.buildConfigOverride() | ||
235 | |||
236 | if (configOverrideArg !== undefined) { | ||
237 | Object.assign(configOverride, configOverrideArg) | ||
238 | } | ||
239 | |||
240 | // Share the environment | ||
241 | const env = { ...process.env } | ||
242 | env['NODE_ENV'] = 'test' | ||
243 | env['NODE_APP_INSTANCE'] = this.internalServerNumber.toString() | ||
244 | env['NODE_CONFIG'] = JSON.stringify(configOverride) | ||
245 | |||
246 | if (options.env) { | ||
247 | Object.assign(env, options.env) | ||
248 | } | ||
249 | |||
250 | const execArgv = options.nodeArgs || [] | ||
251 | // FIXME: too slow :/ | ||
252 | // execArgv.push('--enable-source-maps') | ||
253 | |||
254 | const forkOptions = { | ||
255 | silent: true, | ||
256 | env, | ||
257 | detached: false, | ||
258 | execArgv | ||
259 | } | ||
260 | |||
261 | const peertubeArgs = options.peertubeArgs || [] | ||
262 | |||
263 | return new Promise<void>((res, rej) => { | ||
264 | const self = this | ||
265 | let aggregatedLogs = '' | ||
266 | |||
267 | this.app = fork(join(root(), 'dist', 'server.js'), peertubeArgs, forkOptions) | ||
268 | |||
269 | const onPeerTubeExit = () => rej(new Error('Process exited:\n' + aggregatedLogs)) | ||
270 | const onParentExit = () => { | ||
271 | if (!this.app?.pid) return | ||
272 | |||
273 | try { | ||
274 | process.kill(self.app.pid) | ||
275 | } catch { /* empty */ } | ||
276 | } | ||
277 | |||
278 | this.app.on('exit', onPeerTubeExit) | ||
279 | process.on('exit', onParentExit) | ||
280 | |||
281 | this.app.stdout.on('data', function onStdout (data) { | ||
282 | let dontContinue = false | ||
283 | |||
284 | const log: string = data.toString() | ||
285 | aggregatedLogs += log | ||
286 | |||
287 | // Capture things if we want to | ||
288 | for (const key of Object.keys(regexps)) { | ||
289 | const regexp = regexps[key] | ||
290 | const matches = log.match(regexp) | ||
291 | if (matches !== null) { | ||
292 | if (key === 'client_id') self.store.client.id = matches[1] | ||
293 | else if (key === 'client_secret') self.store.client.secret = matches[1] | ||
294 | else if (key === 'user_username') self.store.user.username = matches[1] | ||
295 | else if (key === 'user_password') self.store.user.password = matches[1] | ||
296 | } | ||
297 | } | ||
298 | |||
299 | // Check if all required sentences are here | ||
300 | for (const key of Object.keys(serverRunString)) { | ||
301 | if (log.includes(key)) serverRunString[key] = true | ||
302 | if (serverRunString[key] === false) dontContinue = true | ||
303 | } | ||
304 | |||
305 | // If no, there is maybe one thing not already initialized (client/user credentials generation...) | ||
306 | if (dontContinue === true) return | ||
307 | |||
308 | if (options.hideLogs === false) { | ||
309 | console.log(log) | ||
310 | } else { | ||
311 | process.removeListener('exit', onParentExit) | ||
312 | self.app.stdout.removeListener('data', onStdout) | ||
313 | self.app.removeListener('exit', onPeerTubeExit) | ||
314 | } | ||
315 | |||
316 | res() | ||
317 | }) | ||
318 | }) | ||
319 | } | ||
320 | |||
321 | kill () { | ||
322 | if (!this.app) return Promise.resolve() | ||
323 | |||
324 | process.kill(this.app.pid) | ||
325 | |||
326 | this.app = null | ||
327 | |||
328 | return Promise.resolve() | ||
329 | } | ||
330 | |||
331 | private randomServer () { | ||
332 | const low = 2500 | ||
333 | const high = 10000 | ||
334 | |||
335 | return randomInt(low, high) | ||
336 | } | ||
337 | |||
338 | private randomRTMP () { | ||
339 | const low = 1900 | ||
340 | const high = 2100 | ||
341 | |||
342 | return randomInt(low, high) | ||
343 | } | ||
344 | |||
345 | private async assignCustomConfigFile () { | ||
346 | if (this.internalServerNumber === this.serverNumber) return | ||
347 | |||
348 | const basePath = join(root(), 'config') | ||
349 | |||
350 | const tmpConfigFile = join(basePath, `test-${this.internalServerNumber}.yaml`) | ||
351 | await copy(join(basePath, `test-${this.serverNumber}.yaml`), tmpConfigFile) | ||
352 | |||
353 | this.customConfigFile = tmpConfigFile | ||
354 | } | ||
355 | |||
356 | private buildConfigOverride () { | ||
357 | if (!this.parallel) return {} | ||
358 | |||
359 | return { | ||
360 | listen: { | ||
361 | port: this.port | ||
362 | }, | ||
363 | webserver: { | ||
364 | port: this.port | ||
365 | }, | ||
366 | database: { | ||
367 | suffix: '_test' + this.internalServerNumber | ||
368 | }, | ||
369 | storage: { | ||
370 | tmp: this.getDirectoryPath('tmp') + '/', | ||
371 | tmp_persistent: this.getDirectoryPath('tmp-persistent') + '/', | ||
372 | bin: this.getDirectoryPath('bin') + '/', | ||
373 | avatars: this.getDirectoryPath('avatars') + '/', | ||
374 | web_videos: this.getDirectoryPath('web-videos') + '/', | ||
375 | streaming_playlists: this.getDirectoryPath('streaming-playlists') + '/', | ||
376 | redundancy: this.getDirectoryPath('redundancy') + '/', | ||
377 | logs: this.getDirectoryPath('logs') + '/', | ||
378 | previews: this.getDirectoryPath('previews') + '/', | ||
379 | thumbnails: this.getDirectoryPath('thumbnails') + '/', | ||
380 | storyboards: this.getDirectoryPath('storyboards') + '/', | ||
381 | torrents: this.getDirectoryPath('torrents') + '/', | ||
382 | captions: this.getDirectoryPath('captions') + '/', | ||
383 | cache: this.getDirectoryPath('cache') + '/', | ||
384 | plugins: this.getDirectoryPath('plugins') + '/', | ||
385 | well_known: this.getDirectoryPath('well-known') + '/' | ||
386 | }, | ||
387 | admin: { | ||
388 | email: `admin${this.internalServerNumber}@example.com` | ||
389 | }, | ||
390 | live: { | ||
391 | rtmp: { | ||
392 | port: this.rtmpPort | ||
393 | } | ||
394 | } | ||
395 | } | ||
396 | } | ||
397 | |||
398 | private assignCommands () { | ||
399 | this.bulk = new BulkCommand(this) | ||
400 | this.cli = new CLICommand(this) | ||
401 | this.customPage = new CustomPagesCommand(this) | ||
402 | this.feed = new FeedCommand(this) | ||
403 | this.logs = new LogsCommand(this) | ||
404 | this.abuses = new AbusesCommand(this) | ||
405 | this.overviews = new OverviewsCommand(this) | ||
406 | this.search = new SearchCommand(this) | ||
407 | this.contactForm = new ContactFormCommand(this) | ||
408 | this.debug = new DebugCommand(this) | ||
409 | this.follows = new FollowsCommand(this) | ||
410 | this.jobs = new JobsCommand(this) | ||
411 | this.metrics = new MetricsCommand(this) | ||
412 | this.plugins = new PluginsCommand(this) | ||
413 | this.redundancy = new RedundancyCommand(this) | ||
414 | this.stats = new StatsCommand(this) | ||
415 | this.config = new ConfigCommand(this) | ||
416 | this.socketIO = new SocketIOCommand(this) | ||
417 | this.accounts = new AccountsCommand(this) | ||
418 | this.blocklist = new BlocklistCommand(this) | ||
419 | this.subscriptions = new SubscriptionsCommand(this) | ||
420 | this.live = new LiveCommand(this) | ||
421 | this.services = new ServicesCommand(this) | ||
422 | this.blacklist = new BlacklistCommand(this) | ||
423 | this.captions = new CaptionsCommand(this) | ||
424 | this.changeOwnership = new ChangeOwnershipCommand(this) | ||
425 | this.playlists = new PlaylistsCommand(this) | ||
426 | this.history = new HistoryCommand(this) | ||
427 | this.imports = new ImportsCommand(this) | ||
428 | this.channelSyncs = new ChannelSyncsCommand(this) | ||
429 | this.streamingPlaylists = new StreamingPlaylistsCommand(this) | ||
430 | this.channels = new ChannelsCommand(this) | ||
431 | this.comments = new CommentsCommand(this) | ||
432 | this.notifications = new NotificationsCommand(this) | ||
433 | this.servers = new ServersCommand(this) | ||
434 | this.login = new LoginCommand(this) | ||
435 | this.users = new UsersCommand(this) | ||
436 | this.videos = new VideosCommand(this) | ||
437 | this.videoStudio = new VideoStudioCommand(this) | ||
438 | this.videoStats = new VideoStatsCommand(this) | ||
439 | this.views = new ViewsCommand(this) | ||
440 | this.twoFactor = new TwoFactorCommand(this) | ||
441 | this.videoToken = new VideoTokenCommand(this) | ||
442 | this.registrations = new RegistrationsCommand(this) | ||
443 | |||
444 | this.storyboard = new StoryboardCommand(this) | ||
445 | |||
446 | this.runners = new RunnersCommand(this) | ||
447 | this.runnerRegistrationTokens = new RunnerRegistrationTokensCommand(this) | ||
448 | this.runnerJobs = new RunnerJobsCommand(this) | ||
449 | this.videoPasswords = new VideoPasswordsCommand(this) | ||
450 | } | ||
451 | } | ||
diff --git a/packages/server-commands/src/server/servers-command.ts b/packages/server-commands/src/server/servers-command.ts new file mode 100644 index 000000000..0b722b62f --- /dev/null +++ b/packages/server-commands/src/server/servers-command.ts | |||
@@ -0,0 +1,104 @@ | |||
1 | import { exec } from 'child_process' | ||
2 | import { copy, ensureDir, remove } from 'fs-extra/esm' | ||
3 | import { readdir, readFile } from 'fs/promises' | ||
4 | import { basename, join } from 'path' | ||
5 | import { wait } from '@peertube/peertube-core-utils' | ||
6 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
7 | import { getFileSize, isGithubCI, root } from '@peertube/peertube-node-utils' | ||
8 | import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js' | ||
9 | |||
10 | export class ServersCommand extends AbstractCommand { | ||
11 | |||
12 | static flushTests (internalServerNumber: number) { | ||
13 | return new Promise<void>((res, rej) => { | ||
14 | const suffix = ` -- ${internalServerNumber}` | ||
15 | |||
16 | return exec('npm run clean:server:test' + suffix, (err, _stdout, stderr) => { | ||
17 | if (err || stderr) return rej(err || new Error(stderr)) | ||
18 | |||
19 | return res() | ||
20 | }) | ||
21 | }) | ||
22 | } | ||
23 | |||
24 | ping (options: OverrideCommandOptions = {}) { | ||
25 | return this.getRequestBody({ | ||
26 | ...options, | ||
27 | |||
28 | path: '/api/v1/ping', | ||
29 | implicitToken: false, | ||
30 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
31 | }) | ||
32 | } | ||
33 | |||
34 | cleanupTests () { | ||
35 | const promises: Promise<any>[] = [] | ||
36 | |||
37 | const saveGithubLogsIfNeeded = async () => { | ||
38 | if (!isGithubCI()) return | ||
39 | |||
40 | await ensureDir('artifacts') | ||
41 | |||
42 | const origin = this.buildDirectory('logs/peertube.log') | ||
43 | const destname = `peertube-${this.server.internalServerNumber}.log` | ||
44 | console.log('Saving logs %s.', destname) | ||
45 | |||
46 | await copy(origin, join('artifacts', destname)) | ||
47 | } | ||
48 | |||
49 | if (this.server.parallel) { | ||
50 | const promise = saveGithubLogsIfNeeded() | ||
51 | .then(() => ServersCommand.flushTests(this.server.internalServerNumber)) | ||
52 | |||
53 | promises.push(promise) | ||
54 | } | ||
55 | |||
56 | if (this.server.customConfigFile) { | ||
57 | promises.push(remove(this.server.customConfigFile)) | ||
58 | } | ||
59 | |||
60 | return promises | ||
61 | } | ||
62 | |||
63 | async waitUntilLog (str: string, count = 1, strictCount = true) { | ||
64 | const logfile = this.buildDirectory('logs/peertube.log') | ||
65 | |||
66 | while (true) { | ||
67 | const buf = await readFile(logfile) | ||
68 | |||
69 | const matches = buf.toString().match(new RegExp(str, 'g')) | ||
70 | if (matches && matches.length === count) return | ||
71 | if (matches && strictCount === false && matches.length >= count) return | ||
72 | |||
73 | await wait(1000) | ||
74 | } | ||
75 | } | ||
76 | |||
77 | buildDirectory (directory: string) { | ||
78 | return join(root(), 'test' + this.server.internalServerNumber, directory) | ||
79 | } | ||
80 | |||
81 | async countFiles (directory: string) { | ||
82 | const files = await readdir(this.buildDirectory(directory)) | ||
83 | |||
84 | return files.length | ||
85 | } | ||
86 | |||
87 | buildWebVideoFilePath (fileUrl: string) { | ||
88 | return this.buildDirectory(join('web-videos', basename(fileUrl))) | ||
89 | } | ||
90 | |||
91 | buildFragmentedFilePath (videoUUID: string, fileUrl: string) { | ||
92 | return this.buildDirectory(join('streaming-playlists', 'hls', videoUUID, basename(fileUrl))) | ||
93 | } | ||
94 | |||
95 | getLogContent () { | ||
96 | return readFile(this.buildDirectory('logs/peertube.log')) | ||
97 | } | ||
98 | |||
99 | async getServerFileSize (subPath: string) { | ||
100 | const path = this.server.servers.buildDirectory(subPath) | ||
101 | |||
102 | return getFileSize(path) | ||
103 | } | ||
104 | } | ||
diff --git a/packages/server-commands/src/server/servers.ts b/packages/server-commands/src/server/servers.ts new file mode 100644 index 000000000..caf9866e1 --- /dev/null +++ b/packages/server-commands/src/server/servers.ts | |||
@@ -0,0 +1,68 @@ | |||
1 | import { ensureDir } from 'fs-extra/esm' | ||
2 | import { isGithubCI } from '@peertube/peertube-node-utils' | ||
3 | import { PeerTubeServer, RunServerOptions } from './server.js' | ||
4 | |||
5 | async function createSingleServer (serverNumber: number, configOverride?: object, options: RunServerOptions = {}) { | ||
6 | const server = new PeerTubeServer({ serverNumber }) | ||
7 | |||
8 | await server.flushAndRun(configOverride, options) | ||
9 | |||
10 | return server | ||
11 | } | ||
12 | |||
13 | function createMultipleServers (totalServers: number, configOverride?: object, options: RunServerOptions = {}) { | ||
14 | const serverPromises: Promise<PeerTubeServer>[] = [] | ||
15 | |||
16 | for (let i = 1; i <= totalServers; i++) { | ||
17 | serverPromises.push(createSingleServer(i, configOverride, options)) | ||
18 | } | ||
19 | |||
20 | return Promise.all(serverPromises) | ||
21 | } | ||
22 | |||
23 | function killallServers (servers: PeerTubeServer[]) { | ||
24 | return Promise.all(servers.map(s => s.kill())) | ||
25 | } | ||
26 | |||
27 | async function cleanupTests (servers: PeerTubeServer[]) { | ||
28 | await killallServers(servers) | ||
29 | |||
30 | if (isGithubCI()) { | ||
31 | await ensureDir('artifacts') | ||
32 | } | ||
33 | |||
34 | let p: Promise<any>[] = [] | ||
35 | for (const server of servers) { | ||
36 | p = p.concat(server.servers.cleanupTests()) | ||
37 | } | ||
38 | |||
39 | return Promise.all(p) | ||
40 | } | ||
41 | |||
42 | function getServerImportConfig (mode: 'youtube-dl' | 'yt-dlp') { | ||
43 | return { | ||
44 | import: { | ||
45 | videos: { | ||
46 | http: { | ||
47 | youtube_dl_release: { | ||
48 | url: mode === 'youtube-dl' | ||
49 | ? 'https://yt-dl.org/downloads/latest/youtube-dl' | ||
50 | : 'https://api.github.com/repos/yt-dlp/yt-dlp/releases', | ||
51 | |||
52 | name: mode | ||
53 | } | ||
54 | } | ||
55 | } | ||
56 | } | ||
57 | } | ||
58 | } | ||
59 | |||
60 | // --------------------------------------------------------------------------- | ||
61 | |||
62 | export { | ||
63 | createSingleServer, | ||
64 | createMultipleServers, | ||
65 | cleanupTests, | ||
66 | killallServers, | ||
67 | getServerImportConfig | ||
68 | } | ||
diff --git a/packages/server-commands/src/server/stats-command.ts b/packages/server-commands/src/server/stats-command.ts new file mode 100644 index 000000000..80acd7bdc --- /dev/null +++ b/packages/server-commands/src/server/stats-command.ts | |||
@@ -0,0 +1,25 @@ | |||
1 | import { HttpStatusCode, ServerStats } from '@peertube/peertube-models' | ||
2 | import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js' | ||
3 | |||
4 | export class StatsCommand extends AbstractCommand { | ||
5 | |||
6 | get (options: OverrideCommandOptions & { | ||
7 | useCache?: boolean // default false | ||
8 | } = {}) { | ||
9 | const { useCache = false } = options | ||
10 | const path = '/api/v1/server/stats' | ||
11 | |||
12 | const query = { | ||
13 | t: useCache ? undefined : new Date().getTime() | ||
14 | } | ||
15 | |||
16 | return this.getRequestBody<ServerStats>({ | ||
17 | ...options, | ||
18 | |||
19 | path, | ||
20 | query, | ||
21 | implicitToken: false, | ||
22 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
23 | }) | ||
24 | } | ||
25 | } | ||