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

     1  package vfs
     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  // Store is essentially a place to store transient objects between two HTTP
    18  // requests: it can be a secret for downloading a file, a list of files in an
    19  // archive, or a metadata object for an upcoming upload.
    20  type Store interface {
    21  	AddFile(db prefixer.Prefixer, filePath string) (string, error)
    22  	AddThumb(db prefixer.Prefixer, fileID string) (string, error)
    23  	AddThumbs(db prefixer.Prefixer, fileIDs []string) (map[string]string, error)
    24  	AddVersion(db prefixer.Prefixer, versionID string) (string, error)
    25  	AddArchive(db prefixer.Prefixer, archive *Archive) (string, error)
    26  	AddMetadata(db prefixer.Prefixer, metadata *Metadata) (string, error)
    27  	GetFile(db prefixer.Prefixer, key string) (string, error)
    28  	GetThumb(db prefixer.Prefixer, key string) (string, error)
    29  	GetVersion(db prefixer.Prefixer, key string) (string, error)
    30  	GetArchive(db prefixer.Prefixer, key string) (*Archive, error)
    31  	GetMetadata(db prefixer.Prefixer, key string) (*Metadata, error)
    32  }
    33  
    34  // storeTTL is time after which the data in the store will be considered stale.
    35  var storeTTL = 10 * time.Minute
    36  
    37  // storeCleanInterval is the time interval between each download cleanup.
    38  var storeCleanInterval = 1 * time.Hour
    39  
    40  var globalStoreMu sync.Mutex
    41  var globalStore Store
    42  
    43  // GetStore returns the global Store.
    44  func GetStore() Store {
    45  	globalStoreMu.Lock()
    46  	defer globalStoreMu.Unlock()
    47  	if globalStore != nil {
    48  		return globalStore
    49  	}
    50  	cli := config.GetConfig().DownloadStorage
    51  	if cli == nil {
    52  		globalStore = newMemStore()
    53  	} else {
    54  		globalStore = newRedisStore(cli)
    55  	}
    56  	return globalStore
    57  }
    58  
    59  func newMemStore() Store {
    60  	store := &memStore{vals: make(map[string]*memRef)}
    61  	go store.cleaner()
    62  	return store
    63  }
    64  
    65  type memStore struct {
    66  	mu   sync.Mutex
    67  	vals map[string]*memRef
    68  }
    69  
    70  type memRef struct {
    71  	val interface{}
    72  	exp time.Time
    73  }
    74  
    75  func (s *memStore) cleaner() {
    76  	for range time.Tick(storeCleanInterval) {
    77  		now := time.Now()
    78  		for k, v := range s.vals {
    79  			if now.After(v.exp) {
    80  				delete(s.vals, k)
    81  			}
    82  		}
    83  	}
    84  }
    85  
    86  func (s *memStore) AddFile(db prefixer.Prefixer, filePath string) (string, error) {
    87  	key := makeSecret()
    88  	s.mu.Lock()
    89  	defer s.mu.Unlock()
    90  	s.vals[db.DBPrefix()+":"+key] = &memRef{
    91  		val: filePath,
    92  		exp: time.Now().Add(storeTTL),
    93  	}
    94  	return key, nil
    95  }
    96  
    97  func (s *memStore) AddThumb(db prefixer.Prefixer, fileID string) (string, error) {
    98  	key := makeSecret()
    99  	s.mu.Lock()
   100  	defer s.mu.Unlock()
   101  	s.vals[db.DBPrefix()+":"+key] = &memRef{
   102  		val: fileID,
   103  		exp: time.Now().Add(storeTTL),
   104  	}
   105  	return key, nil
   106  }
   107  
   108  func (s *memStore) AddThumbs(db prefixer.Prefixer, fileIDs []string) (map[string]string, error) {
   109  	secrets := make(map[string]string)
   110  	for _, fileID := range fileIDs {
   111  		secret, err := s.AddThumb(db, fileID)
   112  		if err != nil {
   113  			return nil, err
   114  		}
   115  		secrets[fileID] = secret
   116  	}
   117  	return secrets, nil
   118  }
   119  
   120  func (s *memStore) AddVersion(db prefixer.Prefixer, versionID string) (string, error) {
   121  	key := makeSecret()
   122  	s.mu.Lock()
   123  	defer s.mu.Unlock()
   124  	s.vals[db.DBPrefix()+":"+key] = &memRef{
   125  		val: versionID,
   126  		exp: time.Now().Add(storeTTL),
   127  	}
   128  	return key, nil
   129  }
   130  
   131  func (s *memStore) AddArchive(db prefixer.Prefixer, archive *Archive) (string, error) {
   132  	key := makeSecret()
   133  	s.mu.Lock()
   134  	defer s.mu.Unlock()
   135  	s.vals[db.DBPrefix()+":"+key] = &memRef{
   136  		val: archive,
   137  		exp: time.Now().Add(storeTTL),
   138  	}
   139  	return key, nil
   140  }
   141  
   142  func (s *memStore) AddMetadata(db prefixer.Prefixer, metadata *Metadata) (string, error) {
   143  	key := makeSecret()
   144  	s.mu.Lock()
   145  	defer s.mu.Unlock()
   146  	s.vals[db.DBPrefix()+":"+key] = &memRef{
   147  		val: metadata,
   148  		exp: time.Now().Add(storeTTL),
   149  	}
   150  	return key, nil
   151  }
   152  
   153  func (s *memStore) GetFile(db prefixer.Prefixer, key string) (string, error) {
   154  	s.mu.Lock()
   155  	defer s.mu.Unlock()
   156  	key = db.DBPrefix() + ":" + key
   157  	ref, ok := s.vals[key]
   158  	if !ok {
   159  		return "", ErrWrongToken
   160  	}
   161  	if time.Now().After(ref.exp) {
   162  		delete(s.vals, key)
   163  		return "", ErrWrongToken
   164  	}
   165  	f, ok := ref.val.(string)
   166  	if !ok {
   167  		return "", ErrWrongToken
   168  	}
   169  	return f, nil
   170  }
   171  
   172  func (s *memStore) GetThumb(db prefixer.Prefixer, key string) (string, error) {
   173  	s.mu.Lock()
   174  	defer s.mu.Unlock()
   175  	key = db.DBPrefix() + ":" + key
   176  	ref, ok := s.vals[key]
   177  	if !ok {
   178  		return "", ErrWrongToken
   179  	}
   180  	if time.Now().After(ref.exp) {
   181  		delete(s.vals, key)
   182  		return "", ErrWrongToken
   183  	}
   184  	f, ok := ref.val.(string)
   185  	if !ok {
   186  		return "", ErrWrongToken
   187  	}
   188  	return f, nil
   189  }
   190  
   191  func (s *memStore) GetVersion(db prefixer.Prefixer, key string) (string, error) {
   192  	s.mu.Lock()
   193  	defer s.mu.Unlock()
   194  	key = db.DBPrefix() + ":" + key
   195  	ref, ok := s.vals[key]
   196  	if !ok {
   197  		return "", ErrWrongToken
   198  	}
   199  	if time.Now().After(ref.exp) {
   200  		delete(s.vals, key)
   201  		return "", ErrWrongToken
   202  	}
   203  	f, ok := ref.val.(string)
   204  	if !ok {
   205  		return "", ErrWrongToken
   206  	}
   207  	return f, nil
   208  }
   209  
   210  func (s *memStore) GetArchive(db prefixer.Prefixer, key string) (*Archive, error) {
   211  	s.mu.Lock()
   212  	defer s.mu.Unlock()
   213  	key = db.DBPrefix() + ":" + key
   214  	ref, ok := s.vals[key]
   215  	if !ok {
   216  		return nil, ErrWrongToken
   217  	}
   218  	if time.Now().After(ref.exp) {
   219  		delete(s.vals, key)
   220  		return nil, ErrWrongToken
   221  	}
   222  	a, ok := ref.val.(*Archive)
   223  	if !ok {
   224  		return nil, ErrWrongToken
   225  	}
   226  	return a, nil
   227  }
   228  
   229  func (s *memStore) GetMetadata(db prefixer.Prefixer, key string) (*Metadata, error) {
   230  	s.mu.Lock()
   231  	defer s.mu.Unlock()
   232  	key = db.DBPrefix() + ":" + key
   233  	ref, ok := s.vals[key]
   234  	if !ok {
   235  		return nil, ErrInvalidMetadataID
   236  	}
   237  	if time.Now().After(ref.exp) {
   238  		delete(s.vals, key)
   239  		return nil, ErrInvalidMetadataID
   240  	}
   241  	m, ok := ref.val.(*Metadata)
   242  	if !ok {
   243  		return nil, ErrInvalidMetadataID
   244  	}
   245  	return m, nil
   246  }
   247  
   248  type redisStore struct {
   249  	c   redis.UniversalClient
   250  	ctx context.Context
   251  }
   252  
   253  func newRedisStore(cli redis.UniversalClient) Store {
   254  	ctx := context.Background()
   255  	return &redisStore{cli, ctx}
   256  }
   257  
   258  func (s *redisStore) AddFile(db prefixer.Prefixer, filePath string) (string, error) {
   259  	key := makeSecret()
   260  	if err := s.c.Set(s.ctx, db.DBPrefix()+":"+key, filePath, storeTTL).Err(); err != nil {
   261  		return "", err
   262  	}
   263  	return key, nil
   264  }
   265  
   266  func (s *redisStore) AddThumb(db prefixer.Prefixer, fileID string) (string, error) {
   267  	key := makeSecret()
   268  	if err := s.c.Set(s.ctx, db.DBPrefix()+":"+key, fileID, storeTTL).Err(); err != nil {
   269  		return "", err
   270  	}
   271  	return key, nil
   272  }
   273  
   274  func (s *redisStore) AddThumbs(db prefixer.Prefixer, fileIDs []string) (map[string]string, error) {
   275  	secrets := make(map[string]string)
   276  	pipe := s.c.Pipeline()
   277  	for _, fileID := range fileIDs {
   278  		key := makeSecret()
   279  		secrets[fileID] = key
   280  		pipe.Set(s.ctx, db.DBPrefix()+":"+key, fileID, storeTTL)
   281  	}
   282  	if _, err := pipe.Exec(s.ctx); err != nil {
   283  		return nil, err
   284  	}
   285  	return secrets, nil
   286  }
   287  
   288  func (s *redisStore) AddVersion(db prefixer.Prefixer, versionID string) (string, error) {
   289  	key := makeSecret()
   290  	if err := s.c.Set(s.ctx, db.DBPrefix()+":"+key, versionID, storeTTL).Err(); err != nil {
   291  		return "", err
   292  	}
   293  	return key, nil
   294  }
   295  
   296  func (s *redisStore) AddArchive(db prefixer.Prefixer, archive *Archive) (string, error) {
   297  	v, err := json.Marshal(archive)
   298  	if err != nil {
   299  		return "", err
   300  	}
   301  	key := makeSecret()
   302  	if err = s.c.Set(s.ctx, db.DBPrefix()+":"+key, v, storeTTL).Err(); err != nil {
   303  		return "", err
   304  	}
   305  	return key, nil
   306  }
   307  
   308  func (s *redisStore) AddMetadata(db prefixer.Prefixer, metadata *Metadata) (string, error) {
   309  	v, err := json.Marshal(metadata)
   310  	if err != nil {
   311  		return "", err
   312  	}
   313  	key := makeSecret()
   314  	if err = s.c.Set(s.ctx, db.DBPrefix()+":"+key, v, storeTTL).Err(); err != nil {
   315  		return "", err
   316  	}
   317  	return key, nil
   318  }
   319  
   320  func (s *redisStore) GetFile(db prefixer.Prefixer, key string) (string, error) {
   321  	f, err := s.c.Get(s.ctx, db.DBPrefix()+":"+key).Result()
   322  	if errors.Is(err, redis.Nil) {
   323  		return "", ErrWrongToken
   324  	}
   325  	if err != nil {
   326  		return "", err
   327  	}
   328  	return f, nil
   329  }
   330  
   331  func (s *redisStore) GetThumb(db prefixer.Prefixer, key string) (string, error) {
   332  	f, err := s.c.Get(s.ctx, db.DBPrefix()+":"+key).Result()
   333  	if errors.Is(err, redis.Nil) {
   334  		return "", ErrWrongToken
   335  	}
   336  	if err != nil {
   337  		return "", err
   338  	}
   339  	return f, nil
   340  }
   341  
   342  func (s *redisStore) GetVersion(db prefixer.Prefixer, key string) (string, error) {
   343  	f, err := s.c.Get(s.ctx, db.DBPrefix()+":"+key).Result()
   344  	if errors.Is(err, redis.Nil) {
   345  		return "", ErrWrongToken
   346  	}
   347  	if err != nil {
   348  		return "", err
   349  	}
   350  	return f, nil
   351  }
   352  
   353  func (s *redisStore) GetArchive(db prefixer.Prefixer, key string) (*Archive, error) {
   354  	b, err := s.c.Get(s.ctx, db.DBPrefix()+":"+key).Bytes()
   355  	if errors.Is(err, redis.Nil) {
   356  		return nil, ErrWrongToken
   357  	}
   358  	if err != nil {
   359  		return nil, err
   360  	}
   361  	arch := &Archive{}
   362  	if err = json.Unmarshal(b, arch); err != nil {
   363  		return nil, err
   364  	}
   365  	return arch, nil
   366  }
   367  
   368  func (s *redisStore) GetMetadata(db prefixer.Prefixer, key string) (*Metadata, error) {
   369  	b, err := s.c.Get(s.ctx, db.DBPrefix()+":"+key).Bytes()
   370  	if errors.Is(err, redis.Nil) {
   371  		return nil, ErrInvalidMetadataID
   372  	}
   373  	if err != nil {
   374  		return nil, err
   375  	}
   376  	meta := &Metadata{}
   377  	if err = json.Unmarshal(b, meta); err != nil {
   378  		return nil, err
   379  	}
   380  	return meta, nil
   381  }
   382  
   383  func makeSecret() string {
   384  	return hex.EncodeToString(crypto.GenerateRandomBytes(8))
   385  }