You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
loop/lsat/store.go

212 lines
6.3 KiB
Go

package lsat
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
var (
// ErrNoToken is the error returned when the store doesn't contain a
// token yet.
ErrNoToken = errors.New("no token in store")
// storeFileName is the name of the file where we store the final,
// valid, token to.
storeFileName = "lsat.token"
// storeFileNamePending is the name of the file where we store a pending
// token until it was successfully paid for.
storeFileNamePending = "lsat.token.pending"
// errNoReplace is the error that is returned if a new token is
// being written to a store that already contains a paid token.
errNoReplace = errors.New("won't replace existing paid token with " +
"new token. " + manualRetryHint)
)
// Store is an interface that allows users to store and retrieve an LSAT token.
type Store interface {
// CurrentToken returns the token that is currently contained in the
// store or an error if there is none.
CurrentToken() (*Token, error)
// AllTokens returns all tokens that the store has knowledge of, even
// if they might be expired. The tokens are mapped by their identifying
// attribute like file name or storage key.
AllTokens() (map[string]*Token, error)
// StoreToken saves a token to the store. Old tokens should be kept for
// accounting purposes but marked as invalid somehow.
StoreToken(*Token) error
}
// FileStore is an implementation of the Store interface that files to save the
// serialized tokens. There is always just one current token that is either
// pending or fully paid.
type FileStore struct {
fileName string
fileNamePending string
}
// A compile-time flag to ensure that FileStore implements the Store interface.
var _ Store = (*FileStore)(nil)
// NewFileStore creates a new file based token store, creating its file in the
// provided directory. If the directory does not exist, it will be created.
func NewFileStore(storeDir string) (*FileStore, error) {
// If the target path for the token store doesn't exist, then we'll
// create it now before we proceed.
if !fileExists(storeDir) {
if err := os.MkdirAll(storeDir, 0700); err != nil {
return nil, err
}
}
return &FileStore{
fileName: filepath.Join(storeDir, storeFileName),
fileNamePending: filepath.Join(storeDir, storeFileNamePending),
}, nil
}
// CurrentToken returns the token that is currently contained in the store or an
// error if there is none.
//
// NOTE: This is part of the Store interface.
func (f *FileStore) CurrentToken() (*Token, error) {
// As this is only a wrapper for external users to make sure the store
// is locked, the actual implementation is in the non-exported method.
return f.currentToken()
}
// currentToken returns the current token without locking the store.
func (f *FileStore) currentToken() (*Token, error) {
switch {
case fileExists(f.fileName):
return readTokenFile(f.fileName)
case fileExists(f.fileNamePending):
return readTokenFile(f.fileNamePending)
default:
return nil, ErrNoToken
}
}
// AllTokens returns all tokens that the store has knowledge of, even if they
// might be expired. The tokens are mapped by their identifying attribute like
// file name or storage key.
//
// NOTE: This is part of the Store interface.
func (f *FileStore) AllTokens() (map[string]*Token, error) {
tokens := make(map[string]*Token)
// All tokens start with the same name so we can get them by the prefix.
// As the tokens don't expire yet, there currently can't be more than
// just one token, either pending or paid.
// TODO(guggero): Update comment once tokens expire and we keep backups.
tokenDir := filepath.Dir(f.fileName)
files, err := ioutil.ReadDir(tokenDir)
if err != nil {
return nil, err
}
for _, file := range files {
name := file.Name()
if !strings.HasPrefix(name, storeFileName) {
continue
}
fileName := filepath.Join(tokenDir, name)
token, err := readTokenFile(fileName)
if err != nil {
return nil, err
}
tokens[fileName] = token
}
return tokens, nil
}
// StoreToken saves a token to the store, overwriting any old token if there is
// one.
//
// NOTE: This is part of the Store interface.
func (f *FileStore) StoreToken(newToken *Token) error {
// Serialize the token first, before we rename anything.
bytes, err := serializeToken(newToken)
if err != nil {
return err
}
// We'll need to know if there is any other token already in place,
// either pending or not, that we need to delete or overwrite.
currentToken, err := f.currentToken()
switch {
// No token in the store yet, just write it to the corresponding file.
case err == ErrNoToken:
// What's the target file name we are going to write?
newFileName := f.fileName
if newToken.isPending() {
newFileName = f.fileNamePending
}
return ioutil.WriteFile(newFileName, bytes, 0600)
// Fail on any other error.
case err != nil:
return err
// Replace a pending token with a paid one.
case currentToken.isPending() && !newToken.isPending():
// Make sure we replace the the same token, just with a
// different state.
if currentToken.PaymentHash != newToken.PaymentHash {
return fmt.Errorf("new paid token doesn't match " +
"existing pending token")
}
// Write the new token first, so we still have the pending
// around if something goes wrong.
err := ioutil.WriteFile(f.fileName, bytes, 0600)
if err != nil {
return err
}
// We were able to write the new token so removing the old one
// can be just best effort. By default, the valid one will be
// read by the store if both exist.
_ = os.Remove(f.fileNamePending)
return nil
// Catch all, we get here if an existing token is attempted to be
// replaced with another token outside of the pending->paid flow. The
// user should manually remove the token in that case.
// TODO(guggero): Once tokens expire, this logic has to be adapted
// accordingly.
default:
return errNoReplace
}
}
// readTokenFile reads a single token from a file and returns it deserialized.
func readTokenFile(tokenFile string) (*Token, error) {
bytes, err := ioutil.ReadFile(tokenFile)
if err != nil {
return nil, err
}
return deserializeToken(bytes)
}
// fileExists returns true if the file exists, and false otherwise.
func fileExists(path string) bool {
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}