diff options
author | Chocobozzz <me@florianbigard.com> | 2023-07-31 14:34:36 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2023-08-11 15:02:33 +0200 |
commit | 3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch) | |
tree | e4510b39bdac9c318fdb4b47018d08f15368b8f0 /server/tools | |
parent | 04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff) | |
download | PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.gz PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.zst PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.zip |
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge
conflicts, but it's a major step forward:
* Server can be faster at startup because imports() are async and we can
easily lazy import big modules
* Angular doesn't seem to support ES import (with .js extension), so we
had to correctly organize peertube into a monorepo:
* Use yarn workspace feature
* Use typescript reference projects for dependencies
* Shared projects have been moved into "packages", each one is now a
node module (with a dedicated package.json/tsconfig.json)
* server/tools have been moved into apps/ and is now a dedicated app
bundled and published on NPM so users don't have to build peertube
cli tools manually
* server/tests have been moved into packages/ so we don't compile
them every time we want to run the server
* Use isolatedModule option:
* Had to move from const enum to const
(https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums)
* Had to explictely specify "type" imports when used in decorators
* Prefer tsx (that uses esbuild under the hood) instead of ts-node to
load typescript files (tests with mocha or scripts):
* To reduce test complexity as esbuild doesn't support decorator
metadata, we only test server files that do not import server
models
* We still build tests files into js files for a faster CI
* Remove unmaintained peertube CLI import script
* Removed some barrels to speed up execution (less imports)
Diffstat (limited to 'server/tools')
-rw-r--r-- | server/tools/README.md | 3 | ||||
-rw-r--r-- | server/tools/package.json | 11 | ||||
-rw-r--r-- | server/tools/peertube-auth.ts | 171 | ||||
-rw-r--r-- | server/tools/peertube-get-access-token.ts | 34 | ||||
-rw-r--r-- | server/tools/peertube-import-videos.ts | 351 | ||||
-rw-r--r-- | server/tools/peertube-plugins.ts | 165 | ||||
-rw-r--r-- | server/tools/peertube-redundancy.ts | 172 | ||||
-rw-r--r-- | server/tools/peertube-upload.ts | 77 | ||||
-rw-r--r-- | server/tools/peertube.ts | 72 | ||||
-rw-r--r-- | server/tools/shared/cli.ts | 262 | ||||
-rw-r--r-- | server/tools/shared/index.ts | 1 | ||||
-rw-r--r-- | server/tools/tsconfig.json | 12 | ||||
-rw-r--r-- | server/tools/yarn.lock | 373 |
13 files changed, 0 insertions, 1704 deletions
diff --git a/server/tools/README.md b/server/tools/README.md deleted file mode 100644 index d7ecd4004..000000000 --- a/server/tools/README.md +++ /dev/null | |||
@@ -1,3 +0,0 @@ | |||
1 | # PeerTube CLI | ||
2 | |||
3 | See https://docs.joinpeertube.org/maintain/tools#remote-tools | ||
diff --git a/server/tools/package.json b/server/tools/package.json deleted file mode 100644 index b20f38244..000000000 --- a/server/tools/package.json +++ /dev/null | |||
@@ -1,11 +0,0 @@ | |||
1 | { | ||
2 | "name": "@peertube/cli", | ||
3 | "version": "1.0.0", | ||
4 | "private": true, | ||
5 | "dependencies": { | ||
6 | "application-config": "^2.0.0", | ||
7 | "cli-table3": "^0.6.0", | ||
8 | "netrc-parser": "^3.1.6" | ||
9 | }, | ||
10 | "devDependencies": {} | ||
11 | } | ||
diff --git a/server/tools/peertube-auth.ts b/server/tools/peertube-auth.ts deleted file mode 100644 index c853469c2..000000000 --- a/server/tools/peertube-auth.ts +++ /dev/null | |||
@@ -1,171 +0,0 @@ | |||
1 | import CliTable3 from 'cli-table3' | ||
2 | import { OptionValues, program } from 'commander' | ||
3 | import { isUserUsernameValid } from '../helpers/custom-validators/users' | ||
4 | import { assignToken, buildServer, getNetrc, getSettings, writeSettings } from './shared' | ||
5 | |||
6 | import prompt = require('prompt') | ||
7 | |||
8 | async function delInstance (url: string) { | ||
9 | const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ]) | ||
10 | |||
11 | const index = settings.remotes.indexOf(url) | ||
12 | settings.remotes.splice(index) | ||
13 | |||
14 | if (settings.default === index) settings.default = -1 | ||
15 | |||
16 | await writeSettings(settings) | ||
17 | |||
18 | delete netrc.machines[url] | ||
19 | |||
20 | await netrc.save() | ||
21 | } | ||
22 | |||
23 | async function setInstance (url: string, username: string, password: string, isDefault: boolean) { | ||
24 | const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ]) | ||
25 | |||
26 | if (settings.remotes.includes(url) === false) { | ||
27 | settings.remotes.push(url) | ||
28 | } | ||
29 | |||
30 | if (isDefault || settings.remotes.length === 1) { | ||
31 | settings.default = settings.remotes.length - 1 | ||
32 | } | ||
33 | |||
34 | await writeSettings(settings) | ||
35 | |||
36 | netrc.machines[url] = { login: username, password } | ||
37 | await netrc.save() | ||
38 | } | ||
39 | |||
40 | function isURLaPeerTubeInstance (url: string) { | ||
41 | return url.startsWith('http://') || url.startsWith('https://') | ||
42 | } | ||
43 | |||
44 | function stripExtraneousFromPeerTubeUrl (url: string) { | ||
45 | // Get everything before the 3rd /. | ||
46 | const urlLength = url.includes('/', 8) | ||
47 | ? url.indexOf('/', 8) | ||
48 | : url.length | ||
49 | |||
50 | return url.substring(0, urlLength) | ||
51 | } | ||
52 | |||
53 | program | ||
54 | .name('auth') | ||
55 | .usage('[command] [options]') | ||
56 | |||
57 | program | ||
58 | .command('add') | ||
59 | .description('remember your accounts on remote instances for easier use') | ||
60 | .option('-u, --url <url>', 'Server url') | ||
61 | .option('-U, --username <username>', 'Username') | ||
62 | .option('-p, --password <token>', 'Password') | ||
63 | .option('--default', 'add the entry as the new default') | ||
64 | .action((options: OptionValues) => { | ||
65 | /* eslint-disable no-import-assign */ | ||
66 | prompt.override = options | ||
67 | prompt.start() | ||
68 | prompt.get({ | ||
69 | properties: { | ||
70 | url: { | ||
71 | description: 'instance url', | ||
72 | conform: (value) => isURLaPeerTubeInstance(value), | ||
73 | message: 'It should be an URL (https://peertube.example.com)', | ||
74 | required: true | ||
75 | }, | ||
76 | username: { | ||
77 | conform: (value) => isUserUsernameValid(value), | ||
78 | message: 'Name must be only letters, spaces, or dashes', | ||
79 | required: true | ||
80 | }, | ||
81 | password: { | ||
82 | hidden: true, | ||
83 | replace: '*', | ||
84 | required: true | ||
85 | } | ||
86 | } | ||
87 | }, async (_, result) => { | ||
88 | |||
89 | // Check credentials | ||
90 | try { | ||
91 | // Strip out everything after the domain:port. | ||
92 | // See https://github.com/Chocobozzz/PeerTube/issues/3520 | ||
93 | result.url = stripExtraneousFromPeerTubeUrl(result.url) | ||
94 | |||
95 | const server = buildServer(result.url) | ||
96 | await assignToken(server, result.username, result.password) | ||
97 | } catch (err) { | ||
98 | console.error(err.message) | ||
99 | process.exit(-1) | ||
100 | } | ||
101 | |||
102 | await setInstance(result.url, result.username, result.password, options.default) | ||
103 | |||
104 | process.exit(0) | ||
105 | }) | ||
106 | }) | ||
107 | |||
108 | program | ||
109 | .command('del <url>') | ||
110 | .description('unregisters a remote instance') | ||
111 | .action(async url => { | ||
112 | await delInstance(url) | ||
113 | |||
114 | process.exit(0) | ||
115 | }) | ||
116 | |||
117 | program | ||
118 | .command('list') | ||
119 | .description('lists registered remote instances') | ||
120 | .action(async () => { | ||
121 | const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ]) | ||
122 | |||
123 | const table = new CliTable3({ | ||
124 | head: [ 'instance', 'login' ], | ||
125 | colWidths: [ 30, 30 ] | ||
126 | }) as any | ||
127 | |||
128 | settings.remotes.forEach(element => { | ||
129 | if (!netrc.machines[element]) return | ||
130 | |||
131 | table.push([ | ||
132 | element, | ||
133 | netrc.machines[element].login | ||
134 | ]) | ||
135 | }) | ||
136 | |||
137 | console.log(table.toString()) | ||
138 | |||
139 | process.exit(0) | ||
140 | }) | ||
141 | |||
142 | program | ||
143 | .command('set-default <url>') | ||
144 | .description('set an existing entry as default') | ||
145 | .action(async url => { | ||
146 | const settings = await getSettings() | ||
147 | const instanceExists = settings.remotes.includes(url) | ||
148 | |||
149 | if (instanceExists) { | ||
150 | settings.default = settings.remotes.indexOf(url) | ||
151 | await writeSettings(settings) | ||
152 | |||
153 | process.exit(0) | ||
154 | } else { | ||
155 | console.log('<url> is not a registered instance.') | ||
156 | process.exit(-1) | ||
157 | } | ||
158 | }) | ||
159 | |||
160 | program.addHelpText('after', '\n\n Examples:\n\n' + | ||
161 | ' $ peertube auth add -u https://peertube.cpy.re -U "PEERTUBE_USER" --password "PEERTUBE_PASSWORD"\n' + | ||
162 | ' $ peertube auth add -u https://peertube.cpy.re -U root\n' + | ||
163 | ' $ peertube auth list\n' + | ||
164 | ' $ peertube auth del https://peertube.cpy.re\n' | ||
165 | ) | ||
166 | |||
167 | if (!process.argv.slice(2).length) { | ||
168 | program.outputHelp() | ||
169 | } | ||
170 | |||
171 | program.parse(process.argv) | ||
diff --git a/server/tools/peertube-get-access-token.ts b/server/tools/peertube-get-access-token.ts deleted file mode 100644 index 71a4826e8..000000000 --- a/server/tools/peertube-get-access-token.ts +++ /dev/null | |||
@@ -1,34 +0,0 @@ | |||
1 | import { program } from 'commander' | ||
2 | import { assignToken, buildServer } from './shared' | ||
3 | |||
4 | program | ||
5 | .option('-u, --url <url>', 'Server url') | ||
6 | .option('-n, --username <username>', 'Username') | ||
7 | .option('-p, --password <token>', 'Password') | ||
8 | .parse(process.argv) | ||
9 | |||
10 | const options = program.opts() | ||
11 | |||
12 | if ( | ||
13 | !options.url || | ||
14 | !options.username || | ||
15 | !options.password | ||
16 | ) { | ||
17 | if (!options.url) console.error('--url field is required.') | ||
18 | if (!options.username) console.error('--username field is required.') | ||
19 | if (!options.password) console.error('--password field is required.') | ||
20 | |||
21 | process.exit(-1) | ||
22 | } | ||
23 | |||
24 | const server = buildServer(options.url) | ||
25 | |||
26 | assignToken(server, options.username, options.password) | ||
27 | .then(() => { | ||
28 | console.log(server.accessToken) | ||
29 | process.exit(0) | ||
30 | }) | ||
31 | .catch(err => { | ||
32 | console.error(err) | ||
33 | process.exit(-1) | ||
34 | }) | ||
diff --git a/server/tools/peertube-import-videos.ts b/server/tools/peertube-import-videos.ts deleted file mode 100644 index bbdaa09c0..000000000 --- a/server/tools/peertube-import-videos.ts +++ /dev/null | |||
@@ -1,351 +0,0 @@ | |||
1 | import { program } from 'commander' | ||
2 | import { accessSync, constants } from 'fs' | ||
3 | import { remove } from 'fs-extra' | ||
4 | import { join } from 'path' | ||
5 | import { YoutubeDLCLI, YoutubeDLInfo, YoutubeDLInfoBuilder } from '@server/helpers/youtube-dl' | ||
6 | import { wait } from '@shared/core-utils' | ||
7 | import { sha256 } from '@shared/extra-utils' | ||
8 | import { doRequestAndSaveToFile } from '../helpers/requests' | ||
9 | import { | ||
10 | assignToken, | ||
11 | buildCommonVideoOptions, | ||
12 | buildServer, | ||
13 | buildVideoAttributesFromCommander, | ||
14 | getLogger, | ||
15 | getServerCredentials | ||
16 | } from './shared' | ||
17 | |||
18 | import prompt = require('prompt') | ||
19 | |||
20 | const processOptions = { | ||
21 | maxBuffer: Infinity | ||
22 | } | ||
23 | |||
24 | let command = program | ||
25 | .name('import-videos') | ||
26 | |||
27 | command = buildCommonVideoOptions(command) | ||
28 | |||
29 | command | ||
30 | .option('-u, --url <url>', 'Server url') | ||
31 | .option('-U, --username <username>', 'Username') | ||
32 | .option('-p, --password <token>', 'Password') | ||
33 | .option('--target-url <targetUrl>', 'Video target URL') | ||
34 | .option('--since <since>', 'Publication date (inclusive) since which the videos can be imported (YYYY-MM-DD)', parseDate) | ||
35 | .option('--until <until>', 'Publication date (inclusive) until which the videos can be imported (YYYY-MM-DD)', parseDate) | ||
36 | .option('--first <first>', 'Process first n elements of returned playlist') | ||
37 | .option('--last <last>', 'Process last n elements of returned playlist') | ||
38 | .option('--wait-interval <waitInterval>', 'Duration between two video imports (in seconds)', convertIntoMs) | ||
39 | .option('-T, --tmpdir <tmpdir>', 'Working directory', __dirname) | ||
40 | .usage('[global options] [ -- youtube-dl options]') | ||
41 | .parse(process.argv) | ||
42 | |||
43 | const options = command.opts() | ||
44 | |||
45 | const log = getLogger(options.verbose) | ||
46 | |||
47 | getServerCredentials(command) | ||
48 | .then(({ url, username, password }) => { | ||
49 | if (!options.targetUrl) { | ||
50 | exitError('--target-url field is required.') | ||
51 | } | ||
52 | |||
53 | try { | ||
54 | accessSync(options.tmpdir, constants.R_OK | constants.W_OK) | ||
55 | } catch (e) { | ||
56 | exitError('--tmpdir %s: directory does not exist or is not accessible', options.tmpdir) | ||
57 | } | ||
58 | |||
59 | url = normalizeTargetUrl(url) | ||
60 | options.targetUrl = normalizeTargetUrl(options.targetUrl) | ||
61 | |||
62 | run(url, username, password) | ||
63 | .catch(err => exitError(err)) | ||
64 | }) | ||
65 | .catch(err => console.error(err)) | ||
66 | |||
67 | async function run (url: string, username: string, password: string) { | ||
68 | if (!password) password = await promptPassword() | ||
69 | |||
70 | const youtubeDLBinary = await YoutubeDLCLI.safeGet() | ||
71 | |||
72 | let info = await getYoutubeDLInfo(youtubeDLBinary, options.targetUrl, command.args) | ||
73 | |||
74 | if (!Array.isArray(info)) info = [ info ] | ||
75 | |||
76 | // Try to fix youtube channels upload | ||
77 | const uploadsObject = info.find(i => !i.ie_key && !i.duration && i.title === 'Uploads') | ||
78 | |||
79 | if (uploadsObject) { | ||
80 | console.log('Fixing URL to %s.', uploadsObject.url) | ||
81 | |||
82 | info = await getYoutubeDLInfo(youtubeDLBinary, uploadsObject.url, command.args) | ||
83 | } | ||
84 | |||
85 | let infoArray: any[] | ||
86 | |||
87 | infoArray = [].concat(info) | ||
88 | if (options.first) { | ||
89 | infoArray = infoArray.slice(0, options.first) | ||
90 | } else if (options.last) { | ||
91 | infoArray = infoArray.slice(-options.last) | ||
92 | } | ||
93 | |||
94 | log.info('Will download and upload %d videos.\n', infoArray.length) | ||
95 | |||
96 | let skipInterval = true | ||
97 | for (const [ index, info ] of infoArray.entries()) { | ||
98 | try { | ||
99 | if (index > 0 && options.waitInterval && !skipInterval) { | ||
100 | log.info('Wait for %d seconds before continuing.', options.waitInterval / 1000) | ||
101 | await wait(options.waitInterval) | ||
102 | } | ||
103 | |||
104 | skipInterval = await processVideo({ | ||
105 | cwd: options.tmpdir, | ||
106 | url, | ||
107 | username, | ||
108 | password, | ||
109 | youtubeInfo: info | ||
110 | }) | ||
111 | } catch (err) { | ||
112 | console.error('Cannot process video.', { info, url, err }) | ||
113 | } | ||
114 | } | ||
115 | |||
116 | log.info('Video/s for user %s imported: %s', username, options.targetUrl) | ||
117 | process.exit(0) | ||
118 | } | ||
119 | |||
120 | async function processVideo (parameters: { | ||
121 | cwd: string | ||
122 | url: string | ||
123 | username: string | ||
124 | password: string | ||
125 | youtubeInfo: any | ||
126 | }) { | ||
127 | const { youtubeInfo, cwd, url, username, password } = parameters | ||
128 | |||
129 | log.debug('Fetching object.', youtubeInfo) | ||
130 | |||
131 | const videoInfo = await fetchObject(youtubeInfo) | ||
132 | log.debug('Fetched object.', videoInfo) | ||
133 | |||
134 | if ( | ||
135 | options.since && | ||
136 | videoInfo.originallyPublishedAtWithoutTime && | ||
137 | videoInfo.originallyPublishedAtWithoutTime.getTime() < options.since.getTime() | ||
138 | ) { | ||
139 | log.info('Video "%s" has been published before "%s", don\'t upload it.\n', videoInfo.name, formatDate(options.since)) | ||
140 | return true | ||
141 | } | ||
142 | |||
143 | if ( | ||
144 | options.until && | ||
145 | videoInfo.originallyPublishedAtWithoutTime && | ||
146 | videoInfo.originallyPublishedAtWithoutTime.getTime() > options.until.getTime() | ||
147 | ) { | ||
148 | log.info('Video "%s" has been published after "%s", don\'t upload it.\n', videoInfo.name, formatDate(options.until)) | ||
149 | return true | ||
150 | } | ||
151 | |||
152 | const server = buildServer(url) | ||
153 | const { data } = await server.search.advancedVideoSearch({ | ||
154 | search: { | ||
155 | search: videoInfo.name, | ||
156 | sort: '-match', | ||
157 | searchTarget: 'local' | ||
158 | } | ||
159 | }) | ||
160 | |||
161 | log.info('############################################################\n') | ||
162 | |||
163 | if (data.find(v => v.name === videoInfo.name)) { | ||
164 | log.info('Video "%s" already exists, don\'t reupload it.\n', videoInfo.name) | ||
165 | return true | ||
166 | } | ||
167 | |||
168 | const path = join(cwd, sha256(videoInfo.url) + '.mp4') | ||
169 | |||
170 | log.info('Downloading video "%s"...', videoInfo.name) | ||
171 | |||
172 | try { | ||
173 | const youtubeDLBinary = await YoutubeDLCLI.safeGet() | ||
174 | const output = await youtubeDLBinary.download({ | ||
175 | url: videoInfo.url, | ||
176 | format: YoutubeDLCLI.getYoutubeDLVideoFormat([], false), | ||
177 | output: path, | ||
178 | additionalYoutubeDLArgs: command.args, | ||
179 | processOptions | ||
180 | }) | ||
181 | |||
182 | log.info(output.join('\n')) | ||
183 | await uploadVideoOnPeerTube({ | ||
184 | cwd, | ||
185 | url, | ||
186 | username, | ||
187 | password, | ||
188 | videoInfo, | ||
189 | videoPath: path | ||
190 | }) | ||
191 | } catch (err) { | ||
192 | log.error(err.message) | ||
193 | } | ||
194 | |||
195 | return false | ||
196 | } | ||
197 | |||
198 | async function uploadVideoOnPeerTube (parameters: { | ||
199 | videoInfo: YoutubeDLInfo | ||
200 | videoPath: string | ||
201 | cwd: string | ||
202 | url: string | ||
203 | username: string | ||
204 | password: string | ||
205 | }) { | ||
206 | const { videoInfo, videoPath, cwd, url, username, password } = parameters | ||
207 | |||
208 | const server = buildServer(url) | ||
209 | await assignToken(server, username, password) | ||
210 | |||
211 | let thumbnailfile: string | ||
212 | if (videoInfo.thumbnailUrl) { | ||
213 | thumbnailfile = join(cwd, sha256(videoInfo.thumbnailUrl) + '.jpg') | ||
214 | |||
215 | await doRequestAndSaveToFile(videoInfo.thumbnailUrl, thumbnailfile) | ||
216 | } | ||
217 | |||
218 | const baseAttributes = await buildVideoAttributesFromCommander(server, program, videoInfo) | ||
219 | |||
220 | const attributes = { | ||
221 | ...baseAttributes, | ||
222 | |||
223 | originallyPublishedAtWithoutTime: videoInfo.originallyPublishedAtWithoutTime | ||
224 | ? videoInfo.originallyPublishedAtWithoutTime.toISOString() | ||
225 | : null, | ||
226 | |||
227 | thumbnailfile, | ||
228 | previewfile: thumbnailfile, | ||
229 | fixture: videoPath | ||
230 | } | ||
231 | |||
232 | log.info('\nUploading on PeerTube video "%s".', attributes.name) | ||
233 | |||
234 | try { | ||
235 | await server.videos.upload({ attributes }) | ||
236 | } catch (err) { | ||
237 | if (err.message.indexOf('401') !== -1) { | ||
238 | log.info('Got 401 Unauthorized, token may have expired, renewing token and retry.') | ||
239 | |||
240 | server.accessToken = await server.login.getAccessToken(username, password) | ||
241 | |||
242 | await server.videos.upload({ attributes }) | ||
243 | } else { | ||
244 | exitError(err.message) | ||
245 | } | ||
246 | } | ||
247 | |||
248 | await remove(videoPath) | ||
249 | if (thumbnailfile) await remove(thumbnailfile) | ||
250 | |||
251 | log.info('Uploaded video "%s"!\n', attributes.name) | ||
252 | } | ||
253 | |||
254 | /* ---------------------------------------------------------- */ | ||
255 | |||
256 | async function fetchObject (info: any) { | ||
257 | const url = buildUrl(info) | ||
258 | |||
259 | const youtubeDLCLI = await YoutubeDLCLI.safeGet() | ||
260 | const result = await youtubeDLCLI.getInfo({ | ||
261 | url, | ||
262 | format: YoutubeDLCLI.getYoutubeDLVideoFormat([], false), | ||
263 | processOptions | ||
264 | }) | ||
265 | |||
266 | const builder = new YoutubeDLInfoBuilder(result) | ||
267 | |||
268 | const videoInfo = builder.getInfo() | ||
269 | |||
270 | return { ...videoInfo, url } | ||
271 | } | ||
272 | |||
273 | function buildUrl (info: any) { | ||
274 | const webpageUrl = info.webpage_url as string | ||
275 | if (webpageUrl?.match(/^https?:\/\//)) return webpageUrl | ||
276 | |||
277 | const url = info.url as string | ||
278 | if (url?.match(/^https?:\/\//)) return url | ||
279 | |||
280 | // It seems youtube-dl does not return the video url | ||
281 | return 'https://www.youtube.com/watch?v=' + info.id | ||
282 | } | ||
283 | |||
284 | function normalizeTargetUrl (url: string) { | ||
285 | let normalizedUrl = url.replace(/\/+$/, '') | ||
286 | |||
287 | if (!normalizedUrl.startsWith('http://') && !normalizedUrl.startsWith('https://')) { | ||
288 | normalizedUrl = 'https://' + normalizedUrl | ||
289 | } | ||
290 | |||
291 | return normalizedUrl | ||
292 | } | ||
293 | |||
294 | async function promptPassword () { | ||
295 | return new Promise<string>((res, rej) => { | ||
296 | prompt.start() | ||
297 | const schema = { | ||
298 | properties: { | ||
299 | password: { | ||
300 | hidden: true, | ||
301 | required: true | ||
302 | } | ||
303 | } | ||
304 | } | ||
305 | prompt.get(schema, function (err, result) { | ||
306 | if (err) { | ||
307 | return rej(err) | ||
308 | } | ||
309 | return res(result.password) | ||
310 | }) | ||
311 | }) | ||
312 | } | ||
313 | |||
314 | function parseDate (dateAsStr: string): Date { | ||
315 | if (!/\d{4}-\d{2}-\d{2}/.test(dateAsStr)) { | ||
316 | exitError(`Invalid date passed: ${dateAsStr}. Expected format: YYYY-MM-DD. See help for usage.`) | ||
317 | } | ||
318 | const date = new Date(dateAsStr) | ||
319 | date.setHours(0, 0, 0) | ||
320 | if (isNaN(date.getTime())) { | ||
321 | exitError(`Invalid date passed: ${dateAsStr}. See help for usage.`) | ||
322 | } | ||
323 | return date | ||
324 | } | ||
325 | |||
326 | function formatDate (date: Date): string { | ||
327 | return date.toISOString().split('T')[0] | ||
328 | } | ||
329 | |||
330 | function convertIntoMs (secondsAsStr: string): number { | ||
331 | const seconds = parseInt(secondsAsStr, 10) | ||
332 | if (seconds <= 0) { | ||
333 | exitError(`Invalid duration passed: ${seconds}. Expected duration to be strictly positive and in seconds`) | ||
334 | } | ||
335 | return Math.round(seconds * 1000) | ||
336 | } | ||
337 | |||
338 | function exitError (message: string, ...meta: any[]) { | ||
339 | // use console.error instead of log.error here | ||
340 | console.error(message, ...meta) | ||
341 | process.exit(-1) | ||
342 | } | ||
343 | |||
344 | function getYoutubeDLInfo (youtubeDLCLI: YoutubeDLCLI, url: string, args: string[]) { | ||
345 | return youtubeDLCLI.getInfo({ | ||
346 | url, | ||
347 | format: YoutubeDLCLI.getYoutubeDLVideoFormat([], false), | ||
348 | additionalYoutubeDLArgs: [ '-j', '--flat-playlist', '--playlist-reverse', ...args ], | ||
349 | processOptions | ||
350 | }) | ||
351 | } | ||
diff --git a/server/tools/peertube-plugins.ts b/server/tools/peertube-plugins.ts deleted file mode 100644 index 0660c855f..000000000 --- a/server/tools/peertube-plugins.ts +++ /dev/null | |||
@@ -1,165 +0,0 @@ | |||
1 | import CliTable3 from 'cli-table3' | ||
2 | import { Command, OptionValues, program } from 'commander' | ||
3 | import { isAbsolute } from 'path' | ||
4 | import { PluginType } from '../../shared/models' | ||
5 | import { assignToken, buildServer, getServerCredentials } from './shared' | ||
6 | |||
7 | program | ||
8 | .name('plugins') | ||
9 | .usage('[command] [options]') | ||
10 | |||
11 | program | ||
12 | .command('list') | ||
13 | .description('List installed plugins') | ||
14 | .option('-u, --url <url>', 'Server url') | ||
15 | .option('-U, --username <username>', 'Username') | ||
16 | .option('-p, --password <token>', 'Password') | ||
17 | .option('-t, --only-themes', 'List themes only') | ||
18 | .option('-P, --only-plugins', 'List plugins only') | ||
19 | .action((options, command) => pluginsListCLI(command, options)) | ||
20 | |||
21 | program | ||
22 | .command('install') | ||
23 | .description('Install a plugin or a theme') | ||
24 | .option('-u, --url <url>', 'Server url') | ||
25 | .option('-U, --username <username>', 'Username') | ||
26 | .option('-p, --password <token>', 'Password') | ||
27 | .option('-P --path <path>', 'Install from a path') | ||
28 | .option('-n, --npm-name <npmName>', 'Install from npm') | ||
29 | .option('--plugin-version <pluginVersion>', 'Specify the plugin version to install (only available when installing from npm)') | ||
30 | .action((options, command) => installPluginCLI(command, options)) | ||
31 | |||
32 | program | ||
33 | .command('update') | ||
34 | .description('Update a plugin or a theme') | ||
35 | .option('-u, --url <url>', 'Server url') | ||
36 | .option('-U, --username <username>', 'Username') | ||
37 | .option('-p, --password <token>', 'Password') | ||
38 | .option('-P --path <path>', 'Update from a path') | ||
39 | .option('-n, --npm-name <npmName>', 'Update from npm') | ||
40 | .action((options, command) => updatePluginCLI(command, options)) | ||
41 | |||
42 | program | ||
43 | .command('uninstall') | ||
44 | .description('Uninstall a plugin or a theme') | ||
45 | .option('-u, --url <url>', 'Server url') | ||
46 | .option('-U, --username <username>', 'Username') | ||
47 | .option('-p, --password <token>', 'Password') | ||
48 | .option('-n, --npm-name <npmName>', 'NPM plugin/theme name') | ||
49 | .action((options, command) => uninstallPluginCLI(command, options)) | ||
50 | |||
51 | if (!process.argv.slice(2).length) { | ||
52 | program.outputHelp() | ||
53 | } | ||
54 | |||
55 | program.parse(process.argv) | ||
56 | |||
57 | // ---------------------------------------------------------------------------- | ||
58 | |||
59 | async function pluginsListCLI (command: Command, options: OptionValues) { | ||
60 | const { url, username, password } = await getServerCredentials(command) | ||
61 | const server = buildServer(url) | ||
62 | await assignToken(server, username, password) | ||
63 | |||
64 | let pluginType: PluginType | ||
65 | if (options.onlyThemes) pluginType = PluginType.THEME | ||
66 | if (options.onlyPlugins) pluginType = PluginType.PLUGIN | ||
67 | |||
68 | const { data } = await server.plugins.list({ start: 0, count: 100, sort: 'name', pluginType }) | ||
69 | |||
70 | const table = new CliTable3({ | ||
71 | head: [ 'name', 'version', 'homepage' ], | ||
72 | colWidths: [ 50, 20, 50 ] | ||
73 | }) as any | ||
74 | |||
75 | for (const plugin of data) { | ||
76 | const npmName = plugin.type === PluginType.PLUGIN | ||
77 | ? 'peertube-plugin-' + plugin.name | ||
78 | : 'peertube-theme-' + plugin.name | ||
79 | |||
80 | table.push([ | ||
81 | npmName, | ||
82 | plugin.version, | ||
83 | plugin.homepage | ||
84 | ]) | ||
85 | } | ||
86 | |||
87 | console.log(table.toString()) | ||
88 | process.exit(0) | ||
89 | } | ||
90 | |||
91 | async function installPluginCLI (command: Command, options: OptionValues) { | ||
92 | if (!options.path && !options.npmName) { | ||
93 | console.error('You need to specify the npm name or the path of the plugin you want to install.\n') | ||
94 | program.outputHelp() | ||
95 | process.exit(-1) | ||
96 | } | ||
97 | |||
98 | if (options.path && !isAbsolute(options.path)) { | ||
99 | console.error('Path should be absolute.') | ||
100 | process.exit(-1) | ||
101 | } | ||
102 | |||
103 | const { url, username, password } = await getServerCredentials(command) | ||
104 | const server = buildServer(url) | ||
105 | await assignToken(server, username, password) | ||
106 | |||
107 | try { | ||
108 | await server.plugins.install({ npmName: options.npmName, path: options.path, pluginVersion: options.pluginVersion }) | ||
109 | } catch (err) { | ||
110 | console.error('Cannot install plugin.', err) | ||
111 | process.exit(-1) | ||
112 | } | ||
113 | |||
114 | console.log('Plugin installed.') | ||
115 | process.exit(0) | ||
116 | } | ||
117 | |||
118 | async function updatePluginCLI (command: Command, options: OptionValues) { | ||
119 | if (!options.path && !options.npmName) { | ||
120 | console.error('You need to specify the npm name or the path of the plugin you want to update.\n') | ||
121 | program.outputHelp() | ||
122 | process.exit(-1) | ||
123 | } | ||
124 | |||
125 | if (options.path && !isAbsolute(options.path)) { | ||
126 | console.error('Path should be absolute.') | ||
127 | process.exit(-1) | ||
128 | } | ||
129 | |||
130 | const { url, username, password } = await getServerCredentials(command) | ||
131 | const server = buildServer(url) | ||
132 | await assignToken(server, username, password) | ||
133 | |||
134 | try { | ||
135 | await server.plugins.update({ npmName: options.npmName, path: options.path }) | ||
136 | } catch (err) { | ||
137 | console.error('Cannot update plugin.', err) | ||
138 | process.exit(-1) | ||
139 | } | ||
140 | |||
141 | console.log('Plugin updated.') | ||
142 | process.exit(0) | ||
143 | } | ||
144 | |||
145 | async function uninstallPluginCLI (command: Command, options: OptionValues) { | ||
146 | if (!options.npmName) { | ||
147 | console.error('You need to specify the npm name of the plugin/theme you want to uninstall.\n') | ||
148 | program.outputHelp() | ||
149 | process.exit(-1) | ||
150 | } | ||
151 | |||
152 | const { url, username, password } = await getServerCredentials(command) | ||
153 | const server = buildServer(url) | ||
154 | await assignToken(server, username, password) | ||
155 | |||
156 | try { | ||
157 | await server.plugins.uninstall({ npmName: options.npmName }) | ||
158 | } catch (err) { | ||
159 | console.error('Cannot uninstall plugin.', err) | ||
160 | process.exit(-1) | ||
161 | } | ||
162 | |||
163 | console.log('Plugin uninstalled.') | ||
164 | process.exit(0) | ||
165 | } | ||
diff --git a/server/tools/peertube-redundancy.ts b/server/tools/peertube-redundancy.ts deleted file mode 100644 index c24eb5233..000000000 --- a/server/tools/peertube-redundancy.ts +++ /dev/null | |||
@@ -1,172 +0,0 @@ | |||
1 | import CliTable3 from 'cli-table3' | ||
2 | import { Command, program } from 'commander' | ||
3 | import { URL } from 'url' | ||
4 | import validator from 'validator' | ||
5 | import { forceNumber, uniqify } from '@shared/core-utils' | ||
6 | import { HttpStatusCode, VideoRedundanciesTarget } from '@shared/models' | ||
7 | import { assignToken, buildServer, getServerCredentials } from './shared' | ||
8 | |||
9 | import bytes = require('bytes') | ||
10 | program | ||
11 | .name('redundancy') | ||
12 | .usage('[command] [options]') | ||
13 | |||
14 | program | ||
15 | .command('list-remote-redundancies') | ||
16 | .description('List remote redundancies on your videos') | ||
17 | .option('-u, --url <url>', 'Server url') | ||
18 | .option('-U, --username <username>', 'Username') | ||
19 | .option('-p, --password <token>', 'Password') | ||
20 | .action(() => listRedundanciesCLI('my-videos')) | ||
21 | |||
22 | program | ||
23 | .command('list-my-redundancies') | ||
24 | .description('List your redundancies of remote videos') | ||
25 | .option('-u, --url <url>', 'Server url') | ||
26 | .option('-U, --username <username>', 'Username') | ||
27 | .option('-p, --password <token>', 'Password') | ||
28 | .action(() => listRedundanciesCLI('remote-videos')) | ||
29 | |||
30 | program | ||
31 | .command('add') | ||
32 | .description('Duplicate a video in your redundancy system') | ||
33 | .option('-u, --url <url>', 'Server url') | ||
34 | .option('-U, --username <username>', 'Username') | ||
35 | .option('-p, --password <token>', 'Password') | ||
36 | .option('-v, --video <videoId>', 'Video id to duplicate') | ||
37 | .action((options, command) => addRedundancyCLI(options, command)) | ||
38 | |||
39 | program | ||
40 | .command('remove') | ||
41 | .description('Remove a video from your redundancies') | ||
42 | .option('-u, --url <url>', 'Server url') | ||
43 | .option('-U, --username <username>', 'Username') | ||
44 | .option('-p, --password <token>', 'Password') | ||
45 | .option('-v, --video <videoId>', 'Video id to remove from redundancies') | ||
46 | .action((options, command) => removeRedundancyCLI(options, command)) | ||
47 | |||
48 | if (!process.argv.slice(2).length) { | ||
49 | program.outputHelp() | ||
50 | } | ||
51 | |||
52 | program.parse(process.argv) | ||
53 | |||
54 | // ---------------------------------------------------------------------------- | ||
55 | |||
56 | async function listRedundanciesCLI (target: VideoRedundanciesTarget) { | ||
57 | const { url, username, password } = await getServerCredentials(program) | ||
58 | const server = buildServer(url) | ||
59 | await assignToken(server, username, password) | ||
60 | |||
61 | const { data } = await server.redundancy.listVideos({ start: 0, count: 100, sort: 'name', target }) | ||
62 | |||
63 | const table = new CliTable3({ | ||
64 | head: [ 'video id', 'video name', 'video url', 'files', 'playlists', 'by instances', 'total size' ] | ||
65 | }) as any | ||
66 | |||
67 | for (const redundancy of data) { | ||
68 | const webVideoFiles = redundancy.redundancies.files | ||
69 | const streamingPlaylists = redundancy.redundancies.streamingPlaylists | ||
70 | |||
71 | let totalSize = '' | ||
72 | if (target === 'remote-videos') { | ||
73 | const tmp = webVideoFiles.concat(streamingPlaylists) | ||
74 | .reduce((a, b) => a + b.size, 0) | ||
75 | |||
76 | totalSize = bytes(tmp) | ||
77 | } | ||
78 | |||
79 | const instances = uniqify( | ||
80 | webVideoFiles.concat(streamingPlaylists) | ||
81 | .map(r => r.fileUrl) | ||
82 | .map(u => new URL(u).host) | ||
83 | ) | ||
84 | |||
85 | table.push([ | ||
86 | redundancy.id.toString(), | ||
87 | redundancy.name, | ||
88 | redundancy.url, | ||
89 | webVideoFiles.length, | ||
90 | streamingPlaylists.length, | ||
91 | instances.join('\n'), | ||
92 | totalSize | ||
93 | ]) | ||
94 | } | ||
95 | |||
96 | console.log(table.toString()) | ||
97 | process.exit(0) | ||
98 | } | ||
99 | |||
100 | async function addRedundancyCLI (options: { video: number }, command: Command) { | ||
101 | const { url, username, password } = await getServerCredentials(command) | ||
102 | const server = buildServer(url) | ||
103 | await assignToken(server, username, password) | ||
104 | |||
105 | if (!options.video || validator.isInt('' + options.video) === false) { | ||
106 | console.error('You need to specify the video id to duplicate and it should be a number.\n') | ||
107 | command.outputHelp() | ||
108 | process.exit(-1) | ||
109 | } | ||
110 | |||
111 | try { | ||
112 | await server.redundancy.addVideo({ videoId: options.video }) | ||
113 | |||
114 | console.log('Video will be duplicated by your instance!') | ||
115 | |||
116 | process.exit(0) | ||
117 | } catch (err) { | ||
118 | if (err.message.includes(HttpStatusCode.CONFLICT_409)) { | ||
119 | console.error('This video is already duplicated by your instance.') | ||
120 | } else if (err.message.includes(HttpStatusCode.NOT_FOUND_404)) { | ||
121 | console.error('This video id does not exist.') | ||
122 | } else { | ||
123 | console.error(err) | ||
124 | } | ||
125 | |||
126 | process.exit(-1) | ||
127 | } | ||
128 | } | ||
129 | |||
130 | async function removeRedundancyCLI (options: { video: number }, command: Command) { | ||
131 | const { url, username, password } = await getServerCredentials(command) | ||
132 | const server = buildServer(url) | ||
133 | await assignToken(server, username, password) | ||
134 | |||
135 | if (!options.video || validator.isInt('' + options.video) === false) { | ||
136 | console.error('You need to specify the video id to remove from your redundancies.\n') | ||
137 | command.outputHelp() | ||
138 | process.exit(-1) | ||
139 | } | ||
140 | |||
141 | const videoId = forceNumber(options.video) | ||
142 | |||
143 | const myVideoRedundancies = await server.redundancy.listVideos({ target: 'my-videos' }) | ||
144 | let videoRedundancy = myVideoRedundancies.data.find(r => videoId === r.id) | ||
145 | |||
146 | if (!videoRedundancy) { | ||
147 | const remoteVideoRedundancies = await server.redundancy.listVideos({ target: 'remote-videos' }) | ||
148 | videoRedundancy = remoteVideoRedundancies.data.find(r => videoId === r.id) | ||
149 | } | ||
150 | |||
151 | if (!videoRedundancy) { | ||
152 | console.error('Video redundancy not found.') | ||
153 | process.exit(-1) | ||
154 | } | ||
155 | |||
156 | try { | ||
157 | const ids = videoRedundancy.redundancies.files | ||
158 | .concat(videoRedundancy.redundancies.streamingPlaylists) | ||
159 | .map(r => r.id) | ||
160 | |||
161 | for (const id of ids) { | ||
162 | await server.redundancy.removeVideo({ redundancyId: id }) | ||
163 | } | ||
164 | |||
165 | console.log('Video redundancy removed!') | ||
166 | |||
167 | process.exit(0) | ||
168 | } catch (err) { | ||
169 | console.error(err) | ||
170 | process.exit(-1) | ||
171 | } | ||
172 | } | ||
diff --git a/server/tools/peertube-upload.ts b/server/tools/peertube-upload.ts deleted file mode 100644 index 87da55005..000000000 --- a/server/tools/peertube-upload.ts +++ /dev/null | |||
@@ -1,77 +0,0 @@ | |||
1 | import { program } from 'commander' | ||
2 | import { access, constants } from 'fs-extra' | ||
3 | import { isAbsolute } from 'path' | ||
4 | import { assignToken, buildCommonVideoOptions, buildServer, buildVideoAttributesFromCommander, getServerCredentials } from './shared' | ||
5 | |||
6 | let command = program | ||
7 | .name('upload') | ||
8 | |||
9 | command = buildCommonVideoOptions(command) | ||
10 | |||
11 | command | ||
12 | .option('-u, --url <url>', 'Server url') | ||
13 | .option('-U, --username <username>', 'Username') | ||
14 | .option('-p, --password <token>', 'Password') | ||
15 | .option('-b, --thumbnail <thumbnailPath>', 'Thumbnail path') | ||
16 | .option('-v, --preview <previewPath>', 'Preview path') | ||
17 | .option('-f, --file <file>', 'Video absolute file path') | ||
18 | .parse(process.argv) | ||
19 | |||
20 | const options = command.opts() | ||
21 | |||
22 | getServerCredentials(command) | ||
23 | .then(({ url, username, password }) => { | ||
24 | if (!options.videoName || !options.file) { | ||
25 | if (!options.videoName) console.error('--video-name is required.') | ||
26 | if (!options.file) console.error('--file is required.') | ||
27 | |||
28 | process.exit(-1) | ||
29 | } | ||
30 | |||
31 | if (isAbsolute(options.file) === false) { | ||
32 | console.error('File path should be absolute.') | ||
33 | process.exit(-1) | ||
34 | } | ||
35 | |||
36 | run(url, username, password).catch(err => { | ||
37 | console.error(err) | ||
38 | process.exit(-1) | ||
39 | }) | ||
40 | }) | ||
41 | .catch(err => console.error(err)) | ||
42 | |||
43 | async function run (url: string, username: string, password: string) { | ||
44 | const server = buildServer(url) | ||
45 | await assignToken(server, username, password) | ||
46 | |||
47 | await access(options.file, constants.F_OK) | ||
48 | |||
49 | console.log('Uploading %s video...', options.videoName) | ||
50 | |||
51 | const baseAttributes = await buildVideoAttributesFromCommander(server, program) | ||
52 | |||
53 | const attributes = { | ||
54 | ...baseAttributes, | ||
55 | |||
56 | fixture: options.file, | ||
57 | thumbnailfile: options.thumbnail, | ||
58 | previewfile: options.preview | ||
59 | } | ||
60 | |||
61 | try { | ||
62 | await server.videos.upload({ attributes }) | ||
63 | console.log(`Video ${options.videoName} uploaded.`) | ||
64 | process.exit(0) | ||
65 | } catch (err) { | ||
66 | const message = err.message || '' | ||
67 | if (message.includes('413')) { | ||
68 | console.error('Aborted: user quota is exceeded or video file is too big for this PeerTube instance.') | ||
69 | } else { | ||
70 | console.error(require('util').inspect(err)) | ||
71 | } | ||
72 | |||
73 | process.exit(-1) | ||
74 | } | ||
75 | } | ||
76 | |||
77 | // ---------------------------------------------------------------------------- | ||
diff --git a/server/tools/peertube.ts b/server/tools/peertube.ts deleted file mode 100644 index b79917b4f..000000000 --- a/server/tools/peertube.ts +++ /dev/null | |||
@@ -1,72 +0,0 @@ | |||
1 | #!/usr/bin/env node | ||
2 | |||
3 | import { CommandOptions, program } from 'commander' | ||
4 | import { getSettings, version } from './shared' | ||
5 | |||
6 | program | ||
7 | .version(version, '-v, --version') | ||
8 | .usage('[command] [options]') | ||
9 | |||
10 | /* Subcommands automatically loaded in the directory and beginning by peertube-* */ | ||
11 | program | ||
12 | .command('auth [action]', 'register your accounts on remote instances to use them with other commands') | ||
13 | .command('upload', 'upload a video').alias('up') | ||
14 | .command('import-videos', 'import a video from a streaming platform').alias('import') | ||
15 | .command('get-access-token', 'get a peertube access token', { noHelp: true }).alias('token') | ||
16 | .command('plugins [action]', 'manage instance plugins/themes').alias('p') | ||
17 | .command('redundancy [action]', 'manage instance redundancies').alias('r') | ||
18 | |||
19 | /* Not Yet Implemented */ | ||
20 | program | ||
21 | .command( | ||
22 | 'diagnostic [action]', | ||
23 | 'like couple therapy, but for your instance', | ||
24 | { noHelp: true } as CommandOptions | ||
25 | ).alias('d') | ||
26 | .command('admin', | ||
27 | 'manage an instance where you have elevated rights', | ||
28 | { noHelp: true } as CommandOptions | ||
29 | ).alias('a') | ||
30 | |||
31 | // help on no command | ||
32 | if (!process.argv.slice(2).length) { | ||
33 | const logo = 'â–‘Pâ–‘eâ–‘eâ–‘râ–‘Tâ–‘uâ–‘bâ–‘eâ–‘' | ||
34 | console.log(` | ||
35 | ___/),.._ ` + logo + ` | ||
36 | /' ,. ."'._ | ||
37 | ( "' '-.__"-._ ,- | ||
38 | \\'='='), "\\ -._-"-. -"/ | ||
39 | / ""/"\\,_\\,__"" _" /,- | ||
40 | / / -" _/"/ | ||
41 | / | ._\\\\ |\\ |_.".-" / | ||
42 | / | __\\)|)|),/|_." _,." | ||
43 | / \\_." " ") | ).-""---''-- | ||
44 | ( "/.""7__-""'' | ||
45 | | " ."._--._ | ||
46 | \\ \\ (_ __ "" ".,_ | ||
47 | \\.,. \\ "" -"".-" | ||
48 | ".,_, (",_-,,,-".- | ||
49 | "'-,\\_ __,-" | ||
50 | ",)" ") | ||
51 | /"\\-" | ||
52 | ,"\\/ | ||
53 | _,.__/"\\/_ (the CLI for red chocobos) | ||
54 | / \\) "./, ". | ||
55 | --/---"---" "-) )---- by Chocobozzz et al.\n`) | ||
56 | } | ||
57 | |||
58 | getSettings() | ||
59 | .then(settings => { | ||
60 | const state = (settings.default === undefined || settings.default === -1) | ||
61 | ? 'no instance selected, commands will require explicit arguments' | ||
62 | : 'instance ' + settings.remotes[settings.default] + ' selected' | ||
63 | |||
64 | program | ||
65 | .addHelpText('after', '\n\n State: ' + state + '\n\n' + | ||
66 | ' Examples:\n\n' + | ||
67 | ' $ peertube auth add -u "PEERTUBE_URL" -U "PEERTUBE_USER" --password "PEERTUBE_PASSWORD"\n' + | ||
68 | ' $ peertube up <videoFile>\n' | ||
69 | ) | ||
70 | .parse(process.argv) | ||
71 | }) | ||
72 | .catch(err => console.error(err)) | ||
diff --git a/server/tools/shared/cli.ts b/server/tools/shared/cli.ts deleted file mode 100644 index e010ab320..000000000 --- a/server/tools/shared/cli.ts +++ /dev/null | |||
@@ -1,262 +0,0 @@ | |||
1 | import { Command } from 'commander' | ||
2 | import { Netrc } from 'netrc-parser' | ||
3 | import { join } from 'path' | ||
4 | import { createLogger, format, transports } from 'winston' | ||
5 | import { getAppNumber, isTestInstance } from '@server/helpers/core-utils' | ||
6 | import { loadLanguages } from '@server/initializers/constants' | ||
7 | import { root } from '@shared/core-utils' | ||
8 | import { UserRole, VideoPrivacy } from '@shared/models' | ||
9 | import { PeerTubeServer } from '@shared/server-commands' | ||
10 | |||
11 | let configName = 'PeerTube/CLI' | ||
12 | if (isTestInstance()) configName += `-${getAppNumber()}` | ||
13 | |||
14 | const config = require('application-config')(configName) | ||
15 | |||
16 | const version = require(join(root(), 'package.json')).version | ||
17 | |||
18 | async function getAdminTokenOrDie (server: PeerTubeServer, username: string, password: string) { | ||
19 | const token = await server.login.getAccessToken(username, password) | ||
20 | const me = await server.users.getMyInfo({ token }) | ||
21 | |||
22 | if (me.role.id !== UserRole.ADMINISTRATOR) { | ||
23 | console.error('You must be an administrator.') | ||
24 | process.exit(-1) | ||
25 | } | ||
26 | |||
27 | return token | ||
28 | } | ||
29 | |||
30 | interface Settings { | ||
31 | remotes: any[] | ||
32 | default: number | ||
33 | } | ||
34 | |||
35 | async function getSettings (): Promise<Settings> { | ||
36 | const defaultSettings = { | ||
37 | remotes: [], | ||
38 | default: -1 | ||
39 | } | ||
40 | |||
41 | const data = await config.read() | ||
42 | |||
43 | return Object.keys(data).length === 0 | ||
44 | ? defaultSettings | ||
45 | : data | ||
46 | } | ||
47 | |||
48 | async function getNetrc () { | ||
49 | const Netrc = require('netrc-parser').Netrc | ||
50 | |||
51 | const netrc = isTestInstance() | ||
52 | ? new Netrc(join(root(), 'test' + getAppNumber(), 'netrc')) | ||
53 | : new Netrc() | ||
54 | |||
55 | await netrc.load() | ||
56 | |||
57 | return netrc | ||
58 | } | ||
59 | |||
60 | function writeSettings (settings: Settings) { | ||
61 | return config.write(settings) | ||
62 | } | ||
63 | |||
64 | function deleteSettings () { | ||
65 | return config.trash() | ||
66 | } | ||
67 | |||
68 | function getRemoteObjectOrDie ( | ||
69 | program: Command, | ||
70 | settings: Settings, | ||
71 | netrc: Netrc | ||
72 | ): { url: string, username: string, password: string } { | ||
73 | const options = program.opts() | ||
74 | |||
75 | function exitIfNoOptions (optionNames: string[], errorPrefix: string = '') { | ||
76 | let exit = false | ||
77 | |||
78 | for (const key of optionNames) { | ||
79 | if (!options[key]) { | ||
80 | if (exit === false && errorPrefix) console.error(errorPrefix) | ||
81 | |||
82 | console.error(`--${key} field is required`) | ||
83 | exit = true | ||
84 | } | ||
85 | } | ||
86 | |||
87 | if (exit) process.exit(-1) | ||
88 | } | ||
89 | |||
90 | // If username or password are specified, both are mandatory | ||
91 | if (options.username || options.password) { | ||
92 | exitIfNoOptions([ 'username', 'password' ]) | ||
93 | } | ||
94 | |||
95 | // If no available machines, url, username and password args are mandatory | ||
96 | if (Object.keys(netrc.machines).length === 0) { | ||
97 | exitIfNoOptions([ 'url', 'username', 'password' ], 'No account found in netrc') | ||
98 | } | ||
99 | |||
100 | if (settings.remotes.length === 0 || settings.default === -1) { | ||
101 | exitIfNoOptions([ 'url' ], 'No default instance found') | ||
102 | } | ||
103 | |||
104 | let url: string = options.url | ||
105 | let username: string = options.username | ||
106 | let password: string = options.password | ||
107 | |||
108 | if (!url && settings.default !== -1) url = settings.remotes[settings.default] | ||
109 | |||
110 | const machine = netrc.machines[url] | ||
111 | if ((!username || !password) && !machine) { | ||
112 | console.error('Cannot find existing configuration for %s.', url) | ||
113 | process.exit(-1) | ||
114 | } | ||
115 | |||
116 | if (!username && machine) username = machine.login | ||
117 | if (!password && machine) password = machine.password | ||
118 | |||
119 | return { url, username, password } | ||
120 | } | ||
121 | |||
122 | function buildCommonVideoOptions (command: Command) { | ||
123 | function list (val) { | ||
124 | return val.split(',') | ||
125 | } | ||
126 | |||
127 | return command | ||
128 | .option('-n, --video-name <name>', 'Video name') | ||
129 | .option('-c, --category <category_number>', 'Category number') | ||
130 | .option('-l, --licence <licence_number>', 'Licence number') | ||
131 | .option('-L, --language <language_code>', 'Language ISO 639 code (fr or en...)') | ||
132 | .option('-t, --tags <tags>', 'Video tags', list) | ||
133 | .option('-N, --nsfw', 'Video is Not Safe For Work') | ||
134 | .option('-d, --video-description <description>', 'Video description') | ||
135 | .option('-P, --privacy <privacy_number>', 'Privacy') | ||
136 | .option('-C, --channel-name <channel_name>', 'Channel name') | ||
137 | .option('--no-comments-enabled', 'Disable video comments') | ||
138 | .option('-s, --support <support>', 'Video support text') | ||
139 | .option('--no-wait-transcoding', 'Do not wait transcoding before publishing the video') | ||
140 | .option('--no-download-enabled', 'Disable video download') | ||
141 | .option('-v, --verbose <verbose>', 'Verbosity, from 0/\'error\' to 4/\'debug\'', 'info') | ||
142 | } | ||
143 | |||
144 | async function buildVideoAttributesFromCommander (server: PeerTubeServer, command: Command, defaultAttributes: any = {}) { | ||
145 | const options = command.opts() | ||
146 | |||
147 | const defaultBooleanAttributes = { | ||
148 | nsfw: false, | ||
149 | commentsEnabled: true, | ||
150 | downloadEnabled: true, | ||
151 | waitTranscoding: true | ||
152 | } | ||
153 | |||
154 | const booleanAttributes: { [id in keyof typeof defaultBooleanAttributes]: boolean } | {} = {} | ||
155 | |||
156 | for (const key of Object.keys(defaultBooleanAttributes)) { | ||
157 | if (options[key] !== undefined) { | ||
158 | booleanAttributes[key] = options[key] | ||
159 | } else if (defaultAttributes[key] !== undefined) { | ||
160 | booleanAttributes[key] = defaultAttributes[key] | ||
161 | } else { | ||
162 | booleanAttributes[key] = defaultBooleanAttributes[key] | ||
163 | } | ||
164 | } | ||
165 | |||
166 | const videoAttributes = { | ||
167 | name: options.videoName || defaultAttributes.name, | ||
168 | category: options.category || defaultAttributes.category || undefined, | ||
169 | licence: options.licence || defaultAttributes.licence || undefined, | ||
170 | language: options.language || defaultAttributes.language || undefined, | ||
171 | privacy: options.privacy || defaultAttributes.privacy || VideoPrivacy.PUBLIC, | ||
172 | support: options.support || defaultAttributes.support || undefined, | ||
173 | description: options.videoDescription || defaultAttributes.description || undefined, | ||
174 | tags: options.tags || defaultAttributes.tags || undefined | ||
175 | } | ||
176 | |||
177 | Object.assign(videoAttributes, booleanAttributes) | ||
178 | |||
179 | if (options.channelName) { | ||
180 | const videoChannel = await server.channels.get({ channelName: options.channelName }) | ||
181 | |||
182 | Object.assign(videoAttributes, { channelId: videoChannel.id }) | ||
183 | |||
184 | if (!videoAttributes.support && videoChannel.support) { | ||
185 | Object.assign(videoAttributes, { support: videoChannel.support }) | ||
186 | } | ||
187 | } | ||
188 | |||
189 | return videoAttributes | ||
190 | } | ||
191 | |||
192 | function getServerCredentials (program: Command) { | ||
193 | return Promise.all([ getSettings(), getNetrc() ]) | ||
194 | .then(([ settings, netrc ]) => { | ||
195 | return getRemoteObjectOrDie(program, settings, netrc) | ||
196 | }) | ||
197 | } | ||
198 | |||
199 | function buildServer (url: string) { | ||
200 | loadLanguages() | ||
201 | return new PeerTubeServer({ url }) | ||
202 | } | ||
203 | |||
204 | async function assignToken (server: PeerTubeServer, username: string, password: string) { | ||
205 | const bodyClient = await server.login.getClient() | ||
206 | const client = { id: bodyClient.client_id, secret: bodyClient.client_secret } | ||
207 | |||
208 | const body = await server.login.login({ client, user: { username, password } }) | ||
209 | |||
210 | server.accessToken = body.access_token | ||
211 | } | ||
212 | |||
213 | function getLogger (logLevel = 'info') { | ||
214 | const logLevels = { | ||
215 | 0: 0, | ||
216 | error: 0, | ||
217 | 1: 1, | ||
218 | warn: 1, | ||
219 | 2: 2, | ||
220 | info: 2, | ||
221 | 3: 3, | ||
222 | verbose: 3, | ||
223 | 4: 4, | ||
224 | debug: 4 | ||
225 | } | ||
226 | |||
227 | const logger = createLogger({ | ||
228 | levels: logLevels, | ||
229 | format: format.combine( | ||
230 | format.splat(), | ||
231 | format.simple() | ||
232 | ), | ||
233 | transports: [ | ||
234 | new (transports.Console)({ | ||
235 | level: logLevel | ||
236 | }) | ||
237 | ] | ||
238 | }) | ||
239 | |||
240 | return logger | ||
241 | } | ||
242 | |||
243 | // --------------------------------------------------------------------------- | ||
244 | |||
245 | export { | ||
246 | version, | ||
247 | getLogger, | ||
248 | getSettings, | ||
249 | getNetrc, | ||
250 | getRemoteObjectOrDie, | ||
251 | writeSettings, | ||
252 | deleteSettings, | ||
253 | |||
254 | getServerCredentials, | ||
255 | |||
256 | buildCommonVideoOptions, | ||
257 | buildVideoAttributesFromCommander, | ||
258 | |||
259 | getAdminTokenOrDie, | ||
260 | buildServer, | ||
261 | assignToken | ||
262 | } | ||
diff --git a/server/tools/shared/index.ts b/server/tools/shared/index.ts deleted file mode 100644 index 8a3f31e2f..000000000 --- a/server/tools/shared/index.ts +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | export * from './cli' | ||
diff --git a/server/tools/tsconfig.json b/server/tools/tsconfig.json deleted file mode 100644 index 39f8e74e4..000000000 --- a/server/tools/tsconfig.json +++ /dev/null | |||
@@ -1,12 +0,0 @@ | |||
1 | { | ||
2 | "extends": "../../tsconfig.json", | ||
3 | "compilerOptions": { | ||
4 | "outDir": "../../dist/server/tools" | ||
5 | }, | ||
6 | "include": [ ".", "../typings" ], | ||
7 | "references": [ | ||
8 | { "path": "../" } | ||
9 | ], | ||
10 | "files": [], | ||
11 | "exclude": [ ] // Overwrite exclude property | ||
12 | } | ||
diff --git a/server/tools/yarn.lock b/server/tools/yarn.lock deleted file mode 100644 index 025ef208d..000000000 --- a/server/tools/yarn.lock +++ /dev/null | |||
@@ -1,373 +0,0 @@ | |||
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. | ||
2 | # yarn lockfile v1 | ||
3 | |||
4 | |||
5 | "@babel/code-frame@^7.0.0": | ||
6 | version "7.16.7" | ||
7 | resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" | ||
8 | integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== | ||
9 | dependencies: | ||
10 | "@babel/highlight" "^7.16.7" | ||
11 | |||
12 | "@babel/helper-validator-identifier@^7.16.7": | ||
13 | version "7.16.7" | ||
14 | resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" | ||
15 | integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== | ||
16 | |||
17 | "@babel/highlight@^7.16.7": | ||
18 | version "7.16.10" | ||
19 | resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.10.tgz#744f2eb81579d6eea753c227b0f570ad785aba88" | ||
20 | integrity sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw== | ||
21 | dependencies: | ||
22 | "@babel/helper-validator-identifier" "^7.16.7" | ||
23 | chalk "^2.0.0" | ||
24 | js-tokens "^4.0.0" | ||
25 | |||
26 | ansi-regex@^5.0.1: | ||
27 | version "5.0.1" | ||
28 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" | ||
29 | integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== | ||
30 | |||
31 | ansi-styles@^3.2.1: | ||
32 | version "3.2.1" | ||
33 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" | ||
34 | integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== | ||
35 | dependencies: | ||
36 | color-convert "^1.9.0" | ||
37 | |||
38 | application-config-path@^0.1.0: | ||
39 | version "0.1.0" | ||
40 | resolved "https://registry.yarnpkg.com/application-config-path/-/application-config-path-0.1.0.tgz#193c5f0a86541a4c66fba1e2dc38583362ea5e8f" | ||
41 | integrity sha1-GTxfCoZUGkxm+6Hi3DhYM2LqXo8= | ||
42 | |||
43 | application-config@^2.0.0: | ||
44 | version "2.0.0" | ||
45 | resolved "https://registry.yarnpkg.com/application-config/-/application-config-2.0.0.tgz#15b4d54d61c0c082f9802227e3e85de876b47747" | ||
46 | integrity sha512-NC5/0guSZK3/UgUDfCk/riByXzqz0owL1L3r63JPSBzYk5QALrp3bLxbsR7qeSfvYfFmAhnp3dbqYsW3U9MpZQ== | ||
47 | dependencies: | ||
48 | application-config-path "^0.1.0" | ||
49 | load-json-file "^6.2.0" | ||
50 | write-json-file "^4.2.0" | ||
51 | |||
52 | chalk@^2.0.0: | ||
53 | version "2.4.2" | ||
54 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" | ||
55 | integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== | ||
56 | dependencies: | ||
57 | ansi-styles "^3.2.1" | ||
58 | escape-string-regexp "^1.0.5" | ||
59 | supports-color "^5.3.0" | ||
60 | |||
61 | cli-table3@^0.6.0: | ||
62 | version "0.6.1" | ||
63 | resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.1.tgz#36ce9b7af4847f288d3cdd081fbd09bf7bd237b8" | ||
64 | integrity sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA== | ||
65 | dependencies: | ||
66 | string-width "^4.2.0" | ||
67 | optionalDependencies: | ||
68 | colors "1.4.0" | ||
69 | |||
70 | color-convert@^1.9.0: | ||
71 | version "1.9.3" | ||
72 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" | ||
73 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== | ||
74 | dependencies: | ||
75 | color-name "1.1.3" | ||
76 | |||
77 | color-name@1.1.3: | ||
78 | version "1.1.3" | ||
79 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" | ||
80 | integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= | ||
81 | |||
82 | colors@1.4.0: | ||
83 | version "1.4.0" | ||
84 | resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" | ||
85 | integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== | ||
86 | |||
87 | cross-spawn@^6.0.0: | ||
88 | version "6.0.5" | ||
89 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" | ||
90 | integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== | ||
91 | dependencies: | ||
92 | nice-try "^1.0.4" | ||
93 | path-key "^2.0.1" | ||
94 | semver "^5.5.0" | ||
95 | shebang-command "^1.2.0" | ||
96 | which "^1.2.9" | ||
97 | |||
98 | debug@^3.1.0: | ||
99 | version "3.2.7" | ||
100 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" | ||
101 | integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== | ||
102 | dependencies: | ||
103 | ms "^2.1.1" | ||
104 | |||
105 | detect-indent@^6.0.0: | ||
106 | version "6.1.0" | ||
107 | resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" | ||
108 | integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== | ||
109 | |||
110 | emoji-regex@^8.0.0: | ||
111 | version "8.0.0" | ||
112 | resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" | ||
113 | integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== | ||
114 | |||
115 | error-ex@^1.3.1: | ||
116 | version "1.3.2" | ||
117 | resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" | ||
118 | integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== | ||
119 | dependencies: | ||
120 | is-arrayish "^0.2.1" | ||
121 | |||
122 | escape-string-regexp@^1.0.5: | ||
123 | version "1.0.5" | ||
124 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" | ||
125 | integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= | ||
126 | |||
127 | execa@^0.10.0: | ||
128 | version "0.10.0" | ||
129 | resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50" | ||
130 | integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw== | ||
131 | dependencies: | ||
132 | cross-spawn "^6.0.0" | ||
133 | get-stream "^3.0.0" | ||
134 | is-stream "^1.1.0" | ||
135 | npm-run-path "^2.0.0" | ||
136 | p-finally "^1.0.0" | ||
137 | signal-exit "^3.0.0" | ||
138 | strip-eof "^1.0.0" | ||
139 | |||
140 | get-stream@^3.0.0: | ||
141 | version "3.0.0" | ||
142 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" | ||
143 | integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= | ||
144 | |||
145 | graceful-fs@^4.1.15: | ||
146 | version "4.2.9" | ||
147 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" | ||
148 | integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== | ||
149 | |||
150 | has-flag@^3.0.0: | ||
151 | version "3.0.0" | ||
152 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" | ||
153 | integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= | ||
154 | |||
155 | imurmurhash@^0.1.4: | ||
156 | version "0.1.4" | ||
157 | resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" | ||
158 | integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= | ||
159 | |||
160 | is-arrayish@^0.2.1: | ||
161 | version "0.2.1" | ||
162 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" | ||
163 | integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= | ||
164 | |||
165 | is-fullwidth-code-point@^3.0.0: | ||
166 | version "3.0.0" | ||
167 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" | ||
168 | integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== | ||
169 | |||
170 | is-plain-obj@^2.0.0: | ||
171 | version "2.1.0" | ||
172 | resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" | ||
173 | integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== | ||
174 | |||
175 | is-stream@^1.1.0: | ||
176 | version "1.1.0" | ||
177 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" | ||
178 | integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= | ||
179 | |||
180 | is-typedarray@^1.0.0: | ||
181 | version "1.0.0" | ||
182 | resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" | ||
183 | integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= | ||
184 | |||
185 | isexe@^2.0.0: | ||
186 | version "2.0.0" | ||
187 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" | ||
188 | integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= | ||
189 | |||
190 | js-tokens@^4.0.0: | ||
191 | version "4.0.0" | ||
192 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" | ||
193 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== | ||
194 | |||
195 | json-parse-even-better-errors@^2.3.0: | ||
196 | version "2.3.1" | ||
197 | resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" | ||
198 | integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== | ||
199 | |||
200 | lines-and-columns@^1.1.6: | ||
201 | version "1.2.4" | ||
202 | resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" | ||
203 | integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== | ||
204 | |||
205 | load-json-file@^6.2.0: | ||
206 | version "6.2.0" | ||
207 | resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-6.2.0.tgz#5c7770b42cafa97074ca2848707c61662f4251a1" | ||
208 | integrity sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ== | ||
209 | dependencies: | ||
210 | graceful-fs "^4.1.15" | ||
211 | parse-json "^5.0.0" | ||
212 | strip-bom "^4.0.0" | ||
213 | type-fest "^0.6.0" | ||
214 | |||
215 | make-dir@^3.0.0: | ||
216 | version "3.1.0" | ||
217 | resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" | ||
218 | integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== | ||
219 | dependencies: | ||
220 | semver "^6.0.0" | ||
221 | |||
222 | ms@^2.1.1: | ||
223 | version "2.1.3" | ||
224 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" | ||
225 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== | ||
226 | |||
227 | netrc-parser@^3.1.6: | ||
228 | version "3.1.6" | ||
229 | resolved "https://registry.yarnpkg.com/netrc-parser/-/netrc-parser-3.1.6.tgz#7243c9ec850b8e805b9bdc7eae7b1450d4a96e72" | ||
230 | integrity sha512-lY+fmkqSwntAAjfP63jB4z5p5WbuZwyMCD3pInT7dpHU/Gc6Vv90SAC6A0aNiqaRGHiuZFBtiwu+pu8W/Eyotw== | ||
231 | dependencies: | ||
232 | debug "^3.1.0" | ||
233 | execa "^0.10.0" | ||
234 | |||
235 | nice-try@^1.0.4: | ||
236 | version "1.0.5" | ||
237 | resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" | ||
238 | integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== | ||
239 | |||
240 | npm-run-path@^2.0.0: | ||
241 | version "2.0.2" | ||
242 | resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" | ||
243 | integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= | ||
244 | dependencies: | ||
245 | path-key "^2.0.0" | ||
246 | |||
247 | p-finally@^1.0.0: | ||
248 | version "1.0.0" | ||
249 | resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" | ||
250 | integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= | ||
251 | |||
252 | parse-json@^5.0.0: | ||
253 | version "5.2.0" | ||
254 | resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" | ||
255 | integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== | ||
256 | dependencies: | ||
257 | "@babel/code-frame" "^7.0.0" | ||
258 | error-ex "^1.3.1" | ||
259 | json-parse-even-better-errors "^2.3.0" | ||
260 | lines-and-columns "^1.1.6" | ||
261 | |||
262 | path-key@^2.0.0, path-key@^2.0.1: | ||
263 | version "2.0.1" | ||
264 | resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" | ||
265 | integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= | ||
266 | |||
267 | semver@^5.5.0: | ||
268 | version "5.7.1" | ||
269 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" | ||
270 | integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== | ||
271 | |||
272 | semver@^6.0.0: | ||
273 | version "6.3.0" | ||
274 | resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" | ||
275 | integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== | ||
276 | |||
277 | shebang-command@^1.2.0: | ||
278 | version "1.2.0" | ||
279 | resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" | ||
280 | integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= | ||
281 | dependencies: | ||
282 | shebang-regex "^1.0.0" | ||
283 | |||
284 | shebang-regex@^1.0.0: | ||
285 | version "1.0.0" | ||
286 | resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" | ||
287 | integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= | ||
288 | |||
289 | signal-exit@^3.0.0, signal-exit@^3.0.2: | ||
290 | version "3.0.7" | ||
291 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" | ||
292 | integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== | ||
293 | |||
294 | sort-keys@^4.0.0: | ||
295 | version "4.2.0" | ||
296 | resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-4.2.0.tgz#6b7638cee42c506fff8c1cecde7376d21315be18" | ||
297 | integrity sha512-aUYIEU/UviqPgc8mHR6IW1EGxkAXpeRETYcrzg8cLAvUPZcpAlleSXHV2mY7G12GphSH6Gzv+4MMVSSkbdteHg== | ||
298 | dependencies: | ||
299 | is-plain-obj "^2.0.0" | ||
300 | |||
301 | string-width@^4.2.0: | ||
302 | version "4.2.3" | ||
303 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" | ||
304 | integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== | ||
305 | dependencies: | ||
306 | emoji-regex "^8.0.0" | ||
307 | is-fullwidth-code-point "^3.0.0" | ||
308 | strip-ansi "^6.0.1" | ||
309 | |||
310 | strip-ansi@^6.0.1: | ||
311 | version "6.0.1" | ||
312 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" | ||
313 | integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== | ||
314 | dependencies: | ||
315 | ansi-regex "^5.0.1" | ||
316 | |||
317 | strip-bom@^4.0.0: | ||
318 | version "4.0.0" | ||
319 | resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" | ||
320 | integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== | ||
321 | |||
322 | strip-eof@^1.0.0: | ||
323 | version "1.0.0" | ||
324 | resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" | ||
325 | integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= | ||
326 | |||
327 | supports-color@^5.3.0: | ||
328 | version "5.5.0" | ||
329 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" | ||
330 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== | ||
331 | dependencies: | ||
332 | has-flag "^3.0.0" | ||
333 | |||
334 | type-fest@^0.6.0: | ||
335 | version "0.6.0" | ||
336 | resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" | ||
337 | integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== | ||
338 | |||
339 | typedarray-to-buffer@^3.1.5: | ||
340 | version "3.1.5" | ||
341 | resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" | ||
342 | integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== | ||
343 | dependencies: | ||
344 | is-typedarray "^1.0.0" | ||
345 | |||
346 | which@^1.2.9: | ||
347 | version "1.3.1" | ||
348 | resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" | ||
349 | integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== | ||
350 | dependencies: | ||
351 | isexe "^2.0.0" | ||
352 | |||
353 | write-file-atomic@^3.0.0: | ||
354 | version "3.0.3" | ||
355 | resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" | ||
356 | integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== | ||
357 | dependencies: | ||
358 | imurmurhash "^0.1.4" | ||
359 | is-typedarray "^1.0.0" | ||
360 | signal-exit "^3.0.2" | ||
361 | typedarray-to-buffer "^3.1.5" | ||
362 | |||
363 | write-json-file@^4.2.0: | ||
364 | version "4.3.0" | ||
365 | resolved "https://registry.yarnpkg.com/write-json-file/-/write-json-file-4.3.0.tgz#908493d6fd23225344af324016e4ca8f702dd12d" | ||
366 | integrity sha512-PxiShnxf0IlnQuMYOPPhPkhExoCQuTUNPOa/2JWCYTmBquU9njyyDuwRKN26IZBlp4yn1nt+Agh2HOOBl+55HQ== | ||
367 | dependencies: | ||
368 | detect-indent "^6.0.0" | ||
369 | graceful-fs "^4.1.15" | ||
370 | is-plain-obj "^2.0.0" | ||
371 | make-dir "^3.0.0" | ||
372 | sort-keys "^4.0.0" | ||
373 | write-file-atomic "^3.0.0" | ||