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  }