9830d41a89bdc375443d3587447d026c50c1d9f9
[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, pseudoRandomBytes } from 'crypto'
9 import { copyFile, readdir, readFile, rename, stat, Stats, unlink, writeFile, mkdirp } from 'fs-extra'
10 import { isAbsolute, join } from 'path'
11 import * as pem from 'pem'
12 import * as rimraf from 'rimraf'
13 import { URL } from 'url'
14 import { truncate } from 'lodash'
15
16 const timeTable = {
17   ms:           1,
18   second:       1000,
19   minute:       60000,
20   hour:         3600000,
21   day:          3600000 * 24,
22   week:         3600000 * 24 * 7,
23   month:        3600000 * 24 * 30
24 }
25 export function parseDuration (duration: number | string): number {
26   if (typeof duration === 'number') return duration
27
28   if (typeof duration === 'string') {
29     const split = duration.match(/^([\d\.,]+)\s?(\w+)$/)
30
31     if (split.length === 3) {
32       const len = parseFloat(split[1])
33       let unit = split[2].replace(/s$/i,'').toLowerCase()
34       if (unit === 'm') {
35         unit = 'ms'
36       }
37
38       return (len || 1) * (timeTable[unit] || 0)
39     }
40   }
41
42   throw new Error('Duration could not be properly parsed')
43 }
44
45 function sanitizeUrl (url: string) {
46   const urlObject = new URL(url)
47
48   if (urlObject.protocol === 'https:' && urlObject.port === '443') {
49     urlObject.port = ''
50   } else if (urlObject.protocol === 'http:' && urlObject.port === '80') {
51     urlObject.port = ''
52   }
53
54   return urlObject.href.replace(/\/$/, '')
55 }
56
57 // Don't import remote scheme from constants because we are in core utils
58 function sanitizeHost (host: string, remoteScheme: string) {
59   const toRemove = remoteScheme === 'https' ? 443 : 80
60
61   return host.replace(new RegExp(`:${toRemove}$`), '')
62 }
63
64 function isTestInstance () {
65   return process.env.NODE_ENV === 'test'
66 }
67
68 function root () {
69   // We are in /helpers/utils.js
70   const paths = [ __dirname, '..', '..' ]
71
72   // We are under /dist directory
73   if (process.mainModule && process.mainModule.filename.endsWith('.ts') === false) {
74     paths.push('..')
75   }
76
77   return join.apply(null, paths)
78 }
79
80 // Thanks: https://stackoverflow.com/a/12034334
81 function escapeHTML (stringParam) {
82   if (!stringParam) return ''
83
84   const entityMap = {
85     '&': '&',
86     '<': '&lt;',
87     '>': '&gt;',
88     '"': '&quot;',
89     '\'': '&#39;',
90     '/': '&#x2F;',
91     '`': '&#x60;',
92     '=': '&#x3D;'
93   }
94
95   return String(stringParam).replace(/[&<>"'`=\/]/g, s => entityMap[s])
96 }
97
98 function pageToStartAndCount (page: number, itemsPerPage: number) {
99   const start = (page - 1) * itemsPerPage
100
101   return { start, count: itemsPerPage }
102 }
103
104 function buildPath (path: string) {
105   if (isAbsolute(path)) return path
106
107   return join(root(), path)
108 }
109
110 // Consistent with .length, lodash truncate function is not
111 function peertubeTruncate (str: string, maxLength: number) {
112   const options = {
113     length: maxLength
114   }
115   const truncatedStr = truncate(str, options)
116
117   // The truncated string is okay, we can return it
118   if (truncatedStr.length <= maxLength) return truncatedStr
119
120   // Lodash takes into account all UTF characters, whereas String.prototype.length does not: some characters have a length of 2
121   // We always use the .length so we need to truncate more if needed
122   options.length -= truncatedStr.length - maxLength
123   return truncate(str, options)
124 }
125
126 function sha256 (str: string) {
127   return createHash('sha256').update(str).digest('hex')
128 }
129
130 function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> {
131   return function promisified (): Promise<A> {
132     return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
133       func.apply(null, [ (err: any, res: A) => err ? reject(err) : resolve(res) ])
134     })
135   }
136 }
137
138 // Thanks to https://gist.github.com/kumasento/617daa7e46f13ecdd9b2
139 function promisify1<T, A> (func: (arg: T, cb: (err: any, result: A) => void) => void): (arg: T) => Promise<A> {
140   return function promisified (arg: T): Promise<A> {
141     return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
142       func.apply(null, [ arg, (err: any, res: A) => err ? reject(err) : resolve(res) ])
143     })
144   }
145 }
146
147 function promisify1WithVoid<T> (func: (arg: T, cb: (err: any) => void) => void): (arg: T) => Promise<void> {
148   return function promisified (arg: T): Promise<void> {
149     return new Promise<void>((resolve: () => void, reject: (err: any) => void) => {
150       func.apply(null, [ arg, (err: any) => err ? reject(err) : resolve() ])
151     })
152   }
153 }
154
155 function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A) => void) => void): (arg1: T, arg2: U) => Promise<A> {
156   return function promisified (arg1: T, arg2: U): Promise<A> {
157     return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
158       func.apply(null, [ arg1, arg2, (err: any, res: A) => err ? reject(err) : resolve(res) ])
159     })
160   }
161 }
162
163 function promisify2WithVoid<T, U> (func: (arg1: T, arg2: U, cb: (err: any) => void) => void): (arg1: T, arg2: U) => Promise<void> {
164   return function promisified (arg1: T, arg2: U): Promise<void> {
165     return new Promise<void>((resolve: () => void, reject: (err: any) => void) => {
166       func.apply(null, [ arg1, arg2, (err: any) => err ? reject(err) : resolve() ])
167     })
168   }
169 }
170
171 const copyFilePromise = promisify2WithVoid<string, string>(copyFile)
172 const readFileBufferPromise = promisify1<string, Buffer>(readFile)
173 const unlinkPromise = promisify1WithVoid<string>(unlink)
174 const renamePromise = promisify2WithVoid<string, string>(rename)
175 const writeFilePromise = promisify2WithVoid<string, any>(writeFile)
176 const readdirPromise = promisify1<string, string[]>(readdir)
177 const mkdirpPromise = promisify1<string, string>(mkdirp)
178 // we cannot modify the Promise types, so we should make the promisify instance check mkdirp
179 const pseudoRandomBytesPromise = promisify1<number, Buffer>(pseudoRandomBytes)
180 const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey)
181 const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey)
182 const bcryptComparePromise = promisify2<any, string, boolean>(bcrypt.compare)
183 const bcryptGenSaltPromise = promisify1<number, string>(bcrypt.genSalt)
184 const bcryptHashPromise = promisify2<any, string | number, string>(bcrypt.hash)
185 const createTorrentPromise = promisify2<string, any, any>(createTorrent)
186 const rimrafPromise = promisify1WithVoid<string>(rimraf)
187 const statPromise = promisify1<string, Stats>(stat)
188
189 // ---------------------------------------------------------------------------
190
191 export {
192   isTestInstance,
193   root,
194   escapeHTML,
195   pageToStartAndCount,
196   sanitizeUrl,
197   sanitizeHost,
198   buildPath,
199   peertubeTruncate,
200   sha256,
201
202   promisify0,
203   promisify1,
204
205   copyFilePromise,
206   readdirPromise,
207   readFileBufferPromise,
208   unlinkPromise,
209   renamePromise,
210   writeFilePromise,
211   mkdirpPromise,
212   pseudoRandomBytesPromise,
213   createPrivateKey,
214   getPublicKey,
215   bcryptComparePromise,
216   bcryptGenSaltPromise,
217   bcryptHashPromise,
218   createTorrentPromise,
219   rimrafPromise,
220   statPromise
221 }