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