github.com/gocaveman/caveman@v0.0.0-20191211162744-0ddf99dbdf6e/regions/regionsdb/regionsdb.go (about)

     1  // Database and flat file persistence for regions.
     2  package regionsdb
     3  
     4  import (
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path"
     9  	"sync"
    10  
    11  	"github.com/gocaveman/caveman/autowire"
    12  	"github.com/gocaveman/caveman/filesystem"
    13  	"github.com/gocaveman/caveman/migrate"
    14  	"github.com/gocaveman/caveman/migrate/migrateregistry"
    15  	"github.com/gocaveman/caveman/regions"
    16  	"github.com/gocraft/dbr"
    17  	yaml "gopkg.in/yaml.v2"
    18  )
    19  
    20  // DefaultRegionsMigrations is all of our migrations for this store.
    21  var DefaultRegionsMigrations migrate.MigrationList
    22  
    23  func init() {
    24  
    25  	// register in migrateregistry and with autowire for all 3 databases
    26  	reg := func(m *migrate.SQLTmplMigration) {
    27  		// TODO: see if we can compact these back down to one-liners
    28  		var rm migrate.Migration
    29  		rm = m.NewWithDriverName("sqlite3")
    30  		DefaultRegionsMigrations = append(DefaultRegionsMigrations, rm)
    31  		autowire.Populate(migrateregistry.MustRegister(rm))
    32  		rm = m.NewWithDriverName("mysql")
    33  		DefaultRegionsMigrations = append(DefaultRegionsMigrations, rm)
    34  		autowire.Populate(migrateregistry.MustRegister(rm))
    35  		rm = m.NewWithDriverName("postgres")
    36  		DefaultRegionsMigrations = append(DefaultRegionsMigrations, rm)
    37  		autowire.Populate(migrateregistry.MustRegister(rm))
    38  	}
    39  
    40  	reg(&migrate.SQLTmplMigration{
    41  		// DriverNameValue set by reg
    42  		CategoryValue: "regions",
    43  		VersionValue:  "0001_region_definition_create", // must be unique and indicates sequence
    44  		UpSQL: []string{
    45  			`CREATE TABLE {{.TablePrefix}}region_definition (
    46  				definition_id VARCHAR(255),
    47  				region_name VARCHAR(255),
    48  				sequence DOUBLE,
    49  				disabled INTEGER,
    50  				template_name VARCHAR(255),
    51  				cond_include_paths TEXT,
    52  				cond_exclude_paths TEXT,
    53  				cond_template TEXT,
    54  				context_meta TEXT,
    55  				PRIMARY KEY (definition_id)
    56  			)`,
    57  		},
    58  		DownSQL: []string{
    59  			`DROP TABLE {{.TablePrefix}}region_definition`,
    60  		},
    61  	})
    62  
    63  }
    64  
    65  // DBStore implements Store against a database table and optionally provides persistence to a flat file.
    66  type DBStore struct {
    67  	DBDriver    string `autowire:"db.DriverName"`
    68  	DBDSN       string `autowire:"db.DataSourceName"`
    69  	TablePrefix string `autowire:"db.TablePrefix,optional"`
    70  
    71  	// if FileSystem is non-nil then flat-file persistence is enabled in addition to database functionality
    72  	FileSystem     filesystem.FileSystem `autowire:"db.FlatFileSystem,optional"`
    73  	FilePathPrefix string                `autowire:"db.FlatFilePrefix,optional"`
    74  	FilePathSuffix string                // defaults to "regions.yaml"
    75  
    76  	conn          *dbr.Connection
    77  	needsFileLoad bool         // set this to trigger a reload before the next operation, used at startup
    78  	rwmu          sync.RWMutex // for file operations
    79  }
    80  
    81  func (s *DBStore) AfterWire() error {
    82  
    83  	if s.FilePathSuffix == "" {
    84  		s.FilePathSuffix = "regions.yaml"
    85  	}
    86  
    87  	var err error
    88  	// FIXME: figure out how we can fill the logger here from autowire - allowing detailed database
    89  	// logging when needed
    90  	s.conn, err = dbr.Open(s.DBDriver, s.DBDSN, nil)
    91  	if err != nil {
    92  		return err
    93  	}
    94  
    95  	if s.FileSystem != nil {
    96  		pdir := path.Dir(path.Join(s.FilePathPrefix, s.FilePathSuffix))
    97  		err := s.FileSystem.MkdirAll(pdir, 0755)
    98  		if err != nil {
    99  			return err
   100  		}
   101  		s.needsFileLoad = true
   102  	}
   103  
   104  	return nil
   105  }
   106  
   107  func (s *DBStore) WriteDefinition(d regions.Definition) error {
   108  
   109  	if err := d.IsValid(); err != nil {
   110  		return err
   111  	}
   112  
   113  	err := s.condLoadFromFlatFile()
   114  	if err != nil {
   115  		return err
   116  	}
   117  
   118  	// FIXME: figure out db logging
   119  	sess := s.conn.NewSession(nil)
   120  
   121  	tx, err := sess.Begin()
   122  	if err != nil {
   123  		return err
   124  	}
   125  	defer tx.RollbackUnlessCommitted()
   126  
   127  	var foundDefID string
   128  	err = tx.Select("definition_id").
   129  		From(s.TablePrefix+"region_definition").
   130  		Where("definition_id=?", d.DefinitionID).LoadOne(&foundDefID)
   131  	if err == dbr.ErrNotFound {
   132  		_, err := tx.InsertInto(s.TablePrefix+"region_definition").
   133  			Columns(
   134  				"definition_id",
   135  				"region_name", "sequence", "disabled", "template_name",
   136  				"cond_include_paths", "cond_exclude_paths", "cond_template",
   137  				"context_meta").
   138  			Record(&d).Exec()
   139  
   140  		if err != nil {
   141  			return err
   142  		}
   143  		foundDefID = d.DefinitionID
   144  
   145  	} else if err != nil {
   146  		return err
   147  	}
   148  
   149  	if foundDefID != d.DefinitionID {
   150  		return fmt.Errorf("selected wrong definition ID from database, should be impossible - is column case insensitive?")
   151  	}
   152  
   153  	_, err = tx.Update(s.TablePrefix+"region_definition").
   154  		Set("region_name", d.RegionName).
   155  		Set("sequence", d.Sequence).
   156  		Set("disabled", d.Disabled).
   157  		Set("template_name", d.TemplateName).
   158  		Set("cond_include_paths", d.CondIncludePaths).
   159  		Set("cond_exclude_paths", d.CondExcludePaths).
   160  		Set("cond_template", d.CondTemplate).
   161  		Set("context_meta", d.ContextMeta).
   162  		Exec()
   163  	if err != nil {
   164  		return err
   165  	}
   166  
   167  	err = tx.Commit()
   168  	if err != nil {
   169  		return err
   170  	}
   171  
   172  	if s.FileSystem != nil {
   173  		return s.SaveToFlatFile()
   174  	}
   175  	return nil
   176  }
   177  
   178  func (s *DBStore) DeleteDefinition(defintionID string) error {
   179  
   180  	err := s.condLoadFromFlatFile()
   181  	if err != nil {
   182  		return err
   183  	}
   184  
   185  	sess := s.conn.NewSession(nil)
   186  	_, err = sess.DeleteFrom(s.TablePrefix+"region_definition").Where("definition_id=?", defintionID).Exec()
   187  	if err != nil {
   188  		return err
   189  	}
   190  	if s.FileSystem != nil {
   191  		return s.SaveToFlatFile()
   192  	}
   193  	return nil
   194  }
   195  
   196  func (s *DBStore) AllDefinitions() (regions.DefinitionList, error) {
   197  
   198  	err := s.condLoadFromFlatFile()
   199  	if err != nil {
   200  		return nil, err
   201  	}
   202  
   203  	var ret regions.DefinitionList
   204  	sess := s.conn.NewSession(nil)
   205  	_, err = sess.Select("*").From(s.TablePrefix + "region_definition").OrderAsc("definition_id").Load(&ret)
   206  	if err != nil {
   207  		return nil, err
   208  	}
   209  	return ret, nil
   210  }
   211  
   212  // load from file if needsFileLoad is set
   213  func (s *DBStore) condLoadFromFlatFile() error {
   214  	s.rwmu.RLock()
   215  	needsFileLoad := s.needsFileLoad
   216  	s.rwmu.RUnlock()
   217  	if needsFileLoad {
   218  		return s.LoadFromFlatFile()
   219  	}
   220  	return nil
   221  }
   222  
   223  // LoadFromFlatFile reads the contents of the flat file and loads into the database.
   224  func (s *DBStore) LoadFromFlatFile() error {
   225  	s.rwmu.Lock()
   226  	defer s.rwmu.Unlock()
   227  	s.needsFileLoad = false
   228  
   229  	f, err := s.FileSystem.Open(path.Join(s.FilePathPrefix, s.FilePathSuffix))
   230  	if err != nil {
   231  		// need to think about this more, but for now it is not an error if the file does yet exist
   232  		if os.IsNotExist(err) {
   233  			return nil
   234  		}
   235  		return err
   236  	}
   237  	defer f.Close()
   238  
   239  	b, err := ioutil.ReadAll(f)
   240  	if err != nil {
   241  		return err
   242  	}
   243  
   244  	var defs regions.DefinitionList
   245  	err = yaml.Unmarshal(b, &defs)
   246  	if err != nil {
   247  		return err
   248  	}
   249  
   250  	sess := s.conn.NewSession(nil)
   251  	tx, err := sess.Begin()
   252  	if err != nil {
   253  		return err
   254  	}
   255  	defer tx.RollbackUnlessCommitted()
   256  
   257  	// remove whatever's there
   258  	_, err = tx.DeleteFrom(s.TablePrefix + "region_definition").Exec()
   259  	if err != nil {
   260  		return err
   261  	}
   262  
   263  	// TODO: batch this so we insert a bunch at a time, might perform better
   264  	for _, def := range defs {
   265  		_, err := tx.InsertInto(s.TablePrefix+"region_definition").
   266  			Columns(
   267  				"region_name", "sequence", "disabled", "template_name",
   268  				"cond_include_paths", "cond_exclude_paths", "cond_template",
   269  				"context_meta").
   270  			Record(&def).Exec()
   271  		if err != nil {
   272  			return err
   273  		}
   274  	}
   275  
   276  	return nil
   277  }
   278  
   279  // SaveToFlatFile writes the contents of the database to the flat file.
   280  func (s *DBStore) SaveToFlatFile() error {
   281  
   282  	s.rwmu.Lock()
   283  	defer s.rwmu.Unlock()
   284  	s.needsFileLoad = false
   285  
   286  	var defs regions.DefinitionList
   287  	sess := s.conn.NewSession(nil)
   288  	_, err := sess.Select("*").From(s.TablePrefix + "region_definition").OrderAsc("definition_id").Load(&defs)
   289  	if err != nil {
   290  		return err
   291  	}
   292  
   293  	b, err := yaml.Marshal(&defs)
   294  	if err != nil {
   295  		return err
   296  	}
   297  
   298  	p := path.Join(s.FilePathPrefix, s.FilePathSuffix)
   299  	f, err := s.FileSystem.OpenFile(p, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
   300  	if err != nil {
   301  		return err
   302  	}
   303  	defer f.Close()
   304  
   305  	_, err = f.Write(b)
   306  	if err != nil {
   307  		return err
   308  	}
   309  
   310  	return nil
   311  
   312  }