]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/helpers/core-utils.ts
Use got instead of request
[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 const randomBytesPromise = promisify1<number, Buffer>(randomBytes)
255 const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey)
256 const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey)
257 const execPromise2 = promisify2<string, any, string>(exec)
258 const execPromise = promisify1<string, string>(exec)
259 const pipelinePromise = promisify(pipeline)
260
261 // ---------------------------------------------------------------------------
262
263 export {
264 isTestInstance,
265 isProdInstance,
266 getAppNumber,
267
268 objectConverter,
269 root,
270 escapeHTML,
271 pageToStartAndCount,
272 sanitizeUrl,
273 sanitizeHost,
274 buildPath,
275 execShell,
276 peertubeTruncate,
277
278 sha256,
279 sha1,
280 mapToJSON,
281
282 promisify0,
283 promisify1,
284 promisify2,
285
286 randomBytesPromise,
287 createPrivateKey,
288 getPublicKey,
289 execPromise2,
290 execPromise,
291 pipelinePromise
292 }