github.com/adacta-ru/mattermost-server/v6@v6.0.0/config/database.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  package config
     5  
     6  import (
     7  	"bytes"
     8  	"database/sql"
     9  	"encoding/json"
    10  	"strings"
    11  
    12  	"github.com/jmoiron/sqlx"
    13  	"github.com/pkg/errors"
    14  
    15  	"github.com/adacta-ru/mattermost-server/v6/mlog"
    16  	"github.com/adacta-ru/mattermost-server/v6/model"
    17  
    18  	// Load the MySQL driver
    19  	_ "github.com/go-sql-driver/mysql"
    20  	// Load the Postgres driver
    21  	_ "github.com/lib/pq"
    22  )
    23  
    24  // MaxWriteLength defines the maximum length accepted for write to the Configurations or
    25  // ConfigurationFiles table.
    26  //
    27  // It is imposed by MySQL's default max_allowed_packet value of 4Mb.
    28  const MaxWriteLength = 4 * 1024 * 1024
    29  
    30  // DatabaseStore is a config store backed by a database.
    31  // Not to be used directly. Only to be used as a backing store for config.Store
    32  type DatabaseStore struct {
    33  	originalDsn    string
    34  	driverName     string
    35  	dataSourceName string
    36  	db             *sqlx.DB
    37  }
    38  
    39  // NewDatabaseStore creates a new instance of a config store backed by the given database.
    40  func NewDatabaseStore(dsn string) (ds *DatabaseStore, err error) {
    41  	driverName, dataSourceName, err := parseDSN(dsn)
    42  	if err != nil {
    43  		return nil, errors.Wrap(err, "invalid DSN")
    44  	}
    45  
    46  	db, err := sqlx.Open(driverName, dataSourceName)
    47  	if err != nil {
    48  		return nil, errors.Wrapf(err, "failed to connect to %s database", driverName)
    49  	}
    50  	// Set conservative connection configuration for configuration database.
    51  	db.SetMaxIdleConns(0)
    52  	db.SetMaxOpenConns(2)
    53  
    54  	defer func() {
    55  		if err != nil {
    56  			db.Close()
    57  		}
    58  	}()
    59  
    60  	ds = &DatabaseStore{
    61  		driverName:     driverName,
    62  		originalDsn:    dsn,
    63  		dataSourceName: dataSourceName,
    64  		db:             db,
    65  	}
    66  	if err = initializeConfigurationsTable(ds.db); err != nil {
    67  		err = errors.Wrap(err, "failed to initialize")
    68  		return nil, err
    69  	}
    70  
    71  	return ds, nil
    72  }
    73  
    74  // initializeConfigurationsTable ensures the requisite tables in place to form the backing store.
    75  //
    76  // Uses MEDIUMTEXT on MySQL, and TEXT on sane databases.
    77  func initializeConfigurationsTable(db *sqlx.DB) error {
    78  	mysqlCharset := ""
    79  	if db.DriverName() == "mysql" {
    80  		mysqlCharset = "DEFAULT CHARACTER SET utf8mb4"
    81  	}
    82  
    83  	_, err := db.Exec(`
    84  		CREATE TABLE IF NOT EXISTS Configurations (
    85  		    Id VARCHAR(26) PRIMARY KEY,
    86  		    Value TEXT NOT NULL,
    87  		    CreateAt BIGINT NOT NULL,
    88  		    Active BOOLEAN NULL UNIQUE
    89  		)
    90  	` + mysqlCharset)
    91  
    92  	if err != nil {
    93  		return errors.Wrap(err, "failed to create Configurations table")
    94  	}
    95  
    96  	_, err = db.Exec(`
    97  		CREATE TABLE IF NOT EXISTS ConfigurationFiles (
    98  		    Name VARCHAR(64) PRIMARY KEY,
    99  		    Data TEXT NOT NULL,
   100  		    CreateAt BIGINT NOT NULL,
   101  		    UpdateAt BIGINT NOT NULL
   102  		)
   103  	` + mysqlCharset)
   104  	if err != nil {
   105  		return errors.Wrap(err, "failed to create ConfigurationFiles table")
   106  	}
   107  
   108  	// Change from TEXT (65535 limit) to MEDIUM TEXT (16777215) on MySQL. This is a
   109  	// backwards-compatible migration for any existing schema.
   110  	// Also fix using the wrong encoding initially
   111  	if db.DriverName() == "mysql" {
   112  		_, err = db.Exec(`ALTER TABLE Configurations MODIFY Value MEDIUMTEXT`)
   113  		if err != nil {
   114  			return errors.Wrap(err, "failed to alter Configurations table")
   115  		}
   116  		_, err = db.Exec(`ALTER TABLE Configurations CONVERT TO CHARACTER SET utf8mb4`)
   117  		if err != nil {
   118  			return errors.Wrap(err, "failed to alter Configurations table character set")
   119  		}
   120  
   121  		_, err = db.Exec(`ALTER TABLE ConfigurationFiles MODIFY Data MEDIUMTEXT`)
   122  		if err != nil {
   123  			return errors.Wrap(err, "failed to alter ConfigurationFiles table")
   124  		}
   125  		_, err = db.Exec(`ALTER TABLE ConfigurationFiles CONVERT TO CHARACTER SET utf8mb4`)
   126  		if err != nil {
   127  			return errors.Wrap(err, "failed to alter ConfigurationFiles table character set")
   128  		}
   129  	}
   130  
   131  	return nil
   132  }
   133  
   134  // parseDSN splits up a connection string into a driver name and data source name.
   135  //
   136  // For example:
   137  //	mysql://mmuser:mostest@localhost:5432/mattermost_test
   138  // returns
   139  //	driverName = mysql
   140  //	dataSourceName = mmuser:mostest@localhost:5432/mattermost_test
   141  //
   142  // By contrast, a Postgres DSN is returned unmodified.
   143  func parseDSN(dsn string) (string, string, error) {
   144  	// Treat the DSN as the URL that it is.
   145  	s := strings.SplitN(dsn, "://", 2)
   146  	if len(s) != 2 {
   147  		return "", "", errors.New("failed to parse DSN as URL")
   148  	}
   149  
   150  	scheme := s[0]
   151  	switch scheme {
   152  	case "mysql":
   153  		// Strip off the mysql:// for the dsn with which to connect.
   154  		dsn = s[1]
   155  
   156  	case "postgres":
   157  		// No changes required
   158  
   159  	default:
   160  		return "", "", errors.Errorf("unsupported scheme %s", scheme)
   161  	}
   162  
   163  	return scheme, dsn, nil
   164  }
   165  
   166  // Set replaces the current configuration in its entirety and updates the backing store.
   167  func (ds *DatabaseStore) Set(newCfg *model.Config) error {
   168  	return ds.persist(newCfg)
   169  }
   170  
   171  // maxLength identifies the maximum length of a configuration or configuration file
   172  func (ds *DatabaseStore) checkLength(length int) error {
   173  	if ds.db.DriverName() == "mysql" && length > MaxWriteLength {
   174  		return errors.Errorf("value is too long: %d > %d bytes", length, MaxWriteLength)
   175  	}
   176  
   177  	return nil
   178  }
   179  
   180  // persist writes the configuration to the configured database.
   181  func (ds *DatabaseStore) persist(cfg *model.Config) error {
   182  	b, err := marshalConfig(cfg)
   183  	if err != nil {
   184  		return errors.Wrap(err, "failed to serialize")
   185  	}
   186  
   187  	id := model.NewId()
   188  	value := string(b)
   189  	createAt := model.GetMillis()
   190  
   191  	err = ds.checkLength(len(value))
   192  	if err != nil {
   193  		return errors.Wrap(err, "marshalled configuration failed length check")
   194  	}
   195  
   196  	tx, err := ds.db.Beginx()
   197  	if err != nil {
   198  		return errors.Wrap(err, "failed to begin transaction")
   199  	}
   200  	defer func() {
   201  		// Rollback after Commit just returns sql.ErrTxDone.
   202  		if err := tx.Rollback(); err != nil && err != sql.ErrTxDone {
   203  			mlog.Error("Failed to rollback configuration transaction", mlog.Err(err))
   204  		}
   205  	}()
   206  
   207  	params := map[string]interface{}{
   208  		"id":        id,
   209  		"value":     value,
   210  		"create_at": createAt,
   211  		"key":       "ConfigurationId",
   212  	}
   213  
   214  	// Skip the persist altogether if we're effectively writing the same configuration.
   215  	var oldValue []byte
   216  	row := ds.db.QueryRow("SELECT Value FROM Configurations WHERE Active")
   217  	if err := row.Scan(&oldValue); err != nil && err != sql.ErrNoRows {
   218  		return errors.Wrap(err, "failed to query active configuration")
   219  	}
   220  	if bytes.Equal(oldValue, b) {
   221  		return nil
   222  	}
   223  
   224  	if _, err := tx.Exec("UPDATE Configurations SET Active = NULL WHERE Active"); err != nil {
   225  		return errors.Wrap(err, "failed to deactivate current configuration")
   226  	}
   227  
   228  	if _, err := tx.NamedExec("INSERT INTO Configurations (Id, Value, CreateAt, Active) VALUES (:id, :value, :create_at, TRUE)", params); err != nil {
   229  		return errors.Wrap(err, "failed to record new configuration")
   230  	}
   231  
   232  	if err := tx.Commit(); err != nil {
   233  		return errors.Wrap(err, "failed to commit transaction")
   234  	}
   235  
   236  	return nil
   237  }
   238  
   239  // Load updates the current configuration from the backing store.
   240  func (ds *DatabaseStore) Load() ([]byte, error) {
   241  	var configurationData []byte
   242  
   243  	row := ds.db.QueryRow("SELECT Value FROM Configurations WHERE Active")
   244  	if err := row.Scan(&configurationData); err != nil && err != sql.ErrNoRows {
   245  		return nil, errors.Wrap(err, "failed to query active configuration")
   246  	}
   247  
   248  	// Initialize from the default config if no active configuration could be found.
   249  	if len(configurationData) == 0 {
   250  		configWithDB := model.Config{}
   251  		configWithDB.SqlSettings.DriverName = model.NewString(ds.driverName)
   252  		configWithDB.SqlSettings.DataSource = model.NewString(ds.dataSourceName)
   253  		return json.Marshal(configWithDB)
   254  	}
   255  
   256  	return configurationData, nil
   257  }
   258  
   259  // GetFile fetches the contents of a previously persisted configuration file.
   260  func (ds *DatabaseStore) GetFile(name string) ([]byte, error) {
   261  	query, args, err := sqlx.Named("SELECT Data FROM ConfigurationFiles WHERE Name = :name", map[string]interface{}{
   262  		"name": name,
   263  	})
   264  	if err != nil {
   265  		return nil, err
   266  	}
   267  
   268  	var data []byte
   269  	row := ds.db.QueryRowx(ds.db.Rebind(query), args...)
   270  	if err = row.Scan(&data); err != nil {
   271  		return nil, errors.Wrapf(err, "failed to scan data from row for %s", name)
   272  	}
   273  
   274  	return data, nil
   275  }
   276  
   277  // SetFile sets or replaces the contents of a configuration file.
   278  func (ds *DatabaseStore) SetFile(name string, data []byte) error {
   279  	err := ds.checkLength(len(data))
   280  	if err != nil {
   281  		return errors.Wrap(err, "file data failed length check")
   282  	}
   283  	params := map[string]interface{}{
   284  		"name":      name,
   285  		"data":      data,
   286  		"create_at": model.GetMillis(),
   287  		"update_at": model.GetMillis(),
   288  	}
   289  
   290  	result, err := ds.db.NamedExec("UPDATE ConfigurationFiles SET Data = :data, UpdateAt = :update_at WHERE Name = :name", params)
   291  	if err != nil {
   292  		return errors.Wrapf(err, "failed to update row for %s", name)
   293  	}
   294  
   295  	count, err := result.RowsAffected()
   296  	if err != nil {
   297  		return errors.Wrapf(err, "failed to count rows affected for %s", name)
   298  	} else if count > 0 {
   299  		return nil
   300  	}
   301  
   302  	_, err = ds.db.NamedExec("INSERT INTO ConfigurationFiles (Name, Data, CreateAt, UpdateAt) VALUES (:name, :data, :create_at, :update_at)", params)
   303  	if err != nil {
   304  		return errors.Wrapf(err, "failed to insert row for %s", name)
   305  	}
   306  
   307  	return nil
   308  }
   309  
   310  // HasFile returns true if the given file was previously persisted.
   311  func (ds *DatabaseStore) HasFile(name string) (bool, error) {
   312  	query, args, err := sqlx.Named("SELECT COUNT(*) FROM ConfigurationFiles WHERE Name = :name", map[string]interface{}{
   313  		"name": name,
   314  	})
   315  	if err != nil {
   316  		return false, err
   317  	}
   318  
   319  	var count int64
   320  	row := ds.db.QueryRowx(ds.db.Rebind(query), args...)
   321  	if err = row.Scan(&count); err != nil {
   322  		return false, errors.Wrapf(err, "failed to scan count of rows for %s", name)
   323  	}
   324  
   325  	return count != 0, nil
   326  }
   327  
   328  // RemoveFile remoevs a previously persisted configuration file.
   329  func (ds *DatabaseStore) RemoveFile(name string) error {
   330  	_, err := ds.db.NamedExec("DELETE FROM ConfigurationFiles WHERE Name = :name", map[string]interface{}{
   331  		"name": name,
   332  	})
   333  	if err != nil {
   334  		return errors.Wrapf(err, "failed to remove row for %s", name)
   335  	}
   336  
   337  	return nil
   338  }
   339  
   340  // String returns the path to the database backing the config, masking the password.
   341  func (ds *DatabaseStore) String() string {
   342  	return stripPassword(ds.originalDsn, ds.driverName)
   343  }
   344  
   345  // Close cleans up resources associated with the store.
   346  func (ds *DatabaseStore) Close() error {
   347  	return ds.db.Close()
   348  }
   349  
   350  // Watch nothing on memory store
   351  func (ds *DatabaseStore) Watch(_ func()) error {
   352  	return nil
   353  }