]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/helpers/core-utils.ts
/!\ Use a dedicated config file for development
[github/Chocobozzz/PeerTube.git] / server / helpers / core-utils.ts
1 /* eslint-disable no-useless-call */
2
3 /*
4 Different from 'utils' because we don't import other PeerTube modules.
5 Useful to avoid circular dependencies.
6 */
7
8 import { exec, ExecOptions } from 'child_process'
9 import { randomBytes } from 'crypto'
10 import { truncate } from 'lodash'
11 import { createPrivateKey as createPrivateKey_1, getPublicKey as getPublicKey_1 } from 'pem'
12 import { pipeline } from 'stream'
13 import { URL } from 'url'
14 import { promisify } from 'util'
15
16 const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => {
17 if (!oldObject || typeof oldObject !== 'object') {
18 return valueConverter(oldObject)
19 }
20
21 if (Array.isArray(oldObject)) {
22 return oldObject.map(e => objectConverter(e, keyConverter, valueConverter))
23 }
24
25 const newObject = {}
26 Object.keys(oldObject).forEach(oldKey => {
27 const newKey = keyConverter(oldKey)
28 newObject[newKey] = objectConverter(oldObject[oldKey], keyConverter, valueConverter)
29 })
30
31 return newObject
32 }
33
34 function mapToJSON (map: Map<any, any>) {
35 const obj: any = {}
36
37 for (const [ k, v ] of map) {
38 obj[k] = v
39 }
40
41 return obj
42 }
43
44 // ---------------------------------------------------------------------------
45
46 const timeTable = {
47 ms: 1,
48 second: 1000,
49 minute: 60000,
50 hour: 3600000,
51 day: 3600000 * 24,
52 week: 3600000 * 24 * 7,
53 month: 3600000 * 24 * 30
54 }
55
56 export function parseDurationToMs (duration: number | string): number {
57 if (duration === null) return null
58 if (typeof duration === 'number') return duration
59 if (!isNaN(+duration)) return +duration
60
61 if (typeof duration === 'string') {
62 const split = duration.match(/^([\d.,]+)\s?(\w+)$/)
63
64 if (split.length === 3) {
65 const len = parseFloat(split[1])
66 let unit = split[2].replace(/s$/i, '').toLowerCase()
67 if (unit === 'm') {
68 unit = 'ms'
69 }
70
71 return (len || 1) * (timeTable[unit] || 0)
72 }
73 }
74
75 throw new Error(`Duration ${duration} could not be properly parsed`)
76 }
77
78 export function parseBytes (value: string | number): number {
79 if (typeof value === 'number') return value
80 if (!isNaN(+value)) return +value
81
82 const tgm = /^(\d+)\s*TB\s*(\d+)\s*GB\s*(\d+)\s*MB$/
83 const tg = /^(\d+)\s*TB\s*(\d+)\s*GB$/
84 const tm = /^(\d+)\s*TB\s*(\d+)\s*MB$/
85 const gm = /^(\d+)\s*GB\s*(\d+)\s*MB$/
86 const t = /^(\d+)\s*TB$/
87 const g = /^(\d+)\s*GB$/
88 const m = /^(\d+)\s*MB$/
89 const b = /^(\d+)\s*B$/
90
91 let match: RegExpMatchArray
92
93 if (value.match(tgm)) {
94 match = value.match(tgm)
95 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
96 parseInt(match[2], 10) * 1024 * 1024 * 1024 +
97 parseInt(match[3], 10) * 1024 * 1024
98 }
99
100 if (value.match(tg)) {
101 match = value.match(tg)
102 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
103 parseInt(match[2], 10) * 1024 * 1024 * 1024
104 }
105
106 if (value.match(tm)) {
107 match = value.match(tm)
108 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
109 parseInt(match[2], 10) * 1024 * 1024
110 }
111
112 if (value.match(gm)) {
113 match = value.match(gm)
114 return parseInt(match[1], 10) * 1024 * 1024 * 1024 +
115 parseInt(match[2], 10) * 1024 * 1024
116 }
117
118 if (value.match(t)) {
119 match = value.match(t)
120 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
121 }
122
123 if (value.match(g)) {
124 match = value.match(g)
125 return parseInt(match[1], 10) * 1024 * 1024 * 1024
126 }
127
128 if (value.match(m)) {
129 match = value.match(m)
130 return parseInt(match[1], 10) * 1024 * 1024
131 }
132
133 if (value.match(b)) {
134 match = value.match(b)
135 return parseInt(match[1], 10) * 1024
136 }
137
138 return parseInt(value, 10)
139 }
140
141 // ---------------------------------------------------------------------------
142
143 function sanitizeUrl (url: string) {
144 const urlObject = new URL(url)
145
146 if (urlObject.protocol === 'https:' && urlObject.port === '443') {
147 urlObject.port = ''
148 } else if (urlObject.protocol === 'http:' && urlObject.port === '80') {
149 urlObject.port = ''
150 }
151
152 return urlObject.href.replace(/\/$/, '')
153 }
154
155 // Don't import remote scheme from constants because we are in core utils
156 function sanitizeHost (host: string, remoteScheme: string) {
157 const toRemove = remoteScheme === 'https' ? 443 : 80
158
159 return host.replace(new RegExp(`:${toRemove}$`), '')
160 }
161
162 // ---------------------------------------------------------------------------
163
164 function isTestInstance () {
165 return process.env.NODE_ENV === 'test'
166 }
167
168 function isDevInstance () {
169 return process.env.NODE_ENV === 'dev'
170 }
171
172 function isTestOrDevInstance () {
173 return isTestInstance() || isDevInstance()
174 }
175
176 function isProdInstance () {
177 return process.env.NODE_ENV === 'production'
178 }
179
180 function getAppNumber () {
181 return process.env.NODE_APP_INSTANCE || ''
182 }
183
184 // ---------------------------------------------------------------------------
185
186 // Consistent with .length, lodash truncate function is not
187 function peertubeTruncate (str: string, options: { length: number, separator?: RegExp, omission?: string }) {
188 const truncatedStr = truncate(str, options)
189
190 // The truncated string is okay, we can return it
191 if (truncatedStr.length <= options.length) return truncatedStr
192
193 // Lodash takes into account all UTF characters, whereas String.prototype.length does not: some characters have a length of 2
194 // We always use the .length so we need to truncate more if needed
195 options.length -= truncatedStr.length - options.length
196 return truncate(str, options)
197 }
198
199 function pageToStartAndCount (page: number, itemsPerPage: number) {
200 const start = (page - 1) * itemsPerPage
201
202 return { start, count: itemsPerPage }
203 }
204
205 // ---------------------------------------------------------------------------
206
207 type SemVersion = { major: number, minor: number, patch: number }
208 function parseSemVersion (s: string) {
209 const parsed = s.match(/^v?(\d+)\.(\d+)\.(\d+)$/i)
210
211 return {
212 major: parseInt(parsed[1]),
213 minor: parseInt(parsed[2]),
214 patch: parseInt(parsed[3])
215 } as SemVersion
216 }
217
218 // ---------------------------------------------------------------------------
219
220 function execShell (command: string, options?: ExecOptions) {
221 return new Promise<{ err?: Error, stdout: string, stderr: string }>((res, rej) => {
222 exec(command, options, (err, stdout, stderr) => {
223 // eslint-disable-next-line prefer-promise-reject-errors
224 if (err) return rej({ err, stdout, stderr })
225
226 return res({ stdout, stderr })
227 })
228 })
229 }
230
231 // ---------------------------------------------------------------------------
232
233 function isOdd (num: number) {
234 return (num % 2) !== 0
235 }
236
237 function toEven (num: number) {
238 if (isOdd(num)) return num + 1
239
240 return num
241 }
242
243 // ---------------------------------------------------------------------------
244
245 function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> {
246 return function promisified (): Promise<A> {
247 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
248 func.apply(null, [ (err: any, res: A) => err ? reject(err) : resolve(res) ])
249 })
250 }
251 }
252
253 // Thanks to https://gist.github.com/kumasento/617daa7e46f13ecdd9b2
254 function promisify1<T, A> (func: (arg: T, cb: (err: any, result: A) => void) => void): (arg: T) => Promise<A> {
255 return function promisified (arg: T): Promise<A> {
256 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
257 func.apply(null, [ arg, (err: any, res: A) => err ? reject(err) : resolve(res) ])
258 })
259 }
260 }
261
262 function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A) => void) => void): (arg1: T, arg2: U) => Promise<A> {
263 return function promisified (arg1: T, arg2: U): Promise<A> {
264 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
265 func.apply(null, [ arg1, arg2, (err: any, res: A) => err ? reject(err) : resolve(res) ])
266 })
267 }
268 }
269
270 const randomBytesPromise = promisify1<number, Buffer>(randomBytes)
271 const createPrivateKey = promisify1<number, { key: string }>(createPrivateKey_1)
272 const getPublicKey = promisify1<string, { publicKey: string }>(getPublicKey_1)
273 const execPromise2 = promisify2<string, any, string>(exec)
274 const execPromise = promisify1<string, string>(exec)
275 const pipelinePromise = promisify(pipeline)
276
277 // ---------------------------------------------------------------------------
278
279 export {
280 isTestInstance,
281 isTestOrDevInstance,
282 isProdInstance,
283 getAppNumber,
284
285 objectConverter,
286 mapToJSON,
287
288 sanitizeUrl,
289 sanitizeHost,
290
291 execShell,
292
293 pageToStartAndCount,
294 peertubeTruncate,
295
296 promisify0,
297 promisify1,
298 promisify2,
299
300 randomBytesPromise,
301 createPrivateKey,
302 getPublicKey,
303 execPromise2,
304 execPromise,
305 pipelinePromise,
306
307 parseSemVersion,
308
309 isOdd,
310 toEven
311 }