From 74bb2cb8348d6794ed3a0e2ec94c8c9abdde82cf Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 27 Nov 2017 09:47:21 +0100 Subject: [PATCH] Add activitypub migration script --- server/initializers/constants.ts | 2 +- server/initializers/database.ts | 6 +- .../migrations/0100-activitypub.ts | 212 ++++++++++++++++++ server/initializers/migrator.ts | 7 +- server/models/video/video-abuse.ts | 2 +- 5 files changed, 224 insertions(+), 5 deletions(-) create mode 100644 server/initializers/migrations/0100-activitypub.ts diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index e7f668ee4..786334d46 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -14,7 +14,7 @@ import { FollowState } from '../../shared/models/accounts/follow.model' // --------------------------------------------------------------------------- -const LAST_MIGRATION_VERSION = 95 +const LAST_MIGRATION_VERSION = 100 // --------------------------------------------------------------------------- diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 5c757694e..9b9a81e26 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts @@ -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, @@ -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 index 000000000..50a0adc14 --- /dev/null +++ b/server/initializers/migrations/0100-activitypub.ts @@ -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 { + 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 +} diff --git a/server/initializers/migrator.ts b/server/initializers/migrator.ts index 4fbe1cf5b..187c9be6e 100644 --- a/server/initializers/migrator.ts +++ b/server/initializers/migrator.ts @@ -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) diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts index e8f4f9a67..d09f5f7a1 100644 --- a/server/models/video/video-abuse.ts +++ b/server/models/video/video-abuse.ts @@ -99,7 +99,7 @@ function associate (models) { VideoAbuse.belongsTo(models.Account, { foreignKey: { name: 'reporterAccountId', - allowNull: true + allowNull: false }, onDelete: 'CASCADE' })