kuvia2/vendor/github.com/flamego/session/file.go

204 lines
4.8 KiB
Go
Raw Normal View History

2022-01-14 23:09:03 +00:00
// Copyright 2021 Flamego. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package session
import (
"context"
"fmt"
"io/fs"
"os"
"path/filepath"
"time"
"github.com/pkg/errors"
)
var _ Store = (*fileStore)(nil)
// fileStore is a file implementation of the session store.
type fileStore struct {
nowFunc func() time.Time // The function to return the current time
lifetime time.Duration // The duration to have no access to a session before being recycled
rootDir string // The root directory of file session items stored on the local file system
encoder Encoder // The encoder to encode the session data before saving
decoder Decoder // The decoder to decode binary to session data after reading
}
// newFileStore returns a new file session store based on given configuration.
func newFileStore(cfg FileConfig) *fileStore {
return &fileStore{
nowFunc: cfg.nowFunc,
lifetime: cfg.Lifetime,
rootDir: cfg.RootDir,
encoder: cfg.Encoder,
decoder: cfg.Decoder,
}
}
// filename returns the computed file name with given sid.
func (s *fileStore) filename(sid string) string {
return filepath.Join(s.rootDir, string(sid[0]), string(sid[1]), sid)
}
// isFile returns true if given path exists as a file (i.e. not a directory).
func isFile(path string) bool {
f, e := os.Stat(path)
if e != nil {
return false
}
return !f.IsDir()
}
func (s *fileStore) Exist(_ context.Context, sid string) bool {
if len(sid) < minimumSIDLength {
return false
}
return isFile(s.filename(sid))
}
func (s *fileStore) Read(ctx context.Context, sid string) (Session, error) {
if len(sid) < minimumSIDLength {
return nil, ErrMinimumSIDLength
}
filename := s.filename(sid)
if !isFile(filename) {
err := os.MkdirAll(filepath.Dir(filename), 0700)
if err != nil {
return nil, errors.Wrap(err, "create parent directory")
}
return NewBaseSession(sid, s.encoder), nil
}
// Discard existing data if it's expired
fi, err := os.Stat(filename)
if err != nil {
return nil, errors.Wrap(err, "stat file")
}
if !fi.ModTime().Add(s.lifetime).After(s.nowFunc()) {
return NewBaseSession(sid, s.encoder), nil
}
binary, err := os.ReadFile(filename)
if err != nil {
return nil, errors.Wrap(err, "read file")
}
data, err := s.decoder(binary)
if err != nil {
return nil, errors.Wrap(err, "decode")
}
sess := NewBaseSession(sid, s.encoder)
sess.SetData(data)
return sess, nil
}
func (s *fileStore) Destroy(_ context.Context, sid string) error {
if len(sid) < minimumSIDLength {
return nil
}
return os.Remove(s.filename(sid))
}
func (s *fileStore) Save(_ context.Context, sess Session) error {
if len(sess.ID()) < minimumSIDLength {
return ErrMinimumSIDLength
}
binary, err := sess.Encode()
if err != nil {
return errors.Wrap(err, "encode")
}
err = os.WriteFile(s.filename(sess.ID()), binary, 0600)
if err != nil {
return errors.Wrap(err, "write file")
}
return nil
}
func (s *fileStore) GC(ctx context.Context) error {
err := filepath.WalkDir(s.rootDir, func(path string, d fs.DirEntry, err error) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if err != nil {
return err
}
if d.IsDir() {
return nil
}
fi, err := d.Info()
if err != nil {
return err
}
if fi.ModTime().Add(s.lifetime).After(s.nowFunc()) {
return nil
}
return os.Remove(path)
})
if err != nil && err != ctx.Err() {
return err
}
return nil
}
// FileConfig contains options for the file session store.
type FileConfig struct {
// For tests only
nowFunc func() time.Time
// Lifetime is the duration to have no access to a session before being
// recycled. Default is 3600 seconds.
Lifetime time.Duration
// RootDir is the root directory of file session items stored on the local file
// system. Default is "sessions".
RootDir string
// Encoder is the encoder to encode session data. Default is GobEncoder.
Encoder Encoder
// Decoder is the decoder to decode session data. Default is GobDecoder.
Decoder Decoder
}
// FileIniter returns the Initer for the file session store.
func FileIniter() Initer {
return func(ctx context.Context, args ...interface{}) (Store, error) {
var cfg *FileConfig
for i := range args {
switch v := args[i].(type) {
case FileConfig:
cfg = &v
}
}
if cfg == nil {
return nil, fmt.Errorf("config object with the type '%T' not found", FileConfig{})
}
if cfg.nowFunc == nil {
cfg.nowFunc = time.Now
}
if cfg.Lifetime.Seconds() < 1 {
cfg.Lifetime = 3600 * time.Second
}
if cfg.RootDir == "" {
cfg.RootDir = "sessions"
}
if cfg.Encoder == nil {
cfg.Encoder = GobEncoder
}
if cfg.Decoder == nil {
cfg.Decoder = GobDecoder
}
return newFileStore(*cfg), nil
}
}