Initial QSfera import
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Sum returns the md5 checksum of the ChangeSet data.
|
||||
func (c *ChangeSet) Sum() string {
|
||||
h := md5.New()
|
||||
h.Write(c.Data)
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
# File Source
|
||||
|
||||
The file source reads config from a file.
|
||||
|
||||
It uses the File extension to determine the Format e.g `config.yaml` has the yaml format.
|
||||
It does not make use of encoders or interpet the file data. If a file extension is not present
|
||||
the source Format will default to the Encoder in options.
|
||||
|
||||
## Example
|
||||
|
||||
A config file format in json
|
||||
|
||||
```json
|
||||
{
|
||||
"hosts": {
|
||||
"database": {
|
||||
"address": "10.0.0.1",
|
||||
"port": 3306
|
||||
},
|
||||
"cache": {
|
||||
"address": "10.0.0.2",
|
||||
"port": 6379
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## New Source
|
||||
|
||||
Specify file source with path to file. Path is optional and will default to `config.json`
|
||||
|
||||
```go
|
||||
fileSource := file.NewSource(
|
||||
file.WithPath("/tmp/config.json"),
|
||||
)
|
||||
```
|
||||
|
||||
## File Format
|
||||
|
||||
To load different file formats e.g yaml, toml, xml simply specify them with their extension
|
||||
|
||||
```go
|
||||
fileSource := file.NewSource(
|
||||
file.WithPath("/tmp/config.yaml"),
|
||||
)
|
||||
```
|
||||
|
||||
If you want to specify a file without extension, ensure you set the encoder to the same format
|
||||
|
||||
```go
|
||||
e := toml.NewEncoder()
|
||||
|
||||
fileSource := file.NewSource(
|
||||
file.WithPath("/tmp/config"),
|
||||
source.WithEncoder(e),
|
||||
)
|
||||
```
|
||||
|
||||
## Load Source
|
||||
|
||||
Load the source into config
|
||||
|
||||
```go
|
||||
// Create new config
|
||||
conf := config.NewConfig()
|
||||
|
||||
// Load file source
|
||||
conf.Load(fileSource)
|
||||
```
|
||||
@@ -0,0 +1,87 @@
|
||||
// Package file is a file source. Expected format is json
|
||||
package file
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
"go-micro.dev/v4/config/source"
|
||||
)
|
||||
|
||||
type file struct {
|
||||
opts source.Options
|
||||
fs fs.FS
|
||||
path string
|
||||
}
|
||||
|
||||
var (
|
||||
DefaultPath = "config.json"
|
||||
)
|
||||
|
||||
func (f *file) Read() (*source.ChangeSet, error) {
|
||||
var fh fs.File
|
||||
var err error
|
||||
|
||||
if f.fs != nil {
|
||||
fh, err = f.fs.Open(f.path)
|
||||
} else {
|
||||
fh, err = os.Open(f.path)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fh.Close()
|
||||
b, err := io.ReadAll(fh)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, err := fh.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cs := &source.ChangeSet{
|
||||
Format: format(f.path, f.opts.Encoder),
|
||||
Source: f.String(),
|
||||
Timestamp: info.ModTime(),
|
||||
Data: b,
|
||||
}
|
||||
cs.Checksum = cs.Sum()
|
||||
|
||||
return cs, nil
|
||||
}
|
||||
|
||||
func (f *file) String() string {
|
||||
return "file"
|
||||
}
|
||||
|
||||
func (f *file) Watch() (source.Watcher, error) {
|
||||
// do not watch if fs.FS instance is provided
|
||||
if f.fs != nil {
|
||||
return source.NewNoopWatcher()
|
||||
}
|
||||
|
||||
if _, err := os.Stat(f.path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newWatcher(f)
|
||||
}
|
||||
|
||||
func (f *file) Write(cs *source.ChangeSet) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewSource(opts ...source.Option) source.Source {
|
||||
options := source.NewOptions(opts...)
|
||||
|
||||
fs, _ := options.Context.Value(fsKey{}).(fs.FS)
|
||||
|
||||
path := DefaultPath
|
||||
f, ok := options.Context.Value(filePathKey{}).(string)
|
||||
if ok {
|
||||
path = f
|
||||
}
|
||||
return &file{opts: options, fs: fs, path: path}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"go-micro.dev/v4/config/encoder"
|
||||
)
|
||||
|
||||
func format(p string, e encoder.Encoder) string {
|
||||
parts := strings.Split(p, ".")
|
||||
if len(parts) > 1 {
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
return e.String()
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
|
||||
"go-micro.dev/v4/config/source"
|
||||
)
|
||||
|
||||
type filePathKey struct{}
|
||||
type fsKey struct{}
|
||||
|
||||
// WithPath sets the path to file.
|
||||
func WithPath(p string) source.Option {
|
||||
return func(o *source.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, filePathKey{}, p)
|
||||
}
|
||||
}
|
||||
|
||||
// WithFS sets the underlying filesystem to lookup file from (default os.FS).
|
||||
func WithFS(fs fs.FS) source.Option {
|
||||
return func(o *source.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, fsKey{}, fs)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"go-micro.dev/v4/config/source"
|
||||
)
|
||||
|
||||
type watcher struct {
|
||||
f *file
|
||||
|
||||
fw *fsnotify.Watcher
|
||||
}
|
||||
|
||||
func newWatcher(f *file) (source.Watcher, error) {
|
||||
fw, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fw.Add(f.path)
|
||||
|
||||
return &watcher{
|
||||
f: f,
|
||||
fw: fw,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *watcher) Next() (*source.ChangeSet, error) {
|
||||
// try get the event
|
||||
select {
|
||||
case event, ok := <-w.fw.Events:
|
||||
// check if channel was closed (i.e. Watcher.Close() was called).
|
||||
if !ok {
|
||||
return nil, source.ErrWatcherStopped
|
||||
}
|
||||
|
||||
if event.Has(fsnotify.Rename) {
|
||||
// check existence of file, and add watch again
|
||||
_, err := os.Stat(event.Name)
|
||||
if err == nil || os.IsExist(err) {
|
||||
w.fw.Add(event.Name)
|
||||
}
|
||||
}
|
||||
|
||||
c, err := w.f.Read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
case err, ok := <-w.fw.Errors:
|
||||
// check if channel was closed (i.e. Watcher.Close() was called).
|
||||
if !ok {
|
||||
return nil, source.ErrWatcherStopped
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (w *watcher) Stop() error {
|
||||
return w.fw.Close()
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"go-micro.dev/v4/config/source"
|
||||
)
|
||||
|
||||
type watcher struct {
|
||||
f *file
|
||||
|
||||
fw *fsnotify.Watcher
|
||||
}
|
||||
|
||||
func newWatcher(f *file) (source.Watcher, error) {
|
||||
fw, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fw.Add(f.path)
|
||||
|
||||
return &watcher{
|
||||
f: f,
|
||||
fw: fw,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *watcher) Next() (*source.ChangeSet, error) {
|
||||
// try get the event
|
||||
select {
|
||||
case event, ok := <-w.fw.Events:
|
||||
// check if channel was closed (i.e. Watcher.Close() was called).
|
||||
if !ok {
|
||||
return nil, source.ErrWatcherStopped
|
||||
}
|
||||
|
||||
if event.Has(fsnotify.Rename) {
|
||||
// check existence of file, and add watch again
|
||||
_, err := os.Stat(event.Name)
|
||||
if err == nil || os.IsExist(err) {
|
||||
w.fw.Add(event.Name)
|
||||
}
|
||||
}
|
||||
|
||||
c, err := w.f.Read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// add path again for the event bug of fsnotify
|
||||
w.fw.Add(w.f.path)
|
||||
|
||||
return c, nil
|
||||
case err, ok := <-w.fw.Errors:
|
||||
// check if channel was closed (i.e. Watcher.Close() was called).
|
||||
if !ok {
|
||||
return nil, source.ErrWatcherStopped
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (w *watcher) Stop() error {
|
||||
return w.fw.Close()
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
type noopWatcher struct {
|
||||
exit chan struct{}
|
||||
}
|
||||
|
||||
func (w *noopWatcher) Next() (*ChangeSet, error) {
|
||||
<-w.exit
|
||||
|
||||
return nil, errors.New("noopWatcher stopped")
|
||||
}
|
||||
|
||||
func (w *noopWatcher) Stop() error {
|
||||
close(w.exit)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewNoopWatcher returns a watcher that blocks on Next() until Stop() is called.
|
||||
func NewNoopWatcher() (Watcher, error) {
|
||||
return &noopWatcher{exit: make(chan struct{})}, nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v4/client"
|
||||
"go-micro.dev/v4/config/encoder"
|
||||
"go-micro.dev/v4/config/encoder/json"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
// Encoder
|
||||
Encoder encoder.Encoder
|
||||
|
||||
// for alternative data
|
||||
Context context.Context
|
||||
|
||||
// Client to use for RPC
|
||||
Client client.Client
|
||||
}
|
||||
|
||||
type Option func(o *Options)
|
||||
|
||||
func NewOptions(opts ...Option) Options {
|
||||
options := Options{
|
||||
Encoder: json.NewEncoder(),
|
||||
Context: context.Background(),
|
||||
Client: client.DefaultClient,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// WithEncoder sets the source encoder.
|
||||
func WithEncoder(e encoder.Encoder) Option {
|
||||
return func(o *Options) {
|
||||
o.Encoder = e
|
||||
}
|
||||
}
|
||||
|
||||
// WithClient sets the source client.
|
||||
func WithClient(c client.Client) Option {
|
||||
return func(o *Options) {
|
||||
o.Client = c
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Package source is the interface for sources
|
||||
package source
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrWatcherStopped is returned when source watcher has been stopped.
|
||||
ErrWatcherStopped = errors.New("watcher stopped")
|
||||
)
|
||||
|
||||
// Source is the source from which config is loaded.
|
||||
type Source interface {
|
||||
Read() (*ChangeSet, error)
|
||||
Write(*ChangeSet) error
|
||||
Watch() (Watcher, error)
|
||||
String() string
|
||||
}
|
||||
|
||||
// ChangeSet represents a set of changes from a source.
|
||||
type ChangeSet struct {
|
||||
Timestamp time.Time
|
||||
Checksum string
|
||||
Format string
|
||||
Source string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// Watcher watches a source for changes.
|
||||
type Watcher interface {
|
||||
Next() (*ChangeSet, error)
|
||||
Stop() error
|
||||
}
|
||||
Reference in New Issue
Block a user