file.go raw

   1  package storage
   2  
   3  import (
   4  	"context"
   5  	"encoding/json"
   6  	"errors"
   7  	"fmt"
   8  	"os"
   9  
  10  	"github.com/nrdcg/goacmedns"
  11  )
  12  
  13  var _ goacmedns.Storage = (*File)(nil)
  14  
  15  // ErrDomainNotFound is returned from `Fetch` when the provided domain is not
  16  // present in the storage.
  17  var ErrDomainNotFound = errors.New("requested domain is not present in storage")
  18  
  19  // File implements the `Storage` interface and persists `Accounts` to
  20  // a JSON file on disk.
  21  type File struct {
  22  	// path is the filepath that the `accounts` are persisted to when the `Save`
  23  	// function is called.
  24  	path string
  25  	// mode is the file mode used when the `path` JSON file must be created
  26  	mode os.FileMode
  27  	// accounts holds the `Account` data that has been `Put` into the storage
  28  	accounts map[string]goacmedns.Account
  29  }
  30  
  31  // NewFile returns a `Storage` implementation backed by JSON content
  32  // saved into the provided `path` on disk. The file at `path` will be created if
  33  // required. When creating a new file the provided `mode` is used to set the
  34  // permissions.
  35  func NewFile(path string, mode os.FileMode) *File {
  36  	fs := &File{
  37  		path:     path,
  38  		mode:     mode,
  39  		accounts: make(map[string]goacmedns.Account),
  40  	}
  41  
  42  	// Opportunistically try to load the account data. Return an empty account if
  43  	// any errors occur.
  44  	if jsonData, err := os.ReadFile(path); err == nil {
  45  		if err := json.Unmarshal(jsonData, &fs.accounts); err != nil {
  46  			return fs
  47  		}
  48  	}
  49  
  50  	return fs
  51  }
  52  
  53  // Save persists the `Account` data to the File's configured path. The
  54  // file at that path will be created with the File's mode if required.
  55  func (f File) Save(_ context.Context) error {
  56  	serialized, err := json.Marshal(f.accounts)
  57  	if err != nil {
  58  		return fmt.Errorf("fFailed to marshal account: %w", err)
  59  	}
  60  
  61  	if err = os.WriteFile(f.path, serialized, f.mode); err != nil {
  62  		return fmt.Errorf("failed to write storage file: %w", err)
  63  	}
  64  
  65  	return nil
  66  }
  67  
  68  // Put saves an `Account` for the given `Domain` into the in-memory accounts of
  69  // the File instance. The `Account` data will not be written to disk
  70  // until the `Save` function is called.
  71  func (f File) Put(_ context.Context, domain string, acct goacmedns.Account) error {
  72  	f.accounts[domain] = acct
  73  
  74  	return nil
  75  }
  76  
  77  // Fetch retrieves the `Account` object for the given `domain` from the
  78  // File in-memory accounts. If the `domain` provided does not have an
  79  // `Account` in the storage an `ErrDomainNotFound` error is returned.
  80  func (f File) Fetch(_ context.Context, domain string) (goacmedns.Account, error) {
  81  	if acct, exists := f.accounts[domain]; exists {
  82  		return acct, nil
  83  	}
  84  
  85  	return goacmedns.Account{}, ErrDomainNotFound
  86  }
  87  
  88  // FetchAll retrieves all the `Account` objects from the File and
  89  // returns a map that has domain names as its keys and `Account` objects
  90  // as values.
  91  func (f File) FetchAll(_ context.Context) (map[string]goacmedns.Account, error) {
  92  	return f.accounts, nil
  93  }
  94