]> git.r.bdr.sh - rbdr/mobius/blob - internal/mobius/account_manager.go
Account for the root
[rbdr/mobius] / internal / mobius / account_manager.go
1 package mobius
2
3 import (
4 "fmt"
5 "github.com/jhalter/mobius/hotline"
6 "github.com/stretchr/testify/mock"
7 "gopkg.in/yaml.v3"
8 "os"
9 "path"
10 "path/filepath"
11 "strings"
12 "sync"
13 )
14
15 // loadFromYAMLFile loads data from a YAML file into the provided data structure.
16 func loadFromYAMLFile(path string, data interface{}) error {
17 fh, err := os.Open(path)
18 if err != nil {
19 return err
20 }
21 defer fh.Close()
22
23 decoder := yaml.NewDecoder(fh)
24 return decoder.Decode(data)
25 }
26
27 type YAMLAccountManager struct {
28 accounts map[string]hotline.Account
29 accountDir string
30
31 mu sync.Mutex
32 }
33
34 func NewYAMLAccountManager(accountDir string) (*YAMLAccountManager, error) {
35 accountMgr := YAMLAccountManager{
36 accountDir: accountDir,
37 accounts: make(map[string]hotline.Account),
38 }
39
40 matches, err := filepath.Glob(filepath.Join(accountDir, "*.yaml"))
41 if err != nil {
42 return nil, fmt.Errorf("list account files: %w", err)
43 }
44
45 if len(matches) == 0 {
46 return nil, fmt.Errorf("no accounts found in directory: %s", accountDir)
47 }
48
49 for _, filePath := range matches {
50 var account hotline.Account
51 fileContents, err := os.ReadFile(filePath)
52 if err != nil {
53 return nil, fmt.Errorf("read file: %v", err)
54 }
55
56 if err := yaml.Unmarshal(fileContents, &account); err != nil {
57 return nil, fmt.Errorf("unmarshal: %v", err)
58 }
59
60 // Check the account file contents for a field name that only appears in the new AccessBitmap flag format.
61 // If not present, re-save the file to migrate it from the old array of ints format to new bool flag format.
62 if !strings.Contains(string(fileContents), " DownloadFile:") {
63 if err := accountMgr.Update(account, account.Login); err != nil {
64 return nil, fmt.Errorf("migrate account to new access flag format: %v", err)
65 }
66 }
67
68 accountMgr.accounts[account.Login] = account
69 }
70
71 return &accountMgr, nil
72 }
73
74 func (am *YAMLAccountManager) Create(account hotline.Account) error {
75 am.mu.Lock()
76 defer am.mu.Unlock()
77
78 // Create account file, returning an error if one already exists.
79 file, err := os.OpenFile(
80 filepath.Join(am.accountDir, path.Join("/", account.Login+".yaml")),
81 os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644,
82 )
83 if err != nil {
84 return fmt.Errorf("create account file: %w", err)
85 }
86 defer file.Close()
87
88 b, err := yaml.Marshal(account)
89 if err != nil {
90 return fmt.Errorf("marshal account to YAML: %v", err)
91 }
92
93 _, err = file.Write(b)
94 if err != nil {
95 return fmt.Errorf("write account file: %w", err)
96 }
97
98 am.accounts[account.Login] = account
99
100 return nil
101 }
102
103 func (am *YAMLAccountManager) Update(account hotline.Account, newLogin string) error {
104 am.mu.Lock()
105 defer am.mu.Unlock()
106
107 // If the login has changed, rename the account file.
108 if account.Login != newLogin {
109 err := os.Rename(
110 filepath.Join(am.accountDir, path.Join("/", account.Login)+".yaml"),
111 filepath.Join(am.accountDir, path.Join("/", newLogin)+".yaml"),
112 )
113 if err != nil {
114 return fmt.Errorf("error renaming account file: %w", err)
115 }
116
117 account.Login = newLogin
118 am.accounts[newLogin] = account
119
120 delete(am.accounts, account.Login)
121 }
122
123 out, err := yaml.Marshal(&account)
124 if err != nil {
125 return err
126 }
127
128 if err := os.WriteFile(filepath.Join(am.accountDir, newLogin+".yaml"), out, 0644); err != nil {
129 return fmt.Errorf("error writing account file: %w", err)
130 }
131
132 am.accounts[account.Login] = account
133
134 return nil
135 }
136
137 func (am *YAMLAccountManager) Get(login string) *hotline.Account {
138 am.mu.Lock()
139 defer am.mu.Unlock()
140
141 account, ok := am.accounts[login]
142 if !ok {
143 return nil
144 }
145
146 return &account
147 }
148
149 func (am *YAMLAccountManager) List() []hotline.Account {
150 am.mu.Lock()
151 defer am.mu.Unlock()
152
153 var accounts []hotline.Account
154 for _, account := range am.accounts {
155 accounts = append(accounts, account)
156 }
157
158 return accounts
159 }
160
161 func (am *YAMLAccountManager) Delete(login string) error {
162 am.mu.Lock()
163 defer am.mu.Unlock()
164
165 err := os.Remove(filepath.Join(am.accountDir, path.Join("/", login+".yaml")))
166 if err != nil {
167 return fmt.Errorf("delete account file: %v", err)
168 }
169
170 delete(am.accounts, login)
171
172 return nil
173 }
174
175 type MockAccountManager struct {
176 mock.Mock
177 }
178
179 func (m *MockAccountManager) Create(account hotline.Account) error {
180 args := m.Called(account)
181
182 return args.Error(0)
183 }
184
185 func (m *MockAccountManager) Update(account hotline.Account, newLogin string) error {
186 args := m.Called(account, newLogin)
187
188 return args.Error(0)
189 }
190
191 func (m *MockAccountManager) Get(login string) *hotline.Account {
192 args := m.Called(login)
193
194 return args.Get(0).(*hotline.Account)
195 }
196
197 func (m *MockAccountManager) List() []hotline.Account {
198 args := m.Called()
199
200 return args.Get(0).([]hotline.Account)
201 }
202
203 func (m *MockAccountManager) Delete(login string) error {
204 args := m.Called(login)
205
206 return args.Error(0)
207 }