]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/helpers/core-utils.ts
Fix "height not divisible by 2" ffmpeg error
[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, extname, 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 function mapToJSON (map: Map<any, any>) {
36 const obj: any = {}
37
38 for (const [ k, v ] of map) {
39 obj[k] = v
40 }
41
42 return obj
43 }
44
45 // ---------------------------------------------------------------------------
46
47 const timeTable = {
48 ms: 1,
49 second: 1000,
50 minute: 60000,
51 hour: 3600000,
52 day: 3600000 * 24,
53 week: 3600000 * 24 * 7,
54 month: 3600000 * 24 * 30
55 }
56
57 export function parseDurationToMs (duration: number | string): number {
58 if (duration === null) return null
59 if (typeof duration === 'number') 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
81 const tgm = /^(\d+)\s*TB\s*(\d+)\s*GB\s*(\d+)\s*MB$/
82 const tg = /^(\d+)\s*TB\s*(\d+)\s*GB$/
83 const tm = /^(\d+)\s*TB\s*(\d+)\s*MB$/
84 const gm = /^(\d+)\s*GB\s*(\d+)\s*MB$/
85 const t = /^(\d+)\s*TB$/
86 const g = /^(\d+)\s*GB$/
87 const m = /^(\d+)\s*MB$/
88 const b = /^(\d+)\s*B$/
89 let match
90
91 if (value.match(tgm)) {
92 match = value.match(tgm)
93 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
94 parseInt(match[2], 10) * 1024 * 1024 * 1024 +
95 parseInt(match[3], 10) * 1024 * 1024
96 } else if (value.match(tg)) {
97 match = value.match(tg)
98 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
99 parseInt(match[2], 10) * 1024 * 1024 * 1024
100 } else if (value.match(tm)) {
101 match = value.match(tm)
102 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
103 parseInt(match[2], 10) * 1024 * 1024
104 } else if (value.match(gm)) {
105 match = value.match(gm)
106 return parseInt(match[1], 10) * 1024 * 1024 * 1024 +
107 parseInt(match[2], 10) * 1024 * 1024
108 } else if (value.match(t)) {
109 match = value.match(t)
110 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
111 } else if (value.match(g)) {
112 match = value.match(g)
113 return parseInt(match[1], 10) * 1024 * 1024 * 1024
114 } else if (value.match(m)) {
115 match = value.match(m)
116 return parseInt(match[1], 10) * 1024 * 1024
117 } else if (value.match(b)) {
118 match = value.match(b)
119 return parseInt(match[1], 10) * 1024
120 } else {
121 return parseInt(value, 10)
122 }
123 }
124
125 // ---------------------------------------------------------------------------
126
127 function sanitizeUrl (url: string) {
128 const urlObject = new URL(url)
129
130 if (urlObject.protocol === 'https:' && urlObject.port === '443') {
131 urlObject.port = ''
132 } else if (urlObject.protocol === 'http:' && urlObject.port === '80') {
133 urlObject.port = ''
134 }
135
136 return urlObject.href.replace(/\/$/, '')
137 }
138
139 // Don't import remote scheme from constants because we are in core utils
140 function sanitizeHost (host: string, remoteScheme: string) {
141 const toRemove = remoteScheme === 'https' ? 443 : 80
142
143 return host.replace(new RegExp(`:${toRemove}$`), '')
144 }
145
146 // ---------------------------------------------------------------------------
147
148 function isTestInstance () {
149 return process.env.NODE_ENV === 'test'
150 }
151
152 function isProdInstance () {
153 return process.env.NODE_ENV === 'production'
154 }
155
156 function getAppNumber () {
157 return process.env.NODE_APP_INSTANCE
158 }
159
160 // ---------------------------------------------------------------------------
161
162 let rootPath: string
163
164 function root () {
165 if (rootPath) return rootPath
166
167 // We are in /helpers/utils.js
168 rootPath = join(__dirname, '..', '..')
169
170 if (basename(rootPath) === 'dist') rootPath = resolve(rootPath, '..')
171
172 return rootPath
173 }
174
175 function buildPath (path: string) {
176 if (isAbsolute(path)) return path
177
178 return join(root(), path)
179 }
180
181 function getLowercaseExtension (filename: string) {
182 const ext = extname(filename) || ''
183
184 return ext.toLowerCase()
185 }
186
187 // ---------------------------------------------------------------------------
188
189 // Consistent with .length, lodash truncate function is not
190 function peertubeTruncate (str: string, options: { length: number, separator?: RegExp, omission?: string }) {
191 const truncatedStr = truncate(str, options)
192
193 // The truncated string is okay, we can return it
194 if (truncatedStr.length <= options.length) return truncatedStr
195
196 // Lodash takes into account all UTF characters, whereas String.prototype.length does not: some characters have a length of 2
197 // We always use the .length so we need to truncate more if needed
198 options.length -= truncatedStr.length - options.length
199 return truncate(str, options)
200 }
201
202 function pageToStartAndCount (page: number, itemsPerPage: number) {
203 const start = (page - 1) * itemsPerPage
204
205 return { start, count: itemsPerPage }
206 }
207
208 // ---------------------------------------------------------------------------
209
210 type SemVersion = { major: number, minor: number, patch: number }
211 function parseSemVersion (s: string) {
212 const parsed = s.match(/^v?(\d+)\.(\d+)\.(\d+)$/i)
213
214 return {
215 major: parseInt(parsed[1]),
216 minor: parseInt(parsed[2]),
217 patch: parseInt(parsed[3])
218 } as SemVersion
219 }
220
221 // ---------------------------------------------------------------------------
222
223 function sha256 (str: string | Buffer, encoding: BinaryToTextEncoding = 'hex') {
224 return createHash('sha256').update(str).digest(encoding)
225 }
226
227 function sha1 (str: string | Buffer, encoding: BinaryToTextEncoding = 'hex') {
228 return createHash('sha1').update(str).digest(encoding)
229 }
230
231 // ---------------------------------------------------------------------------
232
233 function execShell (command: string, options?: ExecOptions) {
234 return new Promise<{ err?: Error, stdout: string, stderr: string }>((res, rej) => {
235 exec(command, options, (err, stdout, stderr) => {
236 // eslint-disable-next-line prefer-promise-reject-errors
237 if (err) return rej({ err, stdout, stderr })
238
239 return res({ stdout, stderr })
240 })
241 })
242 }
243
244 // ---------------------------------------------------------------------------
245
246 function isOdd (num: number) {
247 return (num % 2) !== 0
248 }
249
250 function toEven (num: number) {
251 if (isOdd) return num + 1
252
253 return num
254 }
255
256 // ---------------------------------------------------------------------------
257
258 function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> {
259 return function promisified (): Promise<A> {
260 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
261 func.apply(null, [ (err: any, res: A) => err ? reject(err) : resolve(res) ])
262 })
263 }
264 }
265
266 // Thanks to https://gist.github.com/kumasento/617daa7e46f13ecdd9b2
267 function promisify1<T, A> (func: (arg: T, cb: (err: any, result: A) => void) => void): (arg: T) => Promise<A> {
268 return function promisified (arg: T): Promise<A> {
269 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
270 func.apply(null, [ arg, (err: any, res: A) => err ? reject(err) : resolve(res) ])
271 })
272 }
273 }
274
275 function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A) => void) => void): (arg1: T, arg2: U) => Promise<A> {
276 return function promisified (arg1: T, arg2: U): Promise<A> {
277 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
278 func.apply(null, [ arg1, arg2, (err: any, res: A) => err ? reject(err) : resolve(res) ])
279 })
280 }
281 }
282
283 const randomBytesPromise = promisify1<number, Buffer>(randomBytes)
284 const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey)
285 const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey)
286 const execPromise2 = promisify2<string, any, string>(exec)
287 const execPromise = promisify1<string, string>(exec)
288 const pipelinePromise = promisify(pipeline)
289
290 // ---------------------------------------------------------------------------
291
292 export {
293 isTestInstance,
294 isProdInstance,
295 getAppNumber,
296
297 objectConverter,
298 mapToJSON,
299
300 root,
301 buildPath,
302 getLowercaseExtension,
303 sanitizeUrl,
304 sanitizeHost,
305
306 execShell,
307
308 pageToStartAndCount,
309 peertubeTruncate,
310
311 sha256,
312 sha1,
313
314 promisify0,
315 promisify1,
316 promisify2,
317
318 randomBytesPromise,
319 createPrivateKey,
320 getPublicKey,
321 execPromise2,
322 execPromise,
323 pipelinePromise,
324
325 parseSemVersion,
326
327 isOdd,
328 toEven
329 }