Sessions are now in full sync with the registered database, on acquire(init), set, get, delete, clear, visit, len, release(destroy) as requested by almost everyone. https://github.com/kataras/iris/issues/969

Former-commit-id: 49fcdb93106a78f0a24ad3fb4d8725e35e98451a
This commit is contained in:
Gerasimos Maropoulos 2018-04-22 13:52:36 +03:00
parent f113872b7d
commit b62080c4bb
28 changed files with 496 additions and 1358 deletions

View File

@ -5,4 +5,7 @@ Documentation for the Iris project can be found at
<https://godoc.org/github.com/kataras/iris>. <https://godoc.org/github.com/kataras/iris>.
Love iris? Please consider supporting the project: Love iris? Please consider supporting the project:
👉 https://iris-go.com/donate 👉 https://iris-go.com/donate
Care to be part of a larger community? Fill our user experience form:
👉 https://goo.gl/forms/lnRbVgA6ICTkPyk02

View File

@ -6,6 +6,10 @@ go:
- "go1.9" - "go1.9"
- "go1.10" - "go1.10"
go_import_path: github.com/kataras/iris go_import_path: github.com/kataras/iris
# we disable test caching via GOCACHE=off
env:
global:
- GOCACHE=off
install: install:
- go get ./... # for iris-contrib/httpexpect, kataras/golog - go get ./... # for iris-contrib/httpexpect, kataras/golog
script: script:

View File

@ -85,19 +85,4 @@ Thank you to all the people who have already <a href="graphs/contributors">contr
Thank you to all our backers! [Become a backer](https://iris-go.com/donate) Thank you to all our backers! [Become a backer](https://iris-go.com/donate)
<a href="https://iris-go.com/donate"><img src="https://iris-go.com/backers.svg?v=1"/></a> <a href="https://iris-go.com/donate"><img src="https://iris-go.com/backers.svg?v=2"/></a>
### Sponsors
Thank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/iris#sponsor))
<a href="https://opencollective.com/iris/sponsor/0/website" target="_blank"><img src="https://opencollective.com/iris/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/iris/sponsor/1/website" target="_blank"><img src="https://opencollective.com/iris/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/iris/sponsor/2/website" target="_blank"><img src="https://opencollective.com/iris/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/iris/sponsor/3/website" target="_blank"><img src="https://opencollective.com/iris/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/iris/sponsor/4/website" target="_blank"><img src="https://opencollective.com/iris/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/iris/sponsor/5/website" target="_blank"><img src="https://opencollective.com/iris/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/iris/sponsor/6/website" target="_blank"><img src="https://opencollective.com/iris/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/iris/sponsor/7/website" target="_blank"><img src="https://opencollective.com/iris/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/iris/sponsor/8/website" target="_blank"><img src="https://opencollective.com/iris/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/iris/sponsor/9/website" target="_blank"><img src="https://opencollective.com/iris/sponsor/9/avatar.svg"></a>

View File

@ -16,7 +16,7 @@ Learn what [others say about Iris](#support) and [star](https://github.com/katar
Thank you to all our backers! 🙏 [Become a backer](https://iris-go.com/donate) Thank you to all our backers! 🙏 [Become a backer](https://iris-go.com/donate)
<a href="https://iris-go.com/donate" target="_blank"><img src="https://iris-go.com/backers.svg?v=1"/></a> <a href="https://iris-go.com/donate" target="_blank"><img src="https://iris-go.com/backers.svg?v=2"/></a>
```sh ```sh
$ cat example.go $ cat example.go
@ -216,13 +216,6 @@ Iris, unlike others, is 100% compatible with the standards and that's why the ma
There are many companies and start-ups looking for Go web developers with Iris experience as requirement, we are searching for you every day and we post those information via our [facebook page](https://www.facebook.com/iris.framework), like the page to get notified, we have already posted some of them. There are many companies and start-ups looking for Go web developers with Iris experience as requirement, we are searching for you every day and we post those information via our [facebook page](https://www.facebook.com/iris.framework), like the page to get notified, we have already posted some of them.
### Sponsors
Thank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/iris#sponsor))
<a href="https://opencollective.com/iris/sponsor/0/website" target="_blank"><img src="https://opencollective.com/iris/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/iris/sponsor/1/website" target="_blank"><img src="https://opencollective.com/iris/sponsor/1/avatar.svg"></a>
## License ## License
Iris is licensed under the [3-Clause BSD License](LICENSE). Iris is 100% free and open-source software. Iris is licensed under the [3-Clause BSD License](LICENSE). Iris is 100% free and open-source software.

View File

@ -16,7 +16,7 @@
Eυχαριστούμε όλους τους υποστηρικτές μας! 🙏 [Γίνετε ένας από αυτούς](https://iris-go.com/donate) Eυχαριστούμε όλους τους υποστηρικτές μας! 🙏 [Γίνετε ένας από αυτούς](https://iris-go.com/donate)
<a href="https://iris-go.com/donate" target="_blank"><img src="https://iris-go.com/backers.svg?v=1"/></a> <a href="https://iris-go.com/donate" target="_blank"><img src="https://iris-go.com/backers.svg?v=2"/></a>
```sh ```sh
$ cat example.go $ cat example.go
@ -218,13 +218,6 @@ _Η τελευταία ενημέρωση έγινε την [Τρίτη, 21 Νο
Υπάρχουν πολλές νεοσύστατες εταιρείες που αναζητούν Go web developers με εμπειρία Iris ως απαίτηση, ψάχνουμε καθημερινά και δημοσιεύουμε αυτές τις πληροφορίες μέσω της [σελίδας μας στο facebook](https://www.facebook.com/iris.framework), κάντε like για να λαμβάνετε ειδοποιήσεις, έχουμε ήδη δημοσιεύσει ορισμένες από αυτές(τις θέσεις εργασίας). Υπάρχουν πολλές νεοσύστατες εταιρείες που αναζητούν Go web developers με εμπειρία Iris ως απαίτηση, ψάχνουμε καθημερινά και δημοσιεύουμε αυτές τις πληροφορίες μέσω της [σελίδας μας στο facebook](https://www.facebook.com/iris.framework), κάντε like για να λαμβάνετε ειδοποιήσεις, έχουμε ήδη δημοσιεύσει ορισμένες από αυτές(τις θέσεις εργασίας).
### Χορηγοί
Ευχαριστούμε όλους τους χορηγούς μας! (παρακαλώ ρωτήστε την εταιρία σας να υποστηρίξει επίσης αυτό το έργο ανοιχτού κώδικα με το [να γίνει χορηγός](https://opencollective.com/iris#sponsor))
<a href="https://opencollective.com/iris/sponsor/0/website" target="_blank"><img src="https://opencollective.com/iris/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/iris/sponsor/1/website" target="_blank"><img src="https://opencollective.com/iris/sponsor/1/avatar.svg"></a>
## License ## License
Το Iris διαθέτει άδεια βάσει του [3-Clause BSD License](LICENSE). Το Iris είναι 100% δωρεάν και ανοιχτού κώδικα λογισμικό. Το Iris διαθέτει άδεια βάσει του [3-Clause BSD License](LICENSE). Το Iris είναι 100% δωρεάν και ανοιχτού κώδικα λογισμικό.

View File

@ -16,7 +16,7 @@ Iris предоставляет красиво выразительную и у
Спасибо всем, кто поддерживал нас! 🙏 [Поддержать нас](https://iris-go.com/donate) Спасибо всем, кто поддерживал нас! 🙏 [Поддержать нас](https://iris-go.com/donate)
<a href="https://iris-go.com/donate" target="_blank"><img src="https://iris-go.com/backers.svg?v=1"/></a> <a href="https://iris-go.com/donate" target="_blank"><img src="https://iris-go.com/backers.svg?v=2"/></a>
```sh ```sh
$ cat example.go $ cat example.go
@ -218,13 +218,6 @@ Iris, в отличие от других, на 100% совместим со с
Есть много компаний и стартапов, находящиеся в поисках Go веб-разработчиков с опытом работы с Iris как в качестве требования, которые мы подыскиваем для вас каждый день. Мы публикуем эту информацию на нашей [странице в Facebook](https://www.facebook.com/iris.framework). Ставьте Like, чтобы получите уведомления. Мы уже опубликовали некоторые из них. Есть много компаний и стартапов, находящиеся в поисках Go веб-разработчиков с опытом работы с Iris как в качестве требования, которые мы подыскиваем для вас каждый день. Мы публикуем эту информацию на нашей [странице в Facebook](https://www.facebook.com/iris.framework). Ставьте Like, чтобы получите уведомления. Мы уже опубликовали некоторые из них.
### Спонсоры
Спасибо всем нашим спонсорам! (пожалуйста, попросите вашу компанию также поддержать этот проект с открытым исходным кодом, [став спонсором](https://opencollective.com/iris#sponsor))
<a href="https://opencollective.com/iris/sponsor/0/website" target="_blank"><img src="https://opencollective.com/iris/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/iris/sponsor/1/website" target="_blank"><img src="https://opencollective.com/iris/sponsor/1/avatar.svg"></a>
## Лицензия ## Лицензия
Iris лицензируется в соответствии с [BSD 3-Clause лицензией](LICENSE). Iris - это бесплатное программное обеспечение с открытым исходным кодом на 100%. Iris лицензируется в соответствии с [BSD 3-Clause лицензией](LICENSE). Iris - это бесплатное программное обеспечение с открытым исходным кодом на 100%.

View File

@ -16,7 +16,7 @@ Iris 功能强大、使用简单它将会是你下一个网站、API 服务
感谢所有的支持者! 🙏 [支持我们](https://iris-go.com/donate) 感谢所有的支持者! 🙏 [支持我们](https://iris-go.com/donate)
<a href="https://iris-go.com/donate" target="_blank"><img src="https://iris-go.com/backers.svg?v=1"/></a> <a href="https://iris-go.com/donate" target="_blank"><img src="https://iris-go.com/backers.svg?v=2"/></a>
```sh ```sh
$ cat example.go $ cat example.go
@ -209,13 +209,6 @@ Iris 拥有大量的中间件 [[1]](middleware/)[[2]](https://github.com/iris-co
有很多公司都在寻找具有 Iris 经验的 Go 网站开发者,我们通过 [facebook page](https://www.facebook.com/iris.framework) 发布这些招聘信息。 有很多公司都在寻找具有 Iris 经验的 Go 网站开发者,我们通过 [facebook page](https://www.facebook.com/iris.framework) 发布这些招聘信息。
### 赞助
感谢所有赞助者! (希望贵公司赞助支持这个开源项目)
<a href="https://opencollective.com/iris/sponsor/0/website" target="_blank"><img src="https://opencollective.com/iris/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/iris/sponsor/1/website" target="_blank"><img src="https://opencollective.com/iris/sponsor/1/avatar.svg"></a>
## 授权协议 ## 授权协议
Iris 授权基于 [3-Clause BSD License](LICENSE). Iris 是 100 免费和开源软件。 Iris 授权基于 [3-Clause BSD License](LICENSE). Iris 是 100 免费和开源软件。

View File

@ -400,10 +400,7 @@ iris session manager lives on its own [package](https://github.com/kataras/iris/
- [Secure Cookie](sessions/securecookie/main.go) - [Secure Cookie](sessions/securecookie/main.go)
- [Flash Messages](sessions/flash-messages/main.go) - [Flash Messages](sessions/flash-messages/main.go)
- [Databases](sessions/database) - [Databases](sessions/database)
* [File](sessions/database/file/main.go)
* [BoltDB](sessions/database/boltdb/main.go)
* [Badger](sessions/database/badger/main.go) * [Badger](sessions/database/badger/main.go)
* [LevelDB](sessions/database/leveldb/main.go)
* [Redis](sessions/database/redis/main.go) * [Redis](sessions/database/redis/main.go)
> You're free to use your own favourite sessions package if you'd like so. > You're free to use your own favourite sessions package if you'd like so.

View File

@ -399,10 +399,7 @@ Iris session 管理独立包 [package](https://github.com/kataras/iris/tree/mast
- [Secure Cookie](sessions/securecookie/main.go) - [Secure Cookie](sessions/securecookie/main.go)
- [Flash Messages](sessions/flash-messages/main.go) - [Flash Messages](sessions/flash-messages/main.go)
- [Databases](sessions/database) - [Databases](sessions/database)
* [File](sessions/database/file/main.go)
* [BoltDB](sessions/database/boltdb/main.go)
* [Badger](sessions/database/badger/main.go) * [Badger](sessions/database/badger/main.go)
* [LevelDB](sessions/database/leveldb/main.go)
* [Redis](sessions/database/redis/main.go) * [Redis](sessions/database/redis/main.go)
> 可以随意使用自定义的 Session 管理包。 > 可以随意使用自定义的 Session 管理包。

View File

@ -12,9 +12,6 @@ Some trivial examples,
- [Flash Messages](https://github.com/kataras/iris/blob/master/_examples/sessions/flash-messages/main.go) - [Flash Messages](https://github.com/kataras/iris/blob/master/_examples/sessions/flash-messages/main.go)
- [Databases](https://github.com/kataras/iris/tree/master/_examples/sessions/database) - [Databases](https://github.com/kataras/iris/tree/master/_examples/sessions/database)
* [BadgerDB](https://github.com/kataras/iris/blob/master/_examples/sessions/database/badger/main.go) **fastest** * [BadgerDB](https://github.com/kataras/iris/blob/master/_examples/sessions/database/badger/main.go) **fastest**
* [File](https://github.com/kataras/iris/blob/master/_examples/sessions/database/file/main.go)
* [BoltDB](https://github.com/kataras/iris/blob/master/_examples/sessions/database/boltdb/main.go)
* [LevelDB](https://github.com/kataras/iris/blob/master/_examples/sessions/database/leveldb/main.go)
* [Redis](https://github.com/kataras/iris/blob/master/_examples/sessions/database/redis/main.go) * [Redis](https://github.com/kataras/iris/blob/master/_examples/sessions/database/redis/main.go)
## Overview ## Overview

View File

@ -20,9 +20,11 @@ func main() {
db.Close() db.Close()
}) })
defer db.Close() // close and unlock the database if application errored.
sess := sessions.New(sessions.Config{ sess := sessions.New(sessions.Config{
Cookie: "sessionscookieid", Cookie: "sessionscookieid",
Expires: 45 * time.Minute, // <=0 means unlimited life Expires: 45 * time.Second, // <=0 means unlimited life. Defaults to 0.
}) })
// //
@ -89,5 +91,5 @@ func main() {
sess.ShiftExpiration(ctx) sess.ShiftExpiration(ctx)
}) })
app.Run(iris.Addr(":8080")) app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed))
} }

View File

@ -1,90 +0,0 @@
package main
import (
"time"
"github.com/kataras/iris"
"github.com/kataras/iris/sessions"
"github.com/kataras/iris/sessions/sessiondb/boltdb"
)
func main() {
db, _ := boltdb.New("./sessions/sessions.db", 0666, "users")
// 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"))
}

View File

@ -1,88 +0,0 @@
package main
import (
"time"
"github.com/kataras/iris"
"github.com/gorilla/securecookie"
"github.com/kataras/iris/sessions"
"github.com/kataras/iris/sessions/sessiondb/file"
)
func main() {
db, _ := file.New("./sessions/", 0755)
sess := sessions.New(sessions.Config{
Cookie: "sessionscookieid",
Expires: 24 * time.Hour, // <=0 means unlimited life
Encoding: securecookie.New([]byte("C2O6J6oYTd0CBCNERkWZK8jGOXTXf9X2"),
[]byte("UTp6fJsicraGxA2cslELrrLX7msg5jfE")),
})
//
// 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"), iris.WithoutVersionChecker)
}

View File

@ -1,90 +0,0 @@
package main
import (
"time"
"github.com/kataras/iris"
"github.com/kataras/iris/sessions"
"github.com/kataras/iris/sessions/sessiondb/leveldb"
)
func main() {
db, _ := leveldb.New("./sessions/")
// 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"))
}

View File

@ -10,6 +10,8 @@ import (
"github.com/kataras/iris/sessions/sessiondb/redis/service" "github.com/kataras/iris/sessions/sessiondb/redis/service"
) )
// tested with redis version 3.0.503.
// for windows see: https://github.com/ServiceStack/redis-windows
func main() { func main() {
// replace with your running redis' server settings: // replace with your running redis' server settings:
db := redis.New(service.Config{ db := redis.New(service.Config{
@ -27,7 +29,12 @@ func main() {
db.Close() db.Close()
}) })
sess := sessions.New(sessions.Config{Cookie: "sessionscookieid", Expires: 45 * time.Minute}) defer db.Close() // close the database connection if application errored.
sess := sessions.New(sessions.Config{
Cookie: "sessionscookieid",
Expires: 45 * time.Minute}, // <=0 means unlimited life. Defaults to 0.
)
// //
// IMPORTANT: // IMPORTANT:
@ -46,13 +53,30 @@ func main() {
s.Set("name", "iris") s.Set("name", "iris")
//test if setted here //test if setted here
ctx.Writef("All ok session setted to: %s", s.GetString("name")) ctx.Writef("All ok session value of the 'name' is: %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 value of the '%s' is: %s", key, s.GetString(key))
}) })
app.Get("/get", func(ctx iris.Context) { app.Get("/get", func(ctx iris.Context) {
// get a specific key, as string, if no found returns just an empty string // get a specific key, as string, if no found returns just an empty string
name := sess.Start(ctx).GetString("name") 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) ctx.Writef("The name on the /set was: %s", name)
}) })
@ -76,5 +100,5 @@ func main() {
sess.ShiftExpiration(ctx) sess.ShiftExpiration(ctx)
}) })
app.Run(iris.Addr(":8080")) app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed))
} }

View File

@ -9,7 +9,7 @@ import (
) )
// Client returns a new http.Client using // Client returns a new http.Client using
// the "timeout" for open connection and read-write operations. // the "timeout" for open connection.
func Client(timeout time.Duration) *http.Client { func Client(timeout time.Duration) *http.Client {
transport := http.Transport{ transport := http.Transport{
Dial: func(network string, addr string) (net.Conn, error) { Dial: func(network string, addr string) (net.Conn, error) {
@ -18,9 +18,6 @@ func Client(timeout time.Duration) *http.Client {
golog.Debugf("%v", err) golog.Debugf("%v", err)
return nil, err return nil, err
} }
if err = conn.SetDeadline(time.Now().Add(timeout)); err != nil {
golog.Debugf("%v", err)
}
return conn, err return conn, err
}, },
} }

View File

@ -1,154 +1,93 @@
package sessions package sessions
import ( import (
"bytes" "sync"
"encoding/gob" "time"
"io"
"github.com/kataras/iris/core/memstore" "github.com/kataras/iris/core/memstore"
) )
func init() {
gob.Register(RemoteStore{})
}
// Database is the interface which all session databases should implement // Database is the interface which all session databases should implement
// By design it doesn't support any type of cookie session like other frameworks. // By design it doesn't support any type of cookie session like other frameworks.
// I want to protect you, believe me. // I want to protect you, believe me.
// The scope of the database is to store somewhere the sessions in order to // The scope of the database is to store somewhere the sessions in order to
// keep them after restarting the server, nothing more. // keep them after restarting the server, nothing more.
// //
// Synchronization are made automatically, you can register more than one session database // Synchronization are made automatically, you can register one using `UseDatabase`.
// but the first non-empty Load return data will be used as the session values.
// //
// // Look the `sessiondb` folder for databases implementations.
// Note: Expiration on Load is up to the database, meaning that:
// the database can decide how to retrieve and parse the expiration datetime
//
// I'll try to explain you the flow:
//
// .Start -> if session database attached then load from that storage and save to the memory, otherwise load from memory. The load from database is done once on the initialize of each session.
// .Get (important) -> load from memory,
// if database attached then it already loaded the values
// from database on the .Start action, so it will
// retrieve the data from the memory (fast)
// .Set -> set to the memory, if database attached then update the storage
// .Delete -> clear from memory, if database attached then update the storage
// .Destroy -> destroy from memory and client cookie,
// if database attached then update the storage with empty values,
// empty values means delete the storage with that specific session id.
// Using everything else except memory is slower than memory but database is
// fetched once at each session and its updated on every Set, Delete,
// Destroy at call-time.
// All other external sessions managers out there work different than Iris one as far as I know,
// you may find them more suited to your application, it depends.
type Database interface { type Database interface {
Load(sid string) RemoteStore // Acquire receives a session's lifetime from the database,
Sync(p SyncPayload) // if the return value is LifeTime{} then the session manager sets the life time based on the expiration duration lives in configuration.
Acquire(sid string, expires time.Duration) LifeTime
// Set sets a key value of a specific session.
// The "immutable" input argument depends on the store, it may not implement it at all.
Set(sid string, lifetime LifeTime, key string, value interface{}, immutable bool)
// Get retrieves a session value based on the key.
Get(sid string, key string) interface{}
// Visit loops through all session keys and values.
Visit(sid string, cb func(key string, value interface{}))
// Len returns the length of the session's entries (keys).
Len(sid string) int
// Delete removes a session key value based on its key.
Delete(sid string, key string) (deleted bool)
// Clear removes all session key values but it keeps the session entry.
Clear(sid string)
// Release destroys the session, it clears and removes the session entry,
// session manager will create a new session ID on the next request after this call.
Release(sid string)
} }
// New Idea, it should work faster for the most databases needs type mem struct {
// the only minus is that the databases is coupled with this package, they values map[string]*memstore.Store
// should import the kataras/iris/sessions package, but we don't use any mu sync.RWMutex
// database by-default so that's ok here.
// Action reports the specific action that the memory store
// sends to the database.
type Action uint32
const (
// ActionCreate occurs when add a key-value pair
// on the database session entry for the first time.
ActionCreate Action = iota
// ActionInsert occurs when add a key-value pair
// on the database session entry.
ActionInsert
// ActionUpdate occurs when modify an existing key-value pair
// on the database session entry.
ActionUpdate
// ActionDelete occurs when delete a specific value from
// a specific key from the database session entry.
ActionDelete
// ActionClear occurs when clear all values but keep the database session entry.
ActionClear
// ActionDestroy occurs when destroy,
// destroy is the action when clear all and remove the session entry from the database.
ActionDestroy
)
// SyncPayload reports the state of the session inside a database sync action.
type SyncPayload struct {
SessionID string
Action Action
// on insert it contains the new key and the value
// on update it contains the existing key and the new value
// on delete it contains the key (the value is nil)
// on clear it contains nothing (empty key, value is nil)
// on destroy it contains nothing (empty key, value is nil)
Value memstore.Entry
// Store contains the whole memory store, this store
// contains the current, updated from memory calls,
// session data (keys and values). This way
// the database has access to the whole session's data
// every time.
Store RemoteStore
} }
func newSyncPayload(session *Session, action Action) SyncPayload { var _ Database = (*mem)(nil)
return SyncPayload{
SessionID: session.sid, func newMemDB() Database { return &mem{values: make(map[string]*memstore.Store)} }
Action: action,
Store: RemoteStore{ func (s *mem) Acquire(sid string, expires time.Duration) LifeTime {
Values: session.values, s.mu.Lock()
Lifetime: session.lifetime, s.values[sid] = new(memstore.Store)
}, s.mu.Unlock()
} return LifeTime{}
} }
func syncDatabases(databases []Database, payload SyncPayload) { // immutable depends on the store, it may not implement it at all.
for i, n := 0, len(databases); i < n; i++ { func (s *mem) Set(sid string, lifetime LifeTime, key string, value interface{}, immutable bool) {
databases[i].Sync(payload) s.mu.RLock()
} s.values[sid].Save(key, value, immutable)
s.mu.RUnlock()
} }
// RemoteStore is a helper which is a wrapper func (s *mem) Get(sid string, key string) interface{} {
// for the store, it can be used as the session "table" which will be return s.values[sid].Get(key)
// saved to the session database.
type RemoteStore struct {
// Values contains the whole memory store, this store
// contains the current, updated from memory calls,
// session data (keys and values). This way
// the database has access to the whole session's data
// every time.
Values memstore.Store
// on insert it contains the expiration datetime
// on update it contains the new expiration datetime(if updated or the old one)
// on delete it will be zero
// on clear it will be zero
// on destroy it will be zero
Lifetime LifeTime
} }
// Serialize returns the byte representation of this RemoteStore. func (s *mem) Visit(sid string, cb func(key string, value interface{})) {
func (s RemoteStore) Serialize() ([]byte, error) { s.values[sid].Visit(cb)
w := new(bytes.Buffer)
err := encode(s, w)
return w.Bytes(), err
} }
// encode accepts a store and writes func (s *mem) Len(sid string) int {
// as series of bytes to the "w" writer. return s.values[sid].Len()
func encode(s RemoteStore, w io.Writer) error {
enc := gob.NewEncoder(w)
err := enc.Encode(s)
return err
} }
// DecodeRemoteStore accepts a series of bytes and returns func (s *mem) Delete(sid string, key string) (deleted bool) {
// the store. s.mu.RLock()
func DecodeRemoteStore(b []byte) (store RemoteStore, err error) { deleted = s.values[sid].Remove(key)
dec := gob.NewDecoder(bytes.NewBuffer(b)) s.mu.RUnlock()
err = dec.Decode(&store)
return return
} }
func (s *mem) Clear(sid string) {
s.mu.RLock()
s.values[sid].Reset()
s.mu.RUnlock()
}
func (s *mem) Release(sid string) {
s.mu.Lock()
delete(s.values, sid)
s.mu.Unlock()
}

View File

@ -63,3 +63,9 @@ func (lt *LifeTime) HasExpired() bool {
return lt.Time.Before(time.Now()) return lt.Time.Before(time.Now())
} }
// DurationUntilExpiration returns the duration until expires, it can return negative number if expired,
// a call to `HasExpired` may be useful before calling this `Dur` function.
func (lt *LifeTime) DurationUntilExpiration() time.Duration {
return time.Until(lt.Time)
}

View File

@ -3,8 +3,6 @@ package sessions
import ( import (
"sync" "sync"
"time" "time"
"github.com/kataras/iris/core/memstore"
) )
type ( type (
@ -14,25 +12,24 @@ type (
// we don't use RWMutex because all actions have read and write at the same action function. // we don't use RWMutex because all actions have read and write at the same action function.
// (or write to a *Session's value which is race if we don't lock) // (or write to a *Session's value which is race if we don't lock)
// narrow locks are fasters but are useless here. // narrow locks are fasters but are useless here.
mu sync.Mutex mu sync.Mutex
sessions map[string]*Session sessions map[string]*Session
databases []Database db Database
} }
) )
// newProvider returns a new sessions provider // newProvider returns a new sessions provider
func newProvider() *provider { func newProvider() *provider {
return &provider{ return &provider{
sessions: make(map[string]*Session, 0), sessions: make(map[string]*Session, 0),
databases: make([]Database, 0), db: newMemDB(),
} }
} }
// RegisterDatabase adds a session database // RegisterDatabase sets a session database.
// a session db doesn't have write access
func (p *provider) RegisterDatabase(db Database) { func (p *provider) RegisterDatabase(db Database) {
p.mu.Lock() // for any case p.mu.Lock() // for any case
p.databases = append(p.databases, db) p.db = db
p.mu.Unlock() p.mu.Unlock()
} }
@ -42,7 +39,8 @@ func (p *provider) newSession(sid string, expires time.Duration) *Session {
p.Destroy(sid) p.Destroy(sid)
} }
values, lifetime := p.loadSessionFromDB(sid) lifetime := p.db.Acquire(sid, expires)
// simple and straight: // simple and straight:
if !lifetime.IsZero() { if !lifetime.IsZero() {
// if stored time is not zero // if stored time is not zero
@ -58,89 +56,16 @@ func (p *provider) newSession(sid string, expires time.Duration) *Session {
lifetime.Begin(expires, onExpire) lifetime.Begin(expires, onExpire)
} }
// I ended up without the need of a duration field on lifetime,
// but these are some of my previous comments, they will be required for any future change.
//
// OK I think go has a bug when gob on embedded time.Time
// even if we `gob.Register(LifeTime)`
// the OriginalDuration is not saved to the gob file and it cannot be retrieved, it's always 0.
// But if we do not embed the `time.Time` inside the `LifeTime` then
// it's working.
// i.e type LifeTime struct { time.Time; OriginalDuration time.Duration} -> this doesn't
// type LifeTime struct {Time time.Time; OriginalDuration time.Duration} -> this works
// So we have two options:
// 1. don't embed the time.Time -> we will have to use lifetime.Time to get its functions, which doesn't seems right to me
// 2. embed the time.Time and compare their times with `lifetime.After(time.Now().Add(expires))`, it seems right but it
// should be slower.
//
// I'll use the 1. and put some common time.Time functions, like After, IsZero on the `LifeTime` type too.
//
// if db exists but its lifetime is bigger than the expires (very raire,
// the source code should be compatible with the databases,
// should we print a warning to the user? it is his/her fault
// use the database's lifetime or the configurated?
// if values.Len() > 0 && lifetime.OriginalDuration != expires {
// golog.Warnf(`session database: stored expire time(dur=%d) is differnet than the configuration(dur=%d)
// application will use the configurated one`, lifetime.OriginalDuration, expires)
// lifetime.Reset(expires)
// }
sess := &Session{ sess := &Session{
sid: sid, sid: sid,
provider: p, provider: p,
values: values,
flashes: make(map[string]*flashMessage), flashes: make(map[string]*flashMessage),
lifetime: lifetime, Lifetime: lifetime,
} }
return sess return sess
} }
func (p *provider) loadSessionFromDB(sid string) (memstore.Store, LifeTime) {
var (
store memstore.Store
lifetime LifeTime
firstValidIdx = 1
)
for i, n := 0, len(p.databases); i < n; i++ {
storeDB := p.databases[i].Load(sid)
if storeDB.Lifetime.HasExpired() { // if expired then skip this db
firstValidIdx++
continue
}
if lifetime.IsZero() {
// update the lifetime to the most valid
lifetime = storeDB.Lifetime
}
if n == firstValidIdx {
// if one database then set the store as it is
store = storeDB.Values
} else {
// else append this database's key-value pairs
// to the store
storeDB.Values.Visit(func(key string, value interface{}) {
store.Save(key, value, false)
})
}
}
// default to memstore if no other store given.
// if store == nil {
// store = &memstore.Store{}
// }
// Note: if one database and it's being expired then the lifetime will be zero(unlimited)
// this by itself is wrong but on the `newSession` we make check of this case too and update the lifetime
// if the configuration has expiration registered.
/// TODO: bug on destroy doesn't being remove the file
// we will have to see it, it's not db's problem it's here on provider destroy or lifetime onExpire.
return store, lifetime
}
// Init creates the session and returns it // Init creates the session and returns it
func (p *provider) Init(sid string, expires time.Duration) *Session { func (p *provider) Init(sid string, expires time.Duration) *Session {
newSession := p.newSession(sid, expires) newSession := p.newSession(sid, expires)
@ -165,7 +90,7 @@ func (p *provider) UpdateExpiration(sid string, expires time.Duration) bool {
return false return false
} }
sess.lifetime.Shift(expires) sess.Lifetime.Shift(expires)
return true return true
} }
@ -207,7 +132,5 @@ func (p *provider) DestroyAll() {
func (p *provider) deleteSession(sess *Session) { func (p *provider) deleteSession(sess *Session) {
delete(p.sessions, sess.sid) delete(p.sessions, sess.sid)
if len(p.databases) > 0 { p.db.Release(sess.sid)
syncDatabases(p.databases, newSyncPayload(sess, ActionDestroy))
}
} }

View File

@ -5,7 +5,6 @@ import (
"sync" "sync"
"github.com/kataras/iris/core/errors" "github.com/kataras/iris/core/errors"
"github.com/kataras/iris/core/memstore"
) )
type ( type (
@ -17,10 +16,9 @@ type (
Session struct { Session struct {
sid string sid string
isNew bool isNew bool
values memstore.Store // here are the session's values, managed by memstore.
flashes map[string]*flashMessage flashes map[string]*flashMessage
mu sync.RWMutex // for flashes. mu sync.RWMutex // for flashes.
lifetime LifeTime Lifetime LifeTime
provider *provider provider *provider
} }
@ -56,7 +54,7 @@ func (s *Session) IsNew() bool {
// Get returns a value based on its "key". // Get returns a value based on its "key".
func (s *Session) Get(key string) interface{} { func (s *Session) Get(key string) interface{} {
return s.values.Get(key) return s.provider.db.Get(s.sid, key)
} }
// when running on the session manager removes any 'old' flash messages. // when running on the session manager removes any 'old' flash messages.
@ -366,9 +364,9 @@ func (s *Session) GetBooleanDefault(key string, defaultValue bool) bool {
// GetAll returns a copy of all session's values. // GetAll returns a copy of all session's values.
func (s *Session) GetAll() map[string]interface{} { func (s *Session) GetAll() map[string]interface{} {
items := make(map[string]interface{}, s.values.Len()) items := make(map[string]interface{}, s.provider.db.Len(s.sid))
s.mu.RLock() s.mu.RLock()
s.values.Visit(func(key string, value interface{}) { s.provider.db.Visit(s.sid, func(key string, value interface{}) {
items[key] = value items[key] = value
}) })
s.mu.RUnlock() s.mu.RUnlock()
@ -388,39 +386,17 @@ func (s *Session) GetFlashes() map[string]interface{} {
return flashes return flashes
} }
// VisitAll loop each one entry and calls the callback function func(key,value) // Visit loops each of the entries and calls the callback function func(key, value).
func (s *Session) VisitAll(cb func(k string, v interface{})) { func (s *Session) Visit(cb func(k string, v interface{})) {
s.values.Visit(cb) s.provider.db.Visit(s.sid, cb)
} }
func (s *Session) set(key string, value interface{}, immutable bool) { func (s *Session) set(key string, value interface{}, immutable bool) {
s.provider.db.Set(s.sid, s.Lifetime, key, value, immutable)
isFirst := s.values.Len() == 0
entry, isNew := s.values.Save(key, value, immutable)
s.mu.Lock() s.mu.Lock()
s.isNew = false s.isNew = false
s.mu.Unlock() s.mu.Unlock()
if len(s.provider.databases) > 0 {
action := ActionCreate // defaults to create, means the first insert.
if !isFirst {
// we could use s.isNew
// which is setted at sessions.go#Start when values are empty
// but here we want the specific key-value pair's state.
if isNew {
action = ActionInsert
} else {
action = ActionUpdate
}
}
p := newSyncPayload(s, action)
p.Value = entry
syncDatabases(s.provider.databases, p)
}
} }
// Set fills the session with an entry "value", based on its "key". // Set fills the session with an entry "value", based on its "key".
@ -467,17 +443,11 @@ func (s *Session) SetFlash(key string, value interface{}) {
// Delete removes an entry by its key, // Delete removes an entry by its key,
// returns true if actually something was removed. // returns true if actually something was removed.
func (s *Session) Delete(key string) bool { func (s *Session) Delete(key string) bool {
s.mu.Lock() removed := s.provider.db.Delete(s.sid, key)
removed := s.values.Remove(key)
if removed { if removed {
s.mu.Lock()
s.isNew = false s.isNew = false
} s.mu.Unlock()
s.mu.Unlock()
if len(s.provider.databases) > 0 {
p := newSyncPayload(s, ActionDelete)
p.Value = memstore.Entry{Key: key}
syncDatabases(s.provider.databases, p)
} }
return removed return removed
@ -493,14 +463,9 @@ func (s *Session) DeleteFlash(key string) {
// Clear removes all entries. // Clear removes all entries.
func (s *Session) Clear() { func (s *Session) Clear() {
s.mu.Lock() s.mu.Lock()
s.values.Reset() s.provider.db.Clear(s.sid)
s.isNew = false s.isNew = false
s.mu.Unlock() s.mu.Unlock()
if len(s.provider.databases) > 0 {
p := newSyncPayload(s, ActionClear)
syncDatabases(s.provider.databases, p)
}
} }
// ClearFlashes removes all flash messages. // ClearFlashes removes all flash messages.

View File

@ -3,6 +3,7 @@ package badger
import ( import (
"os" "os"
"runtime" "runtime"
"time"
"github.com/kataras/golog" "github.com/kataras/golog"
"github.com/kataras/iris/core/errors" "github.com/kataras/iris/core/errors"
@ -25,6 +26,8 @@ type Database struct {
Service *badger.DB Service *badger.DB
} }
var _ sessions.Database = (*Database)(nil)
// New creates and returns a new badger(key-value file-based) storage // New creates and returns a new badger(key-value file-based) storage
// instance based on the "directoryPath". // instance based on the "directoryPath".
// DirectoryPath should is the directory which the badger database will store the sessions, // DirectoryPath should is the directory which the badger database will store the sessions,
@ -32,9 +35,8 @@ type Database struct {
// //
// It will remove any old session files. // It will remove any old session files.
func New(directoryPath string) (*Database, error) { func New(directoryPath string) (*Database, error) {
if directoryPath == "" { if directoryPath == "" {
return nil, errors.New("dir is missing") return nil, errors.New("directoryPath is missing")
} }
lindex := directoryPath[len(directoryPath)-1] lindex := directoryPath[len(directoryPath)-1]
@ -57,134 +59,180 @@ func New(directoryPath string) (*Database, error) {
return nil, err return nil, err
} }
return NewFromDB(service) return NewFromDB(service), nil
} }
// NewFromDB same as `New` but accepts an already-created custom badger connection instead. // NewFromDB same as `New` but accepts an already-created custom badger connection instead.
func NewFromDB(service *badger.DB) (*Database, error) { func NewFromDB(service *badger.DB) *Database {
if service == nil {
return nil, errors.New("underline database is missing")
}
db := &Database{Service: service} db := &Database{Service: service}
runtime.SetFinalizer(db, closeDB) 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() (err error) {
rep := errors.NewReporter()
txn := db.Service.NewTransaction(true)
defer txn.Commit(nil)
iter := txn.NewIterator(badger.DefaultIteratorOptions)
defer iter.Close()
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()
b, err := item.Value()
if rep.AddErr(err) {
continue
}
storeDB, err := sessions.DecodeRemoteStore(b)
if rep.AddErr(err) {
continue
}
if storeDB.Lifetime.HasExpired() {
if err := txn.Delete(item.Key()); err != nil {
rep.AddErr(err)
}
}
}
return rep.Return()
}
// Async is DEPRECATED
// if it was true then it could use different to update the back-end storage, now it does nothing.
func (db *Database) Async(useGoRoutines bool) *Database {
return db return db
} }
// Load loads the sessions from the badger(key-value file-based) session storage. // Acquire receives a session's lifetime from the database,
func (db *Database) Load(sid string) (storeDB sessions.RemoteStore) { // if the return value is LifeTime{} then the session manager sets the life time based on the expiration duration lives in configuration.
func (db *Database) Acquire(sid string, expires time.Duration) sessions.LifeTime {
txn := db.Service.NewTransaction(true)
defer txn.Commit(nil)
bsid := []byte(sid) bsid := []byte(sid)
txn := db.Service.NewTransaction(false)
defer txn.Discard()
item, err := txn.Get(bsid) item, err := txn.Get(bsid)
if err == nil {
// found, return the expiration.
return sessions.LifeTime{Time: time.Unix(int64(item.ExpiresAt()), 0)}
}
// not found, create an entry with ttl and return an empty lifetime, session manager will do its job.
if err != nil { if err != nil {
// Key not found, don't report this, session manager will create a new session as it should. if err == badger.ErrKeyNotFound {
// create it and set the expiration, we don't care about the value there.
err = txn.SetWithTTL(bsid, bsid, expires)
}
}
if err != nil {
golog.Error(err)
}
return sessions.LifeTime{} // session manager will handle the rest.
}
var delim = byte('*')
func makeKey(sid, key string) []byte {
return append([]byte(sid), append([]byte(key), delim)...)
}
// Set sets a key value of a specific session.
// Ignore the "immutable".
func (db *Database) Set(sid string, lifetime sessions.LifeTime, key string, value interface{}, immutable bool) {
valueBytes, err := sessions.DefaultTranscoder.Marshal(value)
if err != nil {
golog.Error(err)
return return
} }
b, err := item.Value() err = db.Service.Update(func(txn *badger.Txn) error {
return txn.SetWithTTL(makeKey(sid, key), valueBytes, lifetime.DurationUntilExpiration())
// return txn.Set(makeKey(sid, key), valueBytes)
})
if err != nil { if err != nil {
golog.Errorf("error while trying to get the serialized session(%s) from the remote store: %v", sid, err) golog.Error(err)
return
} }
}
storeDB, err = sessions.DecodeRemoteStore(b) // decode the whole value, as a remote store // Get retrieves a session value based on the key.
if err != nil { func (db *Database) Get(sid string, key string) (value interface{}) {
golog.Errorf("error while trying to load from the remote store: %v", err) err := db.Service.View(func(txn *badger.Txn) error {
item, err := txn.Get(makeKey(sid, key))
if err != nil {
return err
}
// item.ValueCopy
valueBytes, err := item.Value()
if err != nil {
return err
}
return sessions.DefaultTranscoder.Unmarshal(valueBytes, &value)
})
if err != nil && err != badger.ErrKeyNotFound {
golog.Error(err)
return nil
} }
return return
} }
// Sync syncs the database with the session's (memory) store. // Visit loops through all session keys and values.
func (db *Database) Sync(p sessions.SyncPayload) { func (db *Database) Visit(sid string, cb func(key string, value interface{})) {
db.sync(p) prefix := append([]byte(sid), delim)
}
func (db *Database) sync(p sessions.SyncPayload) { txn := db.Service.NewTransaction(false)
bsid := []byte(p.SessionID) defer txn.Discard()
if p.Action == sessions.ActionDestroy { iter := txn.NewIterator(badger.DefaultIteratorOptions)
if err := db.destroy(bsid); err != nil { defer iter.Close()
golog.Errorf("error while destroying a session(%s) from badger: %v",
p.SessionID, err) for iter.Rewind(); iter.ValidForPrefix(prefix); iter.Next() {
item := iter.Item()
valueBytes, err := item.Value()
if err != nil {
golog.Error(err)
continue
} }
return
}
s, err := p.Store.Serialize() var value interface{}
if err != nil { if err = sessions.DefaultTranscoder.Unmarshal(valueBytes, &value); err != nil {
golog.Errorf("error while serializing the remote store: %v", err) golog.Error(err)
} continue
}
txn := db.Service.NewTransaction(true) cb(string(item.Key()), value)
err = txn.Set(bsid, s)
if err != nil {
txn.Discard()
golog.Errorf("error while trying to save the session(%s) to the database: %v", p.SessionID, err)
return
}
if err := txn.Commit(nil); err != nil { // Commit will call the Discard automatically.
golog.Errorf("error while committing the session(%s) changes to the database: %v", p.SessionID, err)
} }
} }
func (db *Database) destroy(bsid []byte) error { var iterOptionsNoValues = badger.IteratorOptions{
txn := db.Service.NewTransaction(true) PrefetchValues: false,
PrefetchSize: 100,
Reverse: false,
AllVersions: false,
}
err := txn.Delete(bsid) // Len returns the length of the session's entries (keys).
if err != nil { func (db *Database) Len(sid string) (n int) {
return err prefix := append([]byte(sid), delim)
txn := db.Service.NewTransaction(false)
iter := txn.NewIterator(iterOptionsNoValues)
for iter.Rewind(); iter.ValidForPrefix(prefix); iter.Next() {
n++
} }
return txn.Commit(nil) iter.Close()
txn.Discard()
return
}
// Delete removes a session key value based on its key.
func (db *Database) Delete(sid string, key string) (deleted bool) {
txn := db.Service.NewTransaction(true)
err := txn.Delete(makeKey(sid, key))
if err != nil {
golog.Error(err)
}
txn.Commit(nil)
return err == nil
}
// Clear removes all session key values but it keeps the session entry.
func (db *Database) Clear(sid string) {
prefix := append([]byte(sid), delim)
txn := db.Service.NewTransaction(true)
defer txn.Commit(nil)
iter := txn.NewIterator(iterOptionsNoValues)
defer iter.Close()
for iter.Rewind(); iter.ValidForPrefix(prefix); iter.Next() {
txn.Delete(iter.Item().Key())
}
}
// Release destroys the session, it clears and removes the session entry,
// session manager will create a new session ID on the next request after this call.
func (db *Database) Release(sid string) {
// clear all $sid-$key.
db.Clear(sid)
// and remove the $sid.
txn := db.Service.NewTransaction(true)
txn.Delete([]byte(sid))
txn.Commit(nil)
} }
// Close shutdowns the badger connection. // Close shutdowns the badger connection.

View File

@ -1,230 +0,0 @@
package boltdb
import (
"bytes"
"os"
"path/filepath"
"runtime"
"time"
"github.com/coreos/bbolt"
"github.com/kataras/golog"
"github.com/kataras/iris/core/errors"
"github.com/kataras/iris/sessions"
)
// DefaultFileMode used as the default database's "fileMode"
// for creating the sessions directory path, opening and write
// the session boltdb(file-based) storage.
var (
DefaultFileMode = 0755
)
// Database the BoltDB(file-based) session storage.
type Database struct {
table []byte
// Service is the underline BoltDB database connection,
// it's initialized at `New` or `NewFromDB`.
// Can be used to get stats.
Service *bolt.DB
}
var (
// ErrOptionsMissing returned on `New` when path or tableName are empty.
ErrOptionsMissing = errors.New("required options are missing")
)
// New creates and returns a new BoltDB(file-based) storage
// instance based on the "path".
// Path should include the filename and the directory(aka fullpath), i.e sessions/store.db.
//
// It will remove any old session files.
func New(path string, fileMode os.FileMode, bucketName string) (*Database, error) {
if path == "" || bucketName == "" {
return nil, ErrOptionsMissing
}
if fileMode <= 0 {
fileMode = os.FileMode(DefaultFileMode)
}
// create directories if necessary
if err := os.MkdirAll(filepath.Dir(path), fileMode); err != nil {
golog.Errorf("error while trying to create the necessary directories for %s: %v", path, err)
return nil, err
}
service, err := bolt.Open(path, 0600,
&bolt.Options{Timeout: 15 * time.Second},
)
if err != nil {
golog.Errorf("unable to initialize the BoltDB-based session database: %v", err)
return nil, err
}
return NewFromDB(service, bucketName)
}
// NewFromDB same as `New` but accepts an already-created custom boltdb connection instead.
func NewFromDB(service *bolt.DB, bucketName string) (*Database, error) {
if bucketName == "" {
return nil, ErrOptionsMissing
}
bucket := []byte(bucketName)
service.Update(func(tx *bolt.Tx) (err error) {
_, err = tx.CreateBucketIfNotExists(bucket)
return
})
db := &Database{table: bucket, 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 {
err := db.Service.Update(func(tx *bolt.Tx) error {
b := db.getBucket(tx)
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
if len(k) == 0 { // empty key, continue to the next pair
continue
}
storeDB, err := sessions.DecodeRemoteStore(v)
if err != nil {
continue
}
if storeDB.Lifetime.HasExpired() {
if err := c.Delete(); err != nil {
golog.Warnf("troubles when cleanup a session remote store from BoltDB: %v", err)
}
}
}
return nil
})
return err
}
// Async is DEPRECATED
// if it was true then it could use different to update the back-end storage, now it does nothing.
func (db *Database) Async(useGoRoutines bool) *Database {
return db
}
// Load loads the sessions from the BoltDB(file-based) session storage.
func (db *Database) Load(sid string) (storeDB sessions.RemoteStore) {
bsid := []byte(sid)
err := db.Service.View(func(tx *bolt.Tx) (err error) {
// db.getSessBucket(tx, sid)
b := db.getBucket(tx)
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
if len(k) == 0 { // empty key, continue to the next pair
continue
}
if bytes.Equal(k, bsid) { // session id should be the name of the key-value pair
storeDB, err = sessions.DecodeRemoteStore(v) // decode the whole value, as a remote store
break
}
}
return
})
if err != nil {
golog.Errorf("error while trying to load from the remote store: %v", err)
}
return
}
// Sync syncs the database with the session's (memory) store.
func (db *Database) Sync(p sessions.SyncPayload) {
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.Update(func(tx *bolt.Tx) error {
return db.getBucket(tx).Put(bsid, s)
})
if err != nil {
golog.Errorf("error while writing the session bucket: %v", err)
}
}
func (db *Database) destroy(bsid []byte) error {
return db.Service.Update(func(tx *bolt.Tx) error {
return db.getBucket(tx).Delete(bsid)
})
}
// we store the whole data to the key-value pair of the root bucket
// so we don't need a separate bucket for each session
// this method could be faster if we had large data to store
// but with sessions we recommend small amount of data, so the method finally chosen
// is faster (decode/encode the whole store + lifetime and return it as it's)
//
// func (db *Database) getSessBucket(tx *bolt.Tx, sid string) (*bolt.Bucket, error) {
// table, err := db.getBucket(tx).CreateBucketIfNotExists([]byte(sid))
// return table, err
// }
func (db *Database) getBucket(tx *bolt.Tx) *bolt.Bucket {
return tx.Bucket(db.table)
}
// Len reports the number of sessions that are stored to the this BoltDB table.
func (db *Database) Len() (num int) {
db.Service.View(func(tx *bolt.Tx) error {
// Assume bucket exists and has keys
b := db.getBucket(tx)
if b == nil {
return nil
}
b.ForEach(func([]byte, []byte) error {
num++
return nil
})
return nil
})
return
}
// Close shutdowns the BoltDB 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 BoltDB connection: %v", err)
}
return err
}

View File

@ -1,203 +0,0 @@
package file
import (
"io/ioutil"
"os"
"path/filepath"
"github.com/kataras/golog"
"github.com/kataras/iris/core/errors"
"github.com/kataras/iris/sessions"
)
// DefaultFileMode used as the default database's "fileMode"
// for creating the sessions directory path, opening and write the session file.
var (
DefaultFileMode = 0755
)
// Database is the basic file-storage session database.
//
// What it does
// It removes old(expired) session files, at init (`Cleanup`).
// It creates a session file on the first inserted key-value session data.
// It removes a session file on destroy.
// It sync the session file to the session's memstore on any other action (insert, delete, clear).
// It automatically remove the session files on runtime when a session is expired.
//
// Remember: sessions are not a storage for large data, everywhere: on any platform on any programming language.
type Database struct {
dir string
fileMode os.FileMode // defaults to DefaultFileMode if missing.
}
// 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]
if lindex != os.PathSeparator && lindex != '/' {
directoryPath += string(os.PathSeparator)
}
if fileMode <= 0 {
fileMode = os.FileMode(DefaultFileMode)
}
// create directories if necessary
if err := os.MkdirAll(directoryPath, fileMode); err != nil {
return nil, err
}
db := &Database{dir: directoryPath, fileMode: fileMode}
return db, db.Cleanup()
}
// Cleanup removes any invalid(have expired) session files, it's being called automatically on `New` as well.
func (db *Database) Cleanup() error {
return filepath.Walk(db.dir, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
sessPath := path
storeDB, _ := db.load(sessPath) // we don't care about errors here, the file may be not a session a file at all.
if storeDB.Lifetime.HasExpired() {
os.Remove(path)
}
return nil
})
}
// FileMode for creating the sessions directory path, opening and write the session file.
//
// Defaults to 0755.
func (db *Database) FileMode(fileMode uint32) *Database {
db.fileMode = os.FileMode(fileMode)
return db
}
// Async is DEPRECATED
// if it was true then it could use different to update the back-end storage, now it does nothing.
func (db *Database) Async(useGoRoutines bool) *Database {
return db
}
func (db *Database) sessPath(sid string) string {
return filepath.Join(db.dir, sid)
}
// Load loads the values from the storage and returns them
func (db *Database) Load(sid string) sessions.RemoteStore {
sessPath := db.sessPath(sid)
store, err := db.load(sessPath)
if err != nil {
golog.Error(err.Error())
}
return store
}
func (db *Database) load(fileName string) (storeDB sessions.RemoteStore, loadErr error) {
f, err := os.OpenFile(fileName, os.O_RDONLY, db.fileMode)
if err != nil {
// we don't care if filepath doesn't exists yet, it will be created later on.
return
}
defer f.Close()
contents, err := ioutil.ReadAll(f)
if err != nil {
loadErr = errors.New("error while reading the session file's data: %v").Format(err)
return
}
storeDB, err = sessions.DecodeRemoteStore(contents)
if err != nil { // we care for this error only
loadErr = errors.New("load error: %v").Format(err)
return
}
return
}
// Sync syncs the database.
func (db *Database) Sync(p sessions.SyncPayload) {
db.sync(p)
}
func (db *Database) sync(p sessions.SyncPayload) {
// if destroy then remove the file from the disk
if p.Action == sessions.ActionDestroy {
if err := db.destroy(p.SessionID); err != nil {
golog.Errorf("error while destroying and removing the session file: %v", err)
}
return
}
if err := db.override(p.SessionID, p.Store); err != nil {
golog.Errorf("error while writing the session file: %v", err)
}
}
// good idea but doesn't work, it is not just an array of entries
// which can be appended with the gob...anyway session data should be small so we don't have problem
// with that:
// on insert new data, it appends to the file
// func (db *Database) insert(sid string, entry memstore.Entry) error {
// f, err := os.OpenFile(
// db.sessPath(sid),
// os.O_WRONLY|os.O_CREATE|os.O_RDWR|os.O_APPEND,
// db.fileMode,
// )
// if err != nil {
// return err
// }
// if _, err := f.Write(serializeEntry(entry)); err != nil {
// f.Close()
// return err
// }
// return f.Close()
// }
// removes all entries but keeps the file.
// func (db *Database) clearAll(sid string) error {
// return ioutil.WriteFile(
// db.sessPath(sid),
// []byte{},
// db.fileMode,
// )
// }
// on update, remove and clear, it re-writes the file to the current values(may empty).
func (db *Database) override(sid string, store sessions.RemoteStore) error {
s, err := store.Serialize()
if err != nil {
return err
}
return ioutil.WriteFile(
db.sessPath(sid),
s,
db.fileMode,
)
}
// on destroy, it removes the file
func (db *Database) destroy(sid string) error {
return db.expireSess(sid)
}
func (db *Database) expireSess(sid string) error {
sessPath := db.sessPath(sid)
return os.Remove(sessPath)
}

View File

@ -1,183 +0,0 @@
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
}
// 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 leveldb 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 is DEPRECATED
// if it was true then it could use different to update the back-end storage, now it does nothing.
func (db *Database) Async(useGoRoutines bool) *Database {
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) {
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 leveldb: %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
}

View File

@ -14,9 +14,17 @@ type Database struct {
redis *service.Service redis *service.Service
} }
var _ sessions.Database = (*Database)(nil)
// New returns a new redis database. // New returns a new redis database.
func New(cfg ...service.Config) *Database { func New(cfg ...service.Config) *Database {
db := &Database{redis: service.New(cfg...)} db := &Database{redis: service.New(cfg...)}
db.redis.Connect()
_, err := db.redis.PingPong()
if err != nil {
golog.Debugf("error connecting to redis: %v", err)
return nil
}
runtime.SetFinalizer(db, closeDB) runtime.SetFinalizer(db, closeDB)
return db return db
} }
@ -26,73 +34,119 @@ func (db *Database) Config() *service.Config {
return db.redis.Config return db.redis.Config
} }
// Async is DEPRECATED // Acquire receives a session's lifetime from the database,
// if it was true then it could use different to update the back-end storage, now it does nothing. // if the return value is LifeTime{} then the session manager sets the life time based on the expiration duration lives in configuration.
func (db *Database) Async(useGoRoutines bool) *Database { func (db *Database) Acquire(sid string, expires time.Duration) sessions.LifeTime {
return db seconds, hasExpiration, found := db.redis.TTL(sid)
if !found {
// not found, create an entry with ttl and return an empty lifetime, session manager will do its job.
if err := db.redis.Set(sid, sid, int64(expires.Seconds())); err != nil {
golog.Debug(err)
}
return sessions.LifeTime{} // session manager will handle the rest.
}
if !hasExpiration {
return sessions.LifeTime{}
}
return sessions.LifeTime{Time: time.Now().Add(time.Duration(seconds) * time.Second)}
} }
// Load loads the values to the underline. const delim = "_"
func (db *Database) Load(sid string) (storeDB sessions.RemoteStore) {
// values := make(map[string]interface{})
if !db.redis.Connected { //yes, check every first time's session for valid redis connection func makeKey(sid, key string) string {
db.redis.Connect() return sid + delim + key
_, err := db.redis.PingPong() }
if err != nil {
golog.Errorf("redis database error on connect: %v", err) // Set sets a key value of a specific session.
return // Ignore the "immutable".
} func (db *Database) Set(sid string, lifetime sessions.LifeTime, key string, value interface{}, immutable bool) {
valueBytes, err := sessions.DefaultTranscoder.Marshal(value)
if err != nil {
golog.Error(err)
return
} }
// fetch the values from this session id and copy-> store them if err = db.redis.Set(makeKey(sid, key), valueBytes, int64(lifetime.DurationUntilExpiration().Seconds())); err != nil {
storeMaybe, err := db.redis.Get(sid) golog.Debug(err)
// exists
if err == nil {
storeB, ok := storeMaybe.([]byte)
if !ok {
golog.Errorf("something wrong, store should be stored as []byte but stored as %#v", storeMaybe)
return
}
storeDB, err = sessions.DecodeRemoteStore(storeB) // decode the whole value, as a remote store
if err != nil {
golog.Errorf(`error while trying to load session values(%s) from redis:
the retrieved value is not a sessions.RemoteStore type, please report that as bug, that should never occur: %v`,
sid, err)
}
} }
}
// Get retrieves a session value based on the key.
func (db *Database) Get(sid string, key string) (value interface{}) {
db.get(makeKey(sid, key), &value)
return return
} }
// Sync syncs the database. func (db *Database) get(key string, outPtr interface{}) {
func (db *Database) Sync(p sessions.SyncPayload) { data, err := db.redis.Get(key)
db.sync(p)
}
func (db *Database) sync(p sessions.SyncPayload) {
if p.Action == sessions.ActionDestroy {
db.redis.Delete(p.SessionID)
return
}
storeB, err := p.Store.Serialize()
if err != nil { if err != nil {
golog.Error("error while encoding the remote session store") // not found.
return return
} }
// not expire if zero if err = sessions.DefaultTranscoder.Unmarshal(data.([]byte), outPtr); err != nil {
seconds := 0 golog.Debugf("unable to unmarshal value of key: '%s': %v", key, err)
if lifetime := p.Store.Lifetime; !lifetime.IsZero() {
seconds = int(lifetime.Sub(time.Now()).Seconds())
} }
db.redis.Set(p.SessionID, storeB, seconds)
} }
// Close shutdowns the redis connection. func (db *Database) keys(sid string) []string {
keys, err := db.redis.GetKeys(sid + delim)
if err != nil {
golog.Debugf("unable to get all redis keys of session '%s': %v", sid, err)
return nil
}
return keys
}
// Visit loops through all session keys and values.
func (db *Database) Visit(sid string, cb func(key string, value interface{})) {
keys := db.keys(sid)
for _, key := range keys {
var value interface{} // new value each time, we don't know what user will do in "cb".
db.get(key, &value)
cb(key, value)
}
}
// Len returns the length of the session's entries (keys).
func (db *Database) Len(sid string) (n int) {
return len(db.keys(sid))
}
// Delete removes a session key value based on its key.
func (db *Database) Delete(sid string, key string) (deleted bool) {
err := db.redis.Delete(makeKey(sid, key))
if err != nil {
golog.Error(err)
}
return err == nil
}
// Clear removes all session key values but it keeps the session entry.
func (db *Database) Clear(sid string) {
keys := db.keys(sid)
for _, key := range keys {
if err := db.redis.Delete(key); err != nil {
golog.Debugf("unable to delete session '%s' value of key: '%s': %v", sid, key, err)
}
}
}
// Release destroys the session, it clears and removes the session entry,
// session manager will create a new session ID on the next request after this call.
func (db *Database) Release(sid string) {
// clear all $sid-$key.
db.Clear(sid)
// and remove the $sid.
db.redis.Delete(sid)
}
// Close terminates the redis connection.
func (db *Database) Close() error { func (db *Database) Close() error {
return closeDB(db) return closeDB(db)
} }

View File

@ -1,9 +1,10 @@
package service package service
import ( import (
"fmt"
"time" "time"
"github.com/garyburd/redigo/redis" "github.com/gomodule/redigo/redis"
"github.com/kataras/iris/core/errors" "github.com/kataras/iris/core/errors"
) )
@ -44,7 +45,7 @@ func (r *Service) CloseConnection() error {
// Set sets a key-value to the redis store. // Set sets a key-value to the redis store.
// The expiration is setted by the MaxAgeSeconds. // The expiration is setted by the MaxAgeSeconds.
func (r *Service) Set(key string, value interface{}, secondsLifetime int) (err error) { func (r *Service) Set(key string, value interface{}, secondsLifetime int64) (err error) {
c := r.pool.Get() c := r.pool.Get()
defer c.Close() defer c.Close()
if c.Err() != nil { if c.Err() != nil {
@ -81,6 +82,23 @@ func (r *Service) Get(key string) (interface{}, error) {
return redisVal, nil return redisVal, nil
} }
// TTL returns the seconds to expire, if the key has expiration and error if action failed.
// Read more at: https://redis.io/commands/ttl
func (r *Service) TTL(key string) (seconds int64, hasExpiration bool, ok bool) {
c := r.pool.Get()
defer c.Close()
redisVal, err := c.Do("TTL", r.Config.Prefix+key)
if err != nil {
return -2, false, false
}
seconds = redisVal.(int64)
// if -1 means the key has unlimited life time.
hasExpiration = seconds == -1
// if -2 means key does not exist.
ok = (c.Err() != nil || seconds == -2)
return
}
// GetAll returns all redis entries using the "SCAN" command (2.8+). // GetAll returns all redis entries using the "SCAN" command (2.8+).
func (r *Service) GetAll() (interface{}, error) { func (r *Service) GetAll() (interface{}, error) {
c := r.pool.Get() c := r.pool.Get()
@ -102,6 +120,48 @@ func (r *Service) GetAll() (interface{}, error) {
return redisVal, nil return redisVal, nil
} }
// GetKeys returns all redis keys using the "SCAN" with MATCH command.
// Read more at: https://redis.io/commands/scan#the-match-option.
func (r *Service) GetKeys(prefix string) ([]string, error) {
c := r.pool.Get()
defer c.Close()
if err := c.Err(); err != nil {
return nil, err
}
if err := c.Send("SCAN", 0, "MATCH", r.Config.Prefix+prefix+"*", "COUNT", 9999999999); err != nil {
return nil, err
}
if err := c.Flush(); err != nil {
return nil, err
}
reply, err := c.Receive()
if err != nil || reply == nil {
return nil, err
}
// it returns []interface, with two entries, the first one is "0" and the second one is a slice of the keys as []interface{uint8....}.
if keysInterface, ok := reply.([]interface{}); ok {
if len(keysInterface) == 2 {
// take the second, it must contain the slice of keys.
if keysSliceAsBytes, ok := keysInterface[1].([]interface{}); ok {
keys := make([]string, len(keysSliceAsBytes), len(keysSliceAsBytes))
for i, k := range keysSliceAsBytes {
keys[i] = fmt.Sprintf("%s", k)
}
return keys, nil
}
}
}
return nil, nil
}
// GetBytes returns value, err by its key // GetBytes returns value, err by its key
// you can use utils.Deserialize((.GetBytes("yourkey"),&theobject{}) // you can use utils.Deserialize((.GetBytes("yourkey"),&theobject{})
//returns nil and a filled error if something wrong happens //returns nil and a filled error if something wrong happens

View File

@ -107,7 +107,7 @@ func (s *Sessions) Start(ctx context.Context) *Session {
sid := s.config.SessionIDGenerator() sid := s.config.SessionIDGenerator()
sess := s.provider.Init(sid, s.config.Expires) sess := s.provider.Init(sid, s.config.Expires)
sess.isNew = sess.values.Len() == 0 sess.isNew = s.provider.db.Len(sid) == 0
s.updateCookie(ctx, sid, s.config.Expires) s.updateCookie(ctx, sid, s.config.Expires)

49
sessions/transcoding.go Normal file
View File

@ -0,0 +1,49 @@
package sessions
import "encoding/json"
type (
// Marshaler is the common marshaler interface, used by transcoder.
Marshaler interface {
Marshal(interface{}) ([]byte, error)
}
// Unmarshaler is the common unmarshaler interface, used by transcoder.
Unmarshaler interface {
Unmarshal([]byte, interface{}) error
}
// Transcoder is the interface that transcoders should implement, it includes just the `Marshaler` and the `Unmarshaler`.
Transcoder interface {
Marshaler
Unmarshaler
}
)
// DefaultTranscoder is the default transcoder across databases, it's the JSON by default.
// Change it if you want a different serialization/deserialization inside your session databases (when `UseDatabase` is used).
var DefaultTranscoder = defaultTranscoder{}
type defaultTranscoder struct{}
func (d defaultTranscoder) Marshal(value interface{}) ([]byte, error) {
if tr, ok := value.(Marshaler); ok {
return tr.Marshal(value)
}
if jsonM, ok := value.(json.Marshaler); ok {
return jsonM.MarshalJSON()
}
return json.Marshal(value)
}
func (d defaultTranscoder) Unmarshal(b []byte, outPtr interface{}) error {
if tr, ok := outPtr.(Unmarshaler); ok {
return tr.Unmarshal(b, outPtr)
}
if jsonUM, ok := outPtr.(json.Unmarshaler); ok {
return jsonUM.UnmarshalJSON(b)
}
return json.Unmarshal(b, outPtr)
}