Skip to content

Commit 9f4309c

Browse files
authored
Support aws s3 (#15)
1 parent 57068cc commit 9f4309c

File tree

7 files changed

+225
-40
lines changed

7 files changed

+225
-40
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# v1.4.0 (Jan 16, 2023)
2+
1. Support AWS S3.
3+
14
# v1.3.2 (Jan 11, 2023)
25

36
1. Fix read non-existed directory in watching mode.

README.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,18 @@ configuration files.
1919
var config = xyconfig.GetConfig("app")
2020

2121
// Read config from a string.
22-
config.Read(xyconfig.JSON, `{"general": {"timeout": 3.14}}`)
22+
config.ReadBytes(xyconfig.JSON, []byte(`{"general": {"timeout": 3.14}}`))
2323

24-
// Read config from default.ini but do not watch the file.
25-
config.ReadFile("config/default.ini", false)
24+
// Read from files.
25+
config.Read("config/default.ini")
26+
config.Read("config/override.yml")
27+
config.Read(".env")
2628

27-
// Read config from override.ini and watch the file change.
28-
config.ReadFile("config/override.yml", true)
29+
// Load global environment variables to config files.
30+
config.Read("env")
31+
32+
// Read config from aws s3 bucket.
33+
config.Read("s3://bucket/item.ini")
2934

3035
fmt.Println(config.MustGet("general.timeout").MustFloat())
3136

config.go

Lines changed: 122 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ import (
2929
"strings"
3030
"time"
3131

32+
"github.com/aws/aws-sdk-go/aws"
33+
"github.com/aws/aws-sdk-go/aws/session"
34+
"github.com/aws/aws-sdk-go/service/s3"
35+
"github.com/aws/aws-sdk-go/service/s3/s3manager"
3236
"github.com/fsnotify/fsnotify"
3337
"github.com/go-ini/ini"
3438
"github.com/joho/godotenv"
@@ -87,8 +91,12 @@ type Config struct {
8791
// watcher tracks changes of files.
8892
watcher *fsnotify.Watcher
8993

90-
// envWatcher tracks the waching of environment variables.
91-
envWatcher *time.Timer
94+
// timerWatchers tracks the waching of non-inotify instances.
95+
timerWatchers map[string]*time.Timer
96+
97+
// watchInterval is used to choose the time interval to watch changes when
98+
// using Read method.
99+
watchInterval time.Duration
92100

93101
// lock avoids race condition.
94102
lock *xylock.RWLock
@@ -124,9 +132,11 @@ func GetConfig(name string) *Config {
124132
}
125133

126134
var cfg = &Config{
127-
config: make(map[string]Value),
128-
hook: make(map[string]func(Event)),
129-
lock: &xylock.RWLock{},
135+
config: make(map[string]Value),
136+
hook: make(map[string]func(Event)),
137+
timerWatchers: make(map[string]*time.Timer),
138+
watchInterval: 5 * time.Minute,
139+
lock: &xylock.RWLock{},
130140
}
131141

132142
if name == "" {
@@ -151,22 +161,41 @@ func (c *Config) CloseWatcher() error {
151161
c.watcher = nil
152162
}
153163

154-
if c.envWatcher != nil {
155-
c.envWatcher.Stop()
156-
c.envWatcher = nil
164+
for k, w := range c.timerWatchers {
165+
w.Stop()
166+
delete(c.timerWatchers, k)
157167
}
158168

159169
return err
160170
}
161171

162-
// UnWatch removes a filename from the watcher.
172+
// SetWatchInterval sets the time interval to watch the change when using Read()
173+
// method.
174+
func (c *Config) SetWatchInterval(d time.Duration) {
175+
c.lock.Lock()
176+
defer c.lock.Unlock()
177+
c.watchInterval = d
178+
}
179+
180+
// UnWatch removes a filename from the watcher. This method also works with s3
181+
// url. Put "env" as parameter if you want to stop watching environment
182+
// variables of LoadEnv().
163183
func (c *Config) UnWatch(filename string) error {
164184
c.lock.Lock()
165185
defer c.lock.Unlock()
166186

187+
if w, ok := c.timerWatchers[filename]; ok {
188+
w.Stop()
189+
delete(c.timerWatchers, filename)
190+
return nil
191+
}
192+
167193
if c.watcher != nil {
168-
return c.watcher.Remove(filename)
194+
if err := c.watcher.Remove(filename); err != nil {
195+
return ConfigError.New(err)
196+
}
169197
}
198+
170199
return nil
171200
}
172201

@@ -329,11 +358,6 @@ func (c *Config) ReadBytes(format Format, b []byte) error {
329358
}
330359
}
331360

332-
// Read reads the config values from a string under any format.
333-
func (c *Config) Read(format Format, s string) error {
334-
return c.ReadBytes(format, []byte(s))
335-
}
336-
337361
// ReadFile reads the config values from a file. If watch is true, it will
338362
// reload config when the file is changed.
339363
func (c *Config) ReadFile(filename string, watch bool) error {
@@ -344,16 +368,16 @@ func (c *Config) ReadFile(filename string, watch bool) error {
344368
}
345369
}
346370

371+
if fileFormat == UnknownFormat {
372+
return FormatError.Newf("unknown extension: %s", filename)
373+
}
374+
347375
if watch {
348376
if err := c.watchFile(filename); err != nil {
349377
return err
350378
}
351379
}
352380

353-
if fileFormat == UnknownFormat {
354-
return ExtensionError.Newf("unknown extension: %s", filename)
355-
}
356-
357381
if data, err := ioutil.ReadFile(filename); err != nil {
358382
if !os.IsNotExist(err) || !watch {
359383
return ConfigError.New(err)
@@ -365,6 +389,66 @@ func (c *Config) ReadFile(filename string, watch bool) error {
365389
return nil
366390
}
367391

392+
// ReadS3 reads a file from AWS S3 bucket and watch for their changes every
393+
// duration. Set the duration as zero if no need to watch the change.
394+
//
395+
// You must provide the aws credentials in ~/.aws/credentials. The AWS_REGION
396+
// is required.
397+
func (c *Config) ReadS3(url string, d time.Duration) error {
398+
var fileFormat = UnknownFormat
399+
for ext, format := range extensions {
400+
if strings.HasSuffix(url, ext) {
401+
fileFormat = format
402+
}
403+
}
404+
405+
if fileFormat == UnknownFormat {
406+
return FormatError.Newf("unknown extension: %s", url)
407+
}
408+
409+
if !strings.HasPrefix(url, "s3://") {
410+
return FormatError.Newf("can not parse the s3 url %s", url)
411+
}
412+
413+
var path = url[5:]
414+
var bucket, item, found = strings.Cut(path, "/")
415+
if !found {
416+
return FormatError.Newf("not found item in path %s", path)
417+
}
418+
419+
var sess, err = session.NewSessionWithOptions(session.Options{
420+
SharedConfigState: session.SharedConfigEnable,
421+
})
422+
423+
if err != nil {
424+
return ConfigError.New(err)
425+
}
426+
427+
var downloader = s3manager.NewDownloader(sess)
428+
var buf = aws.NewWriteAtBuffer([]byte{})
429+
_, err = downloader.Download(
430+
buf,
431+
&s3.GetObjectInput{
432+
Bucket: aws.String(bucket),
433+
Key: aws.String(item),
434+
})
435+
436+
if d != 0 {
437+
c.lock.Lock()
438+
c.timerWatchers[url] = time.AfterFunc(d, func() { c.ReadS3(url, d) })
439+
c.lock.Unlock()
440+
}
441+
442+
if err != nil {
443+
if d == 0 {
444+
return ConfigError.New(err)
445+
}
446+
return nil
447+
}
448+
449+
return c.ReadBytes(fileFormat, buf.Bytes())
450+
}
451+
368452
// LoadEnv loads all environment variables and watch for their changes every
369453
// duration. Set the duration as zero if no need to watch the change.
370454
func (c *Config) LoadEnv(d time.Duration) error {
@@ -379,13 +463,31 @@ func (c *Config) LoadEnv(d time.Duration) error {
379463

380464
if d != 0 {
381465
c.lock.Lock()
382-
c.envWatcher = time.AfterFunc(d, func() { c.LoadEnv(d) })
466+
c.timerWatchers["env"] = time.AfterFunc(d, func() { c.LoadEnv(d) })
383467
c.lock.Unlock()
384468
}
385469

386470
return nil
387471
}
388472

473+
// Read reads the config with any instance. If the instance is s3 url or
474+
// environment variable, the watchInterval is used to choose the time interval
475+
// for watching changes. If the instance is file path, it will watch the change
476+
// if watchInterval > 0.
477+
func (c *Config) Read(path string) error {
478+
switch {
479+
case path == "env":
480+
return c.LoadEnv(c.watchInterval)
481+
case strings.HasPrefix(path, "s3://"):
482+
return c.ReadS3(path, c.watchInterval)
483+
default:
484+
if c.watchInterval > 0 {
485+
return c.ReadFile(path, true)
486+
}
487+
return c.ReadFile(path, false)
488+
}
489+
}
490+
389491
// Get returns the value assigned with the key. The latter returned value is
390492
// false if they key doesn't exist.
391493
func (c *Config) Get(key string) (Value, bool) {

config_test.go

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -167,18 +167,11 @@ func TestConfigReadByteUnknown(t *testing.T) {
167167
xycond.ExpectError(err, xyconfig.FormatError).Test(t)
168168
}
169169

170-
func TestConfigRead(t *testing.T) {
171-
var cfg = xyconfig.GetConfig(t.Name())
172-
var err = cfg.Read(xyconfig.UnknownFormat, "")
173-
174-
xycond.ExpectError(err, xyconfig.FormatError).Test(t)
175-
}
176-
177170
func TestConfigReadFileUnknownExt(t *testing.T) {
178171
var cfg = xyconfig.GetConfig(t.Name())
179172
var err = cfg.ReadFile("foo.bar", false)
180173

181-
xycond.ExpectError(err, xyconfig.ExtensionError).Test(t)
174+
xycond.ExpectError(err, xyconfig.FormatError).Test(t)
182175
}
183176

184177
func TestConfigReadFileNotExist(t *testing.T) {
@@ -215,6 +208,36 @@ func TestConfigReadFileWithChange(t *testing.T) {
215208
xycond.ExpectEqual(cfg.MustGet("foo").MustString(), "buzz").Test(t)
216209
}
217210

211+
func TestConfigReadS3UnknownExt(t *testing.T) {
212+
var cfg = xyconfig.GetConfig(t.Name())
213+
var err = cfg.ReadS3("s3://bucket/abc.unk", 0)
214+
xycond.ExpectError(err, xyconfig.FormatError).Test(t)
215+
}
216+
217+
func TestConfigReadS3InvalidPrefix(t *testing.T) {
218+
var cfg = xyconfig.GetConfig(t.Name())
219+
var err = cfg.ReadS3("s3:/bucket/abc.ini", 0)
220+
xycond.ExpectError(err, xyconfig.FormatError).Test(t)
221+
}
222+
223+
func TestConfigReadS3InvalidFormat(t *testing.T) {
224+
var cfg = xyconfig.GetConfig(t.Name())
225+
var err = cfg.ReadS3("s3://abc.ini", 0)
226+
xycond.ExpectError(err, xyconfig.FormatError).Test(t)
227+
}
228+
229+
func TestConfigReadInvalidAndWatch(t *testing.T) {
230+
var cfg = xyconfig.GetConfig(t.Name())
231+
var err = cfg.ReadS3("s3://bucket/abc.ini", time.Second)
232+
xycond.ExpectNil(err).Test(t)
233+
}
234+
235+
func TestConfigReadInvalidAndNotWatch(t *testing.T) {
236+
var cfg = xyconfig.GetConfig(t.Name())
237+
var err = cfg.ReadS3("s3://bucket/abc.ini", 0)
238+
xycond.ExpectError(err, xyconfig.ConfigError).Test(t)
239+
}
240+
218241
func TestConfigLoadEnvWithChange(t *testing.T) {
219242
os.Setenv("foo", "bar")
220243

@@ -275,3 +298,18 @@ func TestConfigToMap(t *testing.T) {
275298
xycond.ExpectEqual(cfg.ToMap()["foo"], "bar").Test(t)
276299
xycond.ExpectIn("buzz", cfg.ToMap()["subcfg"]).Test(t)
277300
}
301+
302+
func TestConfigUnWatch(t *testing.T) {
303+
ioutil.WriteFile(t.Name()+".json", []byte(`{"error":""}`), 0644)
304+
var cfg = xyconfig.GetConfig(t.Name())
305+
306+
cfg.SetWatchInterval(time.Second)
307+
308+
xycond.ExpectNil(cfg.Read("env")).Test(t)
309+
xycond.ExpectNil(cfg.Read(t.Name() + ".json")).Test(t)
310+
xycond.ExpectError(cfg.Read(""), xyconfig.FormatError).Test(t)
311+
312+
xycond.ExpectNil(cfg.UnWatch("env")).Test(t)
313+
xycond.ExpectNil(cfg.UnWatch(t.Name() + ".json")).Test(t)
314+
xycond.ExpectError(cfg.UnWatch("foo.json"), xyconfig.ConfigError).Test(t)
315+
}

error.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,11 @@ package xyconfig
2323
import "github.com/xybor-x/xyerror"
2424

2525
// ConfigError represents for all error in xyconfig.
26-
var ConfigError = xyerror.NewException("BaseError")
26+
var ConfigError = xyerror.NewException("ConfigError")
2727

2828
// CastError happens when the value cannot cast to a specific type.
2929
var CastError = ConfigError.NewException("CastError")
3030

31-
// ExtensionError represents for file extension error.
32-
var ExtensionError = ConfigError.NewException("ExtensionError")
33-
3431
// FormatError represents for file format error.
3532
var FormatError = ConfigError.NewException("FormatError")
3633

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@ module github.com/xybor-x/xyconfig
33
go 1.18
44

55
require (
6+
github.com/aws/aws-sdk-go v1.44.180
67
github.com/fsnotify/fsnotify v1.6.0
78
github.com/go-ini/ini v1.67.0
89
github.com/joho/godotenv v1.4.0
910
github.com/xybor-x/xycond v1.0.0
1011
github.com/xybor-x/xyerror v1.0.5
1112
github.com/xybor-x/xylock v0.0.1
12-
github.com/xybor-x/xylog v0.3.0
13+
github.com/xybor-x/xylog v0.5.0
1314
)
1415

1516
require (
17+
github.com/jmespath/go-jmespath v0.4.0 // indirect
1618
github.com/stretchr/testify v1.8.1 // indirect
1719
golang.org/x/sync v0.1.0 // indirect
1820
golang.org/x/sys v0.3.0 // indirect

0 commit comments

Comments
 (0)