mirror of
https://github.com/kataras/iris.git
synced 2025-02-02 15:30:36 +01:00
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:
parent
f113872b7d
commit
b62080c4bb
3
.github/ISSUE_TEMPLATE.md
vendored
3
.github/ISSUE_TEMPLATE.md
vendored
|
@ -6,3 +6,6 @@ Documentation for the Iris project can be found at
|
||||||
|
|
||||||
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
|
|
@ -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:
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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% δωρεάν και ανοιχτού κώδικα λογισμικό.
|
||||||
|
|
|
@ -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%.
|
||||||
|
|
|
@ -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% 免费和开源软件。
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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 管理包。
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"))
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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"))
|
|
||||||
}
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -3,8 +3,6 @@ package sessions
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kataras/iris/core/memstore"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -16,7 +14,7 @@ type (
|
||||||
// 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
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,15 +22,14 @@ type (
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
func (db *Database) Get(sid string, key string) (value interface{}) {
|
||||||
|
err := db.Service.View(func(txn *badger.Txn) error {
|
||||||
|
item, err := txn.Get(makeKey(sid, key))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
golog.Errorf("error while trying to load from the remote store: %v", err)
|
return err
|
||||||
}
|
}
|
||||||
|
// item.ValueCopy
|
||||||
return
|
valueBytes, err := item.Value()
|
||||||
}
|
|
||||||
|
|
||||||
// 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 badger: %v",
|
|
||||||
p.SessionID, err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := p.Store.Serialize()
|
|
||||||
if err != nil {
|
|
||||||
golog.Errorf("error while serializing the remote store: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
txn := db.Service.NewTransaction(true)
|
|
||||||
|
|
||||||
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 {
|
|
||||||
txn := db.Service.NewTransaction(true)
|
|
||||||
|
|
||||||
err := txn.Delete(bsid)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return txn.Commit(nil)
|
return sessions.DefaultTranscoder.Unmarshal(valueBytes, &value)
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil && err != badger.ErrKeyNotFound {
|
||||||
|
golog.Error(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visit loops through all session keys and values.
|
||||||
|
func (db *Database) Visit(sid string, cb func(key string, value interface{})) {
|
||||||
|
prefix := append([]byte(sid), delim)
|
||||||
|
|
||||||
|
txn := db.Service.NewTransaction(false)
|
||||||
|
defer txn.Discard()
|
||||||
|
|
||||||
|
iter := txn.NewIterator(badger.DefaultIteratorOptions)
|
||||||
|
defer iter.Close()
|
||||||
|
|
||||||
|
for iter.Rewind(); iter.ValidForPrefix(prefix); iter.Next() {
|
||||||
|
item := iter.Item()
|
||||||
|
valueBytes, err := item.Value()
|
||||||
|
if err != nil {
|
||||||
|
golog.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var value interface{}
|
||||||
|
if err = sessions.DefaultTranscoder.Unmarshal(valueBytes, &value); err != nil {
|
||||||
|
golog.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cb(string(item.Key()), value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var iterOptionsNoValues = badger.IteratorOptions{
|
||||||
|
PrefetchValues: false,
|
||||||
|
PrefetchSize: 100,
|
||||||
|
Reverse: false,
|
||||||
|
AllVersions: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the length of the session's entries (keys).
|
||||||
|
func (db *Database) Len(sid string) (n int) {
|
||||||
|
prefix := append([]byte(sid), delim)
|
||||||
|
|
||||||
|
txn := db.Service.NewTransaction(false)
|
||||||
|
iter := txn.NewIterator(iterOptionsNoValues)
|
||||||
|
|
||||||
|
for iter.Rewind(); iter.ValidForPrefix(prefix); iter.Next() {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load loads the values to the underline.
|
return sessions.LifeTime{} // session manager will handle the rest.
|
||||||
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
|
if !hasExpiration {
|
||||||
db.redis.Connect()
|
return sessions.LifeTime{}
|
||||||
_, err := db.redis.PingPong()
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessions.LifeTime{Time: time.Now().Add(time.Duration(seconds) * time.Second)}
|
||||||
|
}
|
||||||
|
|
||||||
|
const delim = "_"
|
||||||
|
|
||||||
|
func makeKey(sid, key string) string {
|
||||||
|
return sid + delim + key
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
if err != nil {
|
||||||
golog.Errorf("redis database error on connect: %v", err)
|
golog.Error(err)
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch the values from this session id and copy-> store them
|
|
||||||
storeMaybe, err := db.redis.Get(sid)
|
|
||||||
// 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
storeDB, err = sessions.DecodeRemoteStore(storeB) // decode the whole value, as a remote store
|
if err = db.redis.Set(makeKey(sid, key), valueBytes, int64(lifetime.DurationUntilExpiration().Seconds())); err != nil {
|
||||||
|
golog.Debug(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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) get(key string, outPtr interface{}) {
|
||||||
|
data, err := db.redis.Get(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
golog.Errorf(`error while trying to load session values(%s) from redis:
|
// not found.
|
||||||
the retrieved value is not a sessions.RemoteStore type, please report that as bug, that should never occur: %v`,
|
|
||||||
sid, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync syncs the database.
|
if err = sessions.DefaultTranscoder.Unmarshal(data.([]byte), outPtr); err != nil {
|
||||||
func (db *Database) Sync(p sessions.SyncPayload) {
|
golog.Debugf("unable to unmarshal value of key: '%s': %v", key, err)
|
||||||
db.sync(p)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Database) sync(p sessions.SyncPayload) {
|
func (db *Database) keys(sid string) []string {
|
||||||
if p.Action == sessions.ActionDestroy {
|
keys, err := db.redis.GetKeys(sid + delim)
|
||||||
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")
|
golog.Debugf("unable to get all redis keys of session '%s': %v", sid, err)
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// not expire if zero
|
return keys
|
||||||
seconds := 0
|
|
||||||
|
|
||||||
if lifetime := p.Store.Lifetime; !lifetime.IsZero() {
|
|
||||||
seconds = int(lifetime.Sub(time.Now()).Seconds())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
db.redis.Set(p.SessionID, storeB, seconds)
|
// 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close shutdowns the redis connection.
|
// 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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
49
sessions/transcoding.go
Normal 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)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user