// Package files implements sessions saved into filesystem persistently encoded using gob package files import ( "bytes" "container/list" "encoding/gob" "fmt" "log" "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 } /*// Exists check if session sid exists in storage func (pder *ProviderFiles) Exists(sid string) bool { ckf := ckdirpath(sid) if _, err := os.Stat(ckf); errors.Is(err, os.ErrExist) { return false } return true }*/ // 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() { log.Printf("GC remove session: %s atime: %v now: %v", sid, at, time.Now()) 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 }