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