diff options
Diffstat (limited to 'shared/server-commands/server')
-rw-r--r-- | shared/server-commands/server/config-command.ts | 352 | ||||
-rw-r--r-- | shared/server-commands/server/contact-form-command.ts | 31 | ||||
-rw-r--r-- | shared/server-commands/server/debug-command.ts | 33 | ||||
-rw-r--r-- | shared/server-commands/server/follows-command.ts | 139 | ||||
-rw-r--r-- | shared/server-commands/server/follows.ts | 20 | ||||
-rw-r--r-- | shared/server-commands/server/index.ts | 14 | ||||
-rw-r--r-- | shared/server-commands/server/jobs-command.ts | 60 | ||||
-rw-r--r-- | shared/server-commands/server/jobs.ts | 84 | ||||
-rw-r--r-- | shared/server-commands/server/object-storage-command.ts | 77 | ||||
-rw-r--r-- | shared/server-commands/server/plugins-command.ts | 257 | ||||
-rw-r--r-- | shared/server-commands/server/redundancy-command.ts | 80 | ||||
-rw-r--r-- | shared/server-commands/server/server.ts | 398 | ||||
-rw-r--r-- | shared/server-commands/server/servers-command.ts | 92 | ||||
-rw-r--r-- | shared/server-commands/server/servers.ts | 49 | ||||
-rw-r--r-- | shared/server-commands/server/stats-command.ts | 25 |
15 files changed, 1711 insertions, 0 deletions
diff --git a/shared/server-commands/server/config-command.ts b/shared/server-commands/server/config-command.ts new file mode 100644 index 000000000..797231b1d --- /dev/null +++ b/shared/server-commands/server/config-command.ts | |||
@@ -0,0 +1,352 @@ | |||
1 | import { merge } from 'lodash' | ||
2 | import { About, CustomConfig, HttpStatusCode, ServerConfig } from '@shared/models' | ||
3 | import { DeepPartial } from '@shared/typescript-utils' | ||
4 | import { AbstractCommand, OverrideCommandOptions } from '../shared/abstract-command' | ||
5 | |||
6 | export class ConfigCommand extends AbstractCommand { | ||
7 | |||
8 | static getCustomConfigResolutions (enabled: boolean) { | ||
9 | return { | ||
10 | '144p': enabled, | ||
11 | '240p': enabled, | ||
12 | '360p': enabled, | ||
13 | '480p': enabled, | ||
14 | '720p': enabled, | ||
15 | '1080p': enabled, | ||
16 | '1440p': enabled, | ||
17 | '2160p': enabled | ||
18 | } | ||
19 | } | ||
20 | |||
21 | enableImports () { | ||
22 | return this.updateExistingSubConfig({ | ||
23 | newConfig: { | ||
24 | import: { | ||
25 | videos: { | ||
26 | http: { | ||
27 | enabled: true | ||
28 | }, | ||
29 | |||
30 | torrent: { | ||
31 | enabled: true | ||
32 | } | ||
33 | } | ||
34 | } | ||
35 | } | ||
36 | }) | ||
37 | } | ||
38 | |||
39 | enableLive (options: { | ||
40 | allowReplay?: boolean | ||
41 | transcoding?: boolean | ||
42 | } = {}) { | ||
43 | return this.updateExistingSubConfig({ | ||
44 | newConfig: { | ||
45 | live: { | ||
46 | enabled: true, | ||
47 | allowReplay: options.allowReplay ?? true, | ||
48 | transcoding: { | ||
49 | enabled: options.transcoding ?? true, | ||
50 | resolutions: ConfigCommand.getCustomConfigResolutions(true) | ||
51 | } | ||
52 | } | ||
53 | } | ||
54 | }) | ||
55 | } | ||
56 | |||
57 | disableTranscoding () { | ||
58 | return this.updateExistingSubConfig({ | ||
59 | newConfig: { | ||
60 | transcoding: { | ||
61 | enabled: false | ||
62 | } | ||
63 | } | ||
64 | }) | ||
65 | } | ||
66 | |||
67 | enableTranscoding (webtorrent = true, hls = true) { | ||
68 | return this.updateExistingSubConfig({ | ||
69 | newConfig: { | ||
70 | transcoding: { | ||
71 | enabled: true, | ||
72 | resolutions: ConfigCommand.getCustomConfigResolutions(true), | ||
73 | |||
74 | webtorrent: { | ||
75 | enabled: webtorrent | ||
76 | }, | ||
77 | hls: { | ||
78 | enabled: hls | ||
79 | } | ||
80 | } | ||
81 | } | ||
82 | }) | ||
83 | } | ||
84 | |||
85 | getConfig (options: OverrideCommandOptions = {}) { | ||
86 | const path = '/api/v1/config' | ||
87 | |||
88 | return this.getRequestBody<ServerConfig>({ | ||
89 | ...options, | ||
90 | |||
91 | path, | ||
92 | implicitToken: false, | ||
93 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
94 | }) | ||
95 | } | ||
96 | |||
97 | getAbout (options: OverrideCommandOptions = {}) { | ||
98 | const path = '/api/v1/config/about' | ||
99 | |||
100 | return this.getRequestBody<About>({ | ||
101 | ...options, | ||
102 | |||
103 | path, | ||
104 | implicitToken: false, | ||
105 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
106 | }) | ||
107 | } | ||
108 | |||
109 | getCustomConfig (options: OverrideCommandOptions = {}) { | ||
110 | const path = '/api/v1/config/custom' | ||
111 | |||
112 | return this.getRequestBody<CustomConfig>({ | ||
113 | ...options, | ||
114 | |||
115 | path, | ||
116 | implicitToken: true, | ||
117 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
118 | }) | ||
119 | } | ||
120 | |||
121 | updateCustomConfig (options: OverrideCommandOptions & { | ||
122 | newCustomConfig: CustomConfig | ||
123 | }) { | ||
124 | const path = '/api/v1/config/custom' | ||
125 | |||
126 | return this.putBodyRequest({ | ||
127 | ...options, | ||
128 | |||
129 | path, | ||
130 | fields: options.newCustomConfig, | ||
131 | implicitToken: true, | ||
132 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
133 | }) | ||
134 | } | ||
135 | |||
136 | deleteCustomConfig (options: OverrideCommandOptions = {}) { | ||
137 | const path = '/api/v1/config/custom' | ||
138 | |||
139 | return this.deleteRequest({ | ||
140 | ...options, | ||
141 | |||
142 | path, | ||
143 | implicitToken: true, | ||
144 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
145 | }) | ||
146 | } | ||
147 | |||
148 | async updateExistingSubConfig (options: OverrideCommandOptions & { | ||
149 | newConfig: DeepPartial<CustomConfig> | ||
150 | }) { | ||
151 | const existing = await this.getCustomConfig(options) | ||
152 | |||
153 | return this.updateCustomConfig({ ...options, newCustomConfig: merge({}, existing, options.newConfig) }) | ||
154 | } | ||
155 | |||
156 | updateCustomSubConfig (options: OverrideCommandOptions & { | ||
157 | newConfig: DeepPartial<CustomConfig> | ||
158 | }) { | ||
159 | const newCustomConfig: CustomConfig = { | ||
160 | instance: { | ||
161 | name: 'PeerTube updated', | ||
162 | shortDescription: 'my short description', | ||
163 | description: 'my super description', | ||
164 | terms: 'my super terms', | ||
165 | codeOfConduct: 'my super coc', | ||
166 | |||
167 | creationReason: 'my super creation reason', | ||
168 | moderationInformation: 'my super moderation information', | ||
169 | administrator: 'Kuja', | ||
170 | maintenanceLifetime: 'forever', | ||
171 | businessModel: 'my super business model', | ||
172 | hardwareInformation: '2vCore 3GB RAM', | ||
173 | |||
174 | languages: [ 'en', 'es' ], | ||
175 | categories: [ 1, 2 ], | ||
176 | |||
177 | isNSFW: true, | ||
178 | defaultNSFWPolicy: 'blur', | ||
179 | |||
180 | defaultClientRoute: '/videos/recently-added', | ||
181 | |||
182 | customizations: { | ||
183 | javascript: 'alert("coucou")', | ||
184 | css: 'body { background-color: red; }' | ||
185 | } | ||
186 | }, | ||
187 | theme: { | ||
188 | default: 'default' | ||
189 | }, | ||
190 | services: { | ||
191 | twitter: { | ||
192 | username: '@MySuperUsername', | ||
193 | whitelisted: true | ||
194 | } | ||
195 | }, | ||
196 | client: { | ||
197 | videos: { | ||
198 | miniature: { | ||
199 | preferAuthorDisplayName: false | ||
200 | } | ||
201 | }, | ||
202 | menu: { | ||
203 | login: { | ||
204 | redirectOnSingleExternalAuth: false | ||
205 | } | ||
206 | } | ||
207 | }, | ||
208 | cache: { | ||
209 | previews: { | ||
210 | size: 2 | ||
211 | }, | ||
212 | captions: { | ||
213 | size: 3 | ||
214 | }, | ||
215 | torrents: { | ||
216 | size: 4 | ||
217 | } | ||
218 | }, | ||
219 | signup: { | ||
220 | enabled: false, | ||
221 | limit: 5, | ||
222 | requiresEmailVerification: false, | ||
223 | minimumAge: 16 | ||
224 | }, | ||
225 | admin: { | ||
226 | email: 'superadmin1@example.com' | ||
227 | }, | ||
228 | contactForm: { | ||
229 | enabled: true | ||
230 | }, | ||
231 | user: { | ||
232 | videoQuota: 5242881, | ||
233 | videoQuotaDaily: 318742 | ||
234 | }, | ||
235 | videoChannels: { | ||
236 | maxPerUser: 20 | ||
237 | }, | ||
238 | transcoding: { | ||
239 | enabled: true, | ||
240 | allowAdditionalExtensions: true, | ||
241 | allowAudioFiles: true, | ||
242 | threads: 1, | ||
243 | concurrency: 3, | ||
244 | profile: 'default', | ||
245 | resolutions: { | ||
246 | '0p': false, | ||
247 | '144p': false, | ||
248 | '240p': false, | ||
249 | '360p': true, | ||
250 | '480p': true, | ||
251 | '720p': false, | ||
252 | '1080p': false, | ||
253 | '1440p': false, | ||
254 | '2160p': false | ||
255 | }, | ||
256 | webtorrent: { | ||
257 | enabled: true | ||
258 | }, | ||
259 | hls: { | ||
260 | enabled: false | ||
261 | } | ||
262 | }, | ||
263 | live: { | ||
264 | enabled: true, | ||
265 | allowReplay: false, | ||
266 | maxDuration: -1, | ||
267 | maxInstanceLives: -1, | ||
268 | maxUserLives: 50, | ||
269 | transcoding: { | ||
270 | enabled: true, | ||
271 | threads: 4, | ||
272 | profile: 'default', | ||
273 | resolutions: { | ||
274 | '144p': true, | ||
275 | '240p': true, | ||
276 | '360p': true, | ||
277 | '480p': true, | ||
278 | '720p': true, | ||
279 | '1080p': true, | ||
280 | '1440p': true, | ||
281 | '2160p': true | ||
282 | } | ||
283 | } | ||
284 | }, | ||
285 | import: { | ||
286 | videos: { | ||
287 | concurrency: 3, | ||
288 | http: { | ||
289 | enabled: false | ||
290 | }, | ||
291 | torrent: { | ||
292 | enabled: false | ||
293 | } | ||
294 | } | ||
295 | }, | ||
296 | trending: { | ||
297 | videos: { | ||
298 | algorithms: { | ||
299 | enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ], | ||
300 | default: 'hot' | ||
301 | } | ||
302 | } | ||
303 | }, | ||
304 | autoBlacklist: { | ||
305 | videos: { | ||
306 | ofUsers: { | ||
307 | enabled: false | ||
308 | } | ||
309 | } | ||
310 | }, | ||
311 | followers: { | ||
312 | instance: { | ||
313 | enabled: true, | ||
314 | manualApproval: false | ||
315 | } | ||
316 | }, | ||
317 | followings: { | ||
318 | instance: { | ||
319 | autoFollowBack: { | ||
320 | enabled: false | ||
321 | }, | ||
322 | autoFollowIndex: { | ||
323 | indexUrl: 'https://instances.joinpeertube.org/api/v1/instances/hosts', | ||
324 | enabled: false | ||
325 | } | ||
326 | } | ||
327 | }, | ||
328 | broadcastMessage: { | ||
329 | enabled: true, | ||
330 | level: 'warning', | ||
331 | message: 'hello', | ||
332 | dismissable: true | ||
333 | }, | ||
334 | search: { | ||
335 | remoteUri: { | ||
336 | users: true, | ||
337 | anonymous: true | ||
338 | }, | ||
339 | searchIndex: { | ||
340 | enabled: true, | ||
341 | url: 'https://search.joinpeertube.org', | ||
342 | disableLocalSearch: true, | ||
343 | isDefaultSearch: true | ||
344 | } | ||
345 | } | ||
346 | } | ||
347 | |||
348 | merge(newCustomConfig, options.newConfig) | ||
349 | |||
350 | return this.updateCustomConfig({ ...options, newCustomConfig }) | ||
351 | } | ||
352 | } | ||
diff --git a/shared/server-commands/server/contact-form-command.ts b/shared/server-commands/server/contact-form-command.ts new file mode 100644 index 000000000..0e8fd6d84 --- /dev/null +++ b/shared/server-commands/server/contact-form-command.ts | |||
@@ -0,0 +1,31 @@ | |||
1 | import { HttpStatusCode } from '@shared/models' | ||
2 | import { ContactForm } from '../../models/server' | ||
3 | import { AbstractCommand, OverrideCommandOptions } from '../shared' | ||
4 | |||
5 | export class ContactFormCommand extends AbstractCommand { | ||
6 | |||
7 | send (options: OverrideCommandOptions & { | ||
8 | fromEmail: string | ||
9 | fromName: string | ||
10 | subject: string | ||
11 | body: string | ||
12 | }) { | ||
13 | const path = '/api/v1/server/contact' | ||
14 | |||
15 | const body: ContactForm = { | ||
16 | fromEmail: options.fromEmail, | ||
17 | fromName: options.fromName, | ||
18 | subject: options.subject, | ||
19 | body: options.body | ||
20 | } | ||
21 | |||
22 | return this.postBodyRequest({ | ||
23 | ...options, | ||
24 | |||
25 | path, | ||
26 | fields: body, | ||
27 | implicitToken: false, | ||
28 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
29 | }) | ||
30 | } | ||
31 | } | ||
diff --git a/shared/server-commands/server/debug-command.ts b/shared/server-commands/server/debug-command.ts new file mode 100644 index 000000000..3c5a785bb --- /dev/null +++ b/shared/server-commands/server/debug-command.ts | |||
@@ -0,0 +1,33 @@ | |||
1 | import { Debug, HttpStatusCode, SendDebugCommand } from '@shared/models' | ||
2 | import { AbstractCommand, OverrideCommandOptions } from '../shared' | ||
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/shared/server-commands/server/follows-command.ts b/shared/server-commands/server/follows-command.ts new file mode 100644 index 000000000..01ef6f179 --- /dev/null +++ b/shared/server-commands/server/follows-command.ts | |||
@@ -0,0 +1,139 @@ | |||
1 | import { pick } from '@shared/core-utils' | ||
2 | import { ActivityPubActorType, ActorFollow, FollowState, HttpStatusCode, ResultList, ServerFollowCreate } from '@shared/models' | ||
3 | import { AbstractCommand, OverrideCommandOptions } from '../shared' | ||
4 | import { PeerTubeServer } from './server' | ||
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/shared/server-commands/server/follows.ts b/shared/server-commands/server/follows.ts new file mode 100644 index 000000000..698238f29 --- /dev/null +++ b/shared/server-commands/server/follows.ts | |||
@@ -0,0 +1,20 @@ | |||
1 | import { waitJobs } from './jobs' | ||
2 | import { PeerTubeServer } from './server' | ||
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/shared/server-commands/server/index.ts b/shared/server-commands/server/index.ts new file mode 100644 index 000000000..0a4b21fc4 --- /dev/null +++ b/shared/server-commands/server/index.ts | |||
@@ -0,0 +1,14 @@ | |||
1 | export * from './config-command' | ||
2 | export * from './contact-form-command' | ||
3 | export * from './debug-command' | ||
4 | export * from './follows-command' | ||
5 | export * from './follows' | ||
6 | export * from './jobs' | ||
7 | export * from './jobs-command' | ||
8 | export * from './object-storage-command' | ||
9 | export * from './plugins-command' | ||
10 | export * from './redundancy-command' | ||
11 | export * from './server' | ||
12 | export * from './servers-command' | ||
13 | export * from './servers' | ||
14 | export * from './stats-command' | ||
diff --git a/shared/server-commands/server/jobs-command.ts b/shared/server-commands/server/jobs-command.ts new file mode 100644 index 000000000..ac62157d1 --- /dev/null +++ b/shared/server-commands/server/jobs-command.ts | |||
@@ -0,0 +1,60 @@ | |||
1 | import { pick } from '@shared/core-utils' | ||
2 | import { HttpStatusCode, Job, JobState, JobType, ResultList } from '@shared/models' | ||
3 | import { AbstractCommand, OverrideCommandOptions } from '../shared' | ||
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 | list (options: OverrideCommandOptions & { | ||
18 | state?: JobState | ||
19 | jobType?: JobType | ||
20 | start?: number | ||
21 | count?: number | ||
22 | sort?: string | ||
23 | } = {}) { | ||
24 | const path = this.buildJobsUrl(options.state) | ||
25 | |||
26 | const query = pick(options, [ 'start', 'count', 'sort', 'jobType' ]) | ||
27 | |||
28 | return this.getRequestBody<ResultList<Job>>({ | ||
29 | ...options, | ||
30 | |||
31 | path, | ||
32 | query, | ||
33 | implicitToken: true, | ||
34 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
35 | }) | ||
36 | } | ||
37 | |||
38 | listFailed (options: OverrideCommandOptions & { | ||
39 | jobType?: JobType | ||
40 | }) { | ||
41 | const path = this.buildJobsUrl('failed') | ||
42 | |||
43 | return this.getRequestBody<ResultList<Job>>({ | ||
44 | ...options, | ||
45 | |||
46 | path, | ||
47 | query: { start: 0, count: 50 }, | ||
48 | implicitToken: true, | ||
49 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
50 | }) | ||
51 | } | ||
52 | |||
53 | private buildJobsUrl (state?: JobState) { | ||
54 | let path = '/api/v1/jobs' | ||
55 | |||
56 | if (state) path += '/' + state | ||
57 | |||
58 | return path | ||
59 | } | ||
60 | } | ||
diff --git a/shared/server-commands/server/jobs.ts b/shared/server-commands/server/jobs.ts new file mode 100644 index 000000000..fc65a873b --- /dev/null +++ b/shared/server-commands/server/jobs.ts | |||
@@ -0,0 +1,84 @@ | |||
1 | |||
2 | import { expect } from 'chai' | ||
3 | import { wait } from '@shared/core-utils' | ||
4 | import { JobState, JobType } from '../../models' | ||
5 | import { PeerTubeServer } from './server' | ||
6 | |||
7 | async function waitJobs (serversArg: PeerTubeServer[] | PeerTubeServer, skipDelayed = false) { | ||
8 | const pendingJobWait = process.env.NODE_PENDING_JOB_WAIT | ||
9 | ? parseInt(process.env.NODE_PENDING_JOB_WAIT, 10) | ||
10 | : 250 | ||
11 | |||
12 | let servers: PeerTubeServer[] | ||
13 | |||
14 | if (Array.isArray(serversArg) === false) servers = [ serversArg as PeerTubeServer ] | ||
15 | else servers = serversArg as PeerTubeServer[] | ||
16 | |||
17 | const states: JobState[] = [ 'waiting', 'active' ] | ||
18 | if (!skipDelayed) states.push('delayed') | ||
19 | |||
20 | const repeatableJobs: JobType[] = [ 'videos-views-stats', 'activitypub-cleaner' ] | ||
21 | let pendingRequests: boolean | ||
22 | |||
23 | function tasksBuilder () { | ||
24 | const tasks: Promise<any>[] = [] | ||
25 | |||
26 | // Check if each server has pending request | ||
27 | for (const server of servers) { | ||
28 | for (const state of states) { | ||
29 | const p = server.jobs.list({ | ||
30 | state, | ||
31 | start: 0, | ||
32 | count: 10, | ||
33 | sort: '-createdAt' | ||
34 | }).then(body => body.data) | ||
35 | .then(jobs => jobs.filter(j => !repeatableJobs.includes(j.type))) | ||
36 | .then(jobs => { | ||
37 | if (jobs.length !== 0) { | ||
38 | pendingRequests = true | ||
39 | } | ||
40 | }) | ||
41 | |||
42 | tasks.push(p) | ||
43 | } | ||
44 | |||
45 | const p = server.debug.getDebug() | ||
46 | .then(obj => { | ||
47 | if (obj.activityPubMessagesWaiting !== 0) { | ||
48 | pendingRequests = true | ||
49 | } | ||
50 | }) | ||
51 | |||
52 | tasks.push(p) | ||
53 | } | ||
54 | |||
55 | return tasks | ||
56 | } | ||
57 | |||
58 | do { | ||
59 | pendingRequests = false | ||
60 | await Promise.all(tasksBuilder()) | ||
61 | |||
62 | // Retry, in case of new jobs were created | ||
63 | if (pendingRequests === false) { | ||
64 | await wait(pendingJobWait) | ||
65 | await Promise.all(tasksBuilder()) | ||
66 | } | ||
67 | |||
68 | if (pendingRequests) { | ||
69 | await wait(pendingJobWait) | ||
70 | } | ||
71 | } while (pendingRequests) | ||
72 | } | ||
73 | |||
74 | async function expectNoFailedTranscodingJob (server: PeerTubeServer) { | ||
75 | const { data } = await server.jobs.listFailed({ jobType: 'video-transcoding' }) | ||
76 | expect(data).to.have.lengthOf(0) | ||
77 | } | ||
78 | |||
79 | // --------------------------------------------------------------------------- | ||
80 | |||
81 | export { | ||
82 | waitJobs, | ||
83 | expectNoFailedTranscodingJob | ||
84 | } | ||
diff --git a/shared/server-commands/server/object-storage-command.ts b/shared/server-commands/server/object-storage-command.ts new file mode 100644 index 000000000..b4de8f4cb --- /dev/null +++ b/shared/server-commands/server/object-storage-command.ts | |||
@@ -0,0 +1,77 @@ | |||
1 | |||
2 | import { HttpStatusCode } from '@shared/models' | ||
3 | import { makePostBodyRequest } from '../requests' | ||
4 | import { AbstractCommand } from '../shared' | ||
5 | |||
6 | export class ObjectStorageCommand extends AbstractCommand { | ||
7 | static readonly DEFAULT_PLAYLIST_BUCKET = 'streaming-playlists' | ||
8 | static readonly DEFAULT_WEBTORRENT_BUCKET = 'videos' | ||
9 | |||
10 | static getDefaultConfig () { | ||
11 | return { | ||
12 | object_storage: { | ||
13 | enabled: true, | ||
14 | endpoint: 'http://' + this.getEndpointHost(), | ||
15 | region: this.getRegion(), | ||
16 | |||
17 | credentials: this.getCredentialsConfig(), | ||
18 | |||
19 | streaming_playlists: { | ||
20 | bucket_name: this.DEFAULT_PLAYLIST_BUCKET | ||
21 | }, | ||
22 | |||
23 | videos: { | ||
24 | bucket_name: this.DEFAULT_WEBTORRENT_BUCKET | ||
25 | } | ||
26 | } | ||
27 | } | ||
28 | } | ||
29 | |||
30 | static getCredentialsConfig () { | ||
31 | return { | ||
32 | access_key_id: 'AKIAIOSFODNN7EXAMPLE', | ||
33 | secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' | ||
34 | } | ||
35 | } | ||
36 | |||
37 | static getEndpointHost () { | ||
38 | return 'localhost:9444' | ||
39 | } | ||
40 | |||
41 | static getRegion () { | ||
42 | return 'us-east-1' | ||
43 | } | ||
44 | |||
45 | static getWebTorrentBaseUrl () { | ||
46 | return `http://${this.DEFAULT_WEBTORRENT_BUCKET}.${this.getEndpointHost()}/` | ||
47 | } | ||
48 | |||
49 | static getPlaylistBaseUrl () { | ||
50 | return `http://${this.DEFAULT_PLAYLIST_BUCKET}.${this.getEndpointHost()}/` | ||
51 | } | ||
52 | |||
53 | static async prepareDefaultBuckets () { | ||
54 | await this.createBucket(this.DEFAULT_PLAYLIST_BUCKET) | ||
55 | await this.createBucket(this.DEFAULT_WEBTORRENT_BUCKET) | ||
56 | } | ||
57 | |||
58 | static async createBucket (name: string) { | ||
59 | await makePostBodyRequest({ | ||
60 | url: this.getEndpointHost(), | ||
61 | path: '/ui/' + name + '?delete', | ||
62 | expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307 | ||
63 | }) | ||
64 | |||
65 | await makePostBodyRequest({ | ||
66 | url: this.getEndpointHost(), | ||
67 | path: '/ui/' + name + '?create', | ||
68 | expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307 | ||
69 | }) | ||
70 | |||
71 | await makePostBodyRequest({ | ||
72 | url: this.getEndpointHost(), | ||
73 | path: '/ui/' + name + '?make-public', | ||
74 | expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307 | ||
75 | }) | ||
76 | } | ||
77 | } | ||
diff --git a/shared/server-commands/server/plugins-command.ts b/shared/server-commands/server/plugins-command.ts new file mode 100644 index 000000000..bb1277a7c --- /dev/null +++ b/shared/server-commands/server/plugins-command.ts | |||
@@ -0,0 +1,257 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { readJSON, writeJSON } from 'fs-extra' | ||
4 | import { join } from 'path' | ||
5 | import { root } from '@shared/core-utils' | ||
6 | import { | ||
7 | HttpStatusCode, | ||
8 | PeerTubePlugin, | ||
9 | PeerTubePluginIndex, | ||
10 | PeertubePluginIndexList, | ||
11 | PluginPackageJSON, | ||
12 | PluginTranslation, | ||
13 | PluginType, | ||
14 | PublicServerSetting, | ||
15 | RegisteredServerSettings, | ||
16 | ResultList | ||
17 | } from '@shared/models' | ||
18 | import { AbstractCommand, OverrideCommandOptions } from '../shared' | ||
19 | |||
20 | export class PluginsCommand extends AbstractCommand { | ||
21 | |||
22 | static getPluginTestPath (suffix = '') { | ||
23 | return join(root(), 'server', 'tests', 'fixtures', 'peertube-plugin-test' + suffix) | ||
24 | } | ||
25 | |||
26 | list (options: OverrideCommandOptions & { | ||
27 | start?: number | ||
28 | count?: number | ||
29 | sort?: string | ||
30 | pluginType?: PluginType | ||
31 | uninstalled?: boolean | ||
32 | }) { | ||
33 | const { start, count, sort, pluginType, uninstalled } = options | ||
34 | const path = '/api/v1/plugins' | ||
35 | |||
36 | return this.getRequestBody<ResultList<PeerTubePlugin>>({ | ||
37 | ...options, | ||
38 | |||
39 | path, | ||
40 | query: { | ||
41 | start, | ||
42 | count, | ||
43 | sort, | ||
44 | pluginType, | ||
45 | uninstalled | ||
46 | }, | ||
47 | implicitToken: true, | ||
48 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
49 | }) | ||
50 | } | ||
51 | |||
52 | listAvailable (options: OverrideCommandOptions & { | ||
53 | start?: number | ||
54 | count?: number | ||
55 | sort?: string | ||
56 | pluginType?: PluginType | ||
57 | currentPeerTubeEngine?: string | ||
58 | search?: string | ||
59 | expectedStatus?: HttpStatusCode | ||
60 | }) { | ||
61 | const { start, count, sort, pluginType, search, currentPeerTubeEngine } = options | ||
62 | const path = '/api/v1/plugins/available' | ||
63 | |||
64 | const query: PeertubePluginIndexList = { | ||
65 | start, | ||
66 | count, | ||
67 | sort, | ||
68 | pluginType, | ||
69 | currentPeerTubeEngine, | ||
70 | search | ||
71 | } | ||
72 | |||
73 | return this.getRequestBody<ResultList<PeerTubePluginIndex>>({ | ||
74 | ...options, | ||
75 | |||
76 | path, | ||
77 | query, | ||
78 | implicitToken: true, | ||
79 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
80 | }) | ||
81 | } | ||
82 | |||
83 | get (options: OverrideCommandOptions & { | ||
84 | npmName: string | ||
85 | }) { | ||
86 | const path = '/api/v1/plugins/' + options.npmName | ||
87 | |||
88 | return this.getRequestBody<PeerTubePlugin>({ | ||
89 | ...options, | ||
90 | |||
91 | path, | ||
92 | implicitToken: true, | ||
93 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
94 | }) | ||
95 | } | ||
96 | |||
97 | updateSettings (options: OverrideCommandOptions & { | ||
98 | npmName: string | ||
99 | settings: any | ||
100 | }) { | ||
101 | const { npmName, settings } = options | ||
102 | const path = '/api/v1/plugins/' + npmName + '/settings' | ||
103 | |||
104 | return this.putBodyRequest({ | ||
105 | ...options, | ||
106 | |||
107 | path, | ||
108 | fields: { settings }, | ||
109 | implicitToken: true, | ||
110 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
111 | }) | ||
112 | } | ||
113 | |||
114 | getRegisteredSettings (options: OverrideCommandOptions & { | ||
115 | npmName: string | ||
116 | }) { | ||
117 | const path = '/api/v1/plugins/' + options.npmName + '/registered-settings' | ||
118 | |||
119 | return this.getRequestBody<RegisteredServerSettings>({ | ||
120 | ...options, | ||
121 | |||
122 | path, | ||
123 | implicitToken: true, | ||
124 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
125 | }) | ||
126 | } | ||
127 | |||
128 | getPublicSettings (options: OverrideCommandOptions & { | ||
129 | npmName: string | ||
130 | }) { | ||
131 | const { npmName } = options | ||
132 | const path = '/api/v1/plugins/' + npmName + '/public-settings' | ||
133 | |||
134 | return this.getRequestBody<PublicServerSetting>({ | ||
135 | ...options, | ||
136 | |||
137 | path, | ||
138 | implicitToken: false, | ||
139 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
140 | }) | ||
141 | } | ||
142 | |||
143 | getTranslations (options: OverrideCommandOptions & { | ||
144 | locale: string | ||
145 | }) { | ||
146 | const { locale } = options | ||
147 | const path = '/plugins/translations/' + locale + '.json' | ||
148 | |||
149 | return this.getRequestBody<PluginTranslation>({ | ||
150 | ...options, | ||
151 | |||
152 | path, | ||
153 | implicitToken: false, | ||
154 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
155 | }) | ||
156 | } | ||
157 | |||
158 | install (options: OverrideCommandOptions & { | ||
159 | path?: string | ||
160 | npmName?: string | ||
161 | pluginVersion?: string | ||
162 | }) { | ||
163 | const { npmName, path, pluginVersion } = options | ||
164 | const apiPath = '/api/v1/plugins/install' | ||
165 | |||
166 | return this.postBodyRequest({ | ||
167 | ...options, | ||
168 | |||
169 | path: apiPath, | ||
170 | fields: { npmName, path, pluginVersion }, | ||
171 | implicitToken: true, | ||
172 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
173 | }) | ||
174 | } | ||
175 | |||
176 | update (options: OverrideCommandOptions & { | ||
177 | path?: string | ||
178 | npmName?: string | ||
179 | }) { | ||
180 | const { npmName, path } = options | ||
181 | const apiPath = '/api/v1/plugins/update' | ||
182 | |||
183 | return this.postBodyRequest({ | ||
184 | ...options, | ||
185 | |||
186 | path: apiPath, | ||
187 | fields: { npmName, path }, | ||
188 | implicitToken: true, | ||
189 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
190 | }) | ||
191 | } | ||
192 | |||
193 | uninstall (options: OverrideCommandOptions & { | ||
194 | npmName: string | ||
195 | }) { | ||
196 | const { npmName } = options | ||
197 | const apiPath = '/api/v1/plugins/uninstall' | ||
198 | |||
199 | return this.postBodyRequest({ | ||
200 | ...options, | ||
201 | |||
202 | path: apiPath, | ||
203 | fields: { npmName }, | ||
204 | implicitToken: true, | ||
205 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
206 | }) | ||
207 | } | ||
208 | |||
209 | getCSS (options: OverrideCommandOptions = {}) { | ||
210 | const path = '/plugins/global.css' | ||
211 | |||
212 | return this.getRequestText({ | ||
213 | ...options, | ||
214 | |||
215 | path, | ||
216 | implicitToken: false, | ||
217 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
218 | }) | ||
219 | } | ||
220 | |||
221 | getExternalAuth (options: OverrideCommandOptions & { | ||
222 | npmName: string | ||
223 | npmVersion: string | ||
224 | authName: string | ||
225 | query?: any | ||
226 | }) { | ||
227 | const { npmName, npmVersion, authName, query } = options | ||
228 | |||
229 | const path = '/plugins/' + npmName + '/' + npmVersion + '/auth/' + authName | ||
230 | |||
231 | return this.getRequest({ | ||
232 | ...options, | ||
233 | |||
234 | path, | ||
235 | query, | ||
236 | implicitToken: false, | ||
237 | defaultExpectedStatus: HttpStatusCode.OK_200, | ||
238 | redirects: 0 | ||
239 | }) | ||
240 | } | ||
241 | |||
242 | updatePackageJSON (npmName: string, json: any) { | ||
243 | const path = this.getPackageJSONPath(npmName) | ||
244 | |||
245 | return writeJSON(path, json) | ||
246 | } | ||
247 | |||
248 | getPackageJSON (npmName: string): Promise<PluginPackageJSON> { | ||
249 | const path = this.getPackageJSONPath(npmName) | ||
250 | |||
251 | return readJSON(path) | ||
252 | } | ||
253 | |||
254 | private getPackageJSONPath (npmName: string) { | ||
255 | return this.server.servers.buildDirectory(join('plugins', 'node_modules', npmName, 'package.json')) | ||
256 | } | ||
257 | } | ||
diff --git a/shared/server-commands/server/redundancy-command.ts b/shared/server-commands/server/redundancy-command.ts new file mode 100644 index 000000000..e7a8b3c29 --- /dev/null +++ b/shared/server-commands/server/redundancy-command.ts | |||
@@ -0,0 +1,80 @@ | |||
1 | import { HttpStatusCode, ResultList, VideoRedundanciesTarget, VideoRedundancy } from '@shared/models' | ||
2 | import { AbstractCommand, OverrideCommandOptions } from '../shared' | ||
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/shared/server-commands/server/server.ts b/shared/server-commands/server/server.ts new file mode 100644 index 000000000..da89fd876 --- /dev/null +++ b/shared/server-commands/server/server.ts | |||
@@ -0,0 +1,398 @@ | |||
1 | import { ChildProcess, fork } from 'child_process' | ||
2 | import { copy } from 'fs-extra' | ||
3 | import { join } from 'path' | ||
4 | import { parallelTests, randomInt, root } from '@shared/core-utils' | ||
5 | import { Video, VideoChannel, VideoCreateResult, VideoDetails } from '@shared/models' | ||
6 | import { BulkCommand } from '../bulk' | ||
7 | import { CLICommand } from '../cli' | ||
8 | import { CustomPagesCommand } from '../custom-pages' | ||
9 | import { FeedCommand } from '../feeds' | ||
10 | import { LogsCommand } from '../logs' | ||
11 | import { SQLCommand } from '../miscs' | ||
12 | import { AbusesCommand } from '../moderation' | ||
13 | import { OverviewsCommand } from '../overviews' | ||
14 | import { SearchCommand } from '../search' | ||
15 | import { SocketIOCommand } from '../socket' | ||
16 | import { AccountsCommand, BlocklistCommand, LoginCommand, NotificationsCommand, SubscriptionsCommand, UsersCommand } from '../users' | ||
17 | import { | ||
18 | BlacklistCommand, | ||
19 | CaptionsCommand, | ||
20 | ChangeOwnershipCommand, | ||
21 | ChannelsCommand, | ||
22 | HistoryCommand, | ||
23 | ImportsCommand, | ||
24 | LiveCommand, | ||
25 | PlaylistsCommand, | ||
26 | ServicesCommand, | ||
27 | StreamingPlaylistsCommand, | ||
28 | VideosCommand | ||
29 | } from '../videos' | ||
30 | import { CommentsCommand } from '../videos/comments-command' | ||
31 | import { ConfigCommand } from './config-command' | ||
32 | import { ContactFormCommand } from './contact-form-command' | ||
33 | import { DebugCommand } from './debug-command' | ||
34 | import { FollowsCommand } from './follows-command' | ||
35 | import { JobsCommand } from './jobs-command' | ||
36 | import { ObjectStorageCommand } from './object-storage-command' | ||
37 | import { PluginsCommand } from './plugins-command' | ||
38 | import { RedundancyCommand } from './redundancy-command' | ||
39 | import { ServersCommand } from './servers-command' | ||
40 | import { StatsCommand } from './stats-command' | ||
41 | |||
42 | export type RunServerOptions = { | ||
43 | hideLogs?: boolean | ||
44 | nodeArgs?: string[] | ||
45 | peertubeArgs?: string[] | ||
46 | env?: { [ id: string ]: string } | ||
47 | } | ||
48 | |||
49 | export class PeerTubeServer { | ||
50 | app?: ChildProcess | ||
51 | |||
52 | url: string | ||
53 | host?: string | ||
54 | hostname?: string | ||
55 | port?: number | ||
56 | |||
57 | rtmpPort?: number | ||
58 | rtmpsPort?: number | ||
59 | |||
60 | parallel?: boolean | ||
61 | internalServerNumber: number | ||
62 | |||
63 | serverNumber?: number | ||
64 | customConfigFile?: string | ||
65 | |||
66 | store?: { | ||
67 | client?: { | ||
68 | id?: string | ||
69 | secret?: string | ||
70 | } | ||
71 | |||
72 | user?: { | ||
73 | username: string | ||
74 | password: string | ||
75 | email?: string | ||
76 | } | ||
77 | |||
78 | channel?: VideoChannel | ||
79 | |||
80 | video?: Video | ||
81 | videoCreated?: VideoCreateResult | ||
82 | videoDetails?: VideoDetails | ||
83 | |||
84 | videos?: { id: number, uuid: string }[] | ||
85 | } | ||
86 | |||
87 | accessToken?: string | ||
88 | refreshToken?: string | ||
89 | |||
90 | bulk?: BulkCommand | ||
91 | cli?: CLICommand | ||
92 | customPage?: CustomPagesCommand | ||
93 | feed?: FeedCommand | ||
94 | logs?: LogsCommand | ||
95 | abuses?: AbusesCommand | ||
96 | overviews?: OverviewsCommand | ||
97 | search?: SearchCommand | ||
98 | contactForm?: ContactFormCommand | ||
99 | debug?: DebugCommand | ||
100 | follows?: FollowsCommand | ||
101 | jobs?: JobsCommand | ||
102 | plugins?: PluginsCommand | ||
103 | redundancy?: RedundancyCommand | ||
104 | stats?: StatsCommand | ||
105 | config?: ConfigCommand | ||
106 | socketIO?: SocketIOCommand | ||
107 | accounts?: AccountsCommand | ||
108 | blocklist?: BlocklistCommand | ||
109 | subscriptions?: SubscriptionsCommand | ||
110 | live?: LiveCommand | ||
111 | services?: ServicesCommand | ||
112 | blacklist?: BlacklistCommand | ||
113 | captions?: CaptionsCommand | ||
114 | changeOwnership?: ChangeOwnershipCommand | ||
115 | playlists?: PlaylistsCommand | ||
116 | history?: HistoryCommand | ||
117 | imports?: ImportsCommand | ||
118 | streamingPlaylists?: StreamingPlaylistsCommand | ||
119 | channels?: ChannelsCommand | ||
120 | comments?: CommentsCommand | ||
121 | sql?: SQLCommand | ||
122 | notifications?: NotificationsCommand | ||
123 | servers?: ServersCommand | ||
124 | login?: LoginCommand | ||
125 | users?: UsersCommand | ||
126 | objectStorage?: ObjectStorageCommand | ||
127 | videos?: VideosCommand | ||
128 | |||
129 | constructor (options: { serverNumber: number } | { url: string }) { | ||
130 | if ((options as any).url) { | ||
131 | this.setUrl((options as any).url) | ||
132 | } else { | ||
133 | this.setServerNumber((options as any).serverNumber) | ||
134 | } | ||
135 | |||
136 | this.store = { | ||
137 | client: { | ||
138 | id: null, | ||
139 | secret: null | ||
140 | }, | ||
141 | user: { | ||
142 | username: null, | ||
143 | password: null | ||
144 | } | ||
145 | } | ||
146 | |||
147 | this.assignCommands() | ||
148 | } | ||
149 | |||
150 | setServerNumber (serverNumber: number) { | ||
151 | this.serverNumber = serverNumber | ||
152 | |||
153 | this.parallel = parallelTests() | ||
154 | |||
155 | this.internalServerNumber = this.parallel ? this.randomServer() : this.serverNumber | ||
156 | this.rtmpPort = this.parallel ? this.randomRTMP() : 1936 | ||
157 | this.rtmpsPort = this.parallel ? this.randomRTMP() : 1937 | ||
158 | this.port = 9000 + this.internalServerNumber | ||
159 | |||
160 | this.url = `http://localhost:${this.port}` | ||
161 | this.host = `localhost:${this.port}` | ||
162 | this.hostname = 'localhost' | ||
163 | } | ||
164 | |||
165 | setUrl (url: string) { | ||
166 | const parsed = new URL(url) | ||
167 | |||
168 | this.url = url | ||
169 | this.host = parsed.host | ||
170 | this.hostname = parsed.hostname | ||
171 | this.port = parseInt(parsed.port) | ||
172 | } | ||
173 | |||
174 | async flushAndRun (configOverride?: Object, options: RunServerOptions = {}) { | ||
175 | await ServersCommand.flushTests(this.internalServerNumber) | ||
176 | |||
177 | return this.run(configOverride, options) | ||
178 | } | ||
179 | |||
180 | async run (configOverrideArg?: any, options: RunServerOptions = {}) { | ||
181 | // These actions are async so we need to be sure that they have both been done | ||
182 | const serverRunString = { | ||
183 | 'HTTP server listening': false | ||
184 | } | ||
185 | const key = 'Database peertube_test' + this.internalServerNumber + ' is ready' | ||
186 | serverRunString[key] = false | ||
187 | |||
188 | const regexps = { | ||
189 | client_id: 'Client id: (.+)', | ||
190 | client_secret: 'Client secret: (.+)', | ||
191 | user_username: 'Username: (.+)', | ||
192 | user_password: 'User password: (.+)' | ||
193 | } | ||
194 | |||
195 | await this.assignCustomConfigFile() | ||
196 | |||
197 | const configOverride = this.buildConfigOverride() | ||
198 | |||
199 | if (configOverrideArg !== undefined) { | ||
200 | Object.assign(configOverride, configOverrideArg) | ||
201 | } | ||
202 | |||
203 | // Share the environment | ||
204 | const env = Object.create(process.env) | ||
205 | env['NODE_ENV'] = 'test' | ||
206 | env['NODE_APP_INSTANCE'] = this.internalServerNumber.toString() | ||
207 | env['NODE_CONFIG'] = JSON.stringify(configOverride) | ||
208 | |||
209 | if (options.env) { | ||
210 | Object.assign(env, options.env) | ||
211 | } | ||
212 | |||
213 | const execArgv = options.nodeArgs || [] | ||
214 | // FIXME: too slow :/ | ||
215 | // execArgv.push('--enable-source-maps') | ||
216 | |||
217 | const forkOptions = { | ||
218 | silent: true, | ||
219 | env, | ||
220 | detached: true, | ||
221 | execArgv | ||
222 | } | ||
223 | |||
224 | const peertubeArgs = options.peertubeArgs || [] | ||
225 | |||
226 | return new Promise<void>((res, rej) => { | ||
227 | const self = this | ||
228 | let aggregatedLogs = '' | ||
229 | |||
230 | this.app = fork(join(root(), 'dist', 'server.js'), peertubeArgs, forkOptions) | ||
231 | |||
232 | const onPeerTubeExit = () => rej(new Error('Process exited:\n' + aggregatedLogs)) | ||
233 | const onParentExit = () => { | ||
234 | if (!this.app || !this.app.pid) return | ||
235 | |||
236 | try { | ||
237 | process.kill(self.app.pid) | ||
238 | } catch { /* empty */ } | ||
239 | } | ||
240 | |||
241 | this.app.on('exit', onPeerTubeExit) | ||
242 | process.on('exit', onParentExit) | ||
243 | |||
244 | this.app.stdout.on('data', function onStdout (data) { | ||
245 | let dontContinue = false | ||
246 | |||
247 | const log: string = data.toString() | ||
248 | aggregatedLogs += log | ||
249 | |||
250 | // Capture things if we want to | ||
251 | for (const key of Object.keys(regexps)) { | ||
252 | const regexp = regexps[key] | ||
253 | const matches = log.match(regexp) | ||
254 | if (matches !== null) { | ||
255 | if (key === 'client_id') self.store.client.id = matches[1] | ||
256 | else if (key === 'client_secret') self.store.client.secret = matches[1] | ||
257 | else if (key === 'user_username') self.store.user.username = matches[1] | ||
258 | else if (key === 'user_password') self.store.user.password = matches[1] | ||
259 | } | ||
260 | } | ||
261 | |||
262 | // Check if all required sentences are here | ||
263 | for (const key of Object.keys(serverRunString)) { | ||
264 | if (log.includes(key)) serverRunString[key] = true | ||
265 | if (serverRunString[key] === false) dontContinue = true | ||
266 | } | ||
267 | |||
268 | // If no, there is maybe one thing not already initialized (client/user credentials generation...) | ||
269 | if (dontContinue === true) return | ||
270 | |||
271 | if (options.hideLogs === false) { | ||
272 | console.log(log) | ||
273 | } else { | ||
274 | process.removeListener('exit', onParentExit) | ||
275 | self.app.stdout.removeListener('data', onStdout) | ||
276 | self.app.removeListener('exit', onPeerTubeExit) | ||
277 | } | ||
278 | |||
279 | res() | ||
280 | }) | ||
281 | }) | ||
282 | } | ||
283 | |||
284 | async kill () { | ||
285 | if (!this.app) return | ||
286 | |||
287 | await this.sql.cleanup() | ||
288 | |||
289 | process.kill(-this.app.pid) | ||
290 | |||
291 | this.app = null | ||
292 | } | ||
293 | |||
294 | private randomServer () { | ||
295 | const low = 10 | ||
296 | const high = 10000 | ||
297 | |||
298 | return randomInt(low, high) | ||
299 | } | ||
300 | |||
301 | private randomRTMP () { | ||
302 | const low = 1900 | ||
303 | const high = 2100 | ||
304 | |||
305 | return randomInt(low, high) | ||
306 | } | ||
307 | |||
308 | private async assignCustomConfigFile () { | ||
309 | if (this.internalServerNumber === this.serverNumber) return | ||
310 | |||
311 | const basePath = join(root(), 'config') | ||
312 | |||
313 | const tmpConfigFile = join(basePath, `test-${this.internalServerNumber}.yaml`) | ||
314 | await copy(join(basePath, `test-${this.serverNumber}.yaml`), tmpConfigFile) | ||
315 | |||
316 | this.customConfigFile = tmpConfigFile | ||
317 | } | ||
318 | |||
319 | private buildConfigOverride () { | ||
320 | if (!this.parallel) return {} | ||
321 | |||
322 | return { | ||
323 | listen: { | ||
324 | port: this.port | ||
325 | }, | ||
326 | webserver: { | ||
327 | port: this.port | ||
328 | }, | ||
329 | database: { | ||
330 | suffix: '_test' + this.internalServerNumber | ||
331 | }, | ||
332 | storage: { | ||
333 | tmp: `test${this.internalServerNumber}/tmp/`, | ||
334 | bin: `test${this.internalServerNumber}/bin/`, | ||
335 | avatars: `test${this.internalServerNumber}/avatars/`, | ||
336 | videos: `test${this.internalServerNumber}/videos/`, | ||
337 | streaming_playlists: `test${this.internalServerNumber}/streaming-playlists/`, | ||
338 | redundancy: `test${this.internalServerNumber}/redundancy/`, | ||
339 | logs: `test${this.internalServerNumber}/logs/`, | ||
340 | previews: `test${this.internalServerNumber}/previews/`, | ||
341 | thumbnails: `test${this.internalServerNumber}/thumbnails/`, | ||
342 | torrents: `test${this.internalServerNumber}/torrents/`, | ||
343 | captions: `test${this.internalServerNumber}/captions/`, | ||
344 | cache: `test${this.internalServerNumber}/cache/`, | ||
345 | plugins: `test${this.internalServerNumber}/plugins/` | ||
346 | }, | ||
347 | admin: { | ||
348 | email: `admin${this.internalServerNumber}@example.com` | ||
349 | }, | ||
350 | live: { | ||
351 | rtmp: { | ||
352 | port: this.rtmpPort | ||
353 | } | ||
354 | } | ||
355 | } | ||
356 | } | ||
357 | |||
358 | private assignCommands () { | ||
359 | this.bulk = new BulkCommand(this) | ||
360 | this.cli = new CLICommand(this) | ||
361 | this.customPage = new CustomPagesCommand(this) | ||
362 | this.feed = new FeedCommand(this) | ||
363 | this.logs = new LogsCommand(this) | ||
364 | this.abuses = new AbusesCommand(this) | ||
365 | this.overviews = new OverviewsCommand(this) | ||
366 | this.search = new SearchCommand(this) | ||
367 | this.contactForm = new ContactFormCommand(this) | ||
368 | this.debug = new DebugCommand(this) | ||
369 | this.follows = new FollowsCommand(this) | ||
370 | this.jobs = new JobsCommand(this) | ||
371 | this.plugins = new PluginsCommand(this) | ||
372 | this.redundancy = new RedundancyCommand(this) | ||
373 | this.stats = new StatsCommand(this) | ||
374 | this.config = new ConfigCommand(this) | ||
375 | this.socketIO = new SocketIOCommand(this) | ||
376 | this.accounts = new AccountsCommand(this) | ||
377 | this.blocklist = new BlocklistCommand(this) | ||
378 | this.subscriptions = new SubscriptionsCommand(this) | ||
379 | this.live = new LiveCommand(this) | ||
380 | this.services = new ServicesCommand(this) | ||
381 | this.blacklist = new BlacklistCommand(this) | ||
382 | this.captions = new CaptionsCommand(this) | ||
383 | this.changeOwnership = new ChangeOwnershipCommand(this) | ||
384 | this.playlists = new PlaylistsCommand(this) | ||
385 | this.history = new HistoryCommand(this) | ||
386 | this.imports = new ImportsCommand(this) | ||
387 | this.streamingPlaylists = new StreamingPlaylistsCommand(this) | ||
388 | this.channels = new ChannelsCommand(this) | ||
389 | this.comments = new CommentsCommand(this) | ||
390 | this.sql = new SQLCommand(this) | ||
391 | this.notifications = new NotificationsCommand(this) | ||
392 | this.servers = new ServersCommand(this) | ||
393 | this.login = new LoginCommand(this) | ||
394 | this.users = new UsersCommand(this) | ||
395 | this.videos = new VideosCommand(this) | ||
396 | this.objectStorage = new ObjectStorageCommand(this) | ||
397 | } | ||
398 | } | ||
diff --git a/shared/server-commands/server/servers-command.ts b/shared/server-commands/server/servers-command.ts new file mode 100644 index 000000000..c5d8d18dc --- /dev/null +++ b/shared/server-commands/server/servers-command.ts | |||
@@ -0,0 +1,92 @@ | |||
1 | import { exec } from 'child_process' | ||
2 | import { copy, ensureDir, readFile, remove } from 'fs-extra' | ||
3 | import { basename, join } from 'path' | ||
4 | import { isGithubCI, root, wait } from '@shared/core-utils' | ||
5 | import { getFileSize } from '@shared/extra-utils' | ||
6 | import { HttpStatusCode } from '@shared/models' | ||
7 | import { AbstractCommand, OverrideCommandOptions } from '../shared' | ||
8 | |||
9 | export class ServersCommand extends AbstractCommand { | ||
10 | |||
11 | static flushTests (internalServerNumber: number) { | ||
12 | return new Promise<void>((res, rej) => { | ||
13 | const suffix = ` -- ${internalServerNumber}` | ||
14 | |||
15 | return exec('npm run clean:server:test' + suffix, (err, _stdout, stderr) => { | ||
16 | if (err || stderr) return rej(err || new Error(stderr)) | ||
17 | |||
18 | return res() | ||
19 | }) | ||
20 | }) | ||
21 | } | ||
22 | |||
23 | ping (options: OverrideCommandOptions = {}) { | ||
24 | return this.getRequestBody({ | ||
25 | ...options, | ||
26 | |||
27 | path: '/api/v1/ping', | ||
28 | implicitToken: false, | ||
29 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
30 | }) | ||
31 | } | ||
32 | |||
33 | async cleanupTests () { | ||
34 | const p: Promise<any>[] = [] | ||
35 | |||
36 | if (isGithubCI()) { | ||
37 | await ensureDir('artifacts') | ||
38 | |||
39 | const origin = this.buildDirectory('logs/peertube.log') | ||
40 | const destname = `peertube-${this.server.internalServerNumber}.log` | ||
41 | console.log('Saving logs %s.', destname) | ||
42 | |||
43 | await copy(origin, join('artifacts', destname)) | ||
44 | } | ||
45 | |||
46 | if (this.server.parallel) { | ||
47 | p.push(ServersCommand.flushTests(this.server.internalServerNumber)) | ||
48 | } | ||
49 | |||
50 | if (this.server.customConfigFile) { | ||
51 | p.push(remove(this.server.customConfigFile)) | ||
52 | } | ||
53 | |||
54 | return p | ||
55 | } | ||
56 | |||
57 | async waitUntilLog (str: string, count = 1, strictCount = true) { | ||
58 | const logfile = this.buildDirectory('logs/peertube.log') | ||
59 | |||
60 | while (true) { | ||
61 | const buf = await readFile(logfile) | ||
62 | |||
63 | const matches = buf.toString().match(new RegExp(str, 'g')) | ||
64 | if (matches && matches.length === count) return | ||
65 | if (matches && strictCount === false && matches.length >= count) return | ||
66 | |||
67 | await wait(1000) | ||
68 | } | ||
69 | } | ||
70 | |||
71 | buildDirectory (directory: string) { | ||
72 | return join(root(), 'test' + this.server.internalServerNumber, directory) | ||
73 | } | ||
74 | |||
75 | buildWebTorrentFilePath (fileUrl: string) { | ||
76 | return this.buildDirectory(join('videos', basename(fileUrl))) | ||
77 | } | ||
78 | |||
79 | buildFragmentedFilePath (videoUUID: string, fileUrl: string) { | ||
80 | return this.buildDirectory(join('streaming-playlists', 'hls', videoUUID, basename(fileUrl))) | ||
81 | } | ||
82 | |||
83 | getLogContent () { | ||
84 | return readFile(this.buildDirectory('logs/peertube.log')) | ||
85 | } | ||
86 | |||
87 | async getServerFileSize (subPath: string) { | ||
88 | const path = this.server.servers.buildDirectory(subPath) | ||
89 | |||
90 | return getFileSize(path) | ||
91 | } | ||
92 | } | ||
diff --git a/shared/server-commands/server/servers.ts b/shared/server-commands/server/servers.ts new file mode 100644 index 000000000..0faee3a8d --- /dev/null +++ b/shared/server-commands/server/servers.ts | |||
@@ -0,0 +1,49 @@ | |||
1 | import { ensureDir } from 'fs-extra' | ||
2 | import { isGithubCI } from '@shared/core-utils' | ||
3 | import { PeerTubeServer, RunServerOptions } from './server' | ||
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 | async 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 | // --------------------------------------------------------------------------- | ||
43 | |||
44 | export { | ||
45 | createSingleServer, | ||
46 | createMultipleServers, | ||
47 | cleanupTests, | ||
48 | killallServers | ||
49 | } | ||
diff --git a/shared/server-commands/server/stats-command.ts b/shared/server-commands/server/stats-command.ts new file mode 100644 index 000000000..64a452306 --- /dev/null +++ b/shared/server-commands/server/stats-command.ts | |||
@@ -0,0 +1,25 @@ | |||
1 | import { HttpStatusCode, ServerStats } from '@shared/models' | ||
2 | import { AbstractCommand, OverrideCommandOptions } from '../shared' | ||
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 | } | ||