github.com/decred/politeia@v1.4.0/politeiad/backendv2/tstorebe/plugins/comments/recordindex.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 comments 6 7 import ( 8 "encoding/json" 9 "errors" 10 "fmt" 11 "os" 12 "path/filepath" 13 "strings" 14 15 backend "github.com/decred/politeia/politeiad/backendv2" 16 "github.com/decred/politeia/politeiad/plugins/comments" 17 "github.com/decred/politeia/util" 18 ) 19 20 const ( 21 // Filenames of the record indexes that are saved to the comments 22 // plugin data dir. 23 fnRecordIndexUnvetted = "{shorttoken}-index-unvetted.json" 24 fnRecordIndexVetted = "{shorttoken}-index-vetted.json" 25 ) 26 27 // recordIndex contains the indexes for all comments made on a record. 28 type recordIndex struct { 29 Comments map[uint32]commentIndex `json:"comments"` // [commentID]comment 30 } 31 32 // commentIndex contains the digests of all comment add, dels, and votes for a 33 // comment ID. 34 type commentIndex struct { 35 Adds map[uint32][]byte `json:"adds"` // [version]digest 36 Del []byte `json:"del"` 37 38 // Votes contains the vote history for each uuid that voted on the 39 // comment. This data is cached because the effect of a new vote 40 // on a comment depends on the previous vote from that uuid. 41 // Example, a user upvotes a comment that they have already 42 // upvoted, the resulting vote score is 0 due to the second upvote 43 // removing the original upvote. 44 Votes map[string][]voteIndex `json:"votes"` // [uuid]votes 45 } 46 47 // newCommentIndex returns a new commentIndex. 48 func newCommentIndex() commentIndex { 49 return commentIndex{ 50 Adds: make(map[uint32][]byte, 1024), 51 Votes: make(map[string][]voteIndex, 1024), 52 } 53 } 54 55 // voteIndex contains the comment vote and the digest of the vote record. 56 // Caching the vote allows us to tally the votes for a comment without needing 57 // to pull the vote blobs from the backend. The digest allows us to retrieve 58 // the vote blob if we need to. 59 type voteIndex struct { 60 Vote comments.VoteT `json:"vote"` 61 Digest []byte `json:"digest"` 62 } 63 64 // recordIndexPath returns the file path for a cached record index. It accepts 65 // both the full length token or the short token, but the short token is always 66 // used in the file path string. 67 func (p *commentsPlugin) recordIndexPath(token []byte, s backend.StateT) (string, error) { 68 var fn string 69 switch s { 70 case backend.StateUnvetted: 71 fn = fnRecordIndexUnvetted 72 case backend.StateVetted: 73 fn = fnRecordIndexVetted 74 default: 75 return "", fmt.Errorf("invalid state") 76 } 77 78 t, err := util.ShortTokenEncode(token) 79 if err != nil { 80 return "", err 81 } 82 fn = strings.Replace(fn, "{shorttoken}", t, 1) 83 return filepath.Join(p.dataDir, fn), nil 84 } 85 86 // recordIndex returns the cached recordIndex for the provided record. If a 87 // cached recordIndex does not exist, a new one will be returned. 88 // 89 // This function must be called WITHOUT the read lock held. 90 func (p *commentsPlugin) recordIndex(token []byte, s backend.StateT) (*recordIndex, error) { 91 fp, err := p.recordIndexPath(token, s) 92 if err != nil { 93 return nil, err 94 } 95 96 p.RLock() 97 defer p.RUnlock() 98 99 b, err := os.ReadFile(fp) 100 if err != nil { 101 var e *os.PathError 102 if errors.As(err, &e) && !os.IsExist(err) { 103 // File does't exist. Return a new recordIndex instead. 104 return &recordIndex{ 105 Comments: make(map[uint32]commentIndex), 106 }, nil 107 } 108 return nil, err 109 } 110 111 var ridx recordIndex 112 err = json.Unmarshal(b, &ridx) 113 if err != nil { 114 return nil, err 115 } 116 117 return &ridx, nil 118 } 119 120 // _recordIndexSave saves the provided recordIndex to the comments plugin data dir. 121 // 122 // This function must be called WITHOUT the read/write lock held. 123 func (p *commentsPlugin) _recordIndexSave(token []byte, s backend.StateT, ridx recordIndex) error { 124 b, err := json.Marshal(ridx) 125 if err != nil { 126 return err 127 } 128 fp, err := p.recordIndexPath(token, s) 129 if err != nil { 130 return err 131 } 132 133 p.Lock() 134 defer p.Unlock() 135 136 err = os.WriteFile(fp, b, 0664) 137 if err != nil { 138 return err 139 } 140 return nil 141 } 142 143 // recordIndexSave is a wrapper around the _recordIndexSave method that allows 144 // us to decide how update errors should be handled. For now we just panic. 145 // If an error occurs the cache is no longer coherent and the only way to fix 146 // it is to rebuild it. 147 func (p *commentsPlugin) recordIndexSave(token []byte, s backend.StateT, ridx recordIndex) { 148 err := p._recordIndexSave(token, s, ridx) 149 if err != nil { 150 panic(err) 151 } 152 } 153 154 // recordIndexRemove removes the record index cache from the path of the 155 // provided record token and state. 156 // 157 // This function must be called WITHOUT be write lock held. 158 func (p *commentsPlugin) recordIndexRemove(token []byte, s backend.StateT) error { 159 p.Lock() 160 defer p.Unlock() 161 162 path, err := p.recordIndexPath(token, s) 163 if err != nil { 164 return err 165 } 166 167 return os.RemoveAll(path) 168 }