]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/helpers/core-utils.ts
WIP plugins: move plugin CLI in peertube script
[github/Chocobozzz/PeerTube.git] / server / helpers / core-utils.ts
1 /*
2 Different from 'utils' because we don't not import other PeerTube modules.
3 Useful to avoid circular dependencies.
4 */
5
6 import * as createTorrent from 'create-torrent'
7 import { createHash, HexBase64Latin1Encoding, pseudoRandomBytes } from 'crypto'
8 import { isAbsolute, join } from 'path'
9 import * as pem from 'pem'
10 import { URL } from 'url'
11 import { truncate } from 'lodash'
12 import { exec, ExecOptions } from 'child_process'
13
14 const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => {
15 if (!oldObject || typeof oldObject !== 'object') {
16 return valueConverter(oldObject)
17 }
18
19 if (Array.isArray(oldObject)) {
20 return oldObject.map(e => objectConverter(e, keyConverter, valueConverter))
21 }
22
23 const newObject = {}
24 Object.keys(oldObject).forEach(oldKey => {
25 const newKey = keyConverter(oldKey)
26 newObject[ newKey ] = objectConverter(oldObject[ oldKey ], keyConverter, valueConverter)
27 })
28
29 return newObject
30 }
31
32 const timeTable = {
33 ms: 1,
34 second: 1000,
35 minute: 60000,
36 hour: 3600000,
37 day: 3600000 * 24,
38 week: 3600000 * 24 * 7,
39 month: 3600000 * 24 * 30
40 }
41
42 export function parseDurationToMs (duration: number | string): number {
43 if (typeof duration === 'number') return duration
44
45 if (typeof duration === 'string') {
46 const split = duration.match(/^([\d\.,]+)\s?(\w+)$/)
47
48 if (split.length === 3) {
49 const len = parseFloat(split[1])
50 let unit = split[2].replace(/s$/i,'').toLowerCase()
51 if (unit === 'm') {
52 unit = 'ms'
53 }
54
55 return (len || 1) * (timeTable[unit] || 0)
56 }
57 }
58
59 throw new Error(`Duration ${duration} could not be properly parsed`)
60 }
61
62 export function parseBytes (value: string | number): number {
63 if (typeof value === 'number') return value
64
65 const tgm = /^(\d+)\s*TB\s*(\d+)\s*GB\s*(\d+)\s*MB$/
66 const tg = /^(\d+)\s*TB\s*(\d+)\s*GB$/
67 const tm = /^(\d+)\s*TB\s*(\d+)\s*MB$/
68 const gm = /^(\d+)\s*GB\s*(\d+)\s*MB$/
69 const t = /^(\d+)\s*TB$/
70 const g = /^(\d+)\s*GB$/
71 const m = /^(\d+)\s*MB$/
72 const b = /^(\d+)\s*B$/
73 let match
74
75 if (value.match(tgm)) {
76 match = value.match(tgm)
77 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
78 + parseInt(match[2], 10) * 1024 * 1024 * 1024
79 + parseInt(match[3], 10) * 1024 * 1024
80 } else if (value.match(tg)) {
81 match = value.match(tg)
82 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
83 + parseInt(match[2], 10) * 1024 * 1024 * 1024
84 } else if (value.match(tm)) {
85 match = value.match(tm)
86 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
87 + parseInt(match[2], 10) * 1024 * 1024
88 } else if (value.match(gm)) {
89 match = value.match(gm)
90 return parseInt(match[1], 10) * 1024 * 1024 * 1024
91 + parseInt(match[2], 10) * 1024 * 1024
92 } else if (value.match(t)) {
93 match = value.match(t)
94 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
95 } else if (value.match(g)) {
96 match = value.match(g)
97 return parseInt(match[1], 10) * 1024 * 1024 * 1024
98 } else if (value.match(m)) {
99 match = value.match(m)
100 return parseInt(match[1], 10) * 1024 * 1024
101 } else if (value.match(b)) {
102 match = value.match(b)
103 return parseInt(match[1], 10) * 1024
104 } else {
105 return parseInt(value, 10)
106 }
107 }
108
109 function sanitizeUrl (url: string) {
110 const urlObject = new URL(url)
111
112 if (urlObject.protocol === 'https:' && urlObject.port === '443') {
113 urlObject.port = ''
114 } else if (urlObject.protocol === 'http:' && urlObject.port === '80') {
115 urlObject.port = ''
116 }
117
118 return urlObject.href.replace(/\/$/, '')
119 }
120
121 // Don't import remote scheme from constants because we are in core utils
122 function sanitizeHost (host: string, remoteScheme: string) {
123 const toRemove = remoteScheme === 'https' ? 443 : 80
124
125 return host.replace(new RegExp(`:${toRemove}$`), '')
126 }
127
128 function isTestInstance () {
129 return process.env.NODE_ENV === 'test'
130 }
131
132 function isProdInstance () {
133 return process.env.NODE_ENV === 'production'
134 }
135
136 function getAppNumber () {
137 return process.env.NODE_APP_INSTANCE
138 }
139
140 function root () {
141 // We are in /helpers/utils.js
142 const paths = [ __dirname, '..', '..' ]
143
144 // We are under /dist directory
145 if (process.mainModule && process.mainModule.filename.endsWith('.ts') === false) {
146 paths.push('..')
147 }
148
149 return join.apply(null, paths)
150 }
151
152 // Thanks: https://stackoverflow.com/a/12034334
153 function escapeHTML (stringParam) {
154 if (!stringParam) return ''
155
156 const entityMap = {
157 '&': '&',
158 '<': '&lt;',
159 '>': '&gt;',
160 '"': '&quot;',
161 '\'': '&#39;',
162 '/': '&#x2F;',
163 '`': '&#x60;',
164 '=': '&#x3D;'
165 }
166
167 return String(stringParam).replace(/[&<>"'`=\/]/g, s => entityMap[s])
168 }
169
170 function pageToStartAndCount (page: number, itemsPerPage: number) {
171 const start = (page - 1) * itemsPerPage
172
173 return { start, count: itemsPerPage }
174 }
175
176 function buildPath (path: string) {
177 if (isAbsolute(path)) return path
178
179 return join(root(), path)
180 }
181
182 // Consistent with .length, lodash truncate function is not
183 function peertubeTruncate (str: string, maxLength: number) {
184 const options = {
185 length: maxLength
186 }
187 const truncatedStr = truncate(str, options)
188
189 // The truncated string is okay, we can return it
190 if (truncatedStr.length <= maxLength) return truncatedStr
191
192 // Lodash takes into account all UTF characters, whereas String.prototype.length does not: some characters have a length of 2
193 // We always use the .length so we need to truncate more if needed
194 options.length -= truncatedStr.length - maxLength
195 return truncate(str, options)
196 }
197
198 function sha256 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') {
199 return createHash('sha256').update(str).digest(encoding)
200 }
201
202 function sha1 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') {
203 return createHash('sha1').update(str).digest(encoding)
204 }
205
206 function execShell (command: string, options?: ExecOptions) {
207 return new Promise<{ err?: Error, stdout: string, stderr: string }>((res, rej) => {
208 exec(command, options, (err, stdout, stderr) => {
209 if (err) return rej({ err, stdout, stderr })
210
211 return res({ stdout, stderr })
212 })
213 })
214 }
215
216 function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> {
217 return function promisified (): Promise<A> {
218 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
219 func.apply(null, [ (err: any, res: A) => err ? reject(err) : resolve(res) ])
220 })
221 }
222 }
223
224 // Thanks to https://gist.github.com/kumasento/617daa7e46f13ecdd9b2
225 function promisify1<T, A> (func: (arg: T, cb: (err: any, result: A) => void) => void): (arg: T) => Promise<A> {
226 return function promisified (arg: T): Promise<A> {
227 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
228 func.apply(null, [ arg, (err: any, res: A) => err ? reject(err) : resolve(res) ])
229 })
230 }
231 }
232
233 function promisify1WithVoid<T> (func: (arg: T, cb: (err: any) => void) => void): (arg: T) => Promise<void> {
234 return function promisified (arg: T): Promise<void> {
235 return new Promise<void>((resolve: () => void, reject: (err: any) => void) => {
236 func.apply(null, [ arg, (err: any) => err ? reject(err) : resolve() ])
237 })
238 }
239 }
240
241 function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A) => void) => void): (arg1: T, arg2: U) => Promise<A> {
242 return function promisified (arg1: T, arg2: U): Promise<A> {
243 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
244 func.apply(null, [ arg1, arg2, (err: any, res: A) => err ? reject(err) : resolve(res) ])
245 })
246 }
247 }
248
249 function promisify2WithVoid<T, U> (func: (arg1: T, arg2: U, cb: (err: any) => void) => void): (arg1: T, arg2: U) => Promise<void> {
250 return function promisified (arg1: T, arg2: U): Promise<void> {
251 return new Promise<void>((resolve: () => void, reject: (err: any) => void) => {
252 func.apply(null, [ arg1, arg2, (err: any) => err ? reject(err) : resolve() ])
253 })
254 }
255 }
256
257 const pseudoRandomBytesPromise = promisify1<number, Buffer>(pseudoRandomBytes)
258 const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey)
259 const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey)
260 const createTorrentPromise = promisify2<string, any, any>(createTorrent)
261 const execPromise2 = promisify2<string, any, string>(exec)
262 const execPromise = promisify1<string, string>(exec)
263
264 // ---------------------------------------------------------------------------
265
266 export {
267 isTestInstance,
268 isProdInstance,
269 getAppNumber,
270
271 objectConverter,
272 root,
273 escapeHTML,
274 pageToStartAndCount,
275 sanitizeUrl,
276 sanitizeHost,
277 buildPath,
278 execShell,
279 peertubeTruncate,
280
281 sha256,
282 sha1,
283
284 promisify0,
285 promisify1,
286 promisify2,
287
288 pseudoRandomBytesPromise,
289 createPrivateKey,
290 getPublicKey,
291 createTorrentPromise,
292 execPromise2,
293 execPromise
294 }