2021-07-20 05:22:29 +03:00
|
|
|
package data
|
|
|
|
|
|
|
|
import (
|
|
|
|
"database/sql"
|
2022-04-26 00:10:20 +03:00
|
|
|
"fmt"
|
|
|
|
"path/filepath"
|
2021-07-20 05:22:29 +03:00
|
|
|
"time"
|
|
|
|
|
2022-04-26 00:10:20 +03:00
|
|
|
"github.com/owncast/owncast/config"
|
2021-07-20 05:22:29 +03:00
|
|
|
"github.com/owncast/owncast/utils"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/teris-io/shortid"
|
|
|
|
)
|
|
|
|
|
2022-04-26 00:10:20 +03:00
|
|
|
func migrateDatabaseSchema(db *sql.DB, from, to int) error {
|
|
|
|
log.Printf("Migrating database from version %d to %d", from, to)
|
|
|
|
dbBackupFile := filepath.Join(config.BackupDirectory, fmt.Sprintf("owncast-v%d.bak", from))
|
|
|
|
utils.Backup(db, dbBackupFile)
|
|
|
|
for v := from; v < to; v++ {
|
|
|
|
log.Tracef("Migration step from %d to %d\n", v, v+1)
|
|
|
|
switch v {
|
|
|
|
case 0:
|
|
|
|
migrateToSchema1(db)
|
|
|
|
case 1:
|
|
|
|
migrateToSchema2(db)
|
|
|
|
case 2:
|
|
|
|
migrateToSchema3(db)
|
|
|
|
case 3:
|
|
|
|
migrateToSchema4(db)
|
|
|
|
case 4:
|
|
|
|
migrateToSchema5(db)
|
2022-08-03 20:21:55 +03:00
|
|
|
case 5:
|
|
|
|
migrateToSchema6(db)
|
2022-04-26 00:10:20 +03:00
|
|
|
default:
|
|
|
|
log.Fatalln("missing database migration step")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err := db.Exec("UPDATE config SET value = ? WHERE key = ?", to, "version")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-08-03 20:21:55 +03:00
|
|
|
func migrateToSchema6(db *sql.DB) {
|
|
|
|
// Fix chat messages table schema. Since chat is ephemeral we can drop
|
|
|
|
// the table and recreate it.
|
|
|
|
// Drop the old messages table
|
2022-08-03 21:30:06 +03:00
|
|
|
MustExec(`DROP TABLE messages`, db)
|
2022-08-03 20:21:55 +03:00
|
|
|
|
|
|
|
// Recreate it
|
|
|
|
CreateMessagesTable(db)
|
|
|
|
}
|
|
|
|
|
2022-04-24 02:31:20 +03:00
|
|
|
// nolint:cyclop
|
2022-04-22 00:55:26 +03:00
|
|
|
func migrateToSchema5(db *sql.DB) {
|
2022-04-23 23:56:38 +03:00
|
|
|
// Create the access tokens table.
|
|
|
|
createAccessTokenTable(db)
|
2022-04-22 00:55:26 +03:00
|
|
|
|
2022-04-23 23:56:38 +03:00
|
|
|
// 1. Authenticated bool added to the users table.
|
|
|
|
// 2. Access tokens are now stored in their own table.
|
|
|
|
//
|
|
|
|
// Long story short, the access_token used to be the primary key of the users
|
|
|
|
// table. However, now it's going to live in its own table. However, you
|
|
|
|
// cannot change the primary key. So we need to create a copy table, then
|
|
|
|
// migrate the access tokens, and then move the copy into place.
|
|
|
|
createTempTable := `CREATE TABLE IF NOT EXISTS users_copy (
|
|
|
|
"id" TEXT,
|
|
|
|
"display_name" TEXT NOT NULL,
|
|
|
|
"display_color" NUMBER NOT NULL,
|
|
|
|
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
"disabled_at" TIMESTAMP,
|
|
|
|
"previous_names" TEXT DEFAULT '',
|
|
|
|
"namechanged_at" TIMESTAMP,
|
2022-04-24 02:31:20 +03:00
|
|
|
"authenticated_at" TIMESTAMP,
|
2022-04-23 23:56:38 +03:00
|
|
|
"scopes" TEXT,
|
|
|
|
"type" TEXT DEFAULT 'STANDARD',
|
|
|
|
"last_used" DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
PRIMARY KEY (id)
|
|
|
|
);CREATE INDEX user_id_disabled_at_index ON users (id, disabled_at);
|
|
|
|
CREATE INDEX user_id_index ON users (id);
|
2022-04-24 02:31:20 +03:00
|
|
|
CREATE INDEX user_id_disabled_index ON users (id, disabled_at);
|
2022-04-23 23:56:38 +03:00
|
|
|
CREATE INDEX user_disabled_at_index ON USERS (disabled_at);`
|
2022-04-24 02:31:20 +03:00
|
|
|
_, err := db.Exec(createTempTable)
|
2022-04-22 00:55:26 +03:00
|
|
|
if err != nil {
|
2022-04-23 23:56:38 +03:00
|
|
|
log.Errorln("error running migration, you may experience issues: ", err)
|
2022-04-22 00:55:26 +03:00
|
|
|
}
|
2022-04-23 23:56:38 +03:00
|
|
|
|
2022-04-24 02:31:20 +03:00
|
|
|
// Start insert transaction
|
|
|
|
tx, err := db.Begin()
|
2022-04-22 00:55:26 +03:00
|
|
|
if err != nil {
|
2022-04-24 02:31:20 +03:00
|
|
|
log.Errorln(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Migrate the users table to the new users_copy table.
|
|
|
|
rows, err := tx.Query(`SELECT id, access_token, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at, scopes, type, last_used FROM users`)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorln("error migrating access tokens to schema v5", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if rows.Err() != nil {
|
|
|
|
log.Errorln("error migrating access tokens to schema v5", rows.Err())
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
defer tx.Rollback() //nolint:errcheck
|
|
|
|
|
|
|
|
log.Println("Migrating users. This may take time if you have lots of users...")
|
|
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
var id string
|
|
|
|
var accessToken string
|
|
|
|
var displayName string
|
|
|
|
var displayColor int
|
|
|
|
var createdAt time.Time
|
|
|
|
var disabledAt *time.Time
|
|
|
|
var previousNames string
|
|
|
|
var namechangedAt *time.Time
|
|
|
|
var scopes *string
|
|
|
|
var userType string
|
|
|
|
var lastUsed *time.Time
|
|
|
|
|
|
|
|
if err := rows.Scan(&id, &accessToken, &displayName, &displayColor, &createdAt, &disabledAt, &previousNames, &namechangedAt, &scopes, &userType, &lastUsed); err != nil {
|
|
|
|
log.Error("There is a problem reading the database when migrating users.", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
stmt, err := tx.Prepare(`INSERT INTO users_copy (id, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at, scopes, type, last_used) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorln(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer stmt.Close()
|
|
|
|
|
|
|
|
if _, err := stmt.Exec(id, displayName, displayColor, createdAt, disabledAt, previousNames, namechangedAt, scopes, userType, lastUsed); err != nil {
|
|
|
|
log.Errorln(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
stmt, err = tx.Prepare(`INSERT INTO user_access_tokens(token, user_id, timestamp) VALUES (?, ?, ?) ON CONFLICT DO NOTHING`)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorln(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer stmt.Close()
|
|
|
|
|
|
|
|
if _, err := stmt.Exec(accessToken, id, createdAt); err != nil {
|
|
|
|
log.Errorln(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
|
|
log.Errorln(err)
|
2022-04-23 23:56:38 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
_, err = db.Exec(`PRAGMA foreign_keys = OFF;DROP TABLE "users";ALTER TABLE "users_copy" RENAME TO users;PRAGMA foreign_keys = ON;`)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorln("error running migration, you may experience issues: ", err)
|
2022-04-22 00:55:26 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-08 02:14:47 +03:00
|
|
|
func migrateToSchema4(db *sql.DB) {
|
2022-04-23 23:56:38 +03:00
|
|
|
// We now save the follow request object.
|
2022-04-08 02:14:47 +03:00
|
|
|
stmt, err := db.Prepare("ALTER TABLE ap_followers ADD COLUMN request_object BLOB")
|
|
|
|
if err != nil {
|
2022-04-23 23:56:38 +03:00
|
|
|
log.Errorln("Error running migration. This may be because you have already been running a dev version.", err)
|
|
|
|
return
|
2022-04-08 02:14:47 +03:00
|
|
|
}
|
|
|
|
defer stmt.Close()
|
2022-04-23 23:56:38 +03:00
|
|
|
|
2022-04-08 02:14:47 +03:00
|
|
|
_, err = stmt.Exec()
|
|
|
|
if err != nil {
|
|
|
|
log.Warnln(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-13 00:53:10 +03:00
|
|
|
func migrateToSchema3(db *sql.DB) {
|
|
|
|
// Since it's just a backlog of chat messages let's wipe the old messages
|
|
|
|
// and recreate the table.
|
|
|
|
|
|
|
|
// Drop the old messages table
|
|
|
|
stmt, err := db.Prepare("DROP TABLE messages")
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
defer stmt.Close()
|
|
|
|
_, err = stmt.Exec()
|
|
|
|
if err != nil {
|
|
|
|
log.Warnln(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Recreate it
|
|
|
|
CreateMessagesTable(db)
|
|
|
|
}
|
|
|
|
|
|
|
|
func migrateToSchema2(db *sql.DB) {
|
|
|
|
// Since it's just a backlog of chat messages let's wipe the old messages
|
|
|
|
// and recreate the table.
|
|
|
|
|
|
|
|
// Drop the old messages table
|
|
|
|
stmt, err := db.Prepare("DROP TABLE messages")
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
defer stmt.Close()
|
|
|
|
_, err = stmt.Exec()
|
|
|
|
if err != nil {
|
|
|
|
log.Warnln(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Recreate it
|
|
|
|
CreateMessagesTable(db)
|
|
|
|
}
|
|
|
|
|
2021-07-20 05:22:29 +03:00
|
|
|
func migrateToSchema1(db *sql.DB) {
|
|
|
|
// Since it's just a backlog of chat messages let's wipe the old messages
|
|
|
|
// and recreate the table.
|
|
|
|
|
|
|
|
// Drop the old messages table
|
|
|
|
stmt, err := db.Prepare("DROP TABLE messages")
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
defer stmt.Close()
|
|
|
|
_, err = stmt.Exec()
|
|
|
|
if err != nil {
|
|
|
|
log.Warnln(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Recreate it
|
2021-07-23 09:28:01 +03:00
|
|
|
CreateMessagesTable(db)
|
2021-07-20 05:22:29 +03:00
|
|
|
|
|
|
|
// Migrate access tokens to become chat users
|
|
|
|
type oldAccessToken struct {
|
|
|
|
accessToken string
|
|
|
|
displayName string
|
|
|
|
scopes string
|
|
|
|
createdAt time.Time
|
|
|
|
lastUsedAt *time.Time
|
|
|
|
}
|
|
|
|
|
|
|
|
oldAccessTokens := make([]oldAccessToken, 0)
|
|
|
|
|
|
|
|
query := `SELECT * FROM access_tokens`
|
|
|
|
|
|
|
|
rows, err := db.Query(query)
|
|
|
|
if err != nil || rows.Err() != nil {
|
|
|
|
log.Errorln("error migrating access tokens to schema v1", err, rows.Err())
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
var token string
|
|
|
|
var name string
|
|
|
|
var scopes string
|
|
|
|
var timestampString string
|
|
|
|
var lastUsedString *string
|
|
|
|
|
|
|
|
if err := rows.Scan(&token, &name, &scopes, ×tampString, &lastUsedString); err != nil {
|
|
|
|
log.Error("There is a problem reading the database.", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
timestamp, err := time.Parse(time.RFC3339, timestampString)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-09-12 10:18:15 +03:00
|
|
|
var lastUsed *time.Time
|
2021-07-20 05:22:29 +03:00
|
|
|
if lastUsedString != nil {
|
|
|
|
lastUsedTime, _ := time.Parse(time.RFC3339, *lastUsedString)
|
|
|
|
lastUsed = &lastUsedTime
|
|
|
|
}
|
|
|
|
|
|
|
|
oldToken := oldAccessToken{
|
|
|
|
accessToken: token,
|
|
|
|
displayName: name,
|
|
|
|
scopes: scopes,
|
|
|
|
createdAt: timestamp,
|
|
|
|
lastUsedAt: lastUsed,
|
|
|
|
}
|
|
|
|
|
|
|
|
oldAccessTokens = append(oldAccessTokens, oldToken)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Recreate them as users
|
|
|
|
for _, token := range oldAccessTokens {
|
|
|
|
color := utils.GenerateRandomDisplayColor()
|
|
|
|
if err := insertAPIToken(db, token.accessToken, token.displayName, color, token.scopes); err != nil {
|
|
|
|
log.Errorln("Error migrating access token", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func insertAPIToken(db *sql.DB, token string, name string, color int, scopes string) error {
|
|
|
|
log.Debugln("Adding new access token:", name)
|
|
|
|
|
|
|
|
id := shortid.MustGenerate()
|
|
|
|
|
|
|
|
tx, err := db.Begin()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
stmt, err := tx.Prepare("INSERT INTO users(id, access_token, display_name, display_color, scopes, type) values(?, ?, ?, ?, ?, ?)")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer stmt.Close()
|
|
|
|
|
|
|
|
if _, err = stmt.Exec(id, token, name, color, scopes, "API"); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = tx.Commit(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|