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