session/storage/files/files.go
2025-06-23 17:53:41 +02:00

235 lines
5.9 KiB
Go

// Package files implements sessions saved into filesystem persistently encoded using gob
package files
import (
"bytes"
"container/list"
"encoding/gob"
"fmt"
"os"
"path"
"sync"
"time"
"git.mtux.eu/darkgopher/session"
"github.com/djherbis/atime"
)
const (
sessDir string = "go-session"
sessExt string = "gsd"
)
var pder = &ProviderFiles{li: list.New()}
func init() {
pder.sessions = make(map[string]*list.Element, 0)
session.Register("files", pder)
}
// ckdirpath return filename from sid
func ckdirpath(sid string) string {
return fmt.Sprintf("%s/%s/%s.%s", pder.sessPath, sessDir, sid, sessExt)
}
// ProviderFiles implement filesystem session provider
type ProviderFiles struct {
lock sync.Mutex
li *list.List //or gc
sessions map[string]*list.Element
sessPath string
}
func (pder *ProviderFiles) updateAtime(sid string) (err error) {
pth := ckdirpath(sid)
return os.Chtimes(pth, time.Now(), time.Time{})
}
func (pder *ProviderFiles) getAtime(sid string) (atm time.Time, err error) {
pth := ckdirpath(sid)
return atime.Stat(pth)
}
// SetParams for files session provider set base path in filesystem for save sessions
func (pder *ProviderFiles) SetParams(p any) (err error) {
if p != nil {
if s, ok := p.(string); ok {
pder.sessPath = s
sdir := path.Join(pder.sessPath, sessDir)
if err = os.MkdirAll(sdir, 0o700); err != nil {
return fmt.Errorf("make session directory path: %s failed: %v", sdir, err)
}
return
}
return fmt.Errorf("parameter for files session provider is not string")
}
return fmt.Errorf("parameter for files session provider must be provided")
}
// Init create empty session file if not exists and retturn *Session
func (pder *ProviderFiles) Init(sid string) (ses session.Session, err error) {
pder.lock.Lock()
defer pder.lock.Unlock()
var fd *os.File
ckf := ckdirpath(sid)
if fd, err = os.Create(ckf); err != nil {
return nil, fmt.Errorf("create session file: %s failed with err: %w", ckf, err)
}
defer fd.Close()
sess := &SessionFile{sid}
ssel := pder.li.PushBack(sess)
pder.sessions[sid] = ssel
return sess, nil
}
// Load return existing session by sid or new session if not exists
func (pder *ProviderFiles) Load(sid string) (sess session.Session, err error) {
pder.lock.Lock()
defer pder.lock.Unlock()
if pder.Exists(sid) {
if sesel, ok := pder.sessions[sid]; ok {
return sesel.Value.(*SessionFile), nil
}
}
return pder.Init(sid)
}
// Destroy remove session file
func (pder *ProviderFiles) Destroy(sid string) (err error) {
pder.lock.Lock()
defer pder.lock.Unlock()
if pder.Exists(sid) {
ssel := pder.sessions[sid]
delete(pder.sessions, sid)
pder.li.Remove(ssel)
return os.Remove(ckdirpath(sid))
}
return
}
// ChangeID change oldsid to newsid and preserve session data
func (pder *ProviderFiles) ChangeID(oldsid, newsid string) (err error) {
if ssel, ok := pder.sessions[oldsid]; ok {
ckfold := ckdirpath(oldsid)
ckfnew := ckdirpath(newsid)
if err = os.Rename(ckfold, ckfnew); err != nil {
return fmt.Errorf("rename cookie file: %s to: %s failed: %v", ckfold, ckfnew, err)
}
ssel.Value.(*SessionFile).sid = newsid
delete(pder.sessions, oldsid)
pder.sessions[newsid] = ssel
}
return
}
// Exists return true if session with sid exists
func (pder *ProviderFiles) Exists(sid string) (ex bool) {
if _, ex := pder.sessions[sid]; ex {
return ex
}
return
}
// GC periodically remove old sessions rom storages
func (pder *ProviderFiles) GC(maxlifetime int64) {
pder.lock.Lock()
defer pder.lock.Unlock()
for {
ssel := pder.li.Back()
if ssel == nil {
break
}
sid := ssel.Value.(*SessionFile).sid
var at time.Time
var err error
if at, err = pder.getAtime(sid); err != nil {
continue
}
if at.UnixMilli()+(maxlifetime*int64(session.MilisPerSecDuration())) < time.Now().UnixMilli() {
pder.li.Remove(ssel)
delete(pder.sessions, sid)
ckf := ckdirpath(sid)
os.Remove(ckf)
} else {
break
}
}
}
// SessionFile save session data into files using gob encode/dacode
type SessionFile struct {
sid string
}
// loadFromFile data from session file
func (sf *SessionFile) loadFromFile() (data map[any]any, err error) {
var sb []byte
sfp := ckdirpath(sf.sid)
data = make(map[any]any, 0)
if sb, err = os.ReadFile(sfp); err != nil {
return nil, fmt.Errorf("session file: %s read error: %v", sf.sid, err)
}
var gobdata bytes.Buffer
if _, err = gobdata.Write(sb); err != nil {
return nil, fmt.Errorf("load session file: %s into buffer err: %v", sf.sid, err)
}
if gobdata.Len() == 0 {
return data, nil
}
dec := gob.NewDecoder(&gobdata)
if err = dec.Decode(&data); err != nil {
return nil, fmt.Errorf("decode gob data: %d from file: %s error: %v", gobdata.Len(), sf.sid, err)
}
return
}
// saveToFile data into session file
func (sf *SessionFile) saveToFile(data map[any]any) (err error) {
var gobdata bytes.Buffer
enc := gob.NewEncoder(&gobdata)
if err = enc.Encode(data); err != nil {
return fmt.Errorf("gob encode file: %s error: %v", sf.sid, err)
}
sfp := ckdirpath(sf.sid)
if err = os.WriteFile(sfp, gobdata.Bytes(), 0o600); err != nil {
return fmt.Errorf("write gob data into file: %s error: %v", sf.sid, err)
}
return nil
}
// Get value from session key k
func (sf *SessionFile) Get(k any) (v any, err error) {
defer pder.updateAtime(sf.sid)
var data map[any]any
if data, err = sf.loadFromFile(); err != nil {
return nil, err
}
return data[k], nil
}
// Set value of key k to v
func (sf *SessionFile) Set(k, v any) (err error) {
defer pder.updateAtime(sf.sid)
var data map[any]any
if data, err = sf.loadFromFile(); err != nil {
return err
}
data[k] = v
return sf.saveToFile(data)
}
// Delete remove value of key k from session
func (sf *SessionFile) Delete(k any) (err error) {
defer pder.updateAtime(sf.sid)
var data map[any]any
if data, err = sf.loadFromFile(); err != nil {
return err
}
delete(data, k)
return sf.saveToFile(data)
}
// SessionID return sid
func (sf *SessionFile) SessionID() (sid string) {
return sf.sid
}