core-utils.ts 9.26 KB
Newer Older
Chocobozzz's avatar
Chocobozzz committed
1
2
/* eslint-disable no-useless-call */

Chocobozzz's avatar
Chocobozzz committed
3
/*
Marcin Mikolajczak's avatar
Marcin Mikolajczak committed
4
  Different from 'utils' because we don't import other PeerTube modules.
Chocobozzz's avatar
Chocobozzz committed
5
6
7
  Useful to avoid circular dependencies.
*/

Chocobozzz's avatar
Chocobozzz committed
8
9
10
import { exec, ExecOptions } from 'child_process'
import { BinaryToTextEncoding, createHash, randomBytes } from 'crypto'
import { truncate } from 'lodash'
11
import { basename, extname, isAbsolute, join, resolve } from 'path'
Chocobozzz's avatar
Chocobozzz committed
12
import * as pem from 'pem'
Chocobozzz's avatar
Chocobozzz committed
13
import { pipeline } from 'stream'
14
import { URL } from 'url'
Chocobozzz's avatar
Chocobozzz committed
15
import { promisify } from 'util'
16
17
18
19
20
21

const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => {
  if (!oldObject || typeof oldObject !== 'object') {
    return valueConverter(oldObject)
  }

22
  if (Array.isArray(oldObject)) {
23
24
25
26
27
28
    return oldObject.map(e => objectConverter(e, keyConverter, valueConverter))
  }

  const newObject = {}
  Object.keys(oldObject).forEach(oldKey => {
    const newKey = keyConverter(oldKey)
Chocobozzz's avatar
Chocobozzz committed
29
    newObject[newKey] = objectConverter(oldObject[oldKey], keyConverter, valueConverter)
30
31
32
33
  })

  return newObject
}
34

35
36
37
38
39
40
41
42
43
44
45
46
function mapToJSON (map: Map<any, any>) {
  const obj: any = {}

  for (const [ k, v ] of map) {
    obj[k] = v
  }

  return obj
}

// ---------------------------------------------------------------------------

Chocobozzz's avatar
Chocobozzz committed
47
const timeTable = {
Chocobozzz's avatar
Chocobozzz committed
48
49
50
51
52
53
54
  ms: 1,
  second: 1000,
  minute: 60000,
  hour: 3600000,
  day: 3600000 * 24,
  week: 3600000 * 24 * 7,
  month: 3600000 * 24 * 30
Chocobozzz's avatar
Chocobozzz committed
55
}
56

57
export function parseDurationToMs (duration: number | string): number {
Chocobozzz's avatar
Chocobozzz committed
58
  if (duration === null) return null
Chocobozzz's avatar
Chocobozzz committed
59
60
61
  if (typeof duration === 'number') return duration

  if (typeof duration === 'string') {
Chocobozzz's avatar
Chocobozzz committed
62
    const split = duration.match(/^([\d.,]+)\s?(\w+)$/)
Chocobozzz's avatar
Chocobozzz committed
63
64
65

    if (split.length === 3) {
      const len = parseFloat(split[1])
Chocobozzz's avatar
Chocobozzz committed
66
      let unit = split[2].replace(/s$/i, '').toLowerCase()
Chocobozzz's avatar
Chocobozzz committed
67
68
69
70
71
72
73
74
      if (unit === 'm') {
        unit = 'ms'
      }

      return (len || 1) * (timeTable[unit] || 0)
    }
  }

75
  throw new Error(`Duration ${duration} could not be properly parsed`)
Chocobozzz's avatar
Chocobozzz committed
76
77
}

78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
export function parseBytes (value: string | number): number {
  if (typeof value === 'number') return value

  const tgm = /^(\d+)\s*TB\s*(\d+)\s*GB\s*(\d+)\s*MB$/
  const tg = /^(\d+)\s*TB\s*(\d+)\s*GB$/
  const tm = /^(\d+)\s*TB\s*(\d+)\s*MB$/
  const gm = /^(\d+)\s*GB\s*(\d+)\s*MB$/
  const t = /^(\d+)\s*TB$/
  const g = /^(\d+)\s*GB$/
  const m = /^(\d+)\s*MB$/
  const b = /^(\d+)\s*B$/
  let match

  if (value.match(tgm)) {
    match = value.match(tgm)
Chocobozzz's avatar
Chocobozzz committed
93
94
95
    return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
      parseInt(match[2], 10) * 1024 * 1024 * 1024 +
      parseInt(match[3], 10) * 1024 * 1024
96
97
  } else if (value.match(tg)) {
    match = value.match(tg)
Chocobozzz's avatar
Chocobozzz committed
98
99
    return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
      parseInt(match[2], 10) * 1024 * 1024 * 1024
100
101
  } else if (value.match(tm)) {
    match = value.match(tm)
Chocobozzz's avatar
Chocobozzz committed
102
103
    return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
      parseInt(match[2], 10) * 1024 * 1024
104
105
  } else if (value.match(gm)) {
    match = value.match(gm)
Chocobozzz's avatar
Chocobozzz committed
106
107
    return parseInt(match[1], 10) * 1024 * 1024 * 1024 +
      parseInt(match[2], 10) * 1024 * 1024
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
  } else if (value.match(t)) {
    match = value.match(t)
    return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
  } else if (value.match(g)) {
    match = value.match(g)
    return parseInt(match[1], 10) * 1024 * 1024 * 1024
  } else if (value.match(m)) {
    match = value.match(m)
    return parseInt(match[1], 10) * 1024 * 1024
  } else if (value.match(b)) {
    match = value.match(b)
    return parseInt(match[1], 10) * 1024
  } else {
    return parseInt(value, 10)
  }
}

125
126
// ---------------------------------------------------------------------------

127
128
129
130
131
132
133
134
135
136
137
138
139
140
function sanitizeUrl (url: string) {
  const urlObject = new URL(url)

  if (urlObject.protocol === 'https:' && urlObject.port === '443') {
    urlObject.port = ''
  } else if (urlObject.protocol === 'http:' && urlObject.port === '80') {
    urlObject.port = ''
  }

  return urlObject.href.replace(/\/$/, '')
}

// Don't import remote scheme from constants because we are in core utils
function sanitizeHost (host: string, remoteScheme: string) {
Chocobozzz's avatar
Chocobozzz committed
141
  const toRemove = remoteScheme === 'https' ? 443 : 80
142
143
144

  return host.replace(new RegExp(`:${toRemove}$`), '')
}
Chocobozzz's avatar
Chocobozzz committed
145

146
147
// ---------------------------------------------------------------------------

Chocobozzz's avatar
Chocobozzz committed
148
149
150
151
function isTestInstance () {
  return process.env.NODE_ENV === 'test'
}

152
153
154
155
function isProdInstance () {
  return process.env.NODE_ENV === 'production'
}

Chocobozzz's avatar
Chocobozzz committed
156
157
158
159
function getAppNumber () {
  return process.env.NODE_APP_INSTANCE
}

160
161
// ---------------------------------------------------------------------------

Chocobozzz's avatar
Chocobozzz committed
162
let rootPath: string
Chocobozzz's avatar
Chocobozzz committed
163

Chocobozzz's avatar
Chocobozzz committed
164
function root () {
Chocobozzz's avatar
Chocobozzz committed
165
166
  if (rootPath) return rootPath

Chocobozzz's avatar
Chocobozzz committed
167
  // We are in /helpers/utils.js
Chocobozzz's avatar
Chocobozzz committed
168
  rootPath = join(__dirname, '..', '..')
Chocobozzz's avatar
Chocobozzz committed
169

Chocobozzz's avatar
Chocobozzz committed
170
  if (basename(rootPath) === 'dist') rootPath = resolve(rootPath, '..')
Chocobozzz's avatar
Chocobozzz committed
171

Chocobozzz's avatar
Chocobozzz committed
172
  return rootPath
Chocobozzz's avatar
Chocobozzz committed
173
174
}

175
176
function buildPath (path: string) {
  if (isAbsolute(path)) return path
Chocobozzz's avatar
Chocobozzz committed
177

178
  return join(root(), path)
Chocobozzz's avatar
Chocobozzz committed
179
180
}

181
182
function getLowercaseExtension (filename: string) {
  const ext = extname(filename) || ''
183

184
  return ext.toLowerCase()
185
186
}

187
// ---------------------------------------------------------------------------
Chocobozzz's avatar
Chocobozzz committed
188

189
// Consistent with .length, lodash truncate function is not
190
function peertubeTruncate (str: string, options: { length: number, separator?: RegExp, omission?: string }) {
191
192
193
  const truncatedStr = truncate(str, options)

  // The truncated string is okay, we can return it
194
  if (truncatedStr.length <= options.length) return truncatedStr
195
196
197

  // Lodash takes into account all UTF characters, whereas String.prototype.length does not: some characters have a length of 2
  // We always use the .length so we need to truncate more if needed
198
  options.length -= truncatedStr.length - options.length
199
200
201
  return truncate(str, options)
}

202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
function pageToStartAndCount (page: number, itemsPerPage: number) {
  const start = (page - 1) * itemsPerPage

  return { start, count: itemsPerPage }
}

// ---------------------------------------------------------------------------

type SemVersion = { major: number, minor: number, patch: number }
function parseSemVersion (s: string) {
  const parsed = s.match(/^v?(\d+)\.(\d+)\.(\d+)$/i)

  return {
    major: parseInt(parsed[1]),
    minor: parseInt(parsed[2]),
    patch: parseInt(parsed[3])
  } as SemVersion
}

// ---------------------------------------------------------------------------

Chocobozzz's avatar
Chocobozzz committed
223
function sha256 (str: string | Buffer, encoding: BinaryToTextEncoding = 'hex') {
224
  return createHash('sha256').update(str).digest(encoding)
Chocobozzz's avatar
Chocobozzz committed
225
226
}

Chocobozzz's avatar
Chocobozzz committed
227
function sha1 (str: string | Buffer, encoding: BinaryToTextEncoding = 'hex') {
Chocobozzz's avatar
Chocobozzz committed
228
229
230
  return createHash('sha1').update(str).digest(encoding)
}

231
232
// ---------------------------------------------------------------------------

Chocobozzz's avatar
Chocobozzz committed
233
234
235
function execShell (command: string, options?: ExecOptions) {
  return new Promise<{ err?: Error, stdout: string, stderr: string }>((res, rej) => {
    exec(command, options, (err, stdout, stderr) => {
Chocobozzz's avatar
Chocobozzz committed
236
      // eslint-disable-next-line prefer-promise-reject-errors
Chocobozzz's avatar
Chocobozzz committed
237
238
239
240
241
242
243
      if (err) return rej({ err, stdout, stderr })

      return res({ stdout, stderr })
    })
  })
}

244
245
// ---------------------------------------------------------------------------

246
247
248
249
250
251
252
253
254
255
256
257
function isOdd (num: number) {
  return (num % 2) !== 0
}

function toEven (num: number) {
  if (isOdd) return num + 1

  return num
}

// ---------------------------------------------------------------------------

Chocobozzz's avatar
Chocobozzz committed
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> {
  return function promisified (): Promise<A> {
    return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
      func.apply(null, [ (err: any, res: A) => err ? reject(err) : resolve(res) ])
    })
  }
}

// Thanks to https://gist.github.com/kumasento/617daa7e46f13ecdd9b2
function promisify1<T, A> (func: (arg: T, cb: (err: any, result: A) => void) => void): (arg: T) => Promise<A> {
  return function promisified (arg: T): Promise<A> {
    return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
      func.apply(null, [ arg, (err: any, res: A) => err ? reject(err) : resolve(res) ])
    })
  }
}

function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A) => void) => void): (arg1: T, arg2: U) => Promise<A> {
  return function promisified (arg1: T, arg2: U): Promise<A> {
    return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
      func.apply(null, [ arg1, arg2, (err: any, res: A) => err ? reject(err) : resolve(res) ])
    })
  }
}

Chocobozzz's avatar
Chocobozzz committed
283
const randomBytesPromise = promisify1<number, Buffer>(randomBytes)
Chocobozzz's avatar
Chocobozzz committed
284
285
const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey)
const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey)
Chocobozzz's avatar
Chocobozzz committed
286
287
const execPromise2 = promisify2<string, any, string>(exec)
const execPromise = promisify1<string, string>(exec)
Chocobozzz's avatar
Chocobozzz committed
288
const pipelinePromise = promisify(pipeline)
Chocobozzz's avatar
Chocobozzz committed
289

Chocobozzz's avatar
Chocobozzz committed
290
291
292
293
// ---------------------------------------------------------------------------

export {
  isTestInstance,
294
  isProdInstance,
Chocobozzz's avatar
Chocobozzz committed
295
  getAppNumber,
296

297
  objectConverter,
298
299
  mapToJSON,

Chocobozzz's avatar
Chocobozzz committed
300
  root,
301
302
  buildPath,
  getLowercaseExtension,
303
304
  sanitizeUrl,
  sanitizeHost,
305

Chocobozzz's avatar
Chocobozzz committed
306
  execShell,
307
308

  pageToStartAndCount,
309
  peertubeTruncate,
Chocobozzz's avatar
Chocobozzz committed
310

Chocobozzz's avatar
Chocobozzz committed
311
  sha256,
Chocobozzz's avatar
Chocobozzz committed
312
  sha1,
Chocobozzz's avatar
Chocobozzz committed
313
314
315

  promisify0,
  promisify1,
316
  promisify2,
Chocobozzz's avatar
Chocobozzz committed
317

Chocobozzz's avatar
Chocobozzz committed
318
  randomBytesPromise,
Chocobozzz's avatar
Chocobozzz committed
319
320
  createPrivateKey,
  getPublicKey,
Chocobozzz's avatar
Chocobozzz committed
321
  execPromise2,
Chocobozzz's avatar
Chocobozzz committed
322
  execPromise,
Chocobozzz's avatar
Chocobozzz committed
323
324
  pipelinePromise,

325
326
327
328
  parseSemVersion,

  isOdd,
  toEven
Chocobozzz's avatar
Chocobozzz committed
329
}