helper.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. const fs = require('fs')
  2. const merge = require('lodash.merge')
  3. const stripIndent = require('common-tags/lib/stripIndent')
  4. const utils = require('../server/helpers/utils')
  5. const logger = require('../server/logger')
  6. const crypto = require('crypto')
  7. // @ts-ignore
  8. const { version } = require('../../package.json')
  9. /**
  10. * Reads all companion configuration set via environment variables
  11. * and via the config file path
  12. *
  13. * @returns {object}
  14. */
  15. exports.getUppyOptions = () => {
  16. return merge({}, getConfigFromEnv(), getConfigFromFile())
  17. }
  18. /**
  19. * Loads the config from environment variables
  20. *
  21. * @returns {object}
  22. */
  23. const getConfigFromEnv = () => {
  24. const uploadUrls = process.env.COMPANION_UPLOAD_URLS
  25. const domains = process.env.COMPANION_DOMAINS || process.env.COMPANION_DOMAIN || null
  26. const validHosts = domains ? domains.split(',') : []
  27. return {
  28. providerOptions: {
  29. google: {
  30. key: process.env.COMPANION_GOOGLE_KEY,
  31. secret: getSecret('COMPANION_GOOGLE_SECRET')
  32. },
  33. dropbox: {
  34. key: process.env.COMPANION_DROPBOX_KEY,
  35. secret: getSecret('COMPANION_DROPBOX_SECRET')
  36. },
  37. instagram: {
  38. key: process.env.COMPANION_INSTAGRAM_KEY,
  39. secret: getSecret('COMPANION_INSTAGRAM_SECRET')
  40. },
  41. facebook: {
  42. key: process.env.COMPANION_FACEBOOK_KEY,
  43. secret: getSecret('COMPANION_FACEBOOK_SECRET')
  44. },
  45. microsoft: {
  46. key: process.env.COMPANION_ONEDRIVE_KEY,
  47. secret: getSecret('COMPANION_ONEDRIVE_SECRET')
  48. },
  49. s3: {
  50. key: process.env.COMPANION_AWS_KEY,
  51. secret: getSecret('COMPANION_AWS_SECRET'),
  52. bucket: process.env.COMPANION_AWS_BUCKET,
  53. endpoint: process.env.COMPANION_AWS_ENDPOINT,
  54. region: process.env.COMPANION_AWS_REGION
  55. }
  56. },
  57. server: {
  58. host: process.env.COMPANION_DOMAIN,
  59. protocol: process.env.COMPANION_PROTOCOL,
  60. path: process.env.COMPANION_PATH,
  61. implicitPath: process.env.COMPANION_IMPLICIT_PATH,
  62. oauthDomain: process.env.COMPANION_OAUTH_DOMAIN,
  63. validHosts: validHosts
  64. },
  65. filePath: process.env.COMPANION_DATADIR,
  66. redisUrl: process.env.COMPANION_REDIS_URL,
  67. // adding redisOptions to keep all companion options easily visible
  68. // redisOptions refers to https://www.npmjs.com/package/redis#options-object-properties
  69. redisOptions: {},
  70. sendSelfEndpoint: process.env.COMPANION_SELF_ENDPOINT,
  71. uploadUrls: uploadUrls ? uploadUrls.split(',') : null,
  72. secret: getSecret('COMPANION_SECRET') || generateSecret(),
  73. debug: process.env.NODE_ENV !== 'production',
  74. // TODO: this is a temporary hack to support distributed systems.
  75. // it is not documented, because it should be changed soon.
  76. cookieDomain: process.env.COMPANION_COOKIE_DOMAIN,
  77. multipleInstances: true
  78. }
  79. }
  80. /**
  81. * Tries to read the secret from a file if the according environment variable is set.
  82. * Otherwise it falls back to the standard secret environment variable.
  83. *
  84. * @param {string} baseEnvVar
  85. *
  86. * @returns {string}
  87. */
  88. const getSecret = (baseEnvVar) => {
  89. return `${baseEnvVar}_FILE` in process.env
  90. ? fs.readFileSync(process.env[`${baseEnvVar}_FILE`]).toString()
  91. : process.env[baseEnvVar]
  92. }
  93. /**
  94. * Auto-generates server secret
  95. *
  96. * @returns {string}
  97. */
  98. const generateSecret = () => {
  99. logger.warn('auto-generating server secret because none was specified', 'startup.secret')
  100. return crypto.randomBytes(64).toString('hex')
  101. }
  102. /**
  103. * Loads the config from a file and returns it as an object
  104. *
  105. * @returns {object}
  106. */
  107. const getConfigFromFile = () => {
  108. const path = getConfigPath()
  109. if (!path) return {}
  110. const rawdata = fs.readFileSync(getConfigPath())
  111. // @ts-ignore
  112. return JSON.parse(rawdata)
  113. }
  114. /**
  115. * Returns the config path specified via cli arguments
  116. *
  117. * @returns {string}
  118. */
  119. const getConfigPath = () => {
  120. let configPath
  121. for (let i = process.argv.length - 1; i >= 0; i--) {
  122. const isConfigFlag = process.argv[i] === '-c' || process.argv[i] === '--config'
  123. const flagHasValue = i + 1 <= process.argv.length
  124. if (isConfigFlag && flagHasValue) {
  125. configPath = process.argv[i + 1]
  126. break
  127. }
  128. }
  129. return configPath
  130. }
  131. /**
  132. * validates that the mandatory companion options are set.
  133. * If it is invalid, it will console an error of unset options and exits the process.
  134. * If it is valid, nothing happens.
  135. *
  136. * @param {object} config
  137. */
  138. exports.validateConfig = (config) => {
  139. const mandatoryOptions = ['secret', 'filePath', 'server.host']
  140. /** @type {string[]} */
  141. const unspecified = []
  142. mandatoryOptions.forEach((i) => {
  143. const value = i.split('.').reduce((prev, curr) => prev[curr], config)
  144. if (!value) unspecified.push(`"${i}"`)
  145. })
  146. // vaidate that all required config is specified
  147. if (unspecified.length) {
  148. console.error('\x1b[31m', 'Please specify the following options',
  149. 'to run companion as Standalone:\n', unspecified.join(',\n'), '\x1b[0m')
  150. process.exit(1)
  151. }
  152. // validate that specified filePath is writeable/readable.
  153. // TODO: consider moving this into the uppy module itself.
  154. try {
  155. // @ts-ignore
  156. fs.accessSync(`${config.filePath}`, fs.R_OK | fs.W_OK)
  157. } catch (err) {
  158. console.error('\x1b[31m', `No access to "${config.filePath}".`,
  159. 'Please ensure the directory exists and with read/write permissions.', '\x1b[0m')
  160. process.exit(1)
  161. }
  162. }
  163. /**
  164. *
  165. * @param {string} url
  166. */
  167. exports.hasProtocol = (url) => {
  168. return url.startsWith('http://') || url.startsWith('https://')
  169. }
  170. exports.buildHelpfulStartupMessage = (uppyOptions) => {
  171. const buildURL = utils.getURLBuilder(uppyOptions)
  172. const callbackURLs = []
  173. Object.keys(uppyOptions.providerOptions).forEach((providerName) => {
  174. // s3 does not need redirect_uris
  175. if (providerName === 's3') {
  176. return
  177. }
  178. if (providerName === 'google') {
  179. providerName = 'drive'
  180. }
  181. callbackURLs.push(buildURL(`/connect/${providerName}/callback`, true))
  182. })
  183. return stripIndent`
  184. Welcome to Companion v${version}
  185. ===================================
  186. Congratulations on setting up Companion! Thanks for joining our cause, you have taken
  187. the first step towards the future of file uploading! We
  188. hope you are as excited about this as we are!
  189. While you did an awesome job on getting Companion running, this is just the welcome
  190. message, so let's talk about the places that really matter:
  191. - Be sure to add ${callbackURLs.join(', ')} as your Oauth redirect uris on their corresponding developer interfaces.
  192. - The URL ${buildURL('/metrics', true)} is available for statistics to keep Companion running smoothly
  193. - https://github.com/transloadit/uppy/issues - report your bugs here
  194. So quit lollygagging, start uploading and experience the future!
  195. `
  196. }