Container.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. import * as winston from 'winston'
  2. import Redis from 'ioredis'
  3. import { SNSClient, SNSClientConfig } from '@aws-sdk/client-sns'
  4. import { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs'
  5. import { S3Client, S3ClientConfig } from '@aws-sdk/client-s3'
  6. import { Container } from 'inversify'
  7. import { Env } from './Env'
  8. import TYPES from './Types'
  9. import { UploadFileChunk } from '../Domain/UseCase/UploadFileChunk/UploadFileChunk'
  10. import { ValetTokenAuthMiddleware } from '../Infra/InversifyExpress/Middleware/ValetTokenAuthMiddleware'
  11. import { TokenDecoder, TokenDecoderInterface, ValetTokenData } from '@standardnotes/security'
  12. import { Timer, TimerInterface } from '@standardnotes/time'
  13. import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface'
  14. import { DomainEventFactory } from '../Domain/Event/DomainEventFactory'
  15. import {
  16. DirectCallDomainEventPublisher,
  17. DirectCallEventMessageHandler,
  18. SNSDomainEventPublisher,
  19. SQSDomainEventSubscriberFactory,
  20. SQSEventMessageHandler,
  21. } from '@standardnotes/domain-events-infra'
  22. import { StreamDownloadFile } from '../Domain/UseCase/StreamDownloadFile/StreamDownloadFile'
  23. import { FileDownloaderInterface } from '../Domain/Services/FileDownloaderInterface'
  24. import { S3FileDownloader } from '../Infra/S3/S3FileDownloader'
  25. import { FileUploaderInterface } from '../Domain/Services/FileUploaderInterface'
  26. import { S3FileUploader } from '../Infra/S3/S3FileUploader'
  27. import { FSFileDownloader } from '../Infra/FS/FSFileDownloader'
  28. import { FSFileUploader } from '../Infra/FS/FSFileUploader'
  29. import { CreateUploadSession } from '../Domain/UseCase/CreateUploadSession/CreateUploadSession'
  30. import { FinishUploadSession } from '../Domain/UseCase/FinishUploadSession/FinishUploadSession'
  31. import { UploadRepositoryInterface } from '../Domain/Upload/UploadRepositoryInterface'
  32. import { RedisUploadRepository } from '../Infra/Redis/RedisUploadRepository'
  33. import { GetFileMetadata } from '../Domain/UseCase/GetFileMetadata/GetFileMetadata'
  34. import { FileRemoverInterface } from '../Domain/Services/FileRemoverInterface'
  35. import { S3FileRemover } from '../Infra/S3/S3FileRemover'
  36. import { FSFileRemover } from '../Infra/FS/FSFileRemover'
  37. import { RemoveFile } from '../Domain/UseCase/RemoveFile/RemoveFile'
  38. import {
  39. DomainEventHandlerInterface,
  40. DomainEventMessageHandlerInterface,
  41. DomainEventPublisherInterface,
  42. DomainEventSubscriberFactoryInterface,
  43. } from '@standardnotes/domain-events'
  44. import { MarkFilesToBeRemoved } from '../Domain/UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemoved'
  45. import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountDeletionRequestedEventHandler'
  46. import { SharedSubscriptionInvitationCanceledEventHandler } from '../Domain/Handler/SharedSubscriptionInvitationCanceledEventHandler'
  47. import { InMemoryUploadRepository } from '../Infra/InMemory/InMemoryUploadRepository'
  48. import { Transform } from 'stream'
  49. import { FileMoverInterface } from '../Domain/Services/FileMoverInterface'
  50. import { S3FileMover } from '../Infra/S3/S3FileMover'
  51. import { FSFileMover } from '../Infra/FS/FSFileMover'
  52. import { MoveFile } from '../Domain/UseCase/MoveFile/MoveFile'
  53. import { SharedVaultValetTokenAuthMiddleware } from '../Infra/InversifyExpress/Middleware/SharedVaultValetTokenAuthMiddleware'
  54. export class ContainerConfigLoader {
  55. async load(configuration?: {
  56. directCallDomainEventPublisher?: DirectCallDomainEventPublisher
  57. logger?: Transform
  58. environmentOverrides?: { [name: string]: string }
  59. }): Promise<Container> {
  60. const directCallDomainEventPublisher =
  61. configuration?.directCallDomainEventPublisher ?? new DirectCallDomainEventPublisher()
  62. const env: Env = new Env(configuration?.environmentOverrides)
  63. env.load()
  64. const container = new Container()
  65. if (env.get('NEW_RELIC_ENABLED', true) === 'true') {
  66. await import('newrelic')
  67. }
  68. // env vars
  69. container.bind(TYPES.Files_VALET_TOKEN_SECRET).toConstantValue(env.get('VALET_TOKEN_SECRET'))
  70. container
  71. .bind(TYPES.Files_MAX_CHUNK_BYTES)
  72. .toConstantValue(env.get('MAX_CHUNK_BYTES', true) ? +env.get('MAX_CHUNK_BYTES', true) : 100000000)
  73. container.bind(TYPES.Files_VERSION).toConstantValue(env.get('VERSION', true) ?? 'development')
  74. container
  75. .bind(TYPES.Files_FILE_UPLOAD_PATH)
  76. .toConstantValue(env.get('FILE_UPLOAD_PATH', true) ?? `${__dirname}/../../uploads`)
  77. const isConfiguredForHomeServer = env.get('MODE', true) === 'home-server'
  78. const isConfiguredForInMemoryCache = env.get('CACHE_TYPE', true) === 'memory'
  79. let logger: winston.Logger
  80. if (configuration?.logger) {
  81. logger = configuration.logger as winston.Logger
  82. } else {
  83. logger = this.createLogger({ env })
  84. }
  85. container.bind<winston.Logger>(TYPES.Files_Logger).toConstantValue(logger)
  86. container.bind<TimerInterface>(TYPES.Files_Timer).toConstantValue(new Timer())
  87. // services
  88. container
  89. .bind<TokenDecoderInterface<ValetTokenData>>(TYPES.Files_ValetTokenDecoder)
  90. .toConstantValue(new TokenDecoder<ValetTokenData>(container.get(TYPES.Files_VALET_TOKEN_SECRET)))
  91. container
  92. .bind<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory)
  93. .toConstantValue(new DomainEventFactory(container.get<TimerInterface>(TYPES.Files_Timer)))
  94. if (isConfiguredForInMemoryCache) {
  95. container
  96. .bind<UploadRepositoryInterface>(TYPES.Files_UploadRepository)
  97. .toConstantValue(new InMemoryUploadRepository(container.get(TYPES.Files_Timer)))
  98. } else {
  99. container.bind(TYPES.Files_REDIS_URL).toConstantValue(env.get('REDIS_URL'))
  100. const redisUrl = container.get(TYPES.Files_REDIS_URL) as string
  101. const isRedisInClusterMode = redisUrl.indexOf(',') > 0
  102. let redis
  103. if (isRedisInClusterMode) {
  104. redis = new Redis.Cluster(redisUrl.split(','))
  105. } else {
  106. redis = new Redis(redisUrl)
  107. }
  108. container.bind(TYPES.Files_Redis).toConstantValue(redis)
  109. container.bind<UploadRepositoryInterface>(TYPES.Files_UploadRepository).to(RedisUploadRepository)
  110. }
  111. if (isConfiguredForHomeServer) {
  112. container
  113. .bind<DomainEventPublisherInterface>(TYPES.Files_DomainEventPublisher)
  114. .toConstantValue(directCallDomainEventPublisher)
  115. } else {
  116. container.bind(TYPES.Files_S3_BUCKET_NAME).toConstantValue(env.get('S3_BUCKET_NAME', true))
  117. container.bind(TYPES.Files_S3_AWS_REGION).toConstantValue(env.get('S3_AWS_REGION', true))
  118. container.bind(TYPES.Files_SNS_TOPIC_ARN).toConstantValue(env.get('SNS_TOPIC_ARN'))
  119. container.bind(TYPES.Files_SNS_AWS_REGION).toConstantValue(env.get('SNS_AWS_REGION', true))
  120. container.bind(TYPES.Files_SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL'))
  121. if (env.get('SNS_TOPIC_ARN', true)) {
  122. const snsConfig: SNSClientConfig = {
  123. apiVersion: 'latest',
  124. region: env.get('SNS_AWS_REGION', true),
  125. }
  126. if (env.get('SNS_ENDPOINT', true)) {
  127. snsConfig.endpoint = env.get('SNS_ENDPOINT', true)
  128. }
  129. if (env.get('SNS_ACCESS_KEY_ID', true) && env.get('SNS_SECRET_ACCESS_KEY', true)) {
  130. snsConfig.credentials = {
  131. accessKeyId: env.get('SNS_ACCESS_KEY_ID', true),
  132. secretAccessKey: env.get('SNS_SECRET_ACCESS_KEY', true),
  133. }
  134. }
  135. const snsClient = new SNSClient(snsConfig)
  136. container.bind<SNSClient>(TYPES.Files_SNS).toConstantValue(snsClient)
  137. }
  138. if (env.get('SQS_QUEUE_URL', true)) {
  139. const sqsConfig: SQSClientConfig = {
  140. region: env.get('SQS_AWS_REGION', true),
  141. }
  142. if (env.get('SQS_ENDPOINT', true)) {
  143. sqsConfig.endpoint = env.get('SQS_ENDPOINT', true)
  144. }
  145. if (env.get('SQS_ACCESS_KEY_ID', true) && env.get('SQS_SECRET_ACCESS_KEY', true)) {
  146. sqsConfig.credentials = {
  147. accessKeyId: env.get('SQS_ACCESS_KEY_ID', true),
  148. secretAccessKey: env.get('SQS_SECRET_ACCESS_KEY', true),
  149. }
  150. }
  151. const sqsClient = new SQSClient(sqsConfig)
  152. container.bind<SQSClient>(TYPES.Files_SQS).toConstantValue(sqsClient)
  153. }
  154. container
  155. .bind<DomainEventPublisherInterface>(TYPES.Files_DomainEventPublisher)
  156. .toConstantValue(
  157. new SNSDomainEventPublisher(container.get(TYPES.Files_SNS), container.get(TYPES.Files_SNS_TOPIC_ARN)),
  158. )
  159. }
  160. if (!isConfiguredForHomeServer && (env.get('S3_AWS_REGION', true) || env.get('S3_ENDPOINT', true))) {
  161. const s3Opts: S3ClientConfig = {
  162. apiVersion: 'latest',
  163. }
  164. if (env.get('S3_AWS_REGION', true)) {
  165. s3Opts.region = env.get('S3_AWS_REGION', true)
  166. }
  167. if (env.get('S3_ENDPOINT', true)) {
  168. s3Opts.endpoint = env.get('S3_ENDPOINT', true)
  169. }
  170. const s3Client = new S3Client(s3Opts)
  171. container.bind<S3Client>(TYPES.Files_S3).toConstantValue(s3Client)
  172. container.bind<FileDownloaderInterface>(TYPES.Files_FileDownloader).to(S3FileDownloader)
  173. container.bind<FileUploaderInterface>(TYPES.Files_FileUploader).to(S3FileUploader)
  174. container.bind<FileRemoverInterface>(TYPES.Files_FileRemover).to(S3FileRemover)
  175. container.bind<FileMoverInterface>(TYPES.Files_FileMover).to(S3FileMover)
  176. } else {
  177. container.bind<FileDownloaderInterface>(TYPES.Files_FileDownloader).to(FSFileDownloader)
  178. container
  179. .bind<FileUploaderInterface>(TYPES.Files_FileUploader)
  180. .toConstantValue(
  181. new FSFileUploader(container.get(TYPES.Files_FILE_UPLOAD_PATH), container.get(TYPES.Files_Logger)),
  182. )
  183. container
  184. .bind<FileRemoverInterface>(TYPES.Files_FileRemover)
  185. .toConstantValue(new FSFileRemover(container.get<string>(TYPES.Files_FILE_UPLOAD_PATH)))
  186. container.bind<FileMoverInterface>(TYPES.Files_FileMover).to(FSFileMover)
  187. }
  188. // use cases
  189. container.bind<UploadFileChunk>(TYPES.Files_UploadFileChunk).to(UploadFileChunk)
  190. container.bind<StreamDownloadFile>(TYPES.Files_StreamDownloadFile).to(StreamDownloadFile)
  191. container.bind<CreateUploadSession>(TYPES.Files_CreateUploadSession).to(CreateUploadSession)
  192. container
  193. .bind<FinishUploadSession>(TYPES.Files_FinishUploadSession)
  194. .toConstantValue(
  195. new FinishUploadSession(
  196. container.get(TYPES.Files_FileUploader),
  197. container.get(TYPES.Files_UploadRepository),
  198. container.get(TYPES.Files_DomainEventPublisher),
  199. container.get(TYPES.Files_DomainEventFactory),
  200. ),
  201. )
  202. container
  203. .bind<GetFileMetadata>(TYPES.Files_GetFileMetadata)
  204. .toConstantValue(
  205. new GetFileMetadata(
  206. container.get<FileDownloaderInterface>(TYPES.Files_FileDownloader),
  207. container.get<winston.Logger>(TYPES.Files_Logger),
  208. ),
  209. )
  210. container.bind<RemoveFile>(TYPES.Files_RemoveFile).to(RemoveFile)
  211. container
  212. .bind<MoveFile>(TYPES.Files_MoveFile)
  213. .toConstantValue(
  214. new MoveFile(
  215. container.get<GetFileMetadata>(TYPES.Files_GetFileMetadata),
  216. container.get<FileMoverInterface>(TYPES.Files_FileMover),
  217. container.get<DomainEventPublisherInterface>(TYPES.Files_DomainEventPublisher),
  218. container.get<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory),
  219. container.get<winston.Logger>(TYPES.Files_Logger),
  220. ),
  221. )
  222. container.bind<MarkFilesToBeRemoved>(TYPES.Files_MarkFilesToBeRemoved).to(MarkFilesToBeRemoved)
  223. // middleware
  224. container.bind<ValetTokenAuthMiddleware>(TYPES.Files_ValetTokenAuthMiddleware).to(ValetTokenAuthMiddleware)
  225. container
  226. .bind<SharedVaultValetTokenAuthMiddleware>(TYPES.Files_SharedVaultValetTokenAuthMiddleware)
  227. .to(SharedVaultValetTokenAuthMiddleware)
  228. // Handlers
  229. container
  230. .bind<AccountDeletionRequestedEventHandler>(TYPES.Files_AccountDeletionRequestedEventHandler)
  231. .toConstantValue(
  232. new AccountDeletionRequestedEventHandler(
  233. container.get<MarkFilesToBeRemoved>(TYPES.Files_MarkFilesToBeRemoved),
  234. container.get<DomainEventPublisherInterface>(TYPES.Files_DomainEventPublisher),
  235. container.get<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory),
  236. container.get<winston.Logger>(TYPES.Files_Logger),
  237. ),
  238. )
  239. container
  240. .bind<SharedSubscriptionInvitationCanceledEventHandler>(
  241. TYPES.Files_SharedSubscriptionInvitationCanceledEventHandler,
  242. )
  243. .toConstantValue(
  244. new SharedSubscriptionInvitationCanceledEventHandler(
  245. container.get<MarkFilesToBeRemoved>(TYPES.Files_MarkFilesToBeRemoved),
  246. container.get<DomainEventPublisherInterface>(TYPES.Files_DomainEventPublisher),
  247. container.get<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory),
  248. container.get<winston.Logger>(TYPES.Files_Logger),
  249. ),
  250. )
  251. const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
  252. ['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Files_AccountDeletionRequestedEventHandler)],
  253. [
  254. 'SHARED_SUBSCRIPTION_INVITATION_CANCELED',
  255. container.get(TYPES.Files_SharedSubscriptionInvitationCanceledEventHandler),
  256. ],
  257. ])
  258. if (isConfiguredForHomeServer) {
  259. const directCallEventMessageHandler = new DirectCallEventMessageHandler(
  260. eventHandlers,
  261. container.get(TYPES.Files_Logger),
  262. )
  263. directCallDomainEventPublisher.register(directCallEventMessageHandler)
  264. container
  265. .bind<DomainEventMessageHandlerInterface>(TYPES.Files_DomainEventMessageHandler)
  266. .toConstantValue(directCallEventMessageHandler)
  267. } else {
  268. container
  269. .bind<DomainEventMessageHandlerInterface>(TYPES.Files_DomainEventMessageHandler)
  270. .toConstantValue(new SQSEventMessageHandler(eventHandlers, container.get(TYPES.Files_Logger)))
  271. container
  272. .bind<DomainEventSubscriberFactoryInterface>(TYPES.Files_DomainEventSubscriberFactory)
  273. .toConstantValue(
  274. new SQSDomainEventSubscriberFactory(
  275. container.get(TYPES.Files_SQS),
  276. container.get(TYPES.Files_SQS_QUEUE_URL),
  277. container.get(TYPES.Files_DomainEventMessageHandler),
  278. ),
  279. )
  280. }
  281. logger.debug('Configuration complete')
  282. return container
  283. }
  284. createLogger({ env }: { env: Env }): winston.Logger {
  285. return winston.createLogger({
  286. level: env.get('LOG_LEVEL', true) || 'info',
  287. format: winston.format.combine(winston.format.splat(), winston.format.json()),
  288. transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL', true) || 'info' })],
  289. defaultMeta: { service: 'files' },
  290. })
  291. }
  292. }