ChannelsMigration.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. import { AssetsMigration, AssetsMigrationConfig, AssetsMigrationParams } from './AssetsMigration'
  2. import { ChannelMetadata } from '@joystream/metadata-protobuf'
  3. import { ChannelFieldsFragment } from './sumer-query-node/generated/queries'
  4. import { createType } from '@joystream/types'
  5. import Long from 'long'
  6. import { ChannelCreationParameters } from '@joystream/types/content'
  7. import { CHANNEL_AVATAR_TARGET_SIZE, CHANNEL_COVER_TARGET_SIZE } from './ImageResizer'
  8. import { ChannelId } from '@joystream/types/common'
  9. import _ from 'lodash'
  10. import { MigrationResult } from './BaseMigration'
  11. import { Logger } from 'winston'
  12. import { createLogger } from '../logging'
  13. export type ChannelsMigrationConfig = AssetsMigrationConfig & {
  14. channelIds: number[]
  15. channelBatchSize: number
  16. forceChannelOwnerMemberId: number | undefined
  17. }
  18. export type ChannelsMigrationParams = AssetsMigrationParams & {
  19. config: ChannelsMigrationConfig
  20. forcedChannelOwner: { id: string; controllerAccount: string } | undefined
  21. }
  22. export type ChannelsMigrationResult = MigrationResult & {
  23. videoIds: number[]
  24. }
  25. export class ChannelMigration extends AssetsMigration {
  26. name = 'Channels migration'
  27. protected config: ChannelsMigrationConfig
  28. protected videoIds: number[] = []
  29. protected forcedChannelOwner: { id: string; controllerAccount: string } | undefined
  30. protected logger: Logger
  31. public constructor(params: ChannelsMigrationParams) {
  32. super(params)
  33. this.config = params.config
  34. this.forcedChannelOwner = params.forcedChannelOwner
  35. this.logger = createLogger(this.name)
  36. }
  37. private getChannelOwnerMember({ id, ownerMember }: ChannelFieldsFragment) {
  38. if (!ownerMember) {
  39. throw new Error(`Chanel ownerMember missing: ${id}. Only member-owned channels are supported!`)
  40. }
  41. if (this.forcedChannelOwner) {
  42. return this.forcedChannelOwner
  43. }
  44. return ownerMember
  45. }
  46. protected async migrateBatch(channels: ChannelFieldsFragment[]): Promise<void> {
  47. const { api } = this
  48. const txs = _.flatten(await Promise.all(channels.map((c) => this.prepareChannel(c))))
  49. const result = await api.sendExtrinsic(this.sudo, api.tx.utility.batch(txs))
  50. const channelCreatedEvents = api.findEvents(result, 'content', 'ChannelCreated')
  51. const newChannelIds: ChannelId[] = channelCreatedEvents.map((e) => e.data[1])
  52. if (channelCreatedEvents.length !== channels.length) {
  53. this.extractFailedMigrations(result, channels)
  54. }
  55. const newChannelMapEntries: [number, number][] = []
  56. let newChannelIdIndex = 0
  57. channels.forEach(({ id }) => {
  58. if (this.failedMigrations.has(parseInt(id))) {
  59. return
  60. }
  61. const newChannelId = newChannelIds[newChannelIdIndex++].toNumber()
  62. this.idsMap.set(parseInt(id), newChannelId)
  63. newChannelMapEntries.push([parseInt(id), newChannelId])
  64. })
  65. if (newChannelMapEntries.length) {
  66. this.logger.info('Channel map entries added!', { newChannelMapEntries })
  67. const dataObjectsUploadedEvents = this.api.findEvents(result, 'storage', 'DataObjectsUploaded')
  68. this.assetsManager.queueUploadsFromEvents(dataObjectsUploadedEvents)
  69. }
  70. }
  71. public async run(): Promise<ChannelsMigrationResult> {
  72. await this.init()
  73. const {
  74. config: { channelIds, channelBatchSize },
  75. } = this
  76. const ids = channelIds.sort((a, b) => a - b)
  77. while (ids.length) {
  78. const idsBatch = ids.splice(0, channelBatchSize)
  79. this.logger.info(`Fetching a batch of ${idsBatch.length} channels...`)
  80. const channelsBatch = (await this.queryNodeApi.getChannelsByIds(idsBatch)).sort(
  81. (a, b) => parseInt(a.id) - parseInt(b.id)
  82. )
  83. if (channelsBatch.length < idsBatch.length) {
  84. this.logger.warn(
  85. `Some channels were not be found: ${_.difference(
  86. idsBatch,
  87. channelsBatch.map((c) => parseInt(c.id))
  88. )}`
  89. )
  90. }
  91. const channelsToMigrate = channelsBatch.filter((c) => !this.idsMap.has(parseInt(c.id)))
  92. if (channelsToMigrate.length < channelsBatch.length) {
  93. this.logger.info(
  94. `${channelsToMigrate.length ? 'Some' : 'All'} channels in this batch were already migrated ` +
  95. `(${channelsBatch.length - channelsToMigrate.length}/${channelsBatch.length})`
  96. )
  97. }
  98. if (channelsToMigrate.length) {
  99. await this.executeBatchMigration(channelsToMigrate)
  100. await this.assetsManager.processQueuedUploads()
  101. }
  102. const videoIdsToMigrate: number[] = channelsBatch.reduce<number[]>(
  103. (res, { id, videos }) => (this.idsMap.has(parseInt(id)) ? res.concat(videos.map((v) => parseInt(v.id))) : res),
  104. []
  105. )
  106. this.videoIds = this.videoIds.concat(videoIdsToMigrate)
  107. if (videoIdsToMigrate.length) {
  108. this.logger.info(`Added ${videoIdsToMigrate.length} video ids to the list of videos to migrate`)
  109. }
  110. }
  111. return {
  112. ...this.getResult(),
  113. videoIds: [...this.videoIds],
  114. }
  115. }
  116. private async prepareChannel(channel: ChannelFieldsFragment) {
  117. const { api } = this
  118. const { avatarPhotoDataObject, coverPhotoDataObject, title, description, categoryId, isPublic, language } = channel
  119. const ownerMember = this.getChannelOwnerMember(channel)
  120. const assetsToPrepare = {
  121. avatar: { data: avatarPhotoDataObject || undefined, targetSize: CHANNEL_AVATAR_TARGET_SIZE },
  122. coverPhoto: { data: coverPhotoDataObject || undefined, targetSize: CHANNEL_COVER_TARGET_SIZE },
  123. }
  124. const preparedAssets = await this.assetsManager.prepareAssets(assetsToPrepare)
  125. const meta = new ChannelMetadata({
  126. title,
  127. description,
  128. category: categoryId ? Long.fromString(categoryId) : undefined,
  129. avatarPhoto: preparedAssets.avatar?.index,
  130. coverPhoto: preparedAssets.coverPhoto?.index,
  131. isPublic,
  132. language: language?.iso,
  133. })
  134. const assetsParams = Object.values(preparedAssets)
  135. .sort((a, b) => a.index - b.index)
  136. .map((a) => a.params)
  137. const channelCreationParams = createType<ChannelCreationParameters, 'ChannelCreationParameters'>(
  138. 'ChannelCreationParameters',
  139. {
  140. assets: assetsParams.length
  141. ? {
  142. object_creation_list: assetsParams,
  143. expected_data_size_fee: this.assetsManager.dataObjectFeePerMB,
  144. }
  145. : null,
  146. meta: `0x${Buffer.from(ChannelMetadata.encode(meta).finish()).toString('hex')}`,
  147. }
  148. )
  149. const feesToCover = this.assetsManager.calcDataObjectsFee(assetsParams)
  150. return [
  151. api.tx.balances.transferKeepAlive(ownerMember.controllerAccount, feesToCover),
  152. api.tx.sudo.sudoAs(
  153. ownerMember.controllerAccount,
  154. api.tx.content.createChannel({ Member: ownerMember.id }, channelCreationParams)
  155. ),
  156. ]
  157. }
  158. }