diff --git a/_examples/README.md b/_examples/README.md index e3b47b5e..bf637d17 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -290,6 +290,7 @@ iris session manager lives on its own [package](https://github.com/kataras/iris/ - [Databases](sessions/database) * [File](sessions/database/file/main.go) * [BoltDB](sessions/database/boltdb/main.go) + * [Badger](sessions/database/badger/main.go) * [LevelDB](sessions/database/leveldb/main.go) * [Redis](sessions/database/redis/main.go) diff --git a/_examples/sessions/database/badger/main.go b/_examples/sessions/database/badger/main.go new file mode 100644 index 00000000..149fbb0b --- /dev/null +++ b/_examples/sessions/database/badger/main.go @@ -0,0 +1,95 @@ +package main + +import ( + "time" + + "github.com/kataras/iris" + + "github.com/kataras/iris/sessions" + "github.com/kataras/iris/sessions/sessiondb/badger" +) + +func main() { + db, err := badger.New("./data") + if err != nil { + panic(err) + } + // use different go routines to sync the database + db.Async(true) + + // close and unlock the database when control+C/cmd+C pressed + iris.RegisterOnInterrupt(func() { + db.Close() + }) + + sess := sessions.New(sessions.Config{ + Cookie: "sessionscookieid", + Expires: 45 * time.Minute, // <=0 means unlimited life + }) + + // + // IMPORTANT: + // + sess.UseDatabase(db) + + // the rest of the code stays the same. + app := iris.New() + + app.Get("/", func(ctx iris.Context) { + ctx.Writef("You should navigate to the /set, /get, /delete, /clear,/destroy instead") + }) + app.Get("/set", func(ctx iris.Context) { + s := sess.Start(ctx) + //set session values + s.Set("name", "iris") + + //test if setted here + ctx.Writef("All ok session setted to: %s", s.GetString("name")) + }) + + app.Get("/set/{key}/{value}", func(ctx iris.Context) { + key, value := ctx.Params().Get("key"), ctx.Params().Get("value") + s := sess.Start(ctx) + // set session values + s.Set(key, value) + + // test if setted here + ctx.Writef("All ok session setted to: %s", s.GetString(key)) + }) + + app.Get("/get", func(ctx iris.Context) { + // get a specific key, as string, if no found returns just an empty string + name := sess.Start(ctx).GetString("name") + + ctx.Writef("The name on the /set was: %s", name) + }) + + app.Get("/get/{key}", func(ctx iris.Context) { + // get a specific key, as string, if no found returns just an empty string + name := sess.Start(ctx).GetString(ctx.Params().Get("key")) + + ctx.Writef("The name on the /set was: %s", name) + }) + + app.Get("/delete", func(ctx iris.Context) { + // delete a specific key + sess.Start(ctx).Delete("name") + }) + + app.Get("/clear", func(ctx iris.Context) { + // removes all entries + sess.Start(ctx).Clear() + }) + + app.Get("/destroy", func(ctx iris.Context) { + //destroy, removes the entire session data and cookie + sess.Destroy(ctx) + }) + + app.Get("/update", func(ctx iris.Context) { + // updates expire date with a new date + sess.ShiftExpiration(ctx) + }) + + app.Run(iris.Addr(":8080")) +} diff --git a/core/host/world.go b/core/host/world.go index 2ac1ef1f..800b8833 100644 --- a/core/host/world.go +++ b/core/host/world.go @@ -28,7 +28,7 @@ func RegisterOnInterrupt(cb func()) { func notifyInterrupt() { w.mu.Lock() for _, f := range w.onInterrupt { - go f() + f() } w.mu.Unlock() } diff --git a/sessions/sessiondb/badger/database.go b/sessions/sessiondb/badger/database.go new file mode 100644 index 00000000..7a60d182 --- /dev/null +++ b/sessions/sessiondb/badger/database.go @@ -0,0 +1,184 @@ +package badger + +import ( + "os" + "runtime" + + "github.com/kataras/golog" + "github.com/kataras/iris/core/errors" + "github.com/kataras/iris/sessions" + + "github.com/dgraph-io/badger" +) + +// DefaultFileMode used as the default database's "fileMode" +// for creating the sessions directory path, opening and write the session file. +var ( + DefaultFileMode = 0666 +) + +// Database the badger(key-value file-based) session storage. +type Database struct { + // Service is the underline badger database connection, + // it's initialized at `New` or `NewFromDB`. + // Can be used to get stats. + Service *badger.KV + async bool +} + +// New creates and returns a new badger(key-value file-based) storage +// instance based on the "directoryPath". +// DirectoryPath should is the directory which the badger database will store the sessions, +// i.e ./sessions +// +// It will remove any old session files. +func New(directoryPath string) (*Database, error) { + + if directoryPath == "" { + return nil, errors.New("dir is missing") + } + + lindex := directoryPath[len(directoryPath)-1] + if lindex != os.PathSeparator && lindex != '/' { + directoryPath += string(os.PathSeparator) + } + // create directories if necessary + if err := os.MkdirAll(directoryPath, os.FileMode(DefaultFileMode)); err != nil { + return nil, err + } + + opts := badger.DefaultOptions + opts.Dir = directoryPath + opts.ValueDir = directoryPath + + service, err := badger.NewKV(&opts) + + if err != nil { + golog.Errorf("unable to initialize the badger-based session database: %v", err) + return nil, err + } + + return NewFromDB(service) +} + +// NewFromDB same as `New` but accepts an already-created custom badger connection instead. +func NewFromDB(service *badger.KV) (*Database, error) { + if service == nil { + return nil, errors.New("underline database is missing") + } + + db := &Database{Service: service} + + runtime.SetFinalizer(db, closeDB) + return db, db.Cleanup() +} + +// Cleanup removes any invalid(have expired) session entries, +// it's being called automatically on `New` as well. +func (db *Database) Cleanup() error { + rep := errors.NewReporter() + + iter := db.Service.NewIterator(badger.DefaultIteratorOptions) + for iter.Rewind(); iter.Valid(); iter.Next() { + // Remember that the contents of the returned slice should not be modified, and + // only valid until the next call to Next. + item := iter.Item() + err := item.Value(func(b []byte) error { + storeDB, err := sessions.DecodeRemoteStore(b) + if err != nil { + return err + } + + if storeDB.Lifetime.HasExpired() { + err = db.Service.Delete(item.Key()) + } + return err + }) + + rep.AddErr(err) + } + + iter.Close() + return rep.Return() +} + +// Async if true passed then it will use different +// go routines to update the badger(key-value file-based) storage. +func (db *Database) Async(useGoRoutines bool) *Database { + db.async = useGoRoutines + return db +} + +// Load loads the sessions from the badger(key-value file-based) session storage. +func (db *Database) Load(sid string) (storeDB sessions.RemoteStore) { + bsid := []byte(sid) + iter := db.Service.NewIterator(badger.DefaultIteratorOptions) + defer iter.Close() + + iter.Seek(bsid) + if !iter.Valid() { + return + } + item := iter.Item() + item.Value(func(b []byte) (err error) { + storeDB, err = sessions.DecodeRemoteStore(b) // decode the whole value, as a remote store + if err != nil { + golog.Errorf("error while trying to load from the remote store: %v", err) + } + return + }) + return +} + +// Sync syncs the database with the session's (memory) store. +func (db *Database) Sync(p sessions.SyncPayload) { + if db.async { + go db.sync(p) + } else { + db.sync(p) + } +} + +func (db *Database) sync(p sessions.SyncPayload) { + bsid := []byte(p.SessionID) + + if p.Action == sessions.ActionDestroy { + if err := db.destroy(bsid); err != nil { + golog.Errorf("error while destroying a session(%s) from badger: %v", + p.SessionID, err) + } + return + } + + s, err := p.Store.Serialize() + if err != nil { + golog.Errorf("error while serializing the remote store: %v", err) + } + + // err = db.Service.Set(bsid, s, meta) + e := &badger.Entry{ + Key: bsid, + Value: s, + } + err = db.Service.BatchSet([]*badger.Entry{e}) + if err != nil { + golog.Errorf("error while writing the session(%s) to the database: %v", p.SessionID, err) + } +} + +func (db *Database) destroy(bsid []byte) error { + return db.Service.Delete(bsid) +} + +// Close shutdowns the badger connection. +func (db *Database) Close() error { + return closeDB(db) +} + +func closeDB(db *Database) error { + err := db.Service.Close() + if err != nil { + golog.Warnf("closing the badger connection: %v", err) + } + return err +}