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