github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/office/store.go (about)

     1  package office
     2  
     3  import (
     4  	"context"
     5  	"encoding/hex"
     6  	"encoding/json"
     7  	"errors"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/cozy/cozy-stack/pkg/config/config"
    12  	"github.com/cozy/cozy-stack/pkg/crypto"
    13  	"github.com/cozy/cozy-stack/pkg/prefixer"
    14  	"github.com/redis/go-redis/v9"
    15  )
    16  
    17  type ConflictDetector struct {
    18  	ID     string
    19  	Rev    string
    20  	MD5Sum []byte
    21  }
    22  
    23  // Store is an object to store and retrieve document server keys <-> id,rev
    24  type Store interface {
    25  	GetSecretByID(db prefixer.Prefixer, id string) (string, error)
    26  	UpdateSecret(db prefixer.Prefixer, secret, oldID, newID string) error
    27  	AddDoc(db prefixer.Prefixer, payload ConflictDetector) (string, error)
    28  	GetDoc(db prefixer.Prefixer, secret string) (*ConflictDetector, error)
    29  	UpdateDoc(db prefixer.Prefixer, secret string, payload ConflictDetector) error
    30  	RemoveDoc(db prefixer.Prefixer, secret string) error
    31  }
    32  
    33  // storeTTL is the time an entry stay alive
    34  var storeTTL = 30 * 24 * time.Hour
    35  
    36  // storeCleanInterval is the time interval between each cleanup.
    37  var storeCleanInterval = 1 * time.Hour
    38  
    39  var mu sync.Mutex
    40  var globalStore Store
    41  
    42  // GetStore returns the store for temporary move objects.
    43  func GetStore() Store {
    44  	mu.Lock()
    45  	defer mu.Unlock()
    46  	if globalStore != nil {
    47  		return globalStore
    48  	}
    49  	cli := config.GetConfig().SessionStorage
    50  	if cli == nil {
    51  		globalStore = newMemStore()
    52  	} else {
    53  		ctx := context.Background()
    54  		globalStore = &redisStore{cli, ctx}
    55  	}
    56  	return globalStore
    57  }
    58  
    59  type memRef struct {
    60  	val ConflictDetector
    61  	exp time.Time
    62  }
    63  
    64  func newMemStore() Store {
    65  	store := &memStore{
    66  		vals: make(map[string]*memRef),
    67  		byID: make(map[string]string),
    68  	}
    69  	go store.cleaner()
    70  	return store
    71  }
    72  
    73  type memStore struct {
    74  	mu   sync.Mutex
    75  	vals map[string]*memRef
    76  	byID map[string]string // id -> secret
    77  }
    78  
    79  func (s *memStore) cleaner() {
    80  	for range time.Tick(storeCleanInterval) {
    81  		s.mu.Lock()
    82  		now := time.Now()
    83  		for k, v := range s.vals {
    84  			if now.After(v.exp) {
    85  				if s.byID[v.val.ID] == k {
    86  					delete(s.byID, v.val.ID)
    87  				}
    88  				delete(s.vals, k)
    89  			}
    90  		}
    91  		s.mu.Unlock()
    92  	}
    93  }
    94  
    95  func (s *memStore) GetSecretByID(db prefixer.Prefixer, id string) (string, error) {
    96  	s.mu.Lock()
    97  	defer s.mu.Unlock()
    98  	return s.byID[id], nil
    99  }
   100  
   101  func (s *memStore) UpdateSecret(db prefixer.Prefixer, secret, oldID, newID string) error {
   102  	s.mu.Lock()
   103  	defer s.mu.Unlock()
   104  	delete(s.byID, oldID)
   105  	s.byID[newID] = secret
   106  	return nil
   107  }
   108  
   109  func (s *memStore) AddDoc(db prefixer.Prefixer, payload ConflictDetector) (string, error) {
   110  	s.mu.Lock()
   111  	defer s.mu.Unlock()
   112  	secret := makeSecret()
   113  	s.byID[payload.ID] = secret
   114  	key := docKey(db, secret)
   115  	s.vals[key] = &memRef{
   116  		val: payload,
   117  		exp: time.Now().Add(storeTTL),
   118  	}
   119  	return secret, nil
   120  }
   121  
   122  func (s *memStore) GetDoc(db prefixer.Prefixer, secret string) (*ConflictDetector, error) {
   123  	s.mu.Lock()
   124  	defer s.mu.Unlock()
   125  	key := docKey(db, secret)
   126  	ref, ok := s.vals[key]
   127  	if !ok {
   128  		return nil, nil
   129  	}
   130  	if time.Now().After(ref.exp) {
   131  		delete(s.vals, key)
   132  		return nil, nil
   133  	}
   134  	return &ref.val, nil
   135  }
   136  
   137  func (s *memStore) UpdateDoc(db prefixer.Prefixer, secret string, payload ConflictDetector) error {
   138  	s.mu.Lock()
   139  	defer s.mu.Unlock()
   140  	if s.byID[payload.ID] != secret {
   141  		return nil
   142  	}
   143  	key := docKey(db, secret)
   144  	s.vals[key] = &memRef{
   145  		val: payload,
   146  		exp: time.Now().Add(storeTTL),
   147  	}
   148  	return nil
   149  }
   150  
   151  func (s *memStore) RemoveDoc(db prefixer.Prefixer, secret string) error {
   152  	s.mu.Lock()
   153  	defer s.mu.Unlock()
   154  	key := docKey(db, secret)
   155  	if v, ok := s.vals[key]; ok {
   156  		if s.byID[v.val.ID] == secret {
   157  			delete(s.byID, v.val.ID)
   158  		}
   159  	}
   160  	delete(s.vals, key)
   161  	return nil
   162  }
   163  
   164  type redisStore struct {
   165  	c   redis.UniversalClient
   166  	ctx context.Context
   167  }
   168  
   169  func (s *redisStore) GetSecretByID(db prefixer.Prefixer, id string) (string, error) {
   170  	idKey := docKey(db, id)
   171  	res, err := s.c.Get(s.ctx, idKey).Result()
   172  	if errors.Is(err, redis.Nil) {
   173  		return "", nil
   174  	}
   175  	return res, err
   176  }
   177  
   178  func (s *redisStore) UpdateSecret(db prefixer.Prefixer, secret, oldID, newID string) error {
   179  	_ = s.c.Del(s.ctx, docKey(db, oldID))
   180  	idKey := docKey(db, newID)
   181  	return s.c.Set(s.ctx, idKey, secret, storeTTL).Err()
   182  }
   183  
   184  func (s *redisStore) AddDoc(db prefixer.Prefixer, payload ConflictDetector) (string, error) {
   185  	idKey := docKey(db, payload.ID)
   186  	v, err := json.Marshal(payload)
   187  	if err != nil {
   188  		return "", err
   189  	}
   190  	secret := makeSecret()
   191  	key := docKey(db, secret)
   192  	if err = s.c.Set(s.ctx, key, v, storeTTL).Err(); err != nil {
   193  		return "", err
   194  	}
   195  	if err = s.c.Set(s.ctx, idKey, secret, storeTTL).Err(); err != nil {
   196  		return "", err
   197  	}
   198  	return secret, nil
   199  }
   200  
   201  func (s *redisStore) GetDoc(db prefixer.Prefixer, secret string) (*ConflictDetector, error) {
   202  	key := docKey(db, secret)
   203  	b, err := s.c.Get(s.ctx, key).Bytes()
   204  	if errors.Is(err, redis.Nil) {
   205  		return nil, nil
   206  	}
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  	var val ConflictDetector
   211  	if err = json.Unmarshal(b, &val); err != nil {
   212  		return nil, err
   213  	}
   214  	return &val, nil
   215  }
   216  
   217  func (s *redisStore) UpdateDoc(db prefixer.Prefixer, secret string, payload ConflictDetector) error {
   218  	idKey := docKey(db, payload.ID)
   219  	result, err := s.c.Get(s.ctx, idKey).Result()
   220  	if err != nil {
   221  		return err
   222  	}
   223  	if result != secret {
   224  		return nil
   225  	}
   226  	v, err := json.Marshal(payload)
   227  	if err != nil {
   228  		return err
   229  	}
   230  	key := docKey(db, secret)
   231  	if err = s.c.Set(s.ctx, key, v, storeTTL).Err(); err != nil {
   232  		return err
   233  	}
   234  	return nil
   235  }
   236  
   237  func (s *redisStore) RemoveDoc(db prefixer.Prefixer, secret string) error {
   238  	payload, _ := s.GetDoc(db, secret)
   239  	if payload != nil {
   240  		idKey := docKey(db, payload.ID)
   241  		if result, err := s.c.Get(s.ctx, idKey).Result(); err == nil && result == secret {
   242  			_ = s.c.Del(s.ctx, idKey)
   243  		}
   244  	}
   245  	key := docKey(db, secret)
   246  	return s.c.Del(s.ctx, key).Err()
   247  }
   248  
   249  func docKey(db prefixer.Prefixer, suffix string) string {
   250  	return db.DBPrefix() + ":oodoc:" + suffix
   251  }
   252  
   253  func makeSecret() string {
   254  	return hex.EncodeToString(crypto.GenerateRandomBytes(12))
   255  }