github.com/decred/politeia@v1.4.0/politeiad/backendv2/tstorebe/inventory.go (about) 1 // Copyright (c) 2020-2021 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package tstorebe 6 7 import ( 8 "encoding/hex" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "os" 13 "path/filepath" 14 15 backend "github.com/decred/politeia/politeiad/backendv2" 16 ) 17 18 const ( 19 // Filenames of the inventory caches. 20 filenameInvUnvetted = "inv-unvetted.json" 21 filenameInvVetted = "inv-vetted.json" 22 ) 23 24 // entry represents a record entry in the inventory. 25 type entry struct { 26 Token string `json:"token"` 27 Status backend.StatusT `json:"status"` 28 } 29 30 // inventory represents the record inventory. 31 type inventory struct { 32 Entries []entry `json:"entries"` 33 } 34 35 // invPathUnvetted returns the file path for the unvetted inventory. 36 func (t *tstoreBackend) invPathUnvetted() string { 37 return filepath.Join(t.dataDir, filenameInvUnvetted) 38 } 39 40 // invPathVetted returns the file path for the vetted inventory. 41 func (t *tstoreBackend) invPathVetted() string { 42 return filepath.Join(t.dataDir, filenameInvVetted) 43 } 44 45 // invRemoveUnvetted removes the unvetted inventory from its respective path. 46 // 47 // This function must be called WITHOUT the write lock held. 48 func (t *tstoreBackend) invRemoveUnvetted() error { 49 t.Lock() 50 defer t.Unlock() 51 52 return os.RemoveAll(t.invPathUnvetted()) 53 } 54 55 // invRemoveVetted removes the vetted inventory from its respective path. 56 // 57 // This function must be called WITHOUT the write lock held. 58 func (t *tstoreBackend) invRemoveVetted() error { 59 t.Lock() 60 defer t.Unlock() 61 62 return os.RemoveAll(t.invPathVetted()) 63 } 64 65 // invGetLocked retrieves the inventory from disk. A new inventory is returned 66 // if one does not exist yet. 67 // 68 // This function must be called WITH the read lock held. 69 func (t *tstoreBackend) invGetLocked(filePath string) (*inventory, error) { 70 b, err := os.ReadFile(filePath) 71 if err != nil { 72 var e *os.PathError 73 if errors.As(err, &e) && !os.IsExist(err) { 74 // File does't exist. Return a new inventory. 75 return &inventory{ 76 Entries: make([]entry, 0, 1024), 77 }, nil 78 } 79 return nil, err 80 } 81 82 var inv inventory 83 err = json.Unmarshal(b, &inv) 84 if err != nil { 85 return nil, err 86 } 87 88 return &inv, nil 89 } 90 91 // invGet retrieves the inventory from disk. A new inventory is returned if one 92 // does not exist yet. 93 // 94 // This function must be called WITHOUT the read lock held. 95 func (t *tstoreBackend) invGet(filePath string) (*inventory, error) { 96 t.RLock() 97 defer t.RUnlock() 98 99 return t.invGetLocked(filePath) 100 } 101 102 // invSaveLocked writes the inventory to disk. 103 // 104 // This function must be called WITH the read/write lock held. 105 func (t *tstoreBackend) invSaveLocked(filePath string, inv inventory) error { 106 b, err := json.Marshal(inv) 107 if err != nil { 108 return err 109 } 110 return os.WriteFile(filePath, b, 0664) 111 } 112 113 // invAdd adds a new record to the inventory. 114 // 115 // This function must be called WITHOUT the read/write lock held. 116 func (t *tstoreBackend) invAdd(state backend.StateT, token []byte, s backend.StatusT) error { 117 // Get inventory file path 118 var fp string 119 switch state { 120 case backend.StateUnvetted: 121 fp = t.invPathUnvetted() 122 case backend.StateVetted: 123 fp = t.invPathVetted() 124 default: 125 return fmt.Errorf("invalid state %v", state) 126 } 127 128 t.Lock() 129 defer t.Unlock() 130 131 // Get inventory 132 inv, err := t.invGetLocked(fp) 133 if err != nil { 134 return err 135 } 136 137 // Prepend token 138 e := entry{ 139 Token: hex.EncodeToString(token), 140 Status: s, 141 } 142 inv.Entries = append([]entry{e}, inv.Entries...) 143 144 // Save inventory 145 err = t.invSaveLocked(fp, *inv) 146 if err != nil { 147 return err 148 } 149 150 log.Debugf("Inv add %v %x %v", 151 backend.States[state], token, backend.Statuses[s]) 152 153 return nil 154 } 155 156 // invUpdate updates the status of a record in the inventory. The record state 157 // must remain the same. 158 // 159 // This function must be called WITHOUT the read/write lock held. 160 func (t *tstoreBackend) invUpdate(state backend.StateT, token []byte, s backend.StatusT) error { 161 // Get inventory file path 162 var fp string 163 switch state { 164 case backend.StateUnvetted: 165 fp = t.invPathUnvetted() 166 case backend.StateVetted: 167 fp = t.invPathVetted() 168 default: 169 return fmt.Errorf("invalid state %v", state) 170 } 171 172 t.Lock() 173 defer t.Unlock() 174 175 // Get inventory 176 inv, err := t.invGetLocked(fp) 177 if err != nil { 178 return err 179 } 180 181 // Del entry 182 entries, err := entryDel(inv.Entries, token) 183 if err != nil { 184 return fmt.Errorf("%v entry del: %v", state, err) 185 } 186 187 // Prepend new entry to inventory 188 e := entry{ 189 Token: hex.EncodeToString(token), 190 Status: s, 191 } 192 inv.Entries = append([]entry{e}, entries...) 193 194 // Save inventory 195 err = t.invSaveLocked(fp, *inv) 196 if err != nil { 197 return err 198 } 199 200 log.Debugf("Inv update %v %x to %v", 201 backend.States[state], token, backend.Statuses[s]) 202 203 return nil 204 } 205 206 // invMoveToVetted deletes a record from the unvetted inventory then adds it 207 // to the vetted inventory. 208 // 209 // This function must be called WITHOUT the read/write lock held. 210 func (t *tstoreBackend) invMoveToVetted(token []byte, s backend.StatusT) error { 211 var ( 212 upath = t.invPathUnvetted() 213 vpath = t.invPathVetted() 214 ) 215 216 t.Lock() 217 defer t.Unlock() 218 219 // Get unvetted inventory 220 u, err := t.invGetLocked(upath) 221 if err != nil { 222 return fmt.Errorf("unvetted invGetLocked: %v", err) 223 } 224 225 // Del entry 226 u.Entries, err = entryDel(u.Entries, token) 227 if err != nil { 228 return fmt.Errorf("entryDel: %v", err) 229 } 230 231 // Save unvetted inventory 232 err = t.invSaveLocked(upath, *u) 233 if err != nil { 234 return fmt.Errorf("unvetted invSaveLocked: %v", err) 235 } 236 237 // Get vetted inventory 238 v, err := t.invGetLocked(vpath) 239 if err != nil { 240 return fmt.Errorf("vetted invGetLocked: %v", err) 241 } 242 243 // Prepend new entry to inventory 244 e := entry{ 245 Token: hex.EncodeToString(token), 246 Status: s, 247 } 248 v.Entries = append([]entry{e}, v.Entries...) 249 250 // Save vetted inventory 251 err = t.invSaveLocked(vpath, *v) 252 if err != nil { 253 return fmt.Errorf("vetted invSaveLocked: %v", err) 254 } 255 256 log.Debugf("Inv move to vetted %x %v", token, backend.Statuses[s]) 257 258 return nil 259 } 260 261 // inventoryAdd is a wrapper around the invAdd method that allows us to decide 262 // how errors should be handled. For now we just panic. If an error occurs the 263 // cache is no longer coherent and the only way to fix it is to rebuild it. 264 func (t *tstoreBackend) inventoryAdd(state backend.StateT, token []byte, s backend.StatusT) { 265 err := t.invAdd(state, token, s) 266 if err != nil { 267 panic(fmt.Sprintf("invAdd %v %x %v: %v", state, token, s, err)) 268 } 269 } 270 271 // inventoryUpdate is a wrapper around the invUpdate method that allows us to 272 // decide how disk read/write errors should be handled. For now we just panic. 273 // If an error occurs the cache is no longer coherent and the only way to fix 274 // it is to rebuild it. 275 func (t *tstoreBackend) inventoryUpdate(state backend.StateT, token []byte, s backend.StatusT) { 276 err := t.invUpdate(state, token, s) 277 if err != nil { 278 panic(fmt.Sprintf("invUpdate %v %x %v: %v", state, token, s, err)) 279 } 280 } 281 282 // inventoryMoveToVetted is a wrapper around the invMoveToVetted method that 283 // allows us to decide how disk read/write errors should be handled. For now we 284 // just panic. If an error occurs the cache is no longer coherent and the only 285 // way to fix it is to rebuild it. 286 func (t *tstoreBackend) inventoryMoveToVetted(token []byte, s backend.StatusT) { 287 err := t.invMoveToVetted(token, s) 288 if err != nil { 289 panic(fmt.Sprintf("invMoveToVetted %x %v: %v", token, s, err)) 290 } 291 } 292 293 // invByStatus contains the inventory categorized by record state and record 294 // status. Each list contains a page of tokens that are sorted by the timestamp 295 // of the status change from newest to oldest. 296 type invByStatus struct { 297 Unvetted map[backend.StatusT][]string 298 Vetted map[backend.StatusT][]string 299 } 300 301 // invByStatusAll returns a page of tokens for all record states and statuses. 302 func (t *tstoreBackend) invByStatusAll(pageSize uint32) (*invByStatus, error) { 303 // Get unvetted inventory 304 u, err := t.invGet(t.invPathUnvetted()) 305 if err != nil { 306 return nil, err 307 } 308 309 // Prepare unvetted inventory reply 310 var ( 311 unvetted = tokensParse(u.Entries, backend.StatusUnreviewed, pageSize, 1) 312 censored = tokensParse(u.Entries, backend.StatusCensored, pageSize, 1) 313 archived = tokensParse(u.Entries, backend.StatusArchived, pageSize, 1) 314 315 unvettedInv = make(map[backend.StatusT][]string, 16) 316 ) 317 if len(unvetted) != 0 { 318 unvettedInv[backend.StatusUnreviewed] = unvetted 319 } 320 if len(censored) != 0 { 321 unvettedInv[backend.StatusCensored] = censored 322 } 323 if len(archived) != 0 { 324 unvettedInv[backend.StatusArchived] = archived 325 } 326 327 // Get vetted inventory 328 v, err := t.invGet(t.invPathVetted()) 329 if err != nil { 330 return nil, err 331 } 332 333 // Prepare vetted inventory reply 334 var ( 335 vetted = tokensParse(v.Entries, backend.StatusPublic, pageSize, 1) 336 vcensored = tokensParse(v.Entries, backend.StatusCensored, pageSize, 1) 337 varchived = tokensParse(v.Entries, backend.StatusArchived, pageSize, 1) 338 339 vettedInv = make(map[backend.StatusT][]string, 16) 340 ) 341 if len(vetted) != 0 { 342 vettedInv[backend.StatusPublic] = vetted 343 } 344 if len(vcensored) != 0 { 345 vettedInv[backend.StatusCensored] = vcensored 346 } 347 if len(varchived) != 0 { 348 vettedInv[backend.StatusArchived] = varchived 349 } 350 351 return &invByStatus{ 352 Unvetted: unvettedInv, 353 Vetted: vettedInv, 354 }, nil 355 } 356 357 // invByStatus returns the tokens of records in the inventory categorized by 358 // record state and record status. The tokens are ordered by the timestamp of 359 // their most recent status change, sorted from newest to oldest. 360 // 361 // The state, status, and page arguments can be provided to request a specific 362 // page of record tokens. 363 // 364 // If no status is provided then the most recent page of tokens for all 365 // statuses will be returned. All other arguments are ignored. 366 func (t *tstoreBackend) invByStatus(state backend.StateT, s backend.StatusT, pageSize, page uint32) (*invByStatus, error) { 367 // If no status is provided a page of tokens for each status should 368 // be returned. 369 if s == backend.StatusInvalid { 370 return t.invByStatusAll(pageSize) 371 } 372 373 // Get inventory file path 374 var fp string 375 switch state { 376 case backend.StateUnvetted: 377 fp = t.invPathUnvetted() 378 case backend.StateVetted: 379 fp = t.invPathVetted() 380 default: 381 return nil, fmt.Errorf("unknown state '%v'", state) 382 } 383 384 // Get inventory 385 inv, err := t.invGet(fp) 386 if err != nil { 387 return nil, err 388 } 389 390 // Get the page of tokens 391 tokens := tokensParse(inv.Entries, s, pageSize, page) 392 393 // Prepare reply 394 var ibs invByStatus 395 switch state { 396 case backend.StateUnvetted: 397 ibs = invByStatus{ 398 Unvetted: map[backend.StatusT][]string{ 399 s: tokens, 400 }, 401 Vetted: map[backend.StatusT][]string{}, 402 } 403 case backend.StateVetted: 404 ibs = invByStatus{ 405 Unvetted: map[backend.StatusT][]string{}, 406 Vetted: map[backend.StatusT][]string{ 407 s: tokens, 408 }, 409 } 410 } 411 412 return &ibs, nil 413 } 414 415 // invOrdered returns a page of record tokens ordered by the timestamp of their 416 // most recent status change. The returned tokens will include tokens for all 417 // record statuses. 418 func (t *tstoreBackend) invOrdered(state backend.StateT, pageSize, pageNumber uint32) ([]string, error) { 419 // Get inventory file path 420 var fp string 421 switch state { 422 case backend.StateUnvetted: 423 fp = t.invPathUnvetted() 424 case backend.StateVetted: 425 fp = t.invPathVetted() 426 default: 427 return nil, fmt.Errorf("unknown state '%v'", state) 428 } 429 430 // Get inventory 431 inv, err := t.invGet(fp) 432 if err != nil { 433 return nil, err 434 } 435 436 // Return specified page of tokens 437 var ( 438 startIdx = int((pageNumber - 1) * pageSize) 439 endIdx = startIdx + int(pageSize) 440 tokens = make([]string, 0, pageSize) 441 ) 442 for i := startIdx; i < endIdx; i++ { 443 if i >= len(inv.Entries) { 444 // We've reached the end of the inventory. We're done. 445 break 446 } 447 448 tokens = append(tokens, inv.Entries[i].Token) 449 } 450 451 return tokens, nil 452 } 453 454 // entryDel removes the entry for the token and returns the updated slice. 455 func entryDel(entries []entry, token []byte) ([]entry, error) { 456 // Find token in entries 457 var i int 458 var found bool 459 htoken := hex.EncodeToString(token) 460 for k, v := range entries { 461 if v.Token == htoken { 462 i = k 463 found = true 464 break 465 } 466 } 467 if !found { 468 return nil, fmt.Errorf("token not found %x", token) 469 } 470 471 // Del token from entries (linear time) 472 copy(entries[i:], entries[i+1:]) // Shift entries[i+1:] left one index 473 entries[len(entries)-1] = entry{} // Del last element (write zero value) 474 entries = entries[:len(entries)-1] // Truncate slice 475 476 return entries, nil 477 } 478 479 // tokensParse parses a page of tokens from the provided entries that meet the 480 // provided criteria. 481 func tokensParse(entries []entry, s backend.StatusT, countPerPage, page uint32) []string { 482 tokens := make([]string, 0, countPerPage) 483 if countPerPage == 0 || page == 0 { 484 return tokens 485 } 486 487 startAt := (page - 1) * countPerPage 488 var foundCount uint32 489 for _, v := range entries { 490 if v.Status != s { 491 // Status does not match 492 continue 493 } 494 495 // Matching status found 496 if foundCount >= startAt { 497 tokens = append(tokens, v.Token) 498 if len(tokens) == int(countPerPage) { 499 // We have a full page. We're done. 500 return tokens 501 } 502 } 503 504 foundCount++ 505 } 506 507 return tokens 508 }