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  }