forked from erdos-one/r2
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathconfigure.go
329 lines (269 loc) · 9.3 KB
/
configure.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
package cmd
import (
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"github.com/boatkit-io/r2/pkg"
"github.com/spf13/cobra"
)
// configString formats a set of Cloudflare R2 credentials into a string that can be written to the
// ~/.r2 configuration file. Allowing for multiple profiles, each profile is formatted as a section
// with the profile name in square brackets. The profile name is followed by the account ID, access
// key ID, and secret access key.
func configString(c pkg.Config) string {
configTemplate := "[%s]\naccount_id=%s\naccess_key_id=%s\nsecret_access_key=%s"
return fmt.Sprintf(configTemplate, c.Profile, c.AccountID, c.AccessKeyID, c.SecretAccessKey)
}
// getConfigPath returns the path to the ~/.r2 configuration file, accounting for different
// operating systems' conventions for naming the home directory.
func getConfigPath() string {
homeDir, err := os.UserHomeDir()
if err != nil {
log.Fatal(err)
}
return filepath.Join(homeDir, ".r2")
}
// R2ConfigFile globally defines the path to the ~/.r2 configuration file.
var R2ConfigFile = getConfigPath()
// getProfile returns the Cloudflare R2 credentials for the specified profile. If the profile does
// not exist, it is created interactively and saved to the ~/.r2 configuration file.
func getProfile(profileName string) pkg.Config {
// Get profiles
profiles := getConfig(false)
// If profile exists, return it
for _, profile := range profiles {
if profile.Profile == profileName {
return profile
}
}
// Profile doesn't exist, create new one and save to ~/.r2 config file
profile := getCredentials(profileName)
writeConfig(profile)
return profile
}
// getCredentials prompts the user to enter the Cloudflare R2 credentials for a specified profile.
// If no profile is specified, the user is prompted to enter a profile name.
func getCredentials(profile string) pkg.Config {
var c pkg.Config
// Get profile
if profile == "" {
// Get profile name
fmt.Print("Profile [default]: ")
fmt.Scanln(&profile)
if profile == "" {
profile = "default"
}
}
c.Profile = profile
// Get account ID
fmt.Print("Account ID: ")
fmt.Scanln(&c.AccountID)
// Get access key ID
fmt.Print("Access Key ID: ")
fmt.Scanln(&c.AccessKeyID)
// Get secret access key
fmt.Print("Secret Access Key: ")
fmt.Scanln(&c.SecretAccessKey)
return c
}
// Parse configuration file and return profiles
func getConfig(createIfNotPresent bool) map[string]pkg.Config {
// Create configuration file if it doesn't exist
if _, err := os.Stat(R2ConfigFile); os.IsNotExist(err) {
// If not creating configuration file, return empty map
if !createIfNotPresent {
return make(map[string]pkg.Config)
}
f, err := os.Create(R2ConfigFile)
if err != nil {
log.Fatal(err)
}
defer f.Close()
// Get credentials interactively and write to configuration file
writeConfig(getCredentials(""))
}
// Read configuration file
c, err := os.ReadFile(R2ConfigFile)
if err != nil {
log.Fatal(err)
}
// Remove empty lines
configString := regexp.MustCompile(`^\n$`).ReplaceAllString(string(c), "")
// Parse configuration file into profiles
var profiles = make(map[string]pkg.Config)
profilesRe := regexp.MustCompile(`\[[\w\s\]=]+`)
for _, p := range profilesRe.FindAllString(configString, -1) {
// Parse profiles
var profile pkg.Config
// Get profile name
if regexp.MustCompile(`\[\w+\]`).MatchString(p) {
profile.Profile = regexp.MustCompile(`\[(\w+)\]`).FindAllStringSubmatch(p, -1)[0][1]
}
// Get account ID
accountIDRe := regexp.MustCompile(`account_id\s*=\s*(\w+)`)
if accountIDRe.MatchString(p) {
profile.AccountID = accountIDRe.FindAllStringSubmatch(p, -1)[0][1]
}
// Get access key ID
akidRe := regexp.MustCompile(`access_key_id\s*=\s*(\w+)`)
if akidRe.MatchString(p) {
profile.AccessKeyID = akidRe.FindAllStringSubmatch(p, -1)[0][1]
}
// Get secret access key
sakRe := regexp.MustCompile(`secret_access_key\s*=\s*(\w+)`)
if sakRe.MatchString(p) {
profile.SecretAccessKey = sakRe.FindAllStringSubmatch(p, -1)[0][1]
}
profiles[profile.Profile] = profile
}
return profiles
}
// listProfiles returns a list of all profiles in the ~/.r2 configuration file. Profile names are
// sorted alphabetically, irrespective of case, with the default profile always first.
func listProfiles() []string {
// Get profiles
profiles := getConfig(false)
// Get profile names and sort alphabetically (default profile is always first)
var profileNames []string
for _, p := range profiles {
if p.Profile != "default" {
profileNames = append(profileNames, p.Profile)
}
}
sort.Slice(profileNames, func(i, j int) bool {
return strings.ToLower(profileNames[i]) < strings.ToLower(profileNames[j])
})
if _, ok := profiles["default"]; ok {
profileNames = append([]string{"default"}, profileNames...)
}
return profileNames
}
// writeConfig writes the provided profiles to the ~/.r2 configuration file. If a profile already
// exists, it is overwritten. If all credentials are not provided, the function fails. Profiles are
// sorted alphabetically, irrespective of case, with the default profile always first.
func writeConfig(c pkg.Config) {
// Read configuration file
profiles := getConfig(false)
// If not all credentials are provided, fail
if c.AccountID == "" || c.AccessKeyID == "" || c.SecretAccessKey == "" {
log.Fatal("All credentials must be provided")
}
// Add profile to configuration
profiles[c.Profile] = c
// Format profile strings and sort alphabetically (default profile is always first)
var configStrings []string
for _, p := range profiles {
if p.Profile != "default" {
configStrings = append(configStrings, configString(p))
}
}
sort.Slice(configStrings, func(i, j int) bool {
return strings.ToLower(configStrings[i]) < strings.ToLower(configStrings[j])
})
if _, ok := profiles["default"]; ok {
configStrings = append([]string{configString(profiles["default"])}, configStrings...)
}
// Write configuration to file
f, err := os.Create(R2ConfigFile)
if err != nil {
log.Fatal(err)
}
defer f.Close()
_, err = f.WriteString(strings.Join(configStrings, "\n\n") + "\n")
if err != nil {
log.Fatal(err)
}
}
// configureCmd represents the configure command
var configureCmd = &cobra.Command{
Use: "configure",
Short: "Configure R2 access",
Long: `Configure R2 access by providing Cloudflare R2 API Token credentials.
Configuration can be done interactively or by passing flags. If you pass flags,
you must provide both the access key ID and secret access key, otherwise the
command will fail.
To configure interactively, run:
r2 configure
To configure with flags, run:
r2 configure --access-key-id <access-key-id> \
--secret-access-key <secret-access-key>
If you have multiple R2 tokens, you can configure a named profile by passing
the --profile flag.
Interactively:
r2 configure --profile my-profile
With flags:
r2 configure --profile my-profile --access-key-id <access-key-id> \
--secret-access-key <secret-access-key>
Profiles are stored in ~/.r2 and can be used by passing the --profile flag to
any command.
To list available profiles, run:
r2 configure --list
To generate an API Token, follow Cloudflare's guide at:
https://developers.cloudflare.com/r2/data-access/s3-api/tokens/
Be careful not to share your API Token credentials with anyone.`,
Run: func(cmd *cobra.Command, args []string) {
// Handle list flag
list, err := cmd.Flags().GetBool("list")
if err != nil {
log.Fatal(err)
}
if list {
// List profiles
fmt.Println(strings.Join(listProfiles(), "\n"))
} else {
// Parse configuration
var c pkg.Config
var err error
// Get profile name
c.Profile, err = cmd.Flags().GetString("profile")
if err != nil {
log.Fatal(err)
}
// Get account ID
c.AccountID, err = cmd.Flags().GetString("account-id")
if err != nil {
log.Fatal(err)
}
// Get access key ID
c.AccessKeyID, err = cmd.Flags().GetString("access-key-id")
if err != nil {
log.Fatal(err)
}
// Get secret access key
c.SecretAccessKey, err = cmd.Flags().GetString("secret-access-key")
if err != nil {
log.Fatal(err)
}
// Either access key ID or secret access key not passed but not both
if (c.AccessKeyID == "" && c.SecretAccessKey != "") || (c.AccessKeyID != "" && c.SecretAccessKey == "") {
log.Fatal(`Error: You must either provide both the access key ID and secret access key or
neither to configure interactively.
For more information, run:
r2 help configure`)
} else {
// Check if configuration provided
if c.AccountID != "" && c.AccessKeyID != "" && c.SecretAccessKey != "" {
writeConfig(c)
} else {
// If no configuration provided, get configuration interactively
writeConfig(getCredentials(""))
}
}
}
},
}
// init adds the configure command to the root command and adds flags to the configure command
func init() {
// Add the configure command to the root command
rootCmd.AddCommand(configureCmd)
// Add flags to the configure command
configureCmd.Flags().BoolP("list", "l", false, "List all named profiles")
configureCmd.Flags().String("profile", "", "Configure a named profile")
configureCmd.Flags().String("account-id", "", "R2 Account ID")
configureCmd.Flags().String("access-key-id", "", "R2 Access Key ID")
configureCmd.Flags().String("secret-access-key", "", "R2 Secret Access Key")
}