mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 10:41:03 +01:00
Update to 8.2.0 | BoltDB session database, fix file sessiondb, faster, simpler and improvement Session Database API
Former-commit-id: 4034737a65b78a77277e4283fd9289c17f4a452e
This commit is contained in:
parent
36e6fb37b8
commit
48e352e1df
|
@ -7,7 +7,7 @@ go:
|
||||||
- tip
|
- tip
|
||||||
go_import_path: github.com/kataras/iris
|
go_import_path: github.com/kataras/iris
|
||||||
install:
|
install:
|
||||||
- go get ./... # for iris-contrib/httpexpect and kataras/golog
|
- go get ./... # for iris-contrib/httpexpect, kataras/golog, boltdb/bolt(sessiondb, optional)
|
||||||
script:
|
script:
|
||||||
- go test -v -cover ./...
|
- go test -v -cover ./...
|
||||||
after_script:
|
after_script:
|
||||||
|
|
66
HISTORY.md
66
HISTORY.md
|
@ -18,6 +18,72 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene
|
||||||
|
|
||||||
**How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris`.
|
**How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris`.
|
||||||
|
|
||||||
|
# Mo, 07 August 2017 | v8.2.0
|
||||||
|
|
||||||
|
No Common-API Changes.
|
||||||
|
|
||||||
|
Good news for [iris sessions back-end databases](_examples/sessions) users.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Info for session database authors</summary>
|
||||||
|
Session Database API Changed to:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Database interface {
|
||||||
|
Load(sid string) RemoteStore
|
||||||
|
Sync(p SyncPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// RemoteStore is a helper which is a wrapper
|
||||||
|
// for the store, it can be used as the session "table" which will be
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Read more at [sessions/database.go](sessions/database.go), view how three built'n session databases are being implemented [here](sessions/sessiondb).
|
||||||
|
</details>
|
||||||
|
|
||||||
|
All sessions databases are updated and they performant even faster than before.
|
||||||
|
|
||||||
|
- **NEW** raw file-based session database implemented, example [here](_examples/sessions/database/file)
|
||||||
|
- **NEW** [boltdb-based](https://github.com/boltdb/bolt) session database implemented, example [here](_examples/sessions/database/boltdb) (recommended as it's safer and faster)
|
||||||
|
- [redis sessiondb](_examples/sessions/database/redis) updated to the latest api
|
||||||
|
|
||||||
|
Under the cover, session database works entirely differently than before but nothing changed from the user's perspective, so upgrade with `go get -u github.com/kataras/iris` and sleep well.
|
||||||
|
|
||||||
# Tu, 01 August 2017 | v8.1.3
|
# Tu, 01 August 2017 | v8.1.3
|
||||||
|
|
||||||
- Add `Option` function to the `html view engine`: https://github.com/kataras/iris/issues/694
|
- Add `Option` function to the `html view engine`: https://github.com/kataras/iris/issues/694
|
||||||
|
|
|
@ -20,7 +20,7 @@ Iris is a fast, simple and efficient micro web framework for Go. It provides a b
|
||||||
### 📑 Table of contents
|
### 📑 Table of contents
|
||||||
|
|
||||||
* [Installation](#-installation)
|
* [Installation](#-installation)
|
||||||
* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#tu-01-august-2017--v813)
|
* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#mo-07-august-2017--v820)
|
||||||
* [Learn](#-learn)
|
* [Learn](#-learn)
|
||||||
* [HTTP Listening](_examples/#http-listening)
|
* [HTTP Listening](_examples/#http-listening)
|
||||||
* [Configuration](_examples/#configuration)
|
* [Configuration](_examples/#configuration)
|
||||||
|
@ -318,7 +318,7 @@ Thank You for your trust!
|
||||||
|
|
||||||
### 📌 Version
|
### 📌 Version
|
||||||
|
|
||||||
Current: **8.1.3**
|
Current: **8.2.0**
|
||||||
|
|
||||||
Each new release is pushed to the master. It stays there until the next version. When a next version is released then the previous version goes to its own branch with `gopkg.in` as its import path (and its own vendor folder), in order to keep it working "for-ever".
|
Each new release is pushed to the master. It stays there until the next version. When a next version is released then the previous version goes to its own branch with `gopkg.in` as its import path (and its own vendor folder), in order to keep it working "for-ever".
|
||||||
|
|
||||||
|
|
|
@ -194,9 +194,10 @@ iris session manager lives on its own [package](https://github.com/kataras/iris/
|
||||||
- [Standalone](sessions/standalone/main.go)
|
- [Standalone](sessions/standalone/main.go)
|
||||||
- [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)
|
||||||
- [Database](sessions/database)
|
- [Databases](sessions/database)
|
||||||
* [Redis](sessions/database/redis/main.go)
|
|
||||||
* [File](sessions/database/file/main.go)
|
* [File](sessions/database/file/main.go)
|
||||||
|
* [BoltDB](sessions/database/boltdb/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.
|
||||||
|
|
||||||
|
|
|
@ -16,9 +16,9 @@ func main() {
|
||||||
// Prefix: "With", code editors will help you navigate through all
|
// Prefix: "With", code editors will help you navigate through all
|
||||||
// configuration options without even a glitch to the documentation.
|
// configuration options without even a glitch to the documentation.
|
||||||
|
|
||||||
app.Run(iris.Addr(":8080"), iris.WithoutBanner, iris.WithCharset("UTF-8"))
|
app.Run(iris.Addr(":8080"), iris.WithoutStartupLog, iris.WithCharset("UTF-8"))
|
||||||
|
|
||||||
// or before run:
|
// or before run:
|
||||||
// app.Configure(iris.WithoutBanner, iris.WithCharset("UTF-8"))
|
// app.Configure(iris.WithoutStartupLog, iris.WithCharset("UTF-8"))
|
||||||
// app.Run(iris.Addr(":8080"))
|
// app.Run(iris.Addr(":8080"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,6 @@ func Configurator(app *iris.Application) {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
app.Get("/counter", func(ctx context.Context) {
|
app.Get("/counter", func(ctx context.Context) {
|
||||||
ctx.Header("Content-Type", "text/plain")
|
|
||||||
ctx.Writef("Counter value = %d", counterValue)
|
ctx.Writef("Counter value = %d", counterValue)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@ func main() {
|
||||||
ctx.Writef("The path after /anything is: %s", paramValue)
|
ctx.Writef("The path after /anything is: %s", paramValue)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
myroute.Name = "myroute"
|
||||||
|
|
||||||
// useful for links, although iris' view engine has the {{ urlpath "routename" "path values"}} already.
|
// useful for links, although iris' view engine has the {{ urlpath "routename" "path values"}} already.
|
||||||
app.Get("/reverse_myroute", func(ctx context.Context) {
|
app.Get("/reverse_myroute", func(ctx context.Context) {
|
||||||
myrouteRequestPath := rv.Path(myroute.Name, "any/path")
|
myrouteRequestPath := rv.Path(myroute.Name, "any/path")
|
||||||
|
@ -32,4 +34,5 @@ func main() {
|
||||||
// http://localhost:8080/execute_myroute
|
// http://localhost:8080/execute_myroute
|
||||||
// http://localhost:8080/anything/any/path/here
|
// http://localhost:8080/anything/any/path/here
|
||||||
app.Run(iris.Addr(":8080"))
|
app.Run(iris.Addr(":8080"))
|
||||||
}
|
|
||||||
|
} // See view/template_html_4 example for more.
|
||||||
|
|
91
_examples/sessions/database/boltdb/main.go
Normal file
91
_examples/sessions/database/boltdb/main.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kataras/iris"
|
||||||
|
"github.com/kataras/iris/context"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/sessions"
|
||||||
|
"github.com/kataras/iris/sessions/sessiondb/boltdb"
|
||||||
|
)
|
||||||
|
|
||||||
|
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: 1 * time.Minute, // <=0 means unlimited life
|
||||||
|
})
|
||||||
|
|
||||||
|
//
|
||||||
|
// IMPORTANT:
|
||||||
|
//
|
||||||
|
sess.UseDatabase(db)
|
||||||
|
|
||||||
|
// the rest of the code stays the same.
|
||||||
|
app := iris.New()
|
||||||
|
|
||||||
|
app.Get("/", func(ctx context.Context) {
|
||||||
|
ctx.Writef("You should navigate to the /set, /get, /delete, /clear,/destroy instead")
|
||||||
|
})
|
||||||
|
app.Get("/set", func(ctx context.Context) {
|
||||||
|
s := sess.Start(ctx)
|
||||||
|
//set session values
|
||||||
|
s.Set("name", "iris")
|
||||||
|
|
||||||
|
//test if setted here
|
||||||
|
ctx.Writef("All ok session setted to: %s", s.GetString("name"))
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Get("/set/{key}/{value}", func(ctx context.Context) {
|
||||||
|
key, value := ctx.Params().Get("key"), ctx.Params().Get("value")
|
||||||
|
s := sess.Start(ctx)
|
||||||
|
// set session values
|
||||||
|
s.Set(key, value)
|
||||||
|
|
||||||
|
// test if setted here
|
||||||
|
ctx.Writef("All ok session setted to: %s", s.GetString(key))
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Get("/get", func(ctx context.Context) {
|
||||||
|
// get a specific key, as string, if no found returns just an empty string
|
||||||
|
name := sess.Start(ctx).GetString("name")
|
||||||
|
|
||||||
|
ctx.Writef("The name on the /set was: %s", name)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Get("/get/{key}", func(ctx context.Context) {
|
||||||
|
// get a specific key, as string, if no found returns just an empty string
|
||||||
|
name := sess.Start(ctx).GetString(ctx.Params().Get("key"))
|
||||||
|
|
||||||
|
ctx.Writef("The name on the /set was: %s", name)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Get("/delete", func(ctx context.Context) {
|
||||||
|
// delete a specific key
|
||||||
|
sess.Start(ctx).Delete("name")
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Get("/clear", func(ctx context.Context) {
|
||||||
|
// removes all entries
|
||||||
|
sess.Start(ctx).Clear()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Get("/destroy", func(ctx context.Context) {
|
||||||
|
//destroy, removes the entire session data and cookie
|
||||||
|
sess.Destroy(ctx)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Get("/update", func(ctx context.Context) {
|
||||||
|
// updates expire date with a new date
|
||||||
|
sess.ShiftExpiraton(ctx)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Run(iris.Addr(":8080"))
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/kataras/iris"
|
"github.com/kataras/iris"
|
||||||
"github.com/kataras/iris/context"
|
"github.com/kataras/iris/context"
|
||||||
|
|
||||||
|
@ -9,9 +11,12 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
db := file.New("./sessions/")
|
db, _ := file.New("./sessions/", 0666)
|
||||||
|
|
||||||
sess := sessions.New(sessions.Config{Cookie: "sessionscookieid"})
|
sess := sessions.New(sessions.Config{
|
||||||
|
Cookie: "sessionscookieid",
|
||||||
|
Expires: 45 * time.Minute, // <=0 means unlimited life
|
||||||
|
})
|
||||||
|
|
||||||
//
|
//
|
||||||
// IMPORTANT:
|
// IMPORTANT:
|
||||||
|
@ -33,6 +38,16 @@ func main() {
|
||||||
ctx.Writef("All ok session setted to: %s", s.GetString("name"))
|
ctx.Writef("All ok session setted to: %s", s.GetString("name"))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.Get("/set/{key}/{value}", func(ctx context.Context) {
|
||||||
|
key, value := ctx.Params().Get("key"), ctx.Params().Get("value")
|
||||||
|
s := sess.Start(ctx)
|
||||||
|
// set session values
|
||||||
|
s.Set(key, value)
|
||||||
|
|
||||||
|
// test if setted here
|
||||||
|
ctx.Writef("All ok session setted to: %s", s.GetString(key))
|
||||||
|
})
|
||||||
|
|
||||||
app.Get("/get", func(ctx context.Context) {
|
app.Get("/get", func(ctx context.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")
|
||||||
|
@ -40,6 +55,13 @@ func main() {
|
||||||
ctx.Writef("The name on the /set was: %s", name)
|
ctx.Writef("The name on the /set was: %s", name)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.Get("/get/{key}", func(ctx context.Context) {
|
||||||
|
// get a specific key, as string, if no found returns just an empty string
|
||||||
|
name := sess.Start(ctx).GetString(ctx.Params().Get("key"))
|
||||||
|
|
||||||
|
ctx.Writef("The name on the /set was: %s", name)
|
||||||
|
})
|
||||||
|
|
||||||
app.Get("/delete", func(ctx context.Context) {
|
app.Get("/delete", func(ctx context.Context) {
|
||||||
// delete a specific key
|
// delete a specific key
|
||||||
sess.Start(ctx).Delete("name")
|
sess.Start(ctx).Delete("name")
|
||||||
|
|
|
@ -628,13 +628,13 @@ type Context interface {
|
||||||
// BUT it isn't available by browsing, its handlers executed only when other handler's context call them
|
// BUT it isn't available by browsing, its handlers executed only when other handler's context call them
|
||||||
// it can validate paths, has sessions, path parameters and all.
|
// it can validate paths, has sessions, path parameters and all.
|
||||||
//
|
//
|
||||||
// You can find the Route by app.Routes().Lookup("theRouteName")
|
// You can find the Route by app.GetRoute("theRouteName")
|
||||||
// you can set a route name as: myRoute := app.Get("/mypath", handler)("theRouteName")
|
// you can set a route name as: myRoute := app.Get("/mypath", handler)("theRouteName")
|
||||||
// that will set a name to the route and returns its RouteInfo instance for further usage.
|
// that will set a name to the route and returns its RouteInfo instance for further usage.
|
||||||
//
|
//
|
||||||
// It doesn't changes the global state, if a route was "offline" it remains offline.
|
// It doesn't changes the global state, if a route was "offline" it remains offline.
|
||||||
//
|
//
|
||||||
// app.None(...) and app.Routes().Offline(route)/.Online(route, method)
|
// app.None(...) and app.GetRoutes().Offline(route)/.Online(route, method)
|
||||||
//
|
//
|
||||||
// Example: https://github.com/kataras/iris/tree/master/_examples/routing/route-state
|
// Example: https://github.com/kataras/iris/tree/master/_examples/routing/route-state
|
||||||
//
|
//
|
||||||
|
@ -2296,13 +2296,13 @@ func (ctx *context) TransactionsSkipped() bool {
|
||||||
// BUT it isn't available by browsing, its handlers executed only when other handler's context call them
|
// BUT it isn't available by browsing, its handlers executed only when other handler's context call them
|
||||||
// it can validate paths, has sessions, path parameters and all.
|
// it can validate paths, has sessions, path parameters and all.
|
||||||
//
|
//
|
||||||
// You can find the Route by app.Routes().Lookup("theRouteName")
|
// You can find the Route by app.GetRoute("theRouteName")
|
||||||
// you can set a route name as: myRoute := app.Get("/mypath", handler)("theRouteName")
|
// you can set a route name as: myRoute := app.Get("/mypath", handler)("theRouteName")
|
||||||
// that will set a name to the route and returns its RouteInfo instance for further usage.
|
// that will set a name to the route and returns its RouteInfo instance for further usage.
|
||||||
//
|
//
|
||||||
// It doesn't changes the global state, if a route was "offline" it remains offline.
|
// It doesn't changes the global state, if a route was "offline" it remains offline.
|
||||||
//
|
//
|
||||||
// app.None(...) and app.Routes().Offline(route)/.Online(route, method)
|
// app.None(...) and app.GetRoutes().Offline(route)/.Online(route, method)
|
||||||
//
|
//
|
||||||
// Example: https://github.com/kataras/iris/tree/master/_examples/routing/route-state
|
// Example: https://github.com/kataras/iris/tree/master/_examples/routing/route-state
|
||||||
//
|
//
|
||||||
|
|
68
core/memstore/gob.go
Normal file
68
core/memstore/gob.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
package memstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/gob"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// why?
|
||||||
|
// on the future we may change how these encoders/decoders
|
||||||
|
// and we may need different method for store and other for entry.
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
gob.Register(Store{})
|
||||||
|
gob.Register(Entry{})
|
||||||
|
gob.Register(time.Time{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GobEncode accepts a store and writes
|
||||||
|
// as series of bytes to the "w" writer.
|
||||||
|
func GobEncode(store Store, w io.Writer) error {
|
||||||
|
enc := gob.NewEncoder(w)
|
||||||
|
err := enc.Encode(store)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GobSerialize same as GobEncode but it returns
|
||||||
|
// the bytes using a temp buffer.
|
||||||
|
func GobSerialize(store Store) ([]byte, error) {
|
||||||
|
w := new(bytes.Buffer)
|
||||||
|
err := GobEncode(store, w)
|
||||||
|
return w.Bytes(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GobEncodeEntry accepts an entry and writes
|
||||||
|
// as series of bytes to the "w" writer.
|
||||||
|
func GobEncodeEntry(entry Entry, w io.Writer) error {
|
||||||
|
enc := gob.NewEncoder(w)
|
||||||
|
err := enc.Encode(entry)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GobSerializeEntry same as GobEncodeEntry but it returns
|
||||||
|
// the bytes using a temp buffer.
|
||||||
|
func GobSerializeEntry(entry Entry) ([]byte, error) {
|
||||||
|
w := new(bytes.Buffer)
|
||||||
|
err := GobEncodeEntry(entry, w)
|
||||||
|
return w.Bytes(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GobDecode accepts a series of bytes and returns
|
||||||
|
// the store.
|
||||||
|
func GobDecode(b []byte) (store Store, err error) {
|
||||||
|
dec := gob.NewDecoder(bytes.NewBuffer(b))
|
||||||
|
// no reference because of:
|
||||||
|
// gob: decoding into local type *memstore.Store, received remote type Entry
|
||||||
|
err = dec.Decode(&store)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GobDecodeEntry accepts a series of bytes and returns
|
||||||
|
// the entry.
|
||||||
|
func GobDecodeEntry(b []byte) (entry Entry, err error) {
|
||||||
|
dec := gob.NewDecoder(bytes.NewBuffer(b))
|
||||||
|
err = dec.Decode(&entry)
|
||||||
|
return
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ type (
|
||||||
// Entry is the entry of the context storage Store - .Values()
|
// Entry is the entry of the context storage Store - .Values()
|
||||||
Entry struct {
|
Entry struct {
|
||||||
Key string
|
Key string
|
||||||
value interface{}
|
ValueRaw interface{}
|
||||||
immutable bool // if true then it can't change by its caller.
|
immutable bool // if true then it can't change by its caller.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ type (
|
||||||
func (e Entry) Value() interface{} {
|
func (e Entry) Value() interface{} {
|
||||||
if e.immutable {
|
if e.immutable {
|
||||||
// take its value, no pointer even if setted with a rreference.
|
// take its value, no pointer even if setted with a rreference.
|
||||||
vv := reflect.Indirect(reflect.ValueOf(e.value))
|
vv := reflect.Indirect(reflect.ValueOf(e.ValueRaw))
|
||||||
|
|
||||||
// return copy of that slice
|
// return copy of that slice
|
||||||
if vv.Type().Kind() == reflect.Slice {
|
if vv.Type().Kind() == reflect.Slice {
|
||||||
|
@ -48,18 +48,16 @@ func (e Entry) Value() interface{} {
|
||||||
// if was *value it will return value{}.
|
// if was *value it will return value{}.
|
||||||
return vv.Interface()
|
return vv.Interface()
|
||||||
}
|
}
|
||||||
return e.value
|
return e.ValueRaw
|
||||||
}
|
}
|
||||||
|
|
||||||
// the id is immutable(true or false)+key
|
// Save same as `Set`
|
||||||
// so the users will be able to use the same key
|
// However, if "immutable" is true then saves it as immutable (same as `SetImmutable`).
|
||||||
// to store two different entries (one immutable and other mutable).
|
//
|
||||||
// or no? better no, that will confuse and maybe result on unexpected results.
|
//
|
||||||
// I will just replace the value and the immutable bool value when Set if
|
// Returns the entry and true if it was just inserted, meaning that
|
||||||
// a key is already exists.
|
// it will return the entry and a false boolean if the entry exists and it has been updated.
|
||||||
// func (e Entry) identifier() string {}
|
func (r *Store) Save(key string, value interface{}, immutable bool) (Entry, bool) {
|
||||||
|
|
||||||
func (r *Store) save(key string, value interface{}, immutable bool) {
|
|
||||||
args := *r
|
args := *r
|
||||||
n := len(args)
|
n := len(args)
|
||||||
|
|
||||||
|
@ -71,15 +69,15 @@ func (r *Store) save(key string, value interface{}, immutable bool) {
|
||||||
// if called by `SetImmutable`
|
// if called by `SetImmutable`
|
||||||
// then allow the update, maybe it's a slice that user wants to update by SetImmutable method,
|
// then allow the update, maybe it's a slice that user wants to update by SetImmutable method,
|
||||||
// we should allow this
|
// we should allow this
|
||||||
kv.value = value
|
kv.ValueRaw = value
|
||||||
kv.immutable = immutable
|
kv.immutable = immutable
|
||||||
} else if kv.immutable == false {
|
} else if kv.immutable == false {
|
||||||
// if it was not immutable then user can alt it via `Set` and `SetImmutable`
|
// if it was not immutable then user can alt it via `Set` and `SetImmutable`
|
||||||
kv.value = value
|
kv.ValueRaw = value
|
||||||
kv.immutable = immutable
|
kv.immutable = immutable
|
||||||
}
|
}
|
||||||
// else it was immutable and called by `Set` then disallow the update
|
// else it was immutable and called by `Set` then disallow the update
|
||||||
return
|
return *kv, false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,25 +87,29 @@ func (r *Store) save(key string, value interface{}, immutable bool) {
|
||||||
args = args[:n+1]
|
args = args[:n+1]
|
||||||
kv := &args[n]
|
kv := &args[n]
|
||||||
kv.Key = key
|
kv.Key = key
|
||||||
kv.value = value
|
kv.ValueRaw = value
|
||||||
kv.immutable = immutable
|
kv.immutable = immutable
|
||||||
*r = args
|
*r = args
|
||||||
return
|
return *kv, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// add
|
// add
|
||||||
kv := Entry{
|
kv := Entry{
|
||||||
Key: key,
|
Key: key,
|
||||||
value: value,
|
ValueRaw: value,
|
||||||
immutable: immutable,
|
immutable: immutable,
|
||||||
}
|
}
|
||||||
*r = append(args, kv)
|
*r = append(args, kv)
|
||||||
|
return kv, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set saves a value to the key-value storage.
|
// Set saves a value to the key-value storage.
|
||||||
|
// Returns the entry and true if it was just inserted, meaning that
|
||||||
|
// it will return the entry and a false boolean if the entry exists and it has been updated.
|
||||||
|
//
|
||||||
// See `SetImmutable` and `Get`.
|
// See `SetImmutable` and `Get`.
|
||||||
func (r *Store) Set(key string, value interface{}) {
|
func (r *Store) Set(key string, value interface{}) (Entry, bool) {
|
||||||
r.save(key, value, false)
|
return r.Save(key, value, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetImmutable saves a value to the key-value storage.
|
// SetImmutable saves a value to the key-value storage.
|
||||||
|
@ -116,10 +118,13 @@ func (r *Store) Set(key string, value interface{}) {
|
||||||
// An Immutable entry should be only changed with a `SetImmutable`, simple `Set` will not work
|
// An Immutable entry should be only changed with a `SetImmutable`, simple `Set` will not work
|
||||||
// if the entry was immutable, for your own safety.
|
// if the entry was immutable, for your own safety.
|
||||||
//
|
//
|
||||||
|
// Returns the entry and true if it was just inserted, meaning that
|
||||||
|
// it will return the entry and a false boolean if the entry exists and it has been updated.
|
||||||
|
//
|
||||||
// Use it consistently, it's far slower than `Set`.
|
// Use it consistently, it's far slower than `Set`.
|
||||||
// Read more about muttable and immutable go types: https://stackoverflow.com/a/8021081
|
// Read more about muttable and immutable go types: https://stackoverflow.com/a/8021081
|
||||||
func (r *Store) SetImmutable(key string, value interface{}) {
|
func (r *Store) SetImmutable(key string, value interface{}) (Entry, bool) {
|
||||||
r.save(key, value, true)
|
return r.Save(key, value, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the entry's value based on its key.
|
// Get returns the entry's value based on its key.
|
||||||
|
@ -205,3 +210,9 @@ func (r *Store) Len() int {
|
||||||
args := *r
|
args := *r
|
||||||
return len(args)
|
return len(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Serialize returns the byte representation of the current Store.
|
||||||
|
func (r Store) Serialize() []byte { // note: no pointer here, ignore linters if shows up.
|
||||||
|
b, _ := GobSerialize(r)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
|
@ -487,6 +487,7 @@ func (rb *APIBuilder) StaticServe(systemPath string, requestPath ...string) *Rou
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ctx.ServeFile(spath, true); err != nil {
|
if err := ctx.ServeFile(spath, true); err != nil {
|
||||||
|
ctx.Application().Logger().Warnf("while trying to serve static file: '%v' on IP: '%s'", err, ctx.RemoteAddr())
|
||||||
ctx.StatusCode(http.StatusInternalServerError)
|
ctx.StatusCode(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
2
doc.go
2
doc.go
|
@ -35,7 +35,7 @@ Source code and other details for the project are available at GitHub:
|
||||||
|
|
||||||
Current Version
|
Current Version
|
||||||
|
|
||||||
8.1.3
|
8.2.0
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
|
|
||||||
|
|
2
iris.go
2
iris.go
|
@ -33,7 +33,7 @@ import (
|
||||||
const (
|
const (
|
||||||
|
|
||||||
// Version is the current version number of the Iris Web Framework.
|
// Version is the current version number of the Iris Web Framework.
|
||||||
Version = "8.1.3"
|
Version = "8.2.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTTP status codes as registered with IANA.
|
// HTTP status codes as registered with IANA.
|
||||||
|
|
|
@ -1,24 +1,176 @@
|
||||||
package sessions
|
package sessions
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/gob"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"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, no context access (although we could)
|
// I want to protect you, believe me.
|
||||||
// The scope of the database is to session 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.
|
||||||
// the values are sessions by the underline session, the check for new sessions, or
|
//
|
||||||
// 'this session value should added' are made automatically
|
// Synchronization are made automatically, you can register more than one session database
|
||||||
// you are able just to set the values to your backend database with Load function.
|
|
||||||
// session database doesn't have any write or read access to the session, the loading of
|
|
||||||
// the initial data is done by the Load(string) (map[string]interfface{}, *time.Time) function
|
|
||||||
// synchronization are made automatically, you can register more than one session database
|
|
||||||
// but the first non-empty Load return data will be used as the session values.
|
// but the first non-empty Load return data will be used as the session values.
|
||||||
// The Expire Date is given with data to save because the session entry must keep trace
|
//
|
||||||
// of the expire date in the case of the server is restarted. So the server will recover
|
//
|
||||||
// expiration state of session entry and it will track the expiration again.
|
// Note: Expiration on Load is up to the database, meaning that:
|
||||||
// If expireDate is nil, that's means that there is no expire date.
|
// 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) (datas map[string]interface{}, expireDate *time.Time)
|
Load(sid string) RemoteStore
|
||||||
Update(sid string, datas map[string]interface{}, expireDate *time.Time)
|
Sync(p SyncPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New Idea, it should work faster for the most databases needs
|
||||||
|
// the only minus is that the databases is coupled with this package, they
|
||||||
|
// should import the kataras/iris/sessions package, but we don't use any
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
var spPool = sync.Pool{New: func() interface{} { return SyncPayload{} }}
|
||||||
|
|
||||||
|
func acquireSyncPayload(session *Session, action Action) SyncPayload {
|
||||||
|
p := spPool.Get().(SyncPayload)
|
||||||
|
p.SessionID = session.sid
|
||||||
|
|
||||||
|
// clone the life time, except the timer.
|
||||||
|
// lifetime := LifeTime{
|
||||||
|
// Time: session.lifetime.Time,
|
||||||
|
// OriginalDuration: session.lifetime.OriginalDuration,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// lifetime := acquireLifetime(session.lifetime.OriginalDuration, nil)
|
||||||
|
|
||||||
|
p.Store = RemoteStore{
|
||||||
|
Values: session.values,
|
||||||
|
Lifetime: session.lifetime,
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Action = action
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func releaseSyncPayload(p SyncPayload) {
|
||||||
|
p.Value.Key = ""
|
||||||
|
p.Value.ValueRaw = nil
|
||||||
|
|
||||||
|
// releaseLifetime(p.Store.Lifetime)
|
||||||
|
spPool.Put(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncDatabases(databases []Database, payload SyncPayload) {
|
||||||
|
for i, n := 0, len(databases); i < n; i++ {
|
||||||
|
databases[i].Sync(payload)
|
||||||
|
}
|
||||||
|
releaseSyncPayload(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoteStore is a helper which is a wrapper
|
||||||
|
// for the store, it can be used as the session "table" which will be
|
||||||
|
// 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 RemoteStore) Serialize() ([]byte, error) {
|
||||||
|
w := new(bytes.Buffer)
|
||||||
|
err := encode(s, w)
|
||||||
|
return w.Bytes(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// encode accepts a store and writes
|
||||||
|
// as series of bytes to the "w" writer.
|
||||||
|
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
|
||||||
|
// the store.
|
||||||
|
func DecodeRemoteStore(b []byte) (store RemoteStore, err error) {
|
||||||
|
dec := gob.NewDecoder(bytes.NewBuffer(b))
|
||||||
|
err = dec.Decode(&store)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
65
sessions/lifetime.go
Normal file
65
sessions/lifetime.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
package sessions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LifeTime controls the session expiration datetime.
|
||||||
|
type LifeTime struct {
|
||||||
|
// Remember, tip for the future:
|
||||||
|
// No need of gob.Register, because we embed the time.Time.
|
||||||
|
// And serious bug which has a result of me spending my whole evening:
|
||||||
|
// Because of gob encoding it doesn't encodes/decodes the other fields if time.Time is embedded
|
||||||
|
// (this should be a bug(go1.9-rc1) or not. We don't care atm)
|
||||||
|
time.Time
|
||||||
|
timer *time.Timer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin will begin the life based on the time.Now().Add(d).
|
||||||
|
// Use `Continue` to continue from a stored time(database-based session does that).
|
||||||
|
func (lt *LifeTime) Begin(d time.Duration, onExpire func()) {
|
||||||
|
if d <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lt.Time = time.Now().Add(d)
|
||||||
|
lt.timer = time.AfterFunc(d, onExpire)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revive will continue the life based on the stored Time.
|
||||||
|
// Other words that could be used for this func are: Continue, Restore, Resc.
|
||||||
|
func (lt *LifeTime) Revive(onExpire func()) {
|
||||||
|
if lt.Time.IsZero() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
if lt.Time.After(now) {
|
||||||
|
d := lt.Time.Sub(now)
|
||||||
|
lt.timer = time.AfterFunc(d, onExpire)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift resets the lifetime based on "d".
|
||||||
|
func (lt *LifeTime) Shift(d time.Duration) {
|
||||||
|
if d > 0 && lt.timer != nil {
|
||||||
|
lt.timer.Reset(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpireNow reduce the lifetime completely.
|
||||||
|
func (lt *LifeTime) ExpireNow() {
|
||||||
|
lt.Time = CookieExpireDelete
|
||||||
|
if lt.timer != nil {
|
||||||
|
lt.timer.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasExpired reports whether "lt" represents is expired.
|
||||||
|
func (lt *LifeTime) HasExpired() bool {
|
||||||
|
if lt.IsZero() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return lt.Time.Before(time.Now())
|
||||||
|
}
|
|
@ -36,87 +36,102 @@ func (p *provider) RegisterDatabase(db Database) {
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// startAutoDestroy start a task which destoy the session when expire date is reached,
|
|
||||||
// but only if `expires` parameter is positive. It updates the expire date of the session from `expires` parameter.
|
|
||||||
func (p *provider) startAutoDestroy(s *Session, expires time.Duration) bool {
|
|
||||||
res := expires > 0
|
|
||||||
if res { // if not unlimited life duration and no -1 (cookie remove action is based on browser's session)
|
|
||||||
expireDate := time.Now().Add(expires)
|
|
||||||
|
|
||||||
s.expireAt = &expireDate
|
|
||||||
s.timer = time.AfterFunc(expires, func() {
|
|
||||||
// the destroy makes the check if this session is exists then or not,
|
|
||||||
// this is used to destroy the session from the server-side also
|
|
||||||
// it's good to have here for security reasons, I didn't add it on the gc function to separate its action
|
|
||||||
p.Destroy(s.sid)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// newSession returns a new session from sessionid
|
// newSession returns a new session from sessionid
|
||||||
func (p *provider) newSession(sid string, expires time.Duration) *Session {
|
func (p *provider) newSession(sid string, expires time.Duration) *Session {
|
||||||
values, expireAt := p.loadSessionValuesFromDB(sid)
|
onExpire := func() {
|
||||||
|
p.Destroy(sid)
|
||||||
|
}
|
||||||
|
|
||||||
|
values, lifetime := p.loadSessionFromDB(sid)
|
||||||
|
// simple and straight:
|
||||||
|
if !lifetime.IsZero() {
|
||||||
|
// if stored time is not zero
|
||||||
|
// start a timer based on the stored time, if not expired.
|
||||||
|
lifetime.Revive(onExpire)
|
||||||
|
} else {
|
||||||
|
// Remember: if db not exist or it has been expired
|
||||||
|
// then the stored time will be zero(see loadSessionFromDB) and the values will be empty.
|
||||||
|
//
|
||||||
|
// Even if the database has an unlimited session (possible by a previous app run)
|
||||||
|
// priority to the "expires" is given,
|
||||||
|
// again if <=0 then it does nothing.
|
||||||
|
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,
|
values: values,
|
||||||
flashes: make(map[string]*flashMessage),
|
flashes: make(map[string]*flashMessage),
|
||||||
expireAt: expireAt,
|
lifetime: lifetime,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len(values) > 0) && (sess.expireAt != nil) {
|
|
||||||
// Restore expiration state
|
|
||||||
// However, if session save in database has no expiration date,
|
|
||||||
// therefore the expiration will be reinitialised with session configuration
|
|
||||||
expires = sess.expireAt.Sub(time.Now())
|
|
||||||
}
|
|
||||||
|
|
||||||
p.startAutoDestroy(sess, expires)
|
|
||||||
|
|
||||||
return sess
|
return sess
|
||||||
}
|
}
|
||||||
|
|
||||||
// can return nil memstore
|
func (p *provider) loadSessionFromDB(sid string) (memstore.Store, LifeTime) {
|
||||||
func (p *provider) loadSessionValuesFromDB(sid string) (memstore.Store, *time.Time) {
|
|
||||||
var store memstore.Store
|
var store memstore.Store
|
||||||
var expireDate *time.Time
|
var lifetime LifeTime
|
||||||
|
|
||||||
|
firstValidIdx := 1
|
||||||
for i, n := 0, len(p.databases); i < n; i++ {
|
for i, n := 0, len(p.databases); i < n; i++ {
|
||||||
dbValues, currentExpireDate := p.databases[i].Load(sid)
|
storeDB := p.databases[i].Load(sid)
|
||||||
if dbValues != nil && len(dbValues) > 0 {
|
if storeDB.Lifetime.HasExpired() { // if expired then skip this db
|
||||||
for k, v := range dbValues {
|
firstValidIdx++
|
||||||
store.Set(k, v)
|
continue
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentExpireDate != nil) && ((expireDate == nil) || expireDate.After(*currentExpireDate)) {
|
if lifetime.IsZero() {
|
||||||
expireDate = currentExpireDate
|
// update the lifetime to the most valid
|
||||||
}
|
lifetime = storeDB.Lifetime
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if session has already expired
|
if n == firstValidIdx {
|
||||||
if (expireDate != nil) && expireDate.Before(time.Now()) {
|
// if one database then set the store as it is
|
||||||
return nil, nil
|
store = storeDB.Values
|
||||||
}
|
} else {
|
||||||
|
// else append this database's key-value pairs
|
||||||
return store, expireDate
|
// to the store
|
||||||
}
|
storeDB.Values.Visit(func(key string, value interface{}) {
|
||||||
|
store.Set(key, value)
|
||||||
func (p *provider) updateDatabases(sess *Session, store memstore.Store) {
|
|
||||||
if l := store.Len(); l > 0 {
|
|
||||||
mapValues := make(map[string]interface{}, l)
|
|
||||||
|
|
||||||
store.Visit(func(k string, v interface{}) {
|
|
||||||
mapValues[k] = v
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for i, n := 0, len(p.databases); i < n; i++ {
|
// Note: if one database and it's being expired then the lifetime will be zero(unlimited)
|
||||||
p.databases[i].Update(sess.sid, mapValues, sess.expireAt)
|
// 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
|
||||||
|
@ -128,8 +143,10 @@ func (p *provider) Init(sid string, expires time.Duration) *Session {
|
||||||
return newSession
|
return newSession
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateExpiraton update expire date of a session, plus it updates destroy task
|
// UpdateExpiraton update expire date of a session.
|
||||||
func (p *provider) UpdateExpiraton(sid string, expires time.Duration) (done bool) {
|
// if expires > 0 then it updates the destroy task.
|
||||||
|
// if expires <=0 then it does nothing, to destroy a session call the `Destroy` func instead.
|
||||||
|
func (p *provider) UpdateExpiraton(sid string, expires time.Duration) bool {
|
||||||
if expires <= 0 {
|
if expires <= 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -137,26 +154,11 @@ func (p *provider) UpdateExpiraton(sid string, expires time.Duration) (done bool
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
sess, found := p.sessions[sid]
|
sess, found := p.sessions[sid]
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if sess.timer == nil {
|
sess.lifetime.Shift(expires)
|
||||||
return p.startAutoDestroy(sess, expires)
|
|
||||||
} else {
|
|
||||||
if expires <= 0 {
|
|
||||||
sess.timer.Stop()
|
|
||||||
sess.timer = nil
|
|
||||||
sess.expireAt = nil
|
|
||||||
} else {
|
|
||||||
expireDate := time.Now().Add(expires)
|
|
||||||
|
|
||||||
sess.expireAt = &expireDate
|
|
||||||
sess.timer.Reset(expires)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,17 +182,9 @@ func (p *provider) Read(sid string, expires time.Duration) *Session {
|
||||||
func (p *provider) Destroy(sid string) {
|
func (p *provider) Destroy(sid string) {
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
if sess, found := p.sessions[sid]; found {
|
if sess, found := p.sessions[sid]; found {
|
||||||
sess.values = nil
|
p.deleteSession(sess)
|
||||||
sess.flashes = nil
|
|
||||||
if sess.timer != nil {
|
|
||||||
sess.timer.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(p.sessions, sid)
|
|
||||||
p.updateDatabases(sess, nil)
|
|
||||||
}
|
}
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DestroyAll removes all sessions
|
// DestroyAll removes all sessions
|
||||||
|
@ -199,13 +193,12 @@ func (p *provider) Destroy(sid string) {
|
||||||
func (p *provider) DestroyAll() {
|
func (p *provider) DestroyAll() {
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
for _, sess := range p.sessions {
|
for _, sess := range p.sessions {
|
||||||
if sess.timer != nil {
|
p.deleteSession(sess)
|
||||||
sess.timer.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(p.sessions, sess.ID())
|
|
||||||
p.updateDatabases(sess, nil)
|
|
||||||
}
|
}
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *provider) deleteSession(sess *Session) {
|
||||||
|
delete(p.sessions, sess.sid)
|
||||||
|
syncDatabases(p.databases, acquireSyncPayload(sess, ActionDestroy))
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ package sessions
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/kataras/iris/core/errors"
|
"github.com/kataras/iris/core/errors"
|
||||||
"github.com/kataras/iris/core/memstore"
|
"github.com/kataras/iris/core/memstore"
|
||||||
|
@ -27,8 +26,7 @@ type (
|
||||||
// NOTE: flashes are not managed by third-party, only inside session struct.
|
// NOTE: flashes are not managed by third-party, only inside session struct.
|
||||||
flashes map[string]*flashMessage
|
flashes map[string]*flashMessage
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
expireAt *time.Time // nil pointer means no expire date
|
lifetime LifeTime
|
||||||
timer *time.Timer
|
|
||||||
provider *provider
|
provider *provider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,22 +47,6 @@ func (s *Session) IsNew() bool {
|
||||||
return s.isNew
|
return s.isNew
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasExpireDate test if this session has an expire date, if not, this session never expires
|
|
||||||
func (s *Session) HasExpireDate() bool {
|
|
||||||
return s.expireAt != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExpireDate get the expire date, if this session has no expire date, the returned value has the zero value
|
|
||||||
func (s *Session) GetExpireDate() time.Time {
|
|
||||||
var res time.Time
|
|
||||||
|
|
||||||
if s.expireAt != nil {
|
|
||||||
res = *s.expireAt
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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{} {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
|
@ -281,16 +263,34 @@ func (s *Session) VisitAll(cb func(k string, v interface{})) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) set(key string, value interface{}, immutable bool) {
|
func (s *Session) set(key string, value interface{}, immutable bool) {
|
||||||
|
action := ActionCreate // defaults to create, means the first insert.
|
||||||
|
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
if immutable {
|
isFirst := s.values.Len() == 0
|
||||||
s.values.SetImmutable(key, value)
|
entry, isNew := s.values.Save(key, value, immutable)
|
||||||
} else {
|
s.isNew = false
|
||||||
s.values.Set(key, value)
|
|
||||||
}
|
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
s.updateDatabases()
|
if !isFirst {
|
||||||
s.isNew = false
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TODO: remove the expireAt pointer, wtf, we could use zero time instead,
|
||||||
|
// that was not my commit so I will ask for permission first...
|
||||||
|
// rename the expireAt to expiresAt, it seems to make more sense to me
|
||||||
|
|
||||||
|
p := acquireSyncPayload(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".
|
||||||
|
@ -339,20 +339,18 @@ func (s *Session) SetFlash(key string, value interface{}) {
|
||||||
func (s *Session) Delete(key string) bool {
|
func (s *Session) Delete(key string) bool {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
removed := s.values.Remove(key)
|
removed := s.values.Remove(key)
|
||||||
s.mu.Unlock()
|
|
||||||
|
|
||||||
s.updateDatabases()
|
|
||||||
if removed {
|
if removed {
|
||||||
s.isNew = false
|
s.isNew = false
|
||||||
}
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
p := acquireSyncPayload(s, ActionDelete)
|
||||||
|
p.Value = memstore.Entry{Key: key}
|
||||||
|
syncDatabases(s.provider.databases, p)
|
||||||
|
|
||||||
return removed
|
return removed
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) updateDatabases() {
|
|
||||||
s.provider.updateDatabases(s, s.values)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteFlash removes a flash message by its key.
|
// DeleteFlash removes a flash message by its key.
|
||||||
func (s *Session) DeleteFlash(key string) {
|
func (s *Session) DeleteFlash(key string) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
|
@ -364,10 +362,11 @@ func (s *Session) DeleteFlash(key string) {
|
||||||
func (s *Session) Clear() {
|
func (s *Session) Clear() {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.values.Reset()
|
s.values.Reset()
|
||||||
|
s.isNew = false
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
s.updateDatabases()
|
p := acquireSyncPayload(s, ActionClear)
|
||||||
s.isNew = false
|
syncDatabases(s.provider.databases, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearFlashes removes all flash messages.
|
// ClearFlashes removes all flash messages.
|
||||||
|
|
223
sessions/sessiondb/boltdb/database.go
Normal file
223
sessions/sessiondb/boltdb/database.go
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
package boltdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
|
"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 = 0666
|
||||||
|
)
|
||||||
|
|
||||||
|
// Database the BoltDB(file-based) session storage.
|
||||||
|
type Database struct {
|
||||||
|
path string // path included the name, i.e sessions/store.db
|
||||||
|
fileMode os.FileMode // defaults to 0666.
|
||||||
|
table []byte
|
||||||
|
Service *bolt.DB // `New` sets it but it can be override exactly after `New`, use with caution.
|
||||||
|
async bool
|
||||||
|
}
|
||||||
|
|
||||||
|
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},
|
||||||
|
)
|
||||||
|
|
||||||
|
bucket := []byte(bucketName)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
golog.Errorf("unable to initialize the BoltDB-based session database: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
service.Update(func(tx *bolt.Tx) (err error) {
|
||||||
|
_, err = tx.CreateBucketIfNotExists(bucket)
|
||||||
|
return
|
||||||
|
})
|
||||||
|
|
||||||
|
db := &Database{path: path, fileMode: fileMode,
|
||||||
|
table: bucket, Service: service,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 if true passed then it will use different
|
||||||
|
// go routines to update the BoltDB(file-based) storage.
|
||||||
|
func (db *Database) Async(useGoRoutines bool) *Database {
|
||||||
|
db.async = useGoRoutines
|
||||||
|
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) {
|
||||||
|
if db.async {
|
||||||
|
go db.sync(p)
|
||||||
|
} else {
|
||||||
|
db.sync(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) sync(p sessions.SyncPayload) {
|
||||||
|
bsid := []byte(p.SessionID)
|
||||||
|
|
||||||
|
if p.Action == sessions.ActionDestroy {
|
||||||
|
if err := db.destroy(bsid); err != nil {
|
||||||
|
golog.Errorf("error while destroying a session(%s) from boltdb: %v",
|
||||||
|
p.SessionID, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := p.Store.Serialize()
|
||||||
|
if err != nil {
|
||||||
|
golog.Errorf("error while serializing the remote store: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Service.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 ammount of data, so the method finally choosen
|
||||||
|
// 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 {
|
||||||
|
err := db.Service.Close()
|
||||||
|
if err != nil {
|
||||||
|
golog.Warnf("closing the BoltDB connection: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
|
@ -1,128 +1,214 @@
|
||||||
package file
|
package file
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/gob"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/kataras/golog"
|
"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 (
|
var (
|
||||||
// PathFileMode for creating the sessions directory path, opening and write the session file.
|
DefaultFileMode = 0666
|
||||||
// Defaults to 0666.
|
|
||||||
PathFileMode uint32 = 0666
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Database is the basic file-storage session database.
|
// 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 {
|
type Database struct {
|
||||||
path string
|
dir string
|
||||||
|
fileMode os.FileMode // defaults to 0666 if missing.
|
||||||
|
// if true then it will use go routines to:
|
||||||
|
// append or re-write a file
|
||||||
|
// create a file
|
||||||
|
// remove a file
|
||||||
|
async bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new file-storage database instance based on the "path".
|
// New creates and returns a new file-storage database instance based on the "path".
|
||||||
func New(path string) *Database {
|
// It will remove any old session files.
|
||||||
lindex := path[len(path)-1]
|
func New(directoryPath string, fileMode os.FileMode) (*Database, error) {
|
||||||
|
lindex := directoryPath[len(directoryPath)-1]
|
||||||
if lindex != os.PathSeparator && lindex != '/' {
|
if lindex != os.PathSeparator && lindex != '/' {
|
||||||
path += string(os.PathSeparator)
|
directoryPath += string(os.PathSeparator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if fileMode <= 0 {
|
||||||
|
fileMode = os.FileMode(DefaultFileMode)
|
||||||
|
}
|
||||||
|
|
||||||
// create directories if necessary
|
// create directories if necessary
|
||||||
os.MkdirAll(path, os.FileMode(PathFileMode))
|
if err := os.MkdirAll(directoryPath, fileMode); err != nil {
|
||||||
return &Database{path: path}
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Database) sessPath(sid string) string {
|
db := &Database{dir: directoryPath, fileMode: fileMode}
|
||||||
return filepath.Join(d.path, sid)
|
return db, db.Cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load loads the values to the underline
|
// Cleanup removes any invalid(have expired) session files, it's being called automatically on `New` as well.
|
||||||
func (d *Database) Load(sid string) (values map[string]interface{}, expireDate *time.Time) {
|
func (db *Database) Cleanup() error {
|
||||||
sessPath := d.sessPath(sid)
|
return filepath.Walk(db.dir, func(path string, info os.FileInfo, err error) error {
|
||||||
f, err := os.OpenFile(sessPath, os.O_RDONLY, os.FileMode(PathFileMode))
|
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 0666.
|
||||||
|
func (db *Database) FileMode(fileMode uint32) *Database {
|
||||||
|
db.fileMode = os.FileMode(fileMode)
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
// Async if true passed then it will use go routines to:
|
||||||
|
// append or re-write a file
|
||||||
|
// create a file
|
||||||
|
// remove a file.
|
||||||
|
//
|
||||||
|
// Defaults to false.
|
||||||
|
func (db *Database) Async(useGoRoutines bool) *Database {
|
||||||
|
db.async = useGoRoutines
|
||||||
|
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 {
|
if err != nil {
|
||||||
// we don't care if filepath doesn't exists yet, it will be created on Update.
|
// we don't care if filepath doesn't exists yet, it will be created later on.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
val, err := ioutil.ReadAll(f)
|
contents, err := ioutil.ReadAll(f)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// we don't care if filepath doesn't exists yet, it will be created on Update.
|
loadErr = errors.New("error while reading the session file's data: %v").Format(err)
|
||||||
golog.Errorf("error while reading the session file's data: %v", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil {
|
storeDB, err = sessions.DecodeRemoteStore(contents)
|
||||||
err = DeserializeBytes(val, &values)
|
|
||||||
if err != nil { // we care for this error only
|
if err != nil { // we care for this error only
|
||||||
golog.Errorf("load error: %v", err)
|
loadErr = errors.New("load error: %v").Format(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync syncs the database.
|
||||||
|
func (db *Database) Sync(p sessions.SyncPayload) {
|
||||||
|
if db.async {
|
||||||
|
go db.sync(p)
|
||||||
|
} else {
|
||||||
|
db.sync(p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return // no expiration
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// serialize the values to be stored as strings inside the session file-storage.
|
if err := db.override(p.SessionID, p.Store); err != nil {
|
||||||
func serialize(values map[string]interface{}) []byte {
|
golog.Errorf("error while writing the session file: %v", err)
|
||||||
val, err := SerializeBytes(values)
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
if err != nil {
|
||||||
golog.Errorf("serialize error: %v", err)
|
return err
|
||||||
|
}
|
||||||
|
return ioutil.WriteFile(
|
||||||
|
db.sessPath(sid),
|
||||||
|
s,
|
||||||
|
db.fileMode,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return val
|
// on destroy, it removes the file
|
||||||
|
func (db *Database) destroy(sid string) error {
|
||||||
|
return db.expireSess(sid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Database) expireSess(sid string) {
|
func (db *Database) expireSess(sid string) error {
|
||||||
go os.Remove(d.sessPath(sid))
|
sessPath := db.sessPath(sid)
|
||||||
}
|
return os.Remove(sessPath)
|
||||||
|
|
||||||
// Update updates the session file-storage.
|
|
||||||
func (d *Database) Update(sid string, newValues map[string]interface{}, expireDate *time.Time) {
|
|
||||||
|
|
||||||
if len(newValues) == 0 { // means delete by call
|
|
||||||
d.expireSess(sid)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete the file on expiration
|
|
||||||
if expireDate != nil && !expireDate.IsZero() {
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
if expireDate.Before(now) {
|
|
||||||
// already expirated, delete it now and return.
|
|
||||||
d.expireSess(sid)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// otherwise set a timer to delete the file automatically
|
|
||||||
afterDur := expireDate.Sub(now)
|
|
||||||
time.AfterFunc(afterDur, func() {
|
|
||||||
d.expireSess(sid)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ioutil.WriteFile(d.sessPath(sid), serialize(newValues), os.FileMode(PathFileMode)); err != nil {
|
|
||||||
golog.Errorf("error while writing the session to the file: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SerializeBytes serializes the "m" into bytes using gob encoder and returns the result.
|
|
||||||
func SerializeBytes(m interface{}) ([]byte, error) {
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
enc := gob.NewEncoder(buf)
|
|
||||||
err := enc.Encode(m)
|
|
||||||
if err == nil {
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeserializeBytes converts the bytes to a go value and puts that to "m" using the gob decoder.
|
|
||||||
func DeserializeBytes(b []byte, m interface{}) error {
|
|
||||||
dec := gob.NewDecoder(bytes.NewBuffer(b))
|
|
||||||
return dec.Decode(m) //no reference here otherwise doesn't work because of go remote object
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +1,44 @@
|
||||||
package redis
|
package redis
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/gob"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/kataras/golog"
|
"github.com/kataras/golog"
|
||||||
|
"github.com/kataras/iris/sessions"
|
||||||
"github.com/kataras/iris/sessions/sessiondb/redis/service"
|
"github.com/kataras/iris/sessions/sessiondb/redis/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Database the redis back-end session database for the sessions.
|
// Database the redis back-end session database for the sessions.
|
||||||
type Database struct {
|
type Database struct {
|
||||||
redis *service.Service
|
redis *service.Service
|
||||||
|
async bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new redis database.
|
// New returns a new redis database.
|
||||||
func New(cfg ...service.Config) *Database {
|
func New(cfg ...service.Config) *Database {
|
||||||
return &Database{redis: service.New(cfg...)}
|
return &Database{redis: service.New(cfg...)}
|
||||||
|
// Note: no need to clean up here, the redis should handle these automatically because of the "SETEX"
|
||||||
|
// but that expiration doesn't depend on the session, instead it depends on the `MaxAgeSeconds`
|
||||||
|
// of the redis database configuration.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config returns the configuration for the redis server bridge, you can change them.
|
// Config returns the configuration for the redis server bridge, you can change them.
|
||||||
func (d *Database) Config() *service.Config {
|
func (db *Database) Config() *service.Config {
|
||||||
return d.redis.Config
|
return db.redis.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Async if true passed then it will use different
|
||||||
|
// go routines to update the redis storage.
|
||||||
|
func (db *Database) Async(useGoRoutines bool) *Database {
|
||||||
|
db.async = useGoRoutines
|
||||||
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load loads the values to the underline.
|
// Load loads the values to the underline.
|
||||||
func (d *Database) Load(sid string) (datas map[string]interface{}, expireDate *time.Time) {
|
func (db *Database) Load(sid string) (storeDB sessions.RemoteStore) {
|
||||||
values := make(map[string]interface{})
|
// values := make(map[string]interface{})
|
||||||
|
|
||||||
if !d.redis.Connected { //yes, check every first time's session for valid redis connection
|
if !db.redis.Connected { //yes, check every first time's session for valid redis connection
|
||||||
d.redis.Connect()
|
db.redis.Connect()
|
||||||
_, err := d.redis.PingPong()
|
_, err := db.redis.PingPong()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
golog.Errorf("redis database error on connect: %v", err)
|
golog.Errorf("redis database error on connect: %v", err)
|
||||||
return
|
return
|
||||||
|
@ -38,66 +46,42 @@ func (d *Database) Load(sid string) (datas map[string]interface{}, expireDate *t
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch the values from this session id and copy-> store them
|
// fetch the values from this session id and copy-> store them
|
||||||
val, err := d.redis.GetBytes(sid)
|
storeMaybe, err := db.redis.Get(sid)
|
||||||
if err == nil {
|
|
||||||
// err removed because of previous TODO
|
|
||||||
DeserializeBytes(val, &values)
|
|
||||||
}
|
|
||||||
|
|
||||||
datas, _ = values["session-data"].(map[string]interface{})
|
|
||||||
|
|
||||||
dbExpireDateValue, exists := values["expire-date"]
|
|
||||||
if !exists {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
expireDateValue, ok := dbExpireDateValue.(time.Time)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return datas, &expireDateValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// serialize the values to be stored as strings inside the Redis, we panic at any serialization error here
|
|
||||||
func serialize(values map[string]interface{}) []byte {
|
|
||||||
val, err := SerializeBytes(values)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
golog.Errorf("error while trying to load session values(%s) from redis: %v", sid, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return val
|
storeDB, ok := storeMaybe.(sessions.RemoteStore)
|
||||||
|
if !ok {
|
||||||
|
golog.Errorf(`error while trying to load session values(%s) from redis:
|
||||||
|
the retrieved value is not a sessions.RemoteStore type, please report that as bug, it should never occur`,
|
||||||
|
sid)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update updates the real redis store
|
return
|
||||||
func (d *Database) Update(sid string, newValues map[string]interface{}, expireDate *time.Time) {
|
}
|
||||||
if len(newValues) == 0 {
|
|
||||||
go d.redis.Delete(sid)
|
// Sync syncs the database.
|
||||||
|
func (db *Database) Sync(p sessions.SyncPayload) {
|
||||||
|
if db.async {
|
||||||
|
go db.sync(p)
|
||||||
} else {
|
} else {
|
||||||
datas := map[string]interface{}{"session-data": newValues}
|
db.sync(p)
|
||||||
if expireDate != nil {
|
}
|
||||||
datas["expire-date"] = *expireDate
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//set/update all the values
|
func (db *Database) sync(p sessions.SyncPayload) {
|
||||||
go d.redis.Set(sid, serialize(datas))
|
if p.Action == sessions.ActionDestroy {
|
||||||
|
db.redis.Delete(p.SessionID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
storeB, err := p.Store.Serialize()
|
||||||
|
if err != nil {
|
||||||
|
golog.Error("error while encoding the remote session store")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
db.redis.Set(p.SessionID, storeB)
|
||||||
|
|
||||||
// SerializeBytes serialize the "m" into bytes using the gob encoder and returns the result.
|
|
||||||
func SerializeBytes(m interface{}) ([]byte, error) {
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
enc := gob.NewEncoder(buf)
|
|
||||||
err := enc.Encode(m)
|
|
||||||
if err == nil {
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeserializeBytes converts the bytes to a go value and puts that to "m" using the gob decoder.
|
|
||||||
func DeserializeBytes(b []byte, m interface{}) error {
|
|
||||||
dec := gob.NewDecoder(bytes.NewBuffer(b))
|
|
||||||
return dec.Decode(m) //no reference here otherwise doesn't work because of go remote object
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,21 +42,21 @@ func (r *Service) CloseConnection() error {
|
||||||
return ErrRedisClosed
|
return ErrRedisClosed
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set sets to the redis
|
// Set sets a key-value to the redis store.
|
||||||
// key string, value string, you can use utils.Serialize(&myobject{}) to convert an object to []byte
|
// The expiration is setted by the MaxAgeSeconds.
|
||||||
func (r *Service) Set(key string, value []byte) (err error) { // map[interface{}]interface{}) (err error) {
|
func (r *Service) Set(key string, value interface{}) error {
|
||||||
c := r.pool.Get()
|
c := r.pool.Get()
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
if err = c.Err(); err != nil {
|
if c.Err() != nil {
|
||||||
return
|
return c.Err()
|
||||||
}
|
}
|
||||||
_, err = c.Do("SETEX", r.Config.Prefix+key, r.Config.MaxAgeSeconds, value)
|
|
||||||
return
|
_, err := c.Do("SETEX", r.Config.Prefix+key, r.Config.MaxAgeSeconds, value)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns value, err by its key
|
// Get returns value, err by its key
|
||||||
// you can use utils.Deserialize((.Get("yourkey"),&theobject{})
|
//returns nil and a filled error if something bad happened.
|
||||||
//returns nil and a filled error if something wrong happens
|
|
||||||
func (r *Service) Get(key string) (interface{}, error) {
|
func (r *Service) Get(key string) (interface{}, error) {
|
||||||
c := r.pool.Get()
|
c := r.pool.Get()
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
|
@ -75,6 +75,27 @@ func (r *Service) Get(key string) (interface{}, error) {
|
||||||
return redisVal, nil
|
return redisVal, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAll returns all redis entries using the "SCAN" command (2.8+).
|
||||||
|
func (r *Service) GetAll() (interface{}, error) {
|
||||||
|
c := r.pool.Get()
|
||||||
|
defer c.Close()
|
||||||
|
if err := c.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
redisVal, err := c.Do("SCAN", 0) // 0 -> cursor
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if redisVal == nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return redisVal, 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
|
||||||
|
@ -97,95 +118,6 @@ func (r *Service) GetBytes(key string) ([]byte, error) {
|
||||||
return redis.Bytes(redisVal, err)
|
return redis.Bytes(redisVal, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetString returns value, err by its key
|
|
||||||
// you can use utils.Deserialize((.GetString("yourkey"),&theobject{})
|
|
||||||
//returns empty string and a filled error if something wrong happens
|
|
||||||
func (r *Service) GetString(key string) (string, error) {
|
|
||||||
redisVal, err := r.Get(key)
|
|
||||||
if redisVal == nil {
|
|
||||||
return "", ErrKeyNotFound.Format(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
sVal, err := redis.String(redisVal, err)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return sVal, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetInt returns value, err by its key
|
|
||||||
// you can use utils.Deserialize((.GetInt("yourkey"),&theobject{})
|
|
||||||
//returns -1 int and a filled error if something wrong happens
|
|
||||||
func (r *Service) GetInt(key string) (int, error) {
|
|
||||||
redisVal, err := r.Get(key)
|
|
||||||
if redisVal == nil {
|
|
||||||
return -1, ErrKeyNotFound.Format(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
intVal, err := redis.Int(redisVal, err)
|
|
||||||
if err != nil {
|
|
||||||
return -1, err
|
|
||||||
}
|
|
||||||
return intVal, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStringMap returns map[string]string, err by its key
|
|
||||||
//returns nil and a filled error if something wrong happens
|
|
||||||
func (r *Service) GetStringMap(key string) (map[string]string, error) {
|
|
||||||
redisVal, err := r.Get(key)
|
|
||||||
if redisVal == nil {
|
|
||||||
return nil, ErrKeyNotFound.Format(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
_map, err := redis.StringMap(redisVal, err)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return _map, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAll returns all keys and their values from a specific key (map[string]string)
|
|
||||||
// returns a filled error if something bad happened
|
|
||||||
func (r *Service) GetAll(key string) (map[string]string, error) {
|
|
||||||
c := r.pool.Get()
|
|
||||||
defer c.Close()
|
|
||||||
if err := c.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
reply, err := c.Do("HGETALL", r.Config.Prefix+key)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if reply == nil {
|
|
||||||
return nil, ErrKeyNotFound.Format(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
return redis.StringMap(reply, err)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllKeysByPrefix returns all []string keys by a key prefix from the redis
|
|
||||||
func (r *Service) GetAllKeysByPrefix(prefix string) ([]string, error) {
|
|
||||||
c := r.pool.Get()
|
|
||||||
defer c.Close()
|
|
||||||
if err := c.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
reply, err := c.Do("KEYS", r.Config.Prefix+prefix)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if reply == nil {
|
|
||||||
return nil, ErrKeyNotFound.Format(prefix)
|
|
||||||
}
|
|
||||||
return redis.Strings(reply, err)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete removes redis entry by specific key
|
// Delete removes redis entry by specific key
|
||||||
func (r *Service) Delete(key string) error {
|
func (r *Service) Delete(key string) error {
|
||||||
c := r.pool.Get()
|
c := r.pool.Get()
|
||||||
|
|
|
@ -36,7 +36,7 @@ func (s *Sessions) UseDatabase(db Database) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateCookie gains the ability of updating the session browser cookie to any method which wants to update it
|
// updateCookie gains the ability of updating the session browser cookie to any method which wants to update it
|
||||||
func (s *Sessions) updateCookie(sid string, ctx context.Context, expires time.Duration) {
|
func (s *Sessions) updateCookie(ctx context.Context, sid string, expires time.Duration) {
|
||||||
cookie := &http.Cookie{}
|
cookie := &http.Cookie{}
|
||||||
|
|
||||||
// The RFC makes no mention of encoding url value, so here I think to encode both sessionid key and the value using the safe(to put and to use as cookie) url-encoding
|
// The RFC makes no mention of encoding url value, so here I think to encode both sessionid key and the value using the safe(to put and to use as cookie) url-encoding
|
||||||
|
@ -107,9 +107,9 @@ 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 = len(sess.values) == 0
|
sess.isNew = sess.values.Len() == 0
|
||||||
|
|
||||||
s.updateCookie(sid, ctx, s.config.Expires)
|
s.updateCookie(ctx, sid, s.config.Expires)
|
||||||
|
|
||||||
return sess
|
return sess
|
||||||
}
|
}
|
||||||
|
@ -119,18 +119,20 @@ func (s *Sessions) Start(ctx context.Context) *Session {
|
||||||
return sess
|
return sess
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShiftExpiraton move the expire date of a session to a new date by using session default timeout configuration
|
// ShiftExpiraton move the expire date of a session to a new date
|
||||||
|
// by using session default timeout configuration.
|
||||||
func (s *Sessions) ShiftExpiraton(ctx context.Context) {
|
func (s *Sessions) ShiftExpiraton(ctx context.Context) {
|
||||||
s.UpdateExpiraton(ctx, s.config.Expires)
|
s.UpdateExpiraton(ctx, s.config.Expires)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateExpiraton change expire date of a session to a new date by using timeout value passed by `expires` parameter
|
// UpdateExpiraton change expire date of a session to a new date
|
||||||
|
// by using timeout value passed by `expires` receiver.
|
||||||
func (s *Sessions) UpdateExpiraton(ctx context.Context, expires time.Duration) {
|
func (s *Sessions) UpdateExpiraton(ctx context.Context, expires time.Duration) {
|
||||||
cookieValue := s.decodeCookieValue(GetCookie(ctx, s.config.Cookie))
|
cookieValue := s.decodeCookieValue(GetCookie(ctx, s.config.Cookie))
|
||||||
|
|
||||||
if cookieValue != "" {
|
if cookieValue != "" {
|
||||||
if s.provider.UpdateExpiraton(cookieValue, expires) {
|
if s.provider.UpdateExpiraton(cookieValue, expires) {
|
||||||
s.updateCookie(cookieValue, ctx, expires)
|
s.updateCookie(ctx, cookieValue, expires)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user