diff --git a/HISTORY.md b/HISTORY.md index ea0bc634..fe9feea8 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -18,6 +18,23 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene **How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris`. + +# Tu, 08 August 2017 | v8.2.1 + +No API Changes. Great news for the unique iris sessions library, once again. + +**NEW**: [LevelDB-based](https://github.com/google/leveldb) session database implemented, example [here](_examples/sessions/database/leveldb/main.go). + +[Redis-based sessiondb](sessions/sessiondb/redis) has no longer the `MaxAgeSeconds` config field, +this is passed automatically by the session manager, now. + +All [sessions databases](sessions/sessiondb) have an `Async(bool)` function, if turned on +then all synchronization between the memory store and the back-end database will happen +inside different go routines. By-default async is false but it's recommended to turn it on, it will make sessions to be stored faster, at most. + +All reported issues have been fixed, the API is simplified by `v8.2.0` so everyone can +create and use any back-end storage for application's sessions persistence. + # Mo, 07 August 2017 | v8.2.0 No Common-API Changes. diff --git a/README.md b/README.md index 90d35e06..6fd9b61d 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Iris is a fast, simple and efficient micro web framework for Go. It provides a b ### 📑 Table of contents * [Installation](#-installation) -* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#mo-07-august-2017--v820) +* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#tu-08-august-2017--v821) * [Learn](#-learn) * [HTTP Listening](_examples/#http-listening) * [Configuration](_examples/#configuration) @@ -318,7 +318,7 @@ Thank You for your trust! ### 📌 Version -Current: **8.2.0** +Current: **8.2.1** Each new release is pushed to the master. It stays there until the next version. When a next version is released then the previous version goes to its own branch with `gopkg.in` as its import path (and its own vendor folder), in order to keep it working "for-ever". diff --git a/_examples/sessions/database/boltdb/main.go b/_examples/sessions/database/boltdb/main.go index 755eef95..0b4524f5 100644 --- a/_examples/sessions/database/boltdb/main.go +++ b/_examples/sessions/database/boltdb/main.go @@ -12,6 +12,8 @@ import ( func main() { db, _ := boltdb.New("./sessions/sessions.db", 0666, "users") + // 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() { @@ -20,7 +22,7 @@ func main() { sess := sessions.New(sessions.Config{ Cookie: "sessionscookieid", - Expires: 1 * time.Minute, // <=0 means unlimited life + Expires: 45 * time.Minute, // <=0 means unlimited life }) // diff --git a/_examples/sessions/database/file/main.go b/_examples/sessions/database/file/main.go index 40874627..33cc180b 100644 --- a/_examples/sessions/database/file/main.go +++ b/_examples/sessions/database/file/main.go @@ -13,6 +13,9 @@ import ( func main() { db, _ := file.New("./sessions/", 0666) + // use different go routines to sync the database + db.Async(true) + sess := sessions.New(sessions.Config{ Cookie: "sessionscookieid", Expires: 45 * time.Minute, // <=0 means unlimited life diff --git a/_examples/sessions/database/leveldb/main.go b/_examples/sessions/database/leveldb/main.go new file mode 100644 index 00000000..62c1c580 --- /dev/null +++ b/_examples/sessions/database/leveldb/main.go @@ -0,0 +1,94 @@ +package main + +import ( + "time" + + "github.com/kataras/iris" + "github.com/kataras/iris/context" + + "github.com/kataras/iris/sessions" + "github.com/kataras/iris/sessions/sessiondb/leveldb" +) + +func main() { + db, _ := leveldb.New("./sessions/") + + // 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 context.Context) { + ctx.Writef("You should navigate to the /set, /get, /delete, /clear,/destroy instead") + }) + app.Get("/set", func(ctx context.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 context.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 context.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 context.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 context.Context) { + // delete a specific key + sess.Start(ctx).Delete("name") + }) + + app.Get("/clear", func(ctx context.Context) { + // removes all entries + sess.Start(ctx).Clear() + }) + + app.Get("/destroy", func(ctx context.Context) { + //destroy, removes the entire session data and cookie + sess.Destroy(ctx) + }) + + app.Get("/update", func(ctx context.Context) { + // updates expire date with a new date + sess.ShiftExpiraton(ctx) + }) + + app.Run(iris.Addr(":8080")) +} diff --git a/_examples/sessions/database/redis/main.go b/_examples/sessions/database/redis/main.go index 503bb7c6..8114e971 100644 --- a/_examples/sessions/database/redis/main.go +++ b/_examples/sessions/database/redis/main.go @@ -1,6 +1,8 @@ package main import ( + "time" + "github.com/kataras/iris" "github.com/kataras/iris/context" @@ -21,12 +23,14 @@ func main() { IdleTimeout: service.DefaultRedisIdleTimeout, Prefix: ""}) // optionally configure the bridge between your redis server + // use go routines to query the database + db.Async(true) // close connection when control+C/cmd+C iris.RegisterOnInterrupt(func() { db.Close() }) - sess := sessions.New(sessions.Config{Cookie: "sessionscookieid"}) + sess := sessions.New(sessions.Config{Cookie: "sessionscookieid", Expires: 45 * time.Minute}) // // IMPORTANT: diff --git a/doc.go b/doc.go index f2966d44..63d95dd2 100644 --- a/doc.go +++ b/doc.go @@ -35,7 +35,7 @@ Source code and other details for the project are available at GitHub: Current Version -8.2.0 +8.2.1 Installation @@ -1267,6 +1267,106 @@ Running the example: $ curl -s --cookie "mycookiesessionnameid=MTQ4NzE5Mz..." http://localhost:8080/secret The cake is a lie! + +Sessions persistence can be achieved using one (or more) `sessiondb`. + +Example Code: + + package main + + import ( + "time" + + "github.com/kataras/iris" + "github.com/kataras/iris/context" + + "github.com/kataras/iris/sessions" + "github.com/kataras/iris/sessions/sessiondb/boltdb" // <- IMPORTANT + ) + + func main() { + db, _ := boltdb.New("./sessions/sessions.db", 0666, "users") + // 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 context.Context) { + ctx.Writef("You should navigate to the /set, /get, /delete, /clear,/destroy instead") + }) + app.Get("/set", func(ctx context.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 context.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 context.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 context.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 context.Context) { + // delete a specific key + sess.Start(ctx).Delete("name") + }) + + app.Get("/clear", func(ctx context.Context) { + // removes all entries + sess.Start(ctx).Clear() + }) + + app.Get("/destroy", func(ctx context.Context) { + //destroy, removes the entire session data and cookie + sess.Destroy(ctx) + }) + + app.Get("/update", func(ctx context.Context) { + // updates expire date with a new date + sess.ShiftExpiraton(ctx) + }) + + app.Run(iris.Addr(":8080")) + } + + More examples: https://github.com/kataras/iris/tree/master/sessions diff --git a/iris.go b/iris.go index e810f350..ac48696d 100644 --- a/iris.go +++ b/iris.go @@ -32,7 +32,7 @@ import ( const ( // Version is the current version number of the Iris Web Framework. - Version = "8.2.0" + Version = "8.2.1" ) // HTTP status codes as registered with IANA. diff --git a/sessions/sessiondb/file/database.go b/sessions/sessiondb/file/database.go index 3e9ad5fa..3b252f3f 100644 --- a/sessions/sessiondb/file/database.go +++ b/sessions/sessiondb/file/database.go @@ -36,7 +36,10 @@ type Database struct { async bool } -// New creates and returns a new file-storage database instance based on the "path". +// New creates and returns a new file-storage database instance based on the "directoryPath". +// DirectoryPath should is the directory which the leveldb database will store the sessions, +// i.e ./sessions/ +// // It will remove any old session files. func New(directoryPath string, fileMode os.FileMode) (*Database, error) { lindex := directoryPath[len(directoryPath)-1] diff --git a/sessions/sessiondb/leveldb/database.go b/sessions/sessiondb/leveldb/database.go new file mode 100644 index 00000000..21e119ff --- /dev/null +++ b/sessions/sessiondb/leveldb/database.go @@ -0,0 +1,190 @@ +package leveldb + +import ( + "bytes" + + "runtime" + + "github.com/kataras/golog" + "github.com/kataras/iris/core/errors" + "github.com/kataras/iris/sessions" + + "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/opt" +) + +var ( + // Options used to open the leveldb database, defaults to leveldb's default values. + Options = &opt.Options{} + // WriteOptions used to put and delete, defaults to leveldb's default values. + WriteOptions = &opt.WriteOptions{} + // ReadOptions used to iterate over the database, defaults to leveldb's default values. + ReadOptions = &opt.ReadOptions{} +) + +// Database the LevelDB(file-based) session storage. +type Database struct { + // Service is the underline LevelDB database connection, + // it's initialized at `New` or `NewFromDB`. + // Can be used to get stats. + Service *leveldb.DB + async bool +} + +// New creates and returns a new LevelDB(file-based) storage +// instance based on the "directoryPath". +// DirectoryPath should is the directory which the leveldb 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") + } + + // Second parameter is a "github.com/syndtr/goleveldb/leveldb/opt.Options{}" + // user can change the `Options` or create the sessiondb via `NewFromDB` + // if wants to use a customized leveldb database + // or an existing one, we don't require leveldb options at the constructor. + // + // The leveldb creates the directories, if necessary. + service, err := leveldb.OpenFile(directoryPath, Options) + + if err != nil { + golog.Errorf("unable to initialize the LevelDB-based session database: %v", err) + return nil, err + } + + return NewFromDB(service) +} + +// NewFromDB same as `New` but accepts an already-created custom boltdb connection instead. +func NewFromDB(service *leveldb.DB) (*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 { + iter := db.Service.NewIterator(nil, ReadOptions) + for iter.Next() { + // Remember that the contents of the returned slice should not be modified, and + // only valid until the next call to Next. + k := iter.Key() + + if len(k) > 0 { + v := iter.Value() + storeDB, err := sessions.DecodeRemoteStore(v) + if err != nil { + continue + } + + if storeDB.Lifetime.HasExpired() { + if err := db.Service.Delete(k, WriteOptions); err != nil { + golog.Warnf("troubles when cleanup a session remote store from LevelDB: %v", err) + } + } + } + + } + iter.Release() + return iter.Error() +} + +// Async if true passed then it will use different +// go routines to update the LevelDB(file-based) storage. +func (db *Database) Async(useGoRoutines bool) *Database { + db.async = useGoRoutines + return db +} + +// Load loads the sessions from the LevelDB(file-based) session storage. +func (db *Database) Load(sid string) (storeDB sessions.RemoteStore) { + bsid := []byte(sid) + + iter := db.Service.NewIterator(nil, ReadOptions) + for iter.Next() { + // Remember that the contents of the returned slice should not be modified, and + // only valid until the next call to Next. + k := iter.Key() + + if len(k) > 0 { + v := iter.Value() + if bytes.Equal(k, bsid) { // session id should be the name of the key-value pair + store, err := sessions.DecodeRemoteStore(v) // decode the whole value, as a remote store + if err != nil { + golog.Errorf("error while trying to load from the remote store: %v", err) + } else { + storeDB = store + } + break + } + } + + } + + iter.Release() + if err := iter.Error(); err != nil { + golog.Errorf("error while trying to iterate over the database: %v", err) + } + + 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 boltdb: %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.Put(bsid, s, WriteOptions) + + 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, WriteOptions) +} + +// Close shutdowns the LevelDB 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 LevelDB connection: %v", err) + } + + return err +}