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