]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/helpers/core-utils.ts
Merge branch 'release/3.1.0' into develop
[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 type SemVersion = { major: number, minor: number, patch: number }
255 function parseSemVersion (s: string) {
256 const parsed = s.match(/^v?(\d+)\.(\d+)\.(\d+)$/i)
257
258 return {
259 major: parseInt(parsed[1]),
260 minor: parseInt(parsed[2]),
261 patch: parseInt(parsed[3])
262 } as SemVersion
263 }
264
265 const randomBytesPromise = promisify1<number, Buffer>(randomBytes)
266 const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey)
267 const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey)
268 const execPromise2 = promisify2<string, any, string>(exec)
269 const execPromise = promisify1<string, string>(exec)
270 const pipelinePromise = promisify(pipeline)
271
272 // ---------------------------------------------------------------------------
273
274 export {
275 isTestInstance,
276 isProdInstance,
277 getAppNumber,
278
279 objectConverter,
280 root,
281 escapeHTML,
282 pageToStartAndCount,
283 sanitizeUrl,
284 sanitizeHost,
285 buildPath,
286 execShell,
287 peertubeTruncate,
288
289 sha256,
290 sha1,
291 mapToJSON,
292
293 promisify0,
294 promisify1,
295 promisify2,
296
297 randomBytesPromise,
298 createPrivateKey,
299 getPublicKey,
300 execPromise2,
301 execPromise,
302 pipelinePromise,
303
304 parseSemVersion
305 }