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