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