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 }