// // tank // // Import and Playlist Daemon for autoradio project // // // Copyright (C) 2017-2019 Christian Pointner <equinox@helsinki.at> // // This file is part of tank. // // tank is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // any later version. // // tank 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 General Public License for more details. // // You should have received a copy of the GNU General Public License // along with tank. If not, see <http://www.gnu.org/licenses/>. // package store import ( "errors" "time" "github.com/jinzhu/gorm" "gopkg.in/gormigrate.v1" ) 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:"primary_key"` PlaylistID uint64 `json:"-" gorm:"not null;index;unique_index:unique_playlist_line_numbers"` LineNum uint `json:"-" gorm:"not null;unique_index:unique_playlist_line_numbers"` URI string `json:"uri" gorm:"size:1024"` File *File `json:"file,omitempty" gorm:"association_autoupdate:false;association_autocreate:false"` FileID *uint64 `json:"-" gorm:"index"` } type Playlist struct { ID uint64 `json:"id" gorm:"primary_key"` 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:"association_foreignkey:Name"` Entries []PlaylistEntry `json:"entries,omitempty"` } return tx.AutoMigrate(&Playlist{}).Error }, Rollback: func(tx *gorm.DB) error { return tx.Table("playlists").DropColumn("description").Error }, }, { 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;embedded_prefix: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:"primary_key"` CreatedAt time.Time `json:"created"` UpdatedAt time.Time `json:"updated"` ShowName string `json:"show" gorm:"not null;index"` Show Show `json:"-" gorm:"association_foreignkey:Name"` Source FileSource `json:"source" gorm:"embedded;embedded_prefix:source__"` Metadata FileMetadata `json:"metadata" gorm:"embedded;embedded_prefix:metadata__"` Size uint64 `json:"size"` Duration time.Duration `json:"duration"` } return tx.AutoMigrate(&File{}).Error }, Rollback: func(tx *gorm.DB) error { return tx.Table("files").DropColumn("source__import__error").Error }, }, { ID: "201906010144", Migrate: func(tx *gorm.DB) error { type ImportLog struct { ID uint64 `gorm:"primary_key"` File File `gorm:"association_autoupdate:false;association_autocreate:false"` FileID uint64 `gorm:"not null;index;unique_index:unique_import_log_step"` ImportStep string `gorm:"not null;index;unique_index:unique_import_log_step"` Encoded []byte } if err := tx.AutoMigrate(&ImportLog{}).Error; err != nil { return err } if err := tx.Model(ImportLog{}).AddForeignKey("file_id", "files (id)", "CASCADE", "CASCADE").Error; err != nil { return err } return nil }, Rollback: func(tx *gorm.DB) error { if err := tx.Model(ImportLog{}).RemoveForeignKey("file_id", "files (id)").Error; err != nil { return err } return tx.DropTable("import_logs").Error }, }, { ID: "201906051520", Migrate: func(tx *gorm.DB) error { if tx.Dialect().GetName() == "mysql" { tx.Model(ImportLog{}).ModifyColumn("encoded", "longblob") } return nil }, Rollback: func(tx *gorm.DB) error { // to be exact we would need to chang `encoded` back to varbinary(255) for mysql // however this will open a box of worms.... return nil }, }, { ID: "201908110945", Migrate: func(tx *gorm.DB) error { type PlaylistEntry struct { ID uint64 `json:"-" gorm:"primary_key"` PlaylistID uint64 `json:"-" gorm:"not null;index;unique_index:unique_playlist_line_numbers"` LineNum uint `json:"-" gorm:"not null;unique_index:unique_playlist_line_numbers"` URI string `json:"uri" gorm:"size:1024"` File *File `json:"file,omitempty" gorm:"association_autoupdate:false;association_autocreate:false"` FileID *uint64 `json:"-" gorm:"index"` } type Playlist struct { ID uint64 `json:"id" gorm:"primary_key"` CreatedAt time.Time `json:"created"` UpdatedAt time.Time `json:"updated"` Description string `json:"description"` PlayoutMode string `json:"playout-mode"` ShowName string `json:"show" gorm:"not null;index"` Show Show `json:"-" gorm:"association_foreignkey:Name"` Entries []PlaylistEntry `json:"entries,omitempty"` } return tx.AutoMigrate(&Playlist{}).Error }, Rollback: func(tx *gorm.DB) error { return tx.Table("playlists").DropColumn("playout_mode").Error }, }, } ) func initialMigration(tx *gorm.DB) (err error) { err = tx.AutoMigrate( &Show{}, &File{}, &ImportLog{}, &Playlist{}, &PlaylistEntry{}, ).Error if err != nil { return } // TODO: sadly this does not work on sqlite because constraints can only be added with create table... // unfortunately gorm does not create foreign key contstraints in AutoMigrate(), see: https://github.com/jinzhu/gorm/issues/450 if err := tx.Model(File{}).AddForeignKey("show_name", "shows (name)", "CASCADE", "CASCADE").Error; err != nil { return err } if err := tx.Model(ImportLog{}).AddForeignKey("file_id", "files (id)", "CASCADE", "CASCADE").Error; err != nil { return err } if err := tx.Model(Playlist{}).AddForeignKey("show_name", "shows (name)", "CASCADE", "CASCADE").Error; err != nil { return err } if err := tx.Model(PlaylistEntry{}).AddForeignKey("playlist_id", "playlists (id)", "CASCADE", "CASCADE").Error; err != nil { return err } if err := tx.Model(PlaylistEntry{}).AddForeignKey("file_id", "files (id)", "RESTRICT", "CASCADE").Error; err != nil { return err } return nil } func (st *Store) initDBModel(cfg DBConfig) (err error) { opts := gormigrate.DefaultOptions opts.TableName = migrationsTn opts.IDColumnSize = 64 opts.UseTransaction = true if cfg.Type == "mysql" { // mySQl does not support DDL commands inside transactions opts.UseTransaction = false } 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 }