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 }