ニジカ投稿局 https://tv.nizika.tv
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

core-utils.ts 8.1 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. /* eslint-disable no-useless-call */
  2. /*
  3. Different from 'utils' because we don't import other PeerTube modules.
  4. Useful to avoid circular dependencies.
  5. */
  6. import { promisify1, promisify2, promisify3 } from '@peertube/peertube-core-utils'
  7. import { exec, ExecOptions } from 'child_process'
  8. import { ED25519KeyPairOptions, generateKeyPair, randomBytes, RSAKeyPairOptions, scrypt } from 'crypto'
  9. import truncate from 'lodash-es/truncate.js'
  10. import { pipeline } from 'stream'
  11. import { URL } from 'url'
  12. import { promisify } from 'util'
  13. const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => {
  14. if (!oldObject || typeof oldObject !== 'object') {
  15. return valueConverter(oldObject)
  16. }
  17. if (Array.isArray(oldObject)) {
  18. return oldObject.map(e => objectConverter(e, keyConverter, valueConverter))
  19. }
  20. const newObject = {}
  21. Object.keys(oldObject).forEach(oldKey => {
  22. const newKey = keyConverter(oldKey)
  23. newObject[newKey] = objectConverter(oldObject[oldKey], keyConverter, valueConverter)
  24. })
  25. return newObject
  26. }
  27. function mapToJSON (map: Map<any, any>) {
  28. const obj: any = {}
  29. for (const [ k, v ] of map) {
  30. obj[k] = v
  31. }
  32. return obj
  33. }
  34. // ---------------------------------------------------------------------------
  35. const timeTable = {
  36. ms: 1,
  37. second: 1000,
  38. minute: 60000,
  39. hour: 3600000,
  40. day: 3600000 * 24,
  41. week: 3600000 * 24 * 7,
  42. month: 3600000 * 24 * 30
  43. }
  44. export function parseDurationToMs (duration: number | string): number {
  45. if (duration === null) return null
  46. if (typeof duration === 'number') return duration
  47. if (!isNaN(+duration)) return +duration
  48. if (typeof duration === 'string') {
  49. const split = duration.match(/^([\d.,]+)\s?(\w+)$/)
  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. return (len || 1) * (timeTable[unit] || 0)
  57. }
  58. }
  59. throw new Error(`Duration ${duration} could not be properly parsed`)
  60. }
  61. export function parseBytes (value: string | number): number {
  62. if (typeof value === 'number') return value
  63. if (!isNaN(+value)) return +value
  64. const tgm = /^(\d+)\s*TB\s*(\d+)\s*GB\s*(\d+)\s*MB$/
  65. const tg = /^(\d+)\s*TB\s*(\d+)\s*GB$/
  66. const tm = /^(\d+)\s*TB\s*(\d+)\s*MB$/
  67. const gm = /^(\d+)\s*GB\s*(\d+)\s*MB$/
  68. const t = /^(\d+)\s*TB$/
  69. const g = /^(\d+)\s*GB$/
  70. const m = /^(\d+)\s*MB$/
  71. const b = /^(\d+)\s*B$/
  72. let match: RegExpMatchArray
  73. if (value.match(tgm)) {
  74. match = value.match(tgm)
  75. return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
  76. parseInt(match[2], 10) * 1024 * 1024 * 1024 +
  77. parseInt(match[3], 10) * 1024 * 1024
  78. }
  79. if (value.match(tg)) {
  80. match = value.match(tg)
  81. return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
  82. parseInt(match[2], 10) * 1024 * 1024 * 1024
  83. }
  84. 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. }
  89. if (value.match(gm)) {
  90. match = value.match(gm)
  91. return parseInt(match[1], 10) * 1024 * 1024 * 1024 +
  92. parseInt(match[2], 10) * 1024 * 1024
  93. }
  94. if (value.match(t)) {
  95. match = value.match(t)
  96. return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
  97. }
  98. if (value.match(g)) {
  99. match = value.match(g)
  100. return parseInt(match[1], 10) * 1024 * 1024 * 1024
  101. }
  102. if (value.match(m)) {
  103. match = value.match(m)
  104. return parseInt(match[1], 10) * 1024 * 1024
  105. }
  106. if (value.match(b)) {
  107. match = value.match(b)
  108. return parseInt(match[1], 10) * 1024
  109. }
  110. return parseInt(value, 10)
  111. }
  112. // ---------------------------------------------------------------------------
  113. function sanitizeUrl (url: string) {
  114. const urlObject = new URL(url)
  115. if (urlObject.protocol === 'https:' && urlObject.port === '443') {
  116. urlObject.port = ''
  117. } else if (urlObject.protocol === 'http:' && urlObject.port === '80') {
  118. urlObject.port = ''
  119. }
  120. return urlObject.href.replace(/\/$/, '')
  121. }
  122. // Don't import remote scheme from constants because we are in core utils
  123. function sanitizeHost (host: string, remoteScheme: string) {
  124. const toRemove = remoteScheme === 'https' ? 443 : 80
  125. return host.replace(new RegExp(`:${toRemove}$`), '')
  126. }
  127. // ---------------------------------------------------------------------------
  128. // Consistent with .length, lodash truncate function is not
  129. function peertubeTruncate (str: string, options: { length: number, separator?: RegExp, omission?: string }) {
  130. const truncatedStr = truncate(str, options)
  131. // The truncated string is okay, we can return it
  132. if (truncatedStr.length <= options.length) return truncatedStr
  133. // Lodash takes into account all UTF characters, whereas String.prototype.length does not: some characters have a length of 2
  134. // We always use the .length so we need to truncate more if needed
  135. options.length -= truncatedStr.length - options.length
  136. return truncate(str, options)
  137. }
  138. function pageToStartAndCount (page: number, itemsPerPage: number) {
  139. const start = (page - 1) * itemsPerPage
  140. return { start, count: itemsPerPage }
  141. }
  142. // ---------------------------------------------------------------------------
  143. type SemVersion = { major: number, minor: number, patch: number }
  144. /**
  145. * Parses a semantic version string into its separate components.
  146. * Fairly lax, and allows for missing or additional segments in the string.
  147. *
  148. * @param s String to parse semantic version from.
  149. * @returns Major, minor, and patch version, or null if string does not follow semantic version conventions.
  150. */
  151. function parseSemVersion (s: string) {
  152. const parsed = s.match(/v?(\d+)\.(\d+)(?:\.(\d+))?/i)
  153. if (!parsed) return null
  154. return {
  155. major: parseInt(parsed[1]),
  156. minor: parseInt(parsed[2]),
  157. patch: parsed[3] ? parseInt(parsed[3]) : 0
  158. } as SemVersion
  159. }
  160. // ---------------------------------------------------------------------------
  161. function execShell (command: string, options?: ExecOptions) {
  162. return new Promise<{ err?: Error, stdout: string, stderr: string }>((res, rej) => {
  163. exec(command, options, (err, stdout, stderr) => {
  164. // eslint-disable-next-line prefer-promise-reject-errors
  165. if (err) return rej({ err, stdout, stderr })
  166. return res({ stdout, stderr })
  167. })
  168. })
  169. }
  170. // ---------------------------------------------------------------------------
  171. function generateRSAKeyPairPromise (size: number) {
  172. return new Promise<{ publicKey: string, privateKey: string }>((res, rej) => {
  173. const options: RSAKeyPairOptions<'pem', 'pem'> = {
  174. modulusLength: size,
  175. publicKeyEncoding: {
  176. type: 'spki',
  177. format: 'pem'
  178. },
  179. privateKeyEncoding: {
  180. type: 'pkcs1',
  181. format: 'pem'
  182. }
  183. }
  184. generateKeyPair('rsa', options, (err, publicKey, privateKey) => {
  185. if (err) return rej(err)
  186. return res({ publicKey, privateKey })
  187. })
  188. })
  189. }
  190. function generateED25519KeyPairPromise () {
  191. return new Promise<{ publicKey: string, privateKey: string }>((res, rej) => {
  192. const options: ED25519KeyPairOptions<'pem', 'pem'> = {
  193. publicKeyEncoding: {
  194. type: 'spki',
  195. format: 'pem'
  196. },
  197. privateKeyEncoding: {
  198. type: 'pkcs8',
  199. format: 'pem'
  200. }
  201. }
  202. generateKeyPair('ed25519', options, (err, publicKey, privateKey) => {
  203. if (err) return rej(err)
  204. return res({ publicKey, privateKey })
  205. })
  206. })
  207. }
  208. // ---------------------------------------------------------------------------
  209. const randomBytesPromise = promisify1<number, Buffer>(randomBytes)
  210. const scryptPromise = promisify3<string, string, number, Buffer>(scrypt)
  211. const execPromise2 = promisify2<string, any, string>(exec)
  212. const execPromise = promisify1<string, string>(exec)
  213. const pipelinePromise = promisify(pipeline)
  214. // ---------------------------------------------------------------------------
  215. export {
  216. objectConverter,
  217. mapToJSON,
  218. sanitizeUrl,
  219. sanitizeHost,
  220. execShell,
  221. pageToStartAndCount,
  222. peertubeTruncate,
  223. scryptPromise,
  224. randomBytesPromise,
  225. generateRSAKeyPairPromise,
  226. generateED25519KeyPairPromise,
  227. execPromise2,
  228. execPromise,
  229. pipelinePromise,
  230. parseSemVersion
  231. }