WIP: Login "works"
This commit is contained in:
parent
17149f2c58
commit
cb4ccc37b2
10 changed files with 367 additions and 58 deletions
|
@ -25,6 +25,6 @@ func main() {
|
|||
|
||||
err = rootCmd.Execute()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
|
8
go.mod
8
go.mod
|
@ -17,8 +17,8 @@ require (
|
|||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/crypto v0.1.0 // indirect
|
||||
golang.org/x/net v0.1.0 // indirect
|
||||
golang.org/x/sys v0.1.0 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
)
|
||||
|
|
8
go.sum
8
go.sum
|
@ -30,14 +30,22 @@ github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52
|
|||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
|
|
|
@ -1,26 +1,57 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"dynatron.me/x/blasphem/pkg/frontend"
|
||||
"dynatron.me/x/blasphem/pkg/storage"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidAuth = errors.New("invalid auth")
|
||||
ErrInvalidHandler = errors.New("no such handler")
|
||||
)
|
||||
|
||||
type Authenticator struct {
|
||||
Flows FlowStore
|
||||
Flows FlowStore
|
||||
Providers map[string]AuthProvider
|
||||
}
|
||||
|
||||
func (a *Authenticator) InitAuth() {
|
||||
func (a *Authenticator) Provider(name string) AuthProvider {
|
||||
p, ok := a.Providers[name]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (a *Authenticator) InitAuth(s storage.Store) error {
|
||||
a.Flows = make(FlowStore)
|
||||
hap, err := NewHAProvider(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// XXX: yuck
|
||||
a.Providers = map[string]AuthProvider{
|
||||
hap.ProviderType(): hap,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type AuthProvider interface {
|
||||
ProviderName() string
|
||||
ProviderID() *string
|
||||
ProviderType() string
|
||||
ProviderBase() AuthProviderBase
|
||||
FlowSchema() []FlowSchemaItem
|
||||
ValidateCreds(reqMap map[string]interface{}) bool
|
||||
}
|
||||
|
||||
type AuthProviderBase struct {
|
||||
|
@ -29,29 +60,17 @@ type AuthProviderBase struct {
|
|||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func (bp *AuthProviderBase) ProviderName() string { return bp.Name }
|
||||
func (bp *AuthProviderBase) ProviderID() *string { return bp.ID }
|
||||
func (bp *AuthProviderBase) ProviderType() string { return bp.Type }
|
||||
|
||||
type LocalProvider struct {
|
||||
AuthProviderBase
|
||||
}
|
||||
func (bp *AuthProviderBase) ProviderName() string { return bp.Name }
|
||||
func (bp *AuthProviderBase) ProviderID() *string { return bp.ID }
|
||||
func (bp *AuthProviderBase) ProviderType() string { return bp.Type }
|
||||
func (bp *AuthProviderBase) ProviderBase() AuthProviderBase { return *bp }
|
||||
|
||||
var HomeAssistant = "homeassistant"
|
||||
|
||||
func hassProvider() *LocalProvider {
|
||||
return &LocalProvider{
|
||||
AuthProviderBase: AuthProviderBase{
|
||||
Name: "Home Assistant Local",
|
||||
Type: HomeAssistant,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: make this configurable
|
||||
func (s *Authenticator) ProvidersHandler(c echo.Context) error {
|
||||
providers := []AuthProvider{
|
||||
hassProvider(),
|
||||
func (a *Authenticator) ProvidersHandler(c echo.Context) error {
|
||||
providers := []AuthProviderBase{
|
||||
a.Provider(HomeAssistant).ProviderBase(),
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, providers)
|
||||
|
@ -71,3 +90,29 @@ func (s *Authenticator) AuthorizeHandler(c echo.Context) error {
|
|||
|
||||
return c.HTML(http.StatusOK, string(b))
|
||||
}
|
||||
|
||||
func (a *Authenticator) Check(f *Flow, rm map[string]interface{}) error {
|
||||
cID, hasCID := rm["client_id"]
|
||||
if !hasCID || cID != f.request.ClientID {
|
||||
return ErrInvalidAuth
|
||||
}
|
||||
|
||||
for _, h := range f.Handler {
|
||||
if h == nil {
|
||||
return ErrInvalidHandler
|
||||
}
|
||||
|
||||
p := a.Provider(*h)
|
||||
if p == nil {
|
||||
return ErrInvalidAuth
|
||||
}
|
||||
|
||||
success := p.ValidateCreds(rm)
|
||||
|
||||
if success {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return ErrInvalidAuth
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ type Flow struct {
|
|||
Handler []*string `json:"handler"`
|
||||
StepID Step `json:"step_id"`
|
||||
Schema []FlowSchemaItem `json:"data_schema"`
|
||||
Errors []string `json:"errors"`
|
||||
Errors interface{} `json:"errors"`
|
||||
DescPlace *string `json:"description_placeholders"`
|
||||
LastStep *string `json:"last_step"`
|
||||
|
||||
|
@ -89,22 +89,28 @@ func (fs FlowStore) Get(id FlowID) *Flow {
|
|||
}
|
||||
|
||||
func (a *Authenticator) NewFlow(r *FlowRequest) *Flow {
|
||||
var sch []FlowSchemaItem
|
||||
|
||||
for _, h := range r.Handler {
|
||||
if h == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if hand := a.Provider(*h); hand != nil {
|
||||
sch = hand.FlowSchema()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if sch == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
flow := &Flow{
|
||||
Type: TypeForm,
|
||||
ID: genFlowID(),
|
||||
StepID: StepInit,
|
||||
Schema: []FlowSchemaItem{
|
||||
{
|
||||
Type: "string",
|
||||
Name: "username",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Type: "string",
|
||||
Name: "password",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
Type: TypeForm,
|
||||
ID: genFlowID(),
|
||||
StepID: StepInit,
|
||||
Schema: sch,
|
||||
Handler: r.Handler,
|
||||
Errors: []string{},
|
||||
request: r,
|
||||
|
@ -116,10 +122,10 @@ func (a *Authenticator) NewFlow(r *FlowRequest) *Flow {
|
|||
return flow
|
||||
}
|
||||
|
||||
func (f *Flow) progress(c echo.Context) error {
|
||||
func (f *Flow) progress(a *Authenticator, c echo.Context) error {
|
||||
switch f.StepID {
|
||||
case StepInit:
|
||||
var rm map[string]interface{}
|
||||
rm := make(map[string]interface{})
|
||||
|
||||
err := c.Bind(&rm)
|
||||
if err != nil {
|
||||
|
@ -133,7 +139,20 @@ func (f *Flow) progress(c echo.Context) error {
|
|||
}
|
||||
}
|
||||
}
|
||||
// TODO: lookup creds, pass, redirect to url
|
||||
err = a.Check(f, rm)
|
||||
switch err {
|
||||
case nil:
|
||||
return c.String(http.StatusOK, "login success!")
|
||||
case ErrInvalidHandler:
|
||||
return c.String(http.StatusNotFound, err.Error())
|
||||
case ErrInvalidAuth:
|
||||
fallthrough
|
||||
default:
|
||||
f.Errors = map[string]interface{}{
|
||||
"base": "invalid_auth",
|
||||
}
|
||||
return c.JSON(http.StatusOK, f)
|
||||
}
|
||||
default:
|
||||
return c.String(http.StatusBadRequest, "unknown flow step")
|
||||
}
|
||||
|
@ -156,7 +175,7 @@ func (a *Authenticator) LoginFlowDeleteHandler(c echo.Context) error {
|
|||
func (a *Authenticator) LoginFlowHandler(c echo.Context) error {
|
||||
if c.Request().Method == http.MethodPost && strings.HasPrefix(c.Request().Header.Get(echo.HeaderContentType), "text/plain") {
|
||||
// hack around the content-type, Context.JSON refuses to work otherwise
|
||||
c.Request().Header.Set(echo.HeaderContentType, "application/json")
|
||||
c.Request().Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||
}
|
||||
|
||||
flowID := c.Param("flow_id")
|
||||
|
@ -171,6 +190,10 @@ func (a *Authenticator) LoginFlowHandler(c echo.Context) error {
|
|||
|
||||
resp := a.NewFlow(&flowReq)
|
||||
|
||||
if resp == nil {
|
||||
return c.String(http.StatusBadRequest, "no such handler")
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
default:
|
||||
flow := a.Flows.Get(FlowID(flowID))
|
||||
|
@ -178,6 +201,6 @@ func (a *Authenticator) LoginFlowHandler(c echo.Context) error {
|
|||
return c.String(http.StatusNotFound, "no such flow")
|
||||
}
|
||||
|
||||
return flow.progress(c)
|
||||
return flow.progress(a, c)
|
||||
}
|
||||
}
|
||||
|
|
107
pkg/auth/provider.go
Normal file
107
pkg/auth/provider.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"dynatron.me/x/blasphem/pkg/storage"
|
||||
)
|
||||
|
||||
const (
|
||||
HAProviderKey = "auth_provider.homeassistant"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Password string `json:"password"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
type HomeAssistantProvider struct {
|
||||
AuthProviderBase `json:"-"`
|
||||
Users []User `json:"users"`
|
||||
|
||||
salt string `json:"-"`
|
||||
}
|
||||
|
||||
func NewHAProvider(s storage.Store) (*HomeAssistantProvider, error) {
|
||||
hap := &HomeAssistantProvider{
|
||||
AuthProviderBase: AuthProviderBase{
|
||||
Name: "Home Assistant Local",
|
||||
Type: HomeAssistant,
|
||||
},
|
||||
}
|
||||
|
||||
err := s.Get(HAProviderKey, hap)
|
||||
if err != nil {
|
||||
return hap, err
|
||||
}
|
||||
|
||||
return hap, nil
|
||||
}
|
||||
|
||||
func genSalt() string {
|
||||
b := make([]byte, 32)
|
||||
rand.Read(b)
|
||||
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func (hap *HomeAssistantProvider) hashPass(p string) ([]byte, error) {
|
||||
return bcrypt.GenerateFromPassword([]byte(p), bcrypt.DefaultCost)
|
||||
}
|
||||
|
||||
func (hap *HomeAssistantProvider) ValidateCreds(rm map[string]interface{}) bool {
|
||||
usernameE, hasU := rm["username"]
|
||||
passwordE, hasP := rm["password"]
|
||||
username, unStr := usernameE.(string)
|
||||
password, paStr := passwordE.(string)
|
||||
if !hasU || !hasP || !unStr || !paStr || username == "" || password == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
var found *User
|
||||
|
||||
const dummyHash = "$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO"
|
||||
|
||||
for _, v := range hap.Users { // iterate all to thwart timing attacks
|
||||
if v.Username == username {
|
||||
found = &v
|
||||
}
|
||||
}
|
||||
|
||||
if found == nil { // one more compare to thwart timing attacks
|
||||
bcrypt.CompareHashAndPassword([]byte("foo"), []byte(dummyHash))
|
||||
return false
|
||||
}
|
||||
|
||||
var hash []byte
|
||||
hash, err := base64.StdEncoding.DecodeString(found.Password)
|
||||
if err != nil {
|
||||
// XXX: probably log this
|
||||
return false
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword(hash, []byte(password))
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (hap *HomeAssistantProvider) FlowSchema() []FlowSchemaItem {
|
||||
return []FlowSchemaItem{
|
||||
{
|
||||
Type: "string",
|
||||
Name: "username",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Type: "string",
|
||||
Name: "password",
|
||||
Required: true,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,12 +1,74 @@
|
|||
package bus
|
||||
|
||||
import ()
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Bus struct {
|
||||
}
|
||||
type (
|
||||
Event struct {
|
||||
EvType string
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
listeners []chan<- Event
|
||||
|
||||
Bus struct {
|
||||
sync.RWMutex
|
||||
subs map[string]listeners
|
||||
}
|
||||
)
|
||||
|
||||
func New() *Bus {
|
||||
bus := &Bus{}
|
||||
bus := &Bus{
|
||||
subs: make(map[string]listeners),
|
||||
}
|
||||
|
||||
return bus
|
||||
|
||||
}
|
||||
|
||||
func (b *Bus) Sub(topic string, ch chan<- Event) {
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
|
||||
if prev, ok := b.subs[topic]; ok {
|
||||
b.subs[topic] = append(prev, ch)
|
||||
} else {
|
||||
b.subs[topic] = append(listeners{}, ch)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bus) Unsub(topic string, ch chan<- Event) {
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
|
||||
for i, v := range b.subs[topic] {
|
||||
if v == ch {
|
||||
// we don't care about order, replace and reslice
|
||||
b.subs[topic][i] = b.subs[topic][len(b.subs[topic])-1]
|
||||
b.subs[topic] = b.subs[topic][:len(b.subs[topic])-1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bus) Pub(topic string, data interface{}) {
|
||||
b.RLock()
|
||||
defer b.RUnlock()
|
||||
|
||||
tc, ok := b.subs[topic]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for _, ch := range tc {
|
||||
ch <- Event{EvType: topic, Data: data}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bus) Shutdown() {
|
||||
for _, v := range b.subs {
|
||||
for _, c := range v {
|
||||
close(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
)
|
||||
|
||||
func ReadConfig() (*Config, error) {
|
||||
// TODO: make this use an arg or an env, and unify with pkg/storage/'s home determination
|
||||
cfg := &Config{}
|
||||
|
||||
home, err := os.UserHomeDir()
|
||||
|
|
|
@ -1,18 +1,25 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"dynatron.me/x/blasphem/internal/common"
|
||||
"dynatron.me/x/blasphem/pkg/auth"
|
||||
"dynatron.me/x/blasphem/pkg/blas"
|
||||
"dynatron.me/x/blasphem/pkg/config"
|
||||
"dynatron.me/x/blasphem/pkg/frontend"
|
||||
)
|
||||
|
||||
const LogHeader = `${time_rfc3339} ${level} ${prefix} ${short_file}:${line}`
|
||||
|
||||
type Server struct {
|
||||
*blas.Blas
|
||||
*echo.Echo
|
||||
|
@ -42,28 +49,54 @@ func New(cfg *config.Config) (s *Server, err error) {
|
|||
Blas: b,
|
||||
Echo: echo.New(),
|
||||
}
|
||||
s.InitAuth()
|
||||
err = s.InitAuth(b.Store)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
|
||||
s.Echo.Debug = true
|
||||
s.Echo.HideBanner = true
|
||||
s.Echo.Logger.SetPrefix(common.AppName)
|
||||
s.Echo.Logger.SetHeader(LogHeader)
|
||||
|
||||
s.installRoutes()
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Server) Shutdown(ctx context.Context) error {
|
||||
err := s.Blas.Shutdown(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.Echo.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func (s *Server) Go() error {
|
||||
s.wg.Add(1)
|
||||
go func() {
|
||||
err := s.Start(s.Config.Server.Bind)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
s.Logger.Fatal(err)
|
||||
}
|
||||
|
||||
s.wg.Done()
|
||||
}()
|
||||
|
||||
// Wait for interrupt signal to gracefully shutdown the server with a timeout of 10 seconds.
|
||||
// Use a buffered channel to avoid missing signals as recommended for signal.Notify
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, os.Interrupt)
|
||||
<-quit
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
if err := s.Shutdown(ctx); err != nil {
|
||||
s.Logger.Fatal(err)
|
||||
}
|
||||
|
||||
s.wg.Wait()
|
||||
s.Logger.Info("shutdown complete")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
|
@ -14,15 +16,43 @@ type Item struct {
|
|||
Data Data `json:"data"`
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
type store struct {
|
||||
fs.FS
|
||||
}
|
||||
|
||||
func Open(dir fs.FS) (*Store, error) {
|
||||
type Store interface {
|
||||
Get(key string, data interface{}) error
|
||||
}
|
||||
|
||||
func (s *store) Get(key string, data interface{}) error {
|
||||
f, err := s.Open(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
item := Item{
|
||||
Data: data,
|
||||
}
|
||||
d := json.NewDecoder(f)
|
||||
err = d.Decode(&item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if item.Key != key {
|
||||
return fmt.Errorf("key mismatch '%s' != '%s'", item.Key, key)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Open(dir fs.FS) (*store, error) {
|
||||
stor, err := fs.Sub(dir, ".storage")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Store{stor}, nil
|
||||
return &store{stor}, nil
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue