// // tank, Import and Playlist Daemon for Aura project // Copyright (C) 2017-2020 Christian Pointner <equinox@helsinki.at> // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. // package store import ( "errors" "time" "github.com/go-gormigrate/gormigrate/v2" "gorm.io/gorm" ) var ( dbMigrations = []*gormigrate.Migration{ { ID: "201903131716", Migrate: func(tx *gorm.DB) error { return nil }, Rollback: func(tx *gorm.DB) error { return nil }, }, { ID: "201905160033", Migrate: func(tx *gorm.DB) error { type PlaylistEntry struct { ID uint64 `json:"-" gorm:"primaryKey"` PlaylistID uint64 `json:"-" gorm:"not null;index;uniqueIndex:unique_playlist_line_numbers"` LineNum uint `json:"-" gorm:"not null;uniqueIndex:unique_playlist_line_numbers"` URI string `json:"uri" gorm:"size:1024"` File *File `json:"file,omitempty" gorm:"associationAutoUpdate:false;associationAutoCreate:false"` FileID *uint64 `json:"-" gorm:"index"` } type Playlist struct { ID uint64 `json:"id" gorm:"primaryKey"` CreatedAt time.Time `json:"created"` UpdatedAt time.Time `json:"updated"` Description string `json:"description"` ShowName string `json:"show" gorm:"not null;index"` Show Show `json:"-" gorm:"associationForeignKey:Name"` Entries []PlaylistEntry `json:"entries,omitempty"` } return tx.AutoMigrate(&Playlist{}) }, Rollback: func(tx *gorm.DB) error { return tx.Migrator().DropColumn(&Playlist{}, "description") }, }, { ID: "201905291602", Migrate: func(tx *gorm.DB) error { type Import struct { State ImportState `json:"state"` Error string `json:"error,omitempty"` } type FileSource struct { URI string `json:"uri" gorm:"size:1024"` Hash string `json:"hash"` Import Import `json:"import" gorm:"embedded;embeddedPrefix:import__"` } type FileMetadata struct { Artist string `json:"artist,omitempty" gorm:"index"` Title string `json:"title,omitempty" gorm:"index"` Album string `json:"album,omitempty" gorm:"index"` } type File struct { ID uint64 `json:"id" gorm:"primaryKey"` CreatedAt time.Time `json:"created"` UpdatedAt time.Time `json:"updated"` ShowName string `json:"show" gorm:"not null;index"` Show Show `json:"-" gorm:"associationForeignKey:Name"` Source FileSource `json:"source" gorm:"embedded;embeddedPrefix:source__"` Metadata FileMetadata `json:"metadata" gorm:"embedded;embeddedPrefix:metadata__"` Size uint64 `json:"size"` Duration time.Duration `json:"duration"` } return tx.AutoMigrate(&File{}) }, Rollback: func(tx *gorm.DB) error { return tx.Migrator().DropColumn(&File{}, "source__import__error") }, }, { ID: "201906010144", Migrate: func(tx *gorm.DB) error { type ImportLog struct { ID uint64 `gorm:"primaryKey"` File File `gorm:"associationAutoUpdate:false;associationAutoCreate:false"` FileID uint64 `gorm:"not null;index;uniqueIndex:unique_import_log_step"` ImportStep string `gorm:"not null;index;uniqueIndex:unique_import_log_step"` Encoded []byte } if err := tx.AutoMigrate(&ImportLog{}); err != nil { return err } if err := tx.Migrator().CreateConstraint(&ImportLog{}, "file_id"); err != nil { return err } return nil }, Rollback: func(tx *gorm.DB) error { if err := tx.Migrator().DropConstraint(&ImportLog{}, "file_id"); err != nil { return err } return nil }, }, { ID: "201908110945", Migrate: func(tx *gorm.DB) error { type PlaylistEntry struct { ID uint64 `json:"-" gorm:"primaryKey"` PlaylistID uint64 `json:"-" gorm:"not null;index;uniqueIndex:unique_playlist_line_numbers"` LineNum uint `json:"-" gorm:"not null;uniqueIndex:unique_playlist_line_numbers"` URI string `json:"uri" gorm:"size:1024"` File *File `json:"file,omitempty" gorm:"associationAutoUpdate:false;associationAutoCreate:false"` FileID *uint64 `json:"-" gorm:"index"` } type Playlist struct { ID uint64 `json:"id" gorm:"primaryKey"` CreatedAt time.Time `json:"created"` UpdatedAt time.Time `json:"updated"` Description string `json:"description"` PlayoutMode string `json:"playoutMode"` ShowName string `json:"show" gorm:"not null;index"` Show Show `json:"-" gorm:"associationForeignKey:Name"` Entries []PlaylistEntry `json:"entries,omitempty"` } return tx.AutoMigrate(&Playlist{}) }, Rollback: func(tx *gorm.DB) error { return tx.Migrator().DropColumn(&Playlist{}, "playout_mode") }, }, { ID: "201908150104", Migrate: func(tx *gorm.DB) error { type Import struct { State ImportState `json:"state"` Error string `json:"error,omitempty"` } type FileSource struct { URI string `json:"uri" gorm:"size:1024"` Hash string `json:"hash"` Import Import `json:"import" gorm:"embedded;embeddedPrefix:import__"` } type FileMetadata struct { Artist string `json:"artist,omitempty" gorm:"index"` Title string `json:"title,omitempty" gorm:"index"` Album string `json:"album,omitempty" gorm:"index"` Organization string `json:"organization,omitempty" gorm:"index"` ISRC string `json:"isrc,omitempty" gorm:"index"` } type File struct { ID uint64 `json:"id" gorm:"primaryKey"` CreatedAt time.Time `json:"created"` UpdatedAt time.Time `json:"updated"` ShowName string `json:"show" gorm:"not null;index"` Show Show `json:"-" gorm:"associationForeignKey:Name"` Source FileSource `json:"source" gorm:"embedded;embeddedPrefix:source__"` Metadata FileMetadata `json:"metadata" gorm:"embedded;embeddedPrefix:metadata__"` Size uint64 `json:"size"` Duration time.Duration `json:"duration"` } return tx.AutoMigrate(&File{}) }, Rollback: func(tx *gorm.DB) error { if err := tx.Migrator().DropColumn(&File{}, "metadata__organization"); err != nil { return err } return tx.Migrator().DropColumn(&File{}, "metadata__isrc") }, }, { ID: "202006130203", Migrate: func(tx *gorm.DB) error { type PlaylistEntry struct { ID uint64 `json:"-" gorm:"primaryKey"` PlaylistID uint64 `json:"-" gorm:"not null;index;uniqueIndex:unique_playlist_line_numbers"` LineNum uint `json:"-" gorm:"not null;uniqueIndex:unique_playlist_line_numbers"` URI string `json:"uri" gorm:"size:1024"` Duration *time.Duration `json:"duration,omitempty"` File *File `json:"file,omitempty" gorm:"associationAutoUpdate:false;associationAutoCreate:false"` FileID *uint64 `json:"-" gorm:"index"` } // actually all playlists would need to be verified if they still fit the new constraint // that only allows a single non-file entry with duration == NULL per playlist. // However, we are still pre-first-release and all migrations will likely be squashed before that // release anyway... return tx.AutoMigrate(&PlaylistEntry{}) }, Rollback: func(tx *gorm.DB) error { return tx.Migrator().DropColumn(&PlaylistEntry{}, "duration") }, }, { ID: "202309141500", Migrate: func(tx *gorm.DB) error { type File struct { ID uint64 `json:"id" gorm:"primaryKey"` CreatedAt time.Time `json:"created"` UpdatedAt time.Time `json:"updated"` ShowName string `json:"show" gorm:"not null;index"` Show Show `json:"-" gorm:"associationForeignKey:Name"` Source FileSource `json:"source" gorm:"embedded;embeddedPrefix:source__"` Metadata FileMetadata `json:"metadata" gorm:"embedded;embeddedPrefix:metadata__"` Size uint64 `json:"size"` Duration float64 `json:"duration"` } if err := tx.Migrator().AlterColumn(&File{}, "duration"); err != nil { return err } type PlaylistEntry struct { ID uint64 `json:"-" gorm:"primaryKey"` PlaylistID uint64 `json:"-" gorm:"not null;index;uniqueIndex:unique_playlist_line_numbers"` LineNum uint `json:"-" gorm:"not null;uniqueIndex:unique_playlist_line_numbers"` URI string `json:"uri" gorm:"size:1024"` Duration *float64 `json:"duration,omitempty"` File *File `json:"file,omitempty" gorm:"associationAutoUpdate:false;associationAutoCreate:false"` FileID *uint64 `json:"-" gorm:"index"` } return tx.Migrator().AlterColumn(&PlaylistEntry{}, "duration") }, Rollback: func(tx *gorm.DB) error { type File struct { ID uint64 `json:"id" gorm:"primaryKey"` CreatedAt time.Time `json:"created"` UpdatedAt time.Time `json:"updated"` ShowName string `json:"show" gorm:"not null;index"` Show Show `json:"-" gorm:"associationForeignKey:Name"` Source FileSource `json:"source" gorm:"embedded;embeddedPrefix:source__"` Metadata FileMetadata `json:"metadata" gorm:"embedded;embeddedPrefix:metadata__"` Size uint64 `json:"size"` Duration time.Duration `json:"duration"` } if err := tx.Migrator().AlterColumn(&File{}, "duration"); err != nil { return err } return tx.Migrator().AlterColumn(&Playlist{}, "duration") }, }, { ID: "202312011500", Migrate: func(tx *gorm.DB) error { type Show struct { ID uint64 `json:"id" gorm:"primaryKey"` CreatedAt time.Time `json:"created"` UpdatedAt time.Time `json:"updated"` } type File struct { ID uint64 `json:"id" gorm:"primaryKey"` CreatedAt time.Time `json:"created"` UpdatedAt time.Time `json:"updated"` ShowID uint64 `json:"showId" gorm:"not null;index"` Show Show `json:"-" gorm:"associationForeignKey:ID"` Source FileSource `json:"source" gorm:"embedded;embeddedPrefix:source__"` Metadata FileMetadata `json:"metadata" gorm:"embedded;embeddedPrefix:metadata__"` Size uint64 `json:"size"` Duration float64 `json:"duration"` } type Playlist struct { ID uint64 `json:"id" gorm:"primaryKey"` CreatedAt time.Time `json:"created"` UpdatedAt time.Time `json:"updated"` Description string `json:"description"` PlayoutMode string `json:"playoutMode" gorm:"not null;default:'linear'"` ShowID uint64 `json:"showId" gorm:"not null;index"` Show Show `json:"-" gorm:"associationForeignKey:ID"` Entries []PlaylistEntry `json:"entries"` } if err := tx.Migrator().DropConstraint(&File{}, "show_name"); err != nil { return err } if err := tx.Migrator().DropConstraint(&Playlist{}, "show_name"); err != nil { return err } return tx.AutoMigrate(&Show{}, &File{}, &Playlist{}) }, Rollback: func(tx *gorm.DB) error { if err := tx.Migrator().DropColumn(&Show{}, "id"); err != nil { return err } if err := tx.Migrator().DropColumn(&File{}, "show_id"); err != nil { return err } return tx.Migrator().DropColumn(&Playlist{}, "show_id") }, }, } ) func initialMigration(tx *gorm.DB) (err error) { err = tx.AutoMigrate( &Show{}, &File{}, &ImportLog{}, &Playlist{}, &PlaylistEntry{}, ) if err != nil { return } return nil } func (st *Store) initDBModel(cfg DBConfig) (err error) { opts := gormigrate.DefaultOptions opts.TableName = migrationsTn opts.IDColumnSize = 64 opts.UseTransaction = true m := gormigrate.New(st.db, opts, dbMigrations) m.InitSchema(initialMigration) if err = m.Migrate(); err != nil { return errors.New("running database migrations failed: " + err.Error()) } if err = st.db.Table(migrationsTn).Select("id").Not("id = ?", "SCHEMA_INIT").Order("id DESC").Limit(1).Row().Scan(&st.revision); err != nil { return errors.New("fetching current database revision failed: " + err.Error()) } return }