github.com/cozy/cozy-stack@v0.0.0-20240327093429-939e4a21320e/model/sharing/upload_store.go (about)

     1  package sharing
     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  // A UploadStore is essentially an object to store files metadata by key
    18  type UploadStore interface {
    19  	Get(db prefixer.Prefixer, key string) (*FileDocWithRevisions, error)
    20  	Save(db prefixer.Prefixer, doc *FileDocWithRevisions) (string, error)
    21  }
    22  
    23  // uploadStoreTTL is the time an entry stay alive
    24  var uploadStoreTTL = 5 * time.Minute
    25  
    26  // uploadStoreCleanInterval is the time interval between each upload
    27  // cleanup.
    28  var uploadStoreCleanInterval = 1 * time.Hour
    29  
    30  var mu sync.Mutex
    31  var globalStore UploadStore
    32  
    33  func getStore() UploadStore {
    34  	mu.Lock()
    35  	defer mu.Unlock()
    36  	if globalStore != nil {
    37  		return globalStore
    38  	}
    39  	cli := config.GetConfig().DownloadStorage
    40  	if cli == nil {
    41  		globalStore = newMemStore()
    42  	} else {
    43  		ctx := context.Background()
    44  		globalStore = &redisStore{cli, ctx}
    45  	}
    46  	return globalStore
    47  }
    48  
    49  type memRef struct {
    50  	val *FileDocWithRevisions
    51  	exp time.Time
    52  }
    53  
    54  func newMemStore() UploadStore {
    55  	store := &memStore{vals: make(map[string]*memRef)}
    56  	go store.cleaner()
    57  	return store
    58  }
    59  
    60  type memStore struct {
    61  	mu   sync.Mutex
    62  	vals map[string]*memRef
    63  }
    64  
    65  func (s *memStore) cleaner() {
    66  	for range time.Tick(uploadStoreCleanInterval) {
    67  		now := time.Now()
    68  		for k, v := range s.vals {
    69  			if now.After(v.exp) {
    70  				delete(s.vals, k)
    71  			}
    72  		}
    73  	}
    74  }
    75  
    76  func (s *memStore) Get(db prefixer.Prefixer, key string) (*FileDocWithRevisions, error) {
    77  	s.mu.Lock()
    78  	defer s.mu.Unlock()
    79  	key = db.DBPrefix() + ":" + key
    80  	ref, ok := s.vals[key]
    81  	if !ok {
    82  		return nil, nil
    83  	}
    84  	if time.Now().After(ref.exp) {
    85  		delete(s.vals, key)
    86  	}
    87  	return ref.val, nil
    88  }
    89  
    90  func (s *memStore) Save(db prefixer.Prefixer, doc *FileDocWithRevisions) (string, error) {
    91  	key := makeSecret()
    92  	s.mu.Lock()
    93  	defer s.mu.Unlock()
    94  	s.vals[db.DBPrefix()+":"+key] = &memRef{
    95  		val: doc,
    96  		exp: time.Now().Add(uploadStoreTTL),
    97  	}
    98  	return key, nil
    99  }
   100  
   101  type redisStore struct {
   102  	c   redis.UniversalClient
   103  	ctx context.Context
   104  }
   105  
   106  func (s *redisStore) Get(db prefixer.Prefixer, key string) (*FileDocWithRevisions, error) {
   107  	b, err := s.c.Get(s.ctx, db.DBPrefix()+":"+key).Bytes()
   108  	if errors.Is(err, redis.Nil) {
   109  		return nil, nil
   110  	}
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  	var doc FileDocWithRevisions
   115  	if err = json.Unmarshal(b, &doc); err != nil {
   116  		return nil, err
   117  	}
   118  	return &doc, nil
   119  }
   120  
   121  func (s *redisStore) Save(db prefixer.Prefixer, doc *FileDocWithRevisions) (string, error) {
   122  	v, err := json.Marshal(doc)
   123  	if err != nil {
   124  		return "", err
   125  	}
   126  	key := makeSecret()
   127  	if err = s.c.Set(s.ctx, db.DBPrefix()+":"+key, v, uploadStoreTTL).Err(); err != nil {
   128  		return "", err
   129  	}
   130  	return key, nil
   131  }
   132  
   133  func makeSecret() string {
   134  	return hex.EncodeToString(crypto.GenerateRandomBytes(8))
   135  }