backup.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import 'reflect-metadata'
  2. import 'newrelic'
  3. import { Stream } from 'stream'
  4. import { Logger } from 'winston'
  5. import * as dayjs from 'dayjs'
  6. import * as utc from 'dayjs/plugin/utc'
  7. import { AnalyticsActivity, AnalyticsStoreInterface, Period } from '@standardnotes/analytics'
  8. import { ContainerConfigLoader } from '../src/Bootstrap/Container'
  9. import TYPES from '../src/Bootstrap/Types'
  10. import { Env } from '../src/Bootstrap/Env'
  11. import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
  12. import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
  13. import { SettingRepositoryInterface } from '../src/Domain/Setting/SettingRepositoryInterface'
  14. import { MuteFailedBackupsEmailsOption, MuteFailedCloudBackupsEmailsOption, SettingName } from '@standardnotes/settings'
  15. import { RoleServiceInterface } from '../src/Domain/Role/RoleServiceInterface'
  16. import { PermissionName } from '@standardnotes/features'
  17. import { SettingServiceInterface } from '../src/Domain/Setting/SettingServiceInterface'
  18. import { AnalyticsEntityRepositoryInterface } from '../src/Domain/Analytics/AnalyticsEntityRepositoryInterface'
  19. const inputArgs = process.argv.slice(2)
  20. const backupProvider = inputArgs[0]
  21. const backupFrequency = inputArgs[1]
  22. const shouldEmailBackupBeTriggered = async (
  23. analyticsId: number,
  24. analyticsStore: AnalyticsStoreInterface,
  25. ): Promise<boolean> => {
  26. let periods = [Period.Today, Period.Yesterday]
  27. if (backupFrequency === 'weekly') {
  28. periods = [Period.ThisWeek, Period.LastWeek]
  29. }
  30. for (const period of periods) {
  31. const wasUnBackedUpDataCreatedInPeriod = await analyticsStore.wasActivityDone(
  32. AnalyticsActivity.EmailUnbackedUpData,
  33. analyticsId,
  34. period,
  35. )
  36. if (wasUnBackedUpDataCreatedInPeriod) {
  37. return true
  38. }
  39. }
  40. return false
  41. }
  42. const requestBackups = async (
  43. settingRepository: SettingRepositoryInterface,
  44. roleService: RoleServiceInterface,
  45. settingService: SettingServiceInterface,
  46. domainEventFactory: DomainEventFactoryInterface,
  47. domainEventPublisher: DomainEventPublisherInterface,
  48. analyticsEntityRepository: AnalyticsEntityRepositoryInterface,
  49. analyticsStore: AnalyticsStoreInterface,
  50. logger: Logger,
  51. ): Promise<void> => {
  52. let settingName: SettingName,
  53. permissionName: PermissionName,
  54. muteEmailsSettingName: SettingName,
  55. muteEmailsSettingValue: string,
  56. providerTokenSettingName: SettingName
  57. switch (backupProvider) {
  58. case 'email':
  59. settingName = SettingName.EmailBackupFrequency
  60. permissionName = PermissionName.DailyEmailBackup
  61. muteEmailsSettingName = SettingName.MuteFailedBackupsEmails
  62. muteEmailsSettingValue = MuteFailedBackupsEmailsOption.Muted
  63. break
  64. case 'dropbox':
  65. settingName = SettingName.DropboxBackupFrequency
  66. permissionName = PermissionName.DailyDropboxBackup
  67. muteEmailsSettingName = SettingName.MuteFailedCloudBackupsEmails
  68. muteEmailsSettingValue = MuteFailedCloudBackupsEmailsOption.Muted
  69. providerTokenSettingName = SettingName.DropboxBackupToken
  70. break
  71. case 'one_drive':
  72. settingName = SettingName.OneDriveBackupFrequency
  73. permissionName = PermissionName.DailyOneDriveBackup
  74. muteEmailsSettingName = SettingName.MuteFailedCloudBackupsEmails
  75. muteEmailsSettingValue = MuteFailedCloudBackupsEmailsOption.Muted
  76. providerTokenSettingName = SettingName.OneDriveBackupToken
  77. break
  78. case 'google_drive':
  79. settingName = SettingName.GoogleDriveBackupFrequency
  80. permissionName = PermissionName.DailyGDriveBackup
  81. muteEmailsSettingName = SettingName.MuteFailedCloudBackupsEmails
  82. muteEmailsSettingValue = MuteFailedCloudBackupsEmailsOption.Muted
  83. providerTokenSettingName = SettingName.GoogleDriveBackupToken
  84. break
  85. default:
  86. throw new Error(`Not handled backup provider: ${backupProvider}`)
  87. }
  88. const stream = await settingRepository.streamAllByNameAndValue(settingName, backupFrequency)
  89. return new Promise((resolve, reject) => {
  90. stream
  91. .pipe(
  92. new Stream.Transform({
  93. objectMode: true,
  94. transform: async (setting, _encoding, callback) => {
  95. const userIsPermittedForEmailBackups = await roleService.userHasPermission(
  96. setting.setting_user_uuid,
  97. permissionName,
  98. )
  99. if (!userIsPermittedForEmailBackups) {
  100. callback()
  101. return
  102. }
  103. let userHasEmailsMuted = false
  104. const emailsMutedSetting = await settingRepository.findOneByNameAndUserUuid(
  105. muteEmailsSettingName,
  106. setting.setting_user_uuid,
  107. )
  108. if (emailsMutedSetting !== null && emailsMutedSetting.value !== null) {
  109. userHasEmailsMuted = emailsMutedSetting.value === muteEmailsSettingValue
  110. }
  111. if (backupProvider === 'email') {
  112. const analyticsEntity = await analyticsEntityRepository.findOneByUserUuid(setting.setting_user_uuid)
  113. if (analyticsEntity === null) {
  114. callback()
  115. return
  116. }
  117. const emailBackupsShouldBeTriggered = await shouldEmailBackupBeTriggered(
  118. analyticsEntity.id,
  119. analyticsStore,
  120. )
  121. if (!emailBackupsShouldBeTriggered) {
  122. logger.info(
  123. `Email backup for user ${setting.setting_user_uuid} should not be triggered due to inactivity. It will be triggered until further changes.`,
  124. )
  125. }
  126. await domainEventPublisher.publish(
  127. domainEventFactory.createEmailBackupRequestedEvent(
  128. setting.setting_user_uuid,
  129. emailsMutedSetting?.uuid as string,
  130. userHasEmailsMuted,
  131. ),
  132. )
  133. await analyticsStore.markActivity([AnalyticsActivity.EmailBackup], analyticsEntity.id, [
  134. Period.Today,
  135. Period.ThisWeek,
  136. ])
  137. await analyticsStore.unmarkActivity([AnalyticsActivity.EmailUnbackedUpData], analyticsEntity.id, [
  138. Period.Today,
  139. Period.ThisWeek,
  140. ])
  141. callback()
  142. return
  143. }
  144. const cloudBackupProviderToken = await settingService.findSettingWithDecryptedValue({
  145. settingName: providerTokenSettingName,
  146. userUuid: setting.setting_user_uuid,
  147. })
  148. if (cloudBackupProviderToken === null || cloudBackupProviderToken.value === null) {
  149. callback()
  150. return
  151. }
  152. await domainEventPublisher.publish(
  153. domainEventFactory.createCloudBackupRequestedEvent(
  154. backupProvider.toUpperCase() as 'DROPBOX' | 'ONE_DRIVE' | 'GOOGLE_DRIVE',
  155. cloudBackupProviderToken.value,
  156. setting.setting_user_uuid,
  157. emailsMutedSetting?.uuid as string,
  158. userHasEmailsMuted,
  159. ),
  160. )
  161. callback()
  162. },
  163. }),
  164. )
  165. .on('finish', resolve)
  166. .on('error', reject)
  167. })
  168. }
  169. const container = new ContainerConfigLoader()
  170. void container.load().then((container) => {
  171. dayjs.extend(utc)
  172. const env: Env = new Env()
  173. env.load()
  174. const logger: Logger = container.get(TYPES.Logger)
  175. logger.info(`Starting ${backupFrequency} ${backupProvider} backup requesting...`)
  176. const settingRepository: SettingRepositoryInterface = container.get(TYPES.SettingRepository)
  177. const roleService: RoleServiceInterface = container.get(TYPES.RoleService)
  178. const settingService: SettingServiceInterface = container.get(TYPES.SettingService)
  179. const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory)
  180. const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
  181. const analyticsEntityRepository: AnalyticsEntityRepositoryInterface = container.get(TYPES.AnalyticsEntityRepository)
  182. const analyticsStore: AnalyticsStoreInterface = container.get(TYPES.AnalyticsStore)
  183. Promise.resolve(
  184. requestBackups(
  185. settingRepository,
  186. roleService,
  187. settingService,
  188. domainEventFactory,
  189. domainEventPublisher,
  190. analyticsEntityRepository,
  191. analyticsStore,
  192. logger,
  193. ),
  194. )
  195. .then(() => {
  196. logger.info(`${backupFrequency} ${backupProvider} backup requesting complete`)
  197. process.exit(0)
  198. })
  199. .catch((error) => {
  200. logger.error(`Could not finish ${backupFrequency} ${backupProvider} backup requesting: ${error.message}`)
  201. process.exit(1)
  202. })
  203. })