github.com/crspeller/mattermost-server@v0.0.0-20190328001957-a200beb3d111/config/file.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  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  	"os"
    12  	"path/filepath"
    13  
    14  	"github.com/pkg/errors"
    15  
    16  	"github.com/crspeller/mattermost-server/mlog"
    17  	"github.com/crspeller/mattermost-server/model"
    18  	"github.com/crspeller/mattermost-server/utils/fileutils"
    19  )
    20  
    21  var (
    22  	ErrReadOnlyConfiguration = errors.New("configuration is read-only")
    23  )
    24  
    25  // FileStore is a config store backed by a file such as config/config.json.
    26  //
    27  // It also uses the folder containing the configuration file for storing other configuration files.
    28  type FileStore struct {
    29  	commonStore
    30  
    31  	path    string
    32  	watch   bool
    33  	watcher *watcher
    34  }
    35  
    36  // NewFileStore creates a new instance of a config store backed by the given file path.
    37  //
    38  // If watch is true, any external changes to the file will force a reload.
    39  func NewFileStore(path string, watch bool) (fs *FileStore, err error) {
    40  	resolvedPath, err := resolveConfigFilePath(path)
    41  	if err != nil {
    42  		return nil, err
    43  	}
    44  
    45  	fs = &FileStore{
    46  		path:  resolvedPath,
    47  		watch: watch,
    48  	}
    49  	if err = fs.Load(); err != nil {
    50  		return nil, errors.Wrap(err, "failed to load")
    51  	}
    52  
    53  	if fs.watch {
    54  		if err = fs.startWatcher(); err != nil {
    55  			mlog.Error("failed to start config watcher", mlog.String("path", path), mlog.Err(err))
    56  		}
    57  	}
    58  
    59  	return fs, nil
    60  }
    61  
    62  // resolveConfigFilePath attempts to resolve the given configuration file path to an absolute path.
    63  //
    64  // Consideration is given to maintaining backwards compatibility when resolving the path to the
    65  // configuration file.
    66  func resolveConfigFilePath(path string) (string, error) {
    67  	// Absolute paths are explicit and require no resolution.
    68  	if filepath.IsAbs(path) {
    69  		return path, nil
    70  	}
    71  
    72  	// Search for the relative path to the file in the config folder, taking into account
    73  	// various common starting points.
    74  	if configFile := fileutils.FindFile(filepath.Join("config", path)); configFile != "" {
    75  		return configFile, nil
    76  	}
    77  
    78  	// Search for the relative path in the current working directory, also taking into account
    79  	// various common starting points.
    80  	if configFile := fileutils.FindPath(path, []string{"."}, nil); configFile != "" {
    81  		return configFile, nil
    82  	}
    83  
    84  	// Otherwise, search for the config/ folder using the same heuristics as above, and build
    85  	// an absolute path anchored there and joining the given input path (or plain filename).
    86  	if configFolder, found := fileutils.FindDir("config"); found {
    87  		return filepath.Join(configFolder, path), nil
    88  	}
    89  
    90  	// Fail altogether if we can't even find the config/ folder. This should only happen if
    91  	// the executable is relocated away from the supporting files.
    92  	return "", fmt.Errorf("failed to find config file %s", path)
    93  }
    94  
    95  // Set replaces the current configuration in its entirety and updates the backing store.
    96  func (fs *FileStore) Set(newCfg *model.Config) (*model.Config, error) {
    97  	return fs.commonStore.set(newCfg, func(cfg *model.Config) error {
    98  		if *fs.config.ClusterSettings.Enable && *fs.config.ClusterSettings.ReadOnlyConfig {
    99  			return ErrReadOnlyConfiguration
   100  		}
   101  
   102  		return fs.commonStore.validate(cfg)
   103  	}, fs.persist)
   104  }
   105  
   106  // persist writes the configuration to the configured file.
   107  func (fs *FileStore) persist(cfg *model.Config) error {
   108  	fs.stopWatcher()
   109  
   110  	b, err := marshalConfig(cfg)
   111  	if err != nil {
   112  		return errors.Wrap(err, "failed to serialize")
   113  	}
   114  
   115  	err = ioutil.WriteFile(fs.path, b, 0644)
   116  	if err != nil {
   117  		return errors.Wrap(err, "failed to write file")
   118  	}
   119  
   120  	if fs.watch {
   121  		if err = fs.startWatcher(); err != nil {
   122  			mlog.Error("failed to start config watcher", mlog.String("path", fs.path), mlog.Err(err))
   123  		}
   124  	}
   125  
   126  	return nil
   127  }
   128  
   129  // Load updates the current configuration from the backing store.
   130  func (fs *FileStore) Load() (err error) {
   131  	var needsSave bool
   132  	var f io.ReadCloser
   133  
   134  	f, err = os.Open(fs.path)
   135  	if os.IsNotExist(err) {
   136  		needsSave = true
   137  		defaultCfg := &model.Config{}
   138  		defaultCfg.SetDefaults()
   139  
   140  		var defaultCfgBytes []byte
   141  		defaultCfgBytes, err = marshalConfig(defaultCfg)
   142  		if err != nil {
   143  			return errors.Wrap(err, "failed to serialize default config")
   144  		}
   145  
   146  		f = ioutil.NopCloser(bytes.NewReader(defaultCfgBytes))
   147  
   148  	} else if err != nil {
   149  		return errors.Wrapf(err, "failed to open %s for reading", fs.path)
   150  	}
   151  	defer func() {
   152  		closeErr := f.Close()
   153  		if err == nil && closeErr != nil {
   154  			err = errors.Wrap(closeErr, "failed to close")
   155  		}
   156  	}()
   157  
   158  	return fs.commonStore.load(f, needsSave, fs.commonStore.validate, fs.persist)
   159  }
   160  
   161  // GetFile fetches the contents of a previously persisted configuration file.
   162  func (fs *FileStore) GetFile(name string) ([]byte, error) {
   163  	resolvedPath := filepath.Join(filepath.Dir(fs.path), name)
   164  
   165  	data, err := ioutil.ReadFile(resolvedPath)
   166  	if err != nil {
   167  		return nil, errors.Wrapf(err, "failed to read file from %s", resolvedPath)
   168  	}
   169  
   170  	return data, nil
   171  }
   172  
   173  // SetFile sets or replaces the contents of a configuration file.
   174  func (fs *FileStore) SetFile(name string, data []byte) error {
   175  	resolvedPath := filepath.Join(filepath.Dir(fs.path), name)
   176  
   177  	err := ioutil.WriteFile(resolvedPath, data, 0777)
   178  	if err != nil {
   179  		return errors.Wrapf(err, "failed to write file to %s", resolvedPath)
   180  	}
   181  
   182  	return nil
   183  }
   184  
   185  // HasFile returns true if the given file was previously persisted.
   186  func (fs *FileStore) HasFile(name string) (bool, error) {
   187  	resolvedPath := filepath.Join(filepath.Dir(fs.path), name)
   188  
   189  	_, err := os.Stat(resolvedPath)
   190  	if err != nil && os.IsNotExist(err) {
   191  		return false, nil
   192  	} else if err != nil {
   193  		return false, errors.Wrap(err, "failed to check if file exists")
   194  	}
   195  
   196  	return true, nil
   197  }
   198  
   199  // RemoveFile removes a previously persisted configuration file.
   200  func (fs *FileStore) RemoveFile(name string) error {
   201  	resolvedPath := filepath.Join(filepath.Dir(fs.path), name)
   202  
   203  	err := os.Remove(resolvedPath)
   204  	if os.IsNotExist(err) {
   205  		return nil
   206  	}
   207  	if err != nil {
   208  		return errors.Wrap(err, "failed to remove file")
   209  	}
   210  
   211  	return err
   212  }
   213  
   214  // startWatcher starts a watcher to monitor for external config file changes.
   215  func (fs *FileStore) startWatcher() error {
   216  	if fs.watcher != nil {
   217  		return nil
   218  	}
   219  
   220  	watcher, err := newWatcher(fs.path, func() {
   221  		if err := fs.Load(); err != nil {
   222  			mlog.Error("failed to reload file on change", mlog.String("path", fs.path), mlog.Err(err))
   223  		}
   224  	})
   225  	if err != nil {
   226  		return err
   227  	}
   228  
   229  	fs.watcher = watcher
   230  
   231  	return nil
   232  }
   233  
   234  // stopWatcher stops any previously started watcher.
   235  func (fs *FileStore) stopWatcher() {
   236  	if fs.watcher == nil {
   237  		return
   238  	}
   239  
   240  	if err := fs.watcher.Close(); err != nil {
   241  		mlog.Error("failed to close watcher", mlog.Err(err))
   242  	}
   243  	fs.watcher = nil
   244  }
   245  
   246  // String returns the path to the file backing the config.
   247  func (fs *FileStore) String() string {
   248  	return "file://" + fs.path
   249  }
   250  
   251  // Close cleans up resources associated with the store.
   252  func (fs *FileStore) Close() error {
   253  	fs.configLock.Lock()
   254  	defer fs.configLock.Unlock()
   255  
   256  	fs.stopWatcher()
   257  
   258  	return nil
   259  }