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