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