Add activitypub migration script
authorChocobozzz <florian.bigard@gmail.com>
Mon, 27 Nov 2017 08:47:21 +0000 (09:47 +0100)
committerChocobozzz <florian.bigard@gmail.com>
Mon, 27 Nov 2017 18:40:53 +0000 (19:40 +0100)
server/initializers/constants.ts
server/initializers/database.ts
server/initializers/migrations/0100-activitypub.ts [new file with mode: 0644]
server/initializers/migrator.ts
server/models/video/video-abuse.ts

index e7f668ee47a01aaeeaf918fed64891ecf2fa7b33..786334d4659eb940fa6205431d9e5db45055e58a 100644 (file)
@@ -14,7 +14,7 @@ import { FollowState } from '../../shared/models/accounts/follow.model'
 
 // ---------------------------------------------------------------------------
 
-const LAST_MIGRATION_VERSION = 95
+const LAST_MIGRATION_VERSION = 100
 
 // ---------------------------------------------------------------------------
 
index 5c757694e8ff606787d88b7eb4f1a66abcde6135..9b9a81e269a8364ea4667794ff6f5ac9e735cf8d 100644 (file)
@@ -31,7 +31,7 @@ const dbname = CONFIG.DATABASE.DBNAME
 const username = CONFIG.DATABASE.USERNAME
 const password = CONFIG.DATABASE.PASSWORD
 
-const database: {
+export type PeerTubeDatabase = {
   sequelize?: Sequelize.Sequelize,
   init?: (silent: boolean) => Promise<void>,
 
@@ -53,7 +53,9 @@ const database: {
   BlacklistedVideo?: BlacklistedVideoModel,
   VideoTag?: VideoTagModel,
   Video?: VideoModel
-} = {}
+}
+
+const database: PeerTubeDatabase = {}
 
 const sequelize = new Sequelize(dbname, username, password, {
   dialect: 'postgres',
diff --git a/server/initializers/migrations/0100-activitypub.ts b/server/initializers/migrations/0100-activitypub.ts
new file mode 100644 (file)
index 0000000..50a0adc
--- /dev/null
@@ -0,0 +1,212 @@
+import { values } from 'lodash'
+import * as Sequelize from 'sequelize'
+import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
+import { shareVideoByServer } from '../../lib/activitypub/share'
+import { getVideoActivityPubUrl, getVideoChannelActivityPubUrl } from '../../lib/activitypub/url'
+import { createLocalAccountWithoutKeys } from '../../lib/user'
+import { JOB_CATEGORIES, SERVER_ACCOUNT_NAME } from '../constants'
+import { PeerTubeDatabase } from '../database'
+
+async function up (utils: {
+  transaction: Sequelize.Transaction,
+  queryInterface: Sequelize.QueryInterface,
+  sequelize: Sequelize.Sequelize,
+  db: PeerTubeDatabase
+}): Promise<void> {
+  const q = utils.queryInterface
+  const db = utils.db
+
+  // Assert there are no friends
+  {
+    const query = 'SELECT COUNT(*) as total FROM "Pods"'
+    const options = {
+      type: Sequelize.QueryTypes.SELECT
+    }
+    const res = await utils.sequelize.query(query, options)
+
+    if (!res[0] || res[0].total !== 0) {
+      throw new Error('You need to quit friends.')
+    }
+  }
+
+  // Pods -> Servers
+  await utils.queryInterface.renameTable('Pods', 'Servers')
+
+  // Create Account table
+  await db.Account.sync()
+
+  // Create AccountFollows table
+  await db.AccountFollow.sync()
+
+  // Modify video abuse table
+  await db.VideoAbuse.destroy({ truncate: true })
+  await utils.queryInterface.removeColumn('VideoAbuses', 'reporterPodId')
+  await utils.queryInterface.removeColumn('VideoAbuses', 'reporterUsername')
+
+  // Create column link with Account table
+  {
+    const data = {
+      type: Sequelize.INTEGER,
+      allowNull: false,
+      references: {
+        model: 'Accounts',
+        key: 'id'
+      },
+      onDelete: 'CASCADE'
+    }
+    await q.addColumn('VideoAbuses', 'reporterAccountId', data)
+  }
+
+  // Drop request tables
+  await utils.queryInterface.dropTable('RequestToPods')
+  await utils.queryInterface.dropTable('RequestVideoEvents')
+  await utils.queryInterface.dropTable('RequestVideoQadus')
+  await utils.queryInterface.dropTable('Requests')
+
+  // Create application account
+  {
+    const applicationInstance = await db.Application.findOne()
+    const accountCreated = await createLocalAccountWithoutKeys(SERVER_ACCOUNT_NAME, null, applicationInstance.id, undefined)
+
+    const { publicKey, privateKey } = await createPrivateAndPublicKeys()
+    accountCreated.set('publicKey', publicKey)
+    accountCreated.set('privateKey', privateKey)
+
+    await accountCreated.save()
+  }
+
+  // Drop old video channel foreign key (referencing Authors)
+  {
+    const query = 'ALTER TABLE "VideoChannels" DROP CONSTRAINT "VideoChannels_authorId_fkey"'
+    await utils.sequelize.query(query)
+  }
+
+  // Recreate accounts for each user
+  const users = await db.User.findAll()
+  for (const user of users) {
+    const account = await createLocalAccountWithoutKeys(user.username, user.id, null, undefined)
+
+    const { publicKey, privateKey } = await createPrivateAndPublicKeys()
+    account.set('publicKey', publicKey)
+    account.set('privateKey', privateKey)
+    await account.save()
+  }
+
+  {
+    const data = {
+      type: Sequelize.INTEGER,
+      allowNull: true,
+      onDelete: 'CASCADE',
+      reference: {
+        model: 'Account',
+        key: 'id'
+      }
+    }
+    await q.addColumn('VideoChannels', 'accountId', data)
+
+    {
+      const query = 'UPDATE "VideoChannels" SET "accountId" = ' +
+                    '(SELECT "Accounts"."id" FROM "Accounts" INNER JOIN "Authors" ON "Authors"."userId" = "Accounts"."userId" ' +
+                    'WHERE "VideoChannels"."authorId" = "Authors"."id")'
+      await utils.sequelize.query(query)
+    }
+
+    data.allowNull = false
+    await q.changeColumn('VideoChannels', 'accountId', data)
+
+    await q.removeColumn('VideoChannels', 'authorId')
+  }
+
+  // Add url column to "Videos"
+  {
+    const data = {
+      type: Sequelize.STRING,
+      defaultValue: null,
+      allowNull: true
+    }
+    await q.addColumn('Videos', 'url', data)
+
+    const videos = await db.Video.findAll()
+    for (const video of videos) {
+      video.url = getVideoActivityPubUrl(video)
+      await video.save()
+    }
+
+    data.allowNull = false
+    await q.changeColumn('Videos', 'url', data)
+  }
+
+  // Add url column to "VideoChannels"
+  {
+    const data = {
+      type: Sequelize.STRING,
+      defaultValue: null,
+      allowNull: true
+    }
+    await q.addColumn('VideoChannels', 'url', data)
+
+    const videoChannels = await db.VideoChannel.findAll()
+    for (const videoChannel of videoChannels) {
+      videoChannel.url = getVideoChannelActivityPubUrl(videoChannel)
+      await videoChannel.save()
+    }
+
+    data.allowNull = false
+    await q.changeColumn('VideoChannels', 'url', data)
+  }
+
+  // Loss old video rates, whatever
+  await utils.queryInterface.dropTable('UserVideoRates')
+  await db.AccountVideoRate.sync()
+
+  {
+    const data = {
+      type: Sequelize.ENUM(values(JOB_CATEGORIES)),
+      defaultValue: 'transcoding',
+      allowNull: false
+    }
+    await q.addColumn('Jobs', 'category', data)
+  }
+
+  await db.VideoShare.sync()
+  await db.VideoChannelShare.sync()
+
+  {
+    const videos = await db.Video.findAll({
+      include: [
+        {
+          model: db.Video['sequelize'].models.VideoChannel,
+          include: [
+            {
+              model: db.Video['sequelize'].models.Account,
+              include: [ { model: db.Video['sequelize'].models.Server, required: false } ]
+            }
+          ]
+        },
+        {
+          model: db.Video['sequelize'].models.AccountVideoRate,
+          include: [ db.Video['sequelize'].models.Account ]
+        },
+        {
+          model: db.Video['sequelize'].models.VideoShare,
+          include: [ db.Video['sequelize'].models.Account ]
+        },
+        db.Video['sequelize'].models.Tag,
+        db.Video['sequelize'].models.VideoFile
+      ]
+    })
+
+    for (const video of videos) {
+      await shareVideoByServer(video, undefined)
+    }
+  }
+}
+
+function down (options) {
+  throw new Error('Not implemented.')
+}
+
+export {
+  up,
+  down
+}
index 4fbe1cf5b9c0c3c7aa93dd0c1bd06a606257fe7b..187c9be6e5bb3abb305a886434826d2fadb02425 100644 (file)
@@ -26,7 +26,12 @@ async function migrate () {
   const migrationScripts = await getMigrationScripts()
 
   for (const migrationScript of migrationScripts) {
-    await executeMigration(actualVersion, migrationScript)
+    try {
+      await executeMigration(actualVersion, migrationScript)
+    } catch (err) {
+      logger.error('Cannot execute migration %s.', migrationScript.version, err)
+      process.exit(0)
+    }
   }
 
   logger.info('Migrations finished. New migration version schema: %s', LAST_MIGRATION_VERSION)
index e8f4f9a67e53c96eb6d3977263f2dccb5494f87e..d09f5f7a155c4cd545bd91ae675307209bd7dfe8 100644 (file)
@@ -99,7 +99,7 @@ function associate (models) {
   VideoAbuse.belongsTo(models.Account, {
     foreignKey: {
       name: 'reporterAccountId',
-      allowNull: true
+      allowNull: false
     },
     onDelete: 'CASCADE'
   })